Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

19 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 7 de octubre de 2025
Brazo robótico llamado Random usando una ruleta de casino para obtener números aleatorios

Llevamos utilizando la clase Random de .NET para generar números aleatorios desde hace mucho tiempo, en su aparición con la versión 1.1 de .NET Framework (año 2002). Probablemente por eso, muchos de nosotros la usamos de forma automática, sin pararnos a estudiar todas sus posibilidades y las novedades que le han ido añadiendo a lo largo del tiempo.

En este artículo vamos a ver algunas características de la clase Random que quizás no conocías... o quizás sí, pero que no está de más refrescarlas:

  • Los números generados no son realmente aleatorios.
  • En versiones antiguas de .NET, era más probable que Random generara números impares.
  • No se debe usar Random para operaciones criptográficas.
  • No es buena idea crear un objeto Random cada vez que necesitemos un número aleatorio.
  • Si estamos en .NET 6+, mejor usar Random.Shared.
  • El método Shuffle() desordena arrays.
  • El método GetItems() permite obtener elementos aleatorios de una colección.
  • Podemos heredar de Random.

¡Vamos a ello!

Los números generados son pseudoaleatorios, no aleatorios

Aunque su propio nombre suene a aleatoriedad total, en realidad no es así. Los números generados por Random son calculados usando algoritmos matemáticos que simplemente aseguran que los números cumplirán una serie de criterios estadísticos de aleatoriedad. En definitiva, son pseudoaleatorios y siguen patrones predecibles, aunque en en la práctica sean suficientemente aleatorios para la mayoría de los casos.

La clave de su aleatoriedad está en la semilla, un valor que podemos pasar al constructor de Random y que determina la secuencia de números que se generará a continuación.

Por tanto, si creamos instancias de esta clase usando la misma semilla, obtendremos siempre la misma secuencia de números. Podéis comprobarlo fácilmente si ejecutáis este código varias veces en una aplicación consola:

var rnd = new Random(1);

for (int i = 0; i < 5; i++)
{
    Console.WriteLine($"{rnd.Next()}");
}

La salida obtenida será siempre la siguiente:

534011718
237820880
1002897798
1657007234
1412011072

Si no indicamos ninguna semilla al crear la instancia de Random, en .NET Framework se utilizará el número de ticks de la hora actual del sistema. Por tanto, si creásemos varias instancias justo en el mismo instante (por ejemplo, de forma muy seguida o desde hilos distintos), los valores coincidirían, incluso en dispositivos diferentes.

Sin embargo, en .NET Core/.NET 5 y superiores, el seed se obtendrá usando otros mecanismos de aleatoriedad estáticos por hilo. Gracias a ello, las semillas generadas serán diferentes en cada instancia de Random aunque hayan sido creadas en el mismo instante.

En versiones antiguas de .NET era más probable que se generaran números impares

La generación de números aleatorios es un proceso algorítmico y, como tal, está sujeto a errores.

En 2017, Søren Fuglede Jørgensen publicó el post "Bias in the .NET random number generator", donde mostraba que en .NET Framework 4.7 y anteriores había una tendencia estadísticamente significativa a que los números generados por la clase Random fueran impares al obtenerlos usando el método Next(). Esto se debía a errores de redondeo en las operaciones de punto flotante usadas durante el cálculo necesario para obtener los valores aleatorios.

Aunque no he podido encontrar referencias directas al momento de la subsanación del problema, aunque la introducción en .NET 6 de un nuevo algoritmo de generación de números aleatorios (Xoshiro256**) parece indicar que el problema fue corregido en esa versión.

No se debe usar Random para operaciones criptográficas

Visto el punto anterior, está claro que la clase Random no es adecuada para operaciones criptográficas, porque para "adivinar" los números generados bastaría con conocer la semilla utilizada en su creación.

Las operaciones criptográficamente seguras requiere una aleatoriedad real (o, al menos, lo más real posible) y sin posibilidad de predecir los valores generados.

Para estos casos, .NET proporciona la clase System.Security.Cryptography.RandomNumberGenerator, que genera números aleatorios de alta calidad y considerados seguros para operaciones criptográficas, aunque es a costa de un rendimiento peor que Random.

No es buena idea crear una instancia de Random cada vez que necesitemos un número aleatorio

Si cada vez que necesitamos un número aleatorio creamos una nueva instancia de Random:

  • Si usamos .NET Framework, y sobre todo si estamos en un entorno multi-hilo, hay muchas posibilidades de que generemos valores repetidos.
  • Si especificamos manualmente la semilla, obtendremos siempre la misma secuencia de números.
  • Estamos introduciendo una penalización de rendimiento, porque la creación de Random es un proceso relativamente costoso debido a la inicialización del generador de números aleatorios.
  • Estamos consumiendo más memoria de la necesaria, ya que cada instancia de Random almacena su propio estado interno.

Por esta razón, es recomendable la creación de un único objeto Random y reutilizarlo en toda la aplicación. Sin embargo, hay que tener en cuenta que Random no es thread-safe: distintos hilos accediendo a la misma instancia podrían dar lugar a condiciones de carrera y corrupción del estado interno del generador. Por tanto, en entornos multi-hilo, habría que sincronizar el acceso a la instancia compartida para que sólo un thread acceda a la instancia en un momento determinado.

Si usamos .NET 6 o superior, mucho mejor usar Random.Shared

En .NET 6 y superiores, se introdujo la propiedad estática Random.Shared, que devuelve una instancia de Random que puede ser compartida y reutilizada de forma segura desde cualquier punto de la aplicación, incluso si necesitamos generar números aleatorios desde distintos hilos concurrentemente, porque la instancia retornada es thread-safe.

De esta forma, de un plumazo evitamos:

  • La repetición de valores debido al uso del mismo seed.
  • La penalización de rendimiento por la creación de múltiples instancias.
  • El consumo innecesario de memoria.
  • La necesidad de sincronizar el acceso a la instancia compartida en entornos multi-hilo.

Y obviamente, su uso es muy sencillo. La referencia a Random.Shared, retornará ya una instancia de Random lista para ser usada, así que basta con llamar directamente a sus métodos de generación de números aleatorios:

var numberBetween0And100 = Random.Shared.Next(0, 101);

El método Shuffle() permite desordenar un array

Hace muuuuchos años vimos por aquí cómo desordenar un array en C# y VB.NET utilizando el algoritmo llamado Fisher-Yates shuffle.

Pues bien, desde .NET 8 no es necesario andarse con esas soluciones manuales que, aunque no son complejas, al final tenemos que dedicarle su tiempo. La necesidad es tan frecuente que la propia clase Random incluye el método Shuffle<T>() que se encarga de desordenar el array de elementos de tipo T que le pasemos como argumento, y además hacerlo de forma eficiente utilizando optimizaciones internas.

Por ejemplo, si tenemos un array de números enteros y queremos desordenarlo, basta con hacer lo siguiente:

int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
Random.Shared.Shuffle(numbers);
Console.WriteLine(string.Join(", ", numbers));

string[] texts = ["one", "two", "three", "four", "five"];
Random.Shared.Shuffle(texts);
Console.WriteLine(string.Join(", ", texts));
10, 2, 1, 9, 5, 4, 8, 6, 7, 3
two, three, four, one, five

Fijaos que el método Shuffle() no devuelve nada, sino que modifica el array que le pasamos como argumento. Por tanto, si queremos mantener el array original, tendríamos que hacer una copia antes de llamar a Shuffle().

Nota extra: recientemente se ha valorado la introducción de un método Shuffle() en LINQ de .NET 10, aunque de momento parece que no ha salido adelante.

El método GetItems() permite obtener elementos aleatorios de una colección

Otra necesidad bastante frecuente es, dada una colección de elementos, obtener un número determinado de ellos de forma aleatoria.

Para ello, también en .NET 8 se introdujo el método GetItems<T>(). Este método recibe como argumento una colección de elementos de tipo T y el número de ellos que queremos obtener aleatoriamente, devolviendo un array del mismo tipo con los elementos seleccionados.

Internamente, lo único que hace es construir un array del tamaño del número de elementos solicitados y rellenar cada posición con un elemento aleatorio de la colección original. Ojo, porque esto implica que los elementos obtenidos podrían repetirse.

int[] numbers = [1, 2, 3, 4, 5];
var items = Random.Shared.GetItems(numbers, 5);
Console.WriteLine(string.Join(", ", items));
5, 3, 1, 3, 2

Podemos heredar de Random para crear generadores de números aleatorios personalizados

Por último, es interesante saber que la clase Random no está sellada, como ocurre con muchas otras clases del framework. Por tanto, aunque no es algo que vayamos a usar normalmente, es posible heredar de ella para crear generadores de números aleatorios personalizados.

Esto podría ser interesante para realizar tests unitarios o pruebas en las que queremos tener el control total sobre los números generados. Por ejemplo, el siguiente código muestra una clase MyRandom descendiente de Random donde hemos sobrescrito todos sus métodos para conseguir que devuelvan secuencialmente valores entre 0 y 4:

public class MyRandom: Random
{
    private int _value;

    public MyRandom(int value) => _value = value;

    private int GetNext() => _value++ % 5;
    public override int Next() => GetNext();
    public override int Next(int maxValue) => GetNext();
    public override int Next(int minValue, int maxValue) => GetNext();
    public override double NextDouble() => GetNext();
    protected override double Sample() => GetNext();
    public override long NextInt64() => GetNext();
    public override long NextInt64(long maxValue) => GetNext();
    public override long NextInt64(long minValue, long maxValue) => GetNext();
    public override float NextSingle() => GetNext();

    public override void NextBytes(byte[] buffer) 
        => Array.Fill(buffer, (byte)GetNext());
    public override void NextBytes(Span<byte> buffer) 
        => buffer.Fill((byte)GetNext());
}

Con esto, podríamos reemplazar la clase Random por nuestra implementación a la hora de invocar código que la utilice, por ejemplo, como lo que mostramos a continuación, invocando al método PrintRandom():

var rnd = new MyRandom(3);
for(int i=0; i < 5; i++)
{
    PrintRandom(rnd);
}

void PrintRandom(Random rnd)
{
    Console.WriteLine($"{rnd.Next()}");
}
3
4
0
1
2

¡Espero que os haya resultado interesante!

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!