La batallita del Guille
Estaba leyendo el número 29 de la revista hakin9 y vi un código en C#
para generar cadenas de forma aleatoria (para un motor polimórfico) y al ver
el código de esa función, me dio no-se-qué por el cuerpo... y es que usaba
de forma errónea la clase Random así como el método Next para
generar el siguiente número aleatorio.
Lo malo es que ese tipo de error es muy común. Ya lo había vivido en mis
tiempos de Visual Basic 6.0 (y anterior), no por el método Next,
ya que en esa versión de Visual Basic las "randomizaciones" se
generaban de forma más artesanal y no tenemos
ese método mágico. Pero si existía la posibilidad de "hacer
mal las cosas" al definir o inicializar la generación de números aleatorios.
Pero dejemos el Visual Basic 6.0 y pasemos a .NET que es lo que ahora
interesa.
Generar de forma correcta números aleatorios en .NET
En las clases de .NET Framework está la clase Random que sirve para
generar números seudo-aleatorios.
Esa clase define varios métodos, pero básicamente usaremos el método
Next.
Ese método permite que se indiquen dos valores, de esa forma sacará un
número aleatorio que esté dentro del rango de
esos números indicados.
Y aquí reside el problema, que el valor que genera está DENTRO (o casi)
del rango indicado o dicho de otra forma, el valor generado será un número
mayor o igual que el primer argumento y menor que el segundo, pero muchos
piensan que el número que se genera puede incluir los dos valores
indicados.
En el caso que te comentaba, para generar caracteres aleatorios, usaba
esta llamada al método Next:
int e = randomNumber.Next(97, 122);
Seguramente la intención era generar un valor entre 97 y 122, es decir,
un valor que devolviera una letra en minúscula desde la a (97) hasta la z
(122), pero lo más que puede generar es un valor que va desde el 97 (a)
hasta el 121 (y).
Recuerda:
El valor que genera el método Next(n1, n2) es un valor que puede ser igual o
mayor que el primer argumento (n1) y menor que el segundo (n2).
El otro fallo garrafal del código ese (no tengo nada en contra del autor,
ni lo conozco), pero es que es algo que ya he visto en varios sitios y creo
que hacía falta que se aclararan estos conceptos, y como da la casualidad que
hoy me he topado con ese código, pues me he dicho: venga Guille,
explícalo... además de que también es un tema que tengo incluido en el libro
que estoy escribiendo con mis recetas sobre
muchas situaciones en las que algunos no saben qué hacer... y mucho más,
pero como ese libro estará dirigido para los que usan Visual Basic, he
pensado que también es bueno que la gente que usa C# sepan de estas cosas,
je, je... pero bueno... dejemos la publicidad de mi nuevo libro y sigamos con esto de los
números aleatorios...
Otro fallo que se comete con más frecuencia de la deseada es crear la
instancia de la clase Random dentro de un bucle o un método.
Te explico, para que lo entiendas.
La clase Random es la encargada de generar los números aleatorios y
permite indicar un valor para usarlo como semilla de los números aleatorios.
(No son totalmente aleatorios, -la propia documentación de Visual Studio
deja claro que son seudoaleatorios- ya que, según dicen, es casi imposible
encontrar un algoritmo que genere números totalmente aleatorios.)
Esa semilla es la que se usa para generar la secuencia
de números. Es decir, si usamos siempre la misma semilla, los números
aleatorios siempre se generarán en la misma secuencia, por tanto, siempre
serán los mismos. (No que siempre se genera el mismo número, sino que si
generas 10 números, esos 10 números -que pueden ser diferentes- siempre
serán esos mismos 10 números y además generados en el mismo orden.)
Un ejemplo que aclare todo esto
En el listado con el código de ejemplo tienes un programa
que genera una cadena con
la cantidad
de caracteres indicados. Los caracteres que incluirá esa cadena estarán
comprendidos entre la 'a' y
la 'z' (ambos incluidos).
Nota:
Este es el listado para Visual Basic 2005
y este otro para Visual C# 2005.
Ese código se puede usar en versiones posteriores sin cambios, pero para
usarlo en versiones anteriores, tendrás que quitar todo el código que
manipula la ventana de la consola.
Si ejecutas ese código tal como está en el listado, la salida puede ser
como esta:
Milisegundos: 895
01 lawknpfywupmphypupmbttslynnypubgebxxzcbqcfpwwttgdnqietksiklz
02 bgibsjiojbqrowzodwpeuhvcdsckqfporbovxodnlfwbxzuhuwplvgeulfxz
03 hyxembamidqpsbtpkteeyupuqdskwukrregmispvyhzqfattmpjrbsbmicoi
04 jqqntzxovghsejgqvzpqkyxhktqydtefdgmirlwfceyghqmrmkrjmyyksica
05 jzlccboxxxzjcjofarzugyltbvsfibpiiwwctvdfcpupkhgcjsjbjiulorag
06 iiovhqagnllentsqdwiuoqwqrozablocvwwtazhthapyhfnpqdhrjgssmyds
07 raecxjjqxyqeoomnaxjmbjjydwspedgcvobhwmaxvxsrnccutesooqrhqfya
08 qumtsniamfrnxlrdrslatxvalrnfwtrrozgbkzzzucionerdznqyuxhtzlha
09 zcdsnazxfpssslgrpgmhakfidtwqsnczrzarzpametxjuyfjrqesyzzoxupu
10 djfrntaskorzfadhmvnfnnycjjaralueafjuofwbxmycyemywsbbgkhqkcid
11 tiszumzzqisgzmhapveewbvhjtyqytjmxthouzileninzvctchaoydgsnlpa
12 ojbxzgjffsgqnsajthhorenqsuntnoajjhwvlkotlwojbkbfdwzgdajkvkfe
13 sqrqrxgfwexisodupephtipqpufiymabfkavexredwjzyropxourvxlkaabx
14 rygjewcinhqsmldwyqsygqkkrighzpudwumfpzucogbpycfktomkicrbnwsv
15 pujxnmvryyhyvpjaoulpdwnkdkutdlrthymtwdclgnlvhnotsrmskdstgyud
16 rbomgorxcqdlsziqksdqvbalzyiegtmyjyufyzptvqqcnaakvciovdhpacnh
17 fnrjeectfcrlcjuflyyyyyjdkxcfreevmhnizhyjygfskxlliceceoihdnyg
18 cwcaywyuvmfdapartrsnlqwpacqxzgeljrhbeetoyqpwwabpoqomvkulamav
19 fbvphtuihynoxeovbaszaajisrqyxvbrdyixvaodgshpdzpqvzjzubvfrnxq
20 cvlmxvlzvraalpsistfsbacxmbwcddficyalhvshoquezjofjnmxhfocsnzv
21 zjwslsynohawbyekfyydnrtqqiaxjxstvwifucthkbqbmpvjadxupwihqgbd
22 radrohafpzqcfrsmardbmqwgzydynaoobqpkxzneoqtbnvoyewgzcaqbdxrp
23 dbrpkbbpbhobdzmvwvsejfurtpmazogzhxamzermtzwwifvcoueifmgyhavu
24 oapnaoufzzwsdglkjxjoujmekyuurcfyixxtjiwtkqdoyleybvokrgzeoldc
25 tznwtyjgjrabbpdegxgyuwsvnvhbxseyetzkcgvabxpjvnjbcvyqefhxhrjy
26 qhjqncwhptbzcgwwgiaalfybtotzlcdkxwjuoywtqbljuzoyilqyrimlihxy
27 iwvnpqbgxciilhjktohuzvralftpjxlgbyktgyudxiagdimjycsuacymfvcj
28 wmxzrbgnwqevudwrukwcsjqgfrfyzxnlvkglkbobcyniywzxfgsvyzrnvqta
29 whoycszjxlhtwuatwihjrissfsmwttpuhofukoffxqzuzzydyxqnikjnowtj
30 gdpdwmlhpgjhcblaiysftjxwdclwfsfovwzuawynnrosoilhyieqdnqwftat
Milisegundos: 895
Figura 1
Nota:
He modificado los valores del ancho y alto, además de
mostrar el valor de los
milisegundos transcurridos, que como puedes ver son los mismos en este caso,
pero las cadenas son todas diferentes.
Como puedes ver por la salida del ejemplo, (figura 1), el código funciona
como se espera: que cada cadena sea diferente, incluso aunque se generen en
un "pis-pas".
En ese listado está comentado lo que NO se debe hacer, que te resumo
aquí:
1- NO generar la instancia de clase Random, ni la semilla, dentro de un
bucle.
Ya que si lo haces, puedes tener la seguridad de que los valores se repitan.
Para probarlo, quita los comentarios que hay en las dos líneas en la que
digo Prueba 1 (dentro del bucle del método cadenaAleatoria).
La salida resultante sería la de la figura 2, que si la comparas con la "figura 1", es
totalmente NO ALEATORIA.
Milisegundos: 52
01 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
02 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
03 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
04 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
05 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
06 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
07 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
08 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
09 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
10 gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
11 gggggggggggggggggccccccccccccccccccccccccccccccccccccccccccc
12 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
13 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
14 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
15 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
16 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
17 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
18 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
19 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
20 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
21 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
22 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
23 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
24 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
25 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
26 ccccccccccccccccccccccccllllllllllllllllllllllllllllllllllll
27 llllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
28 llllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
29 llllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
30 llllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
Milisegundos: 99
Figura 2. Prueba 1
2- NO generar la semilla ni la instancia del objeto Random dentro de un
método que será llamado de forma consecutiva.
Podías pensar, que si en lugar de crear la instancia dentro del bucle, la
creas fuera se soluciona, pero si ese "fuera" es dentro del método, la
verdad es que no.
Si vuelves a comentar las dos líneas que te indiqué antes, y le quitas
los comentarios a las dos que están dentro del método (la que están
indicadas con Prueba 2), verás que la salida es parecida a la de la figura 3
(no tiene porqué ser igual, pero si que no se crean cadenas aleatorias).
Milisegundos: 502
01 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
02 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
03 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
04 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
05 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
06 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
07 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
08 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
09 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
10 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
11 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
12 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
13 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
14 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
15 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
16 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
17 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
18 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
19 zcrswgvhckdktqjowggrnavkxkntfjnreoynwmizxeicpwdieotzltszhgvl
20 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
21 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
22 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
23 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
24 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
25 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
26 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
27 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
28 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
29 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
30 ivvdbgzibhxwtzonpesxqynzmshqqgkxpznfckbceqmxazootrpecaxkhwrj
Milisegundos: 518
Figura 3. Prueba 2
Resumiendo que es gerundio
Seguro que piensas que usando otra semilla diferente se soluciona, pero
la verdad es que no demasiado.
Lo que debes tener en cuenta (y no tener miedo a hacerlo), es la de
generar la instancia de la clase Random al inicio del programa, usando una
semilla que sea diferente para cada ejecución del programa, por ejemplo,
usando el valor de los milisegundos actuales (o el valor de Ticks,
como te explico más abajo).
Será mucha casualidad que en
dos ejecuciones del programa el valor coincida, pero puede ocurrir...
(A ver, no es normal que ocurra, incluso puede que haya
muy pocas probabilidades de que ocurra, sobre todo si se usa Ticks, pero
como "posible" de que ocurra, si es posible, je, je.)
En cualquier caso, puedes instanciar el objeto de la clase Random antes
de llamar a la función o método que lo vaya a usar, pero asegúrate de que
siempre haya algún intervalo entre cada instanciación.
Usar un valor más grande para la semilla de Random
También puede que se te haya ocurrido usar DateTime.Now.Ticks como valor
de la semilla. La diferencia con DateTime.Now.Millisecond es que el número
de Ticks puede ser enorme.
Si estás usando Visual Basic, el problema es que Ticks
devuelve un valor Long y aunque se haga una conversión a entero, con total
seguridad se producirá una excepción de overflow (desbordamiento).
Para solucionarlo debes modificar las propiedades del proyecto para que se
quite la comprobación de desbordamiento de enteros:
Propiedades del proyecto, ficha Compilar, botón
Opciones de compilación avanzadas y en el primero grupo de opciones
(Optimizaciones), habilita la casilla de Quitar
comprobaciones de desbordamiento con enteros (en la versión en
inglés será Remove integers
overflow checks).
La forma de generar una semilla usando Ticks es esta (recuerda
deshabilitar las comprobaciones de desbordamiento):
semilla = CInt(DateTime.Now.Ticks)
Si estás usando C#, de forma predeterminada la opción esa de comprobación
de desbordamiento de enteros está deshabilitada, es decir, no se comprueba el desbordamiento
de enteros; pero también puedes usar unchecked para evitar la comprobación de
desbordamiento, independientemente de como esté en las propiedades del
proyecto:
unchecked
{
semilla = (int)DateTime.Now.Ticks;
}
o de forma simplificada:
semilla = unchecked( (int)DateTime.Now.Ticks );
Nota:
Cuando no se comprueba el desbordamiento de enteros, si asignamos un valor
entero de un tipo con más capacidad a otro con menos capacidad, y el valor
que tiene el de mayor capacidad excede al valor máximo que puede contener el
de menos capacidad, se recortará dicho valor para que quepa correctamente.
Por ejemplo, si tenemos un entero largo (Int64) que tiene el valor
633267851083732000 y lo asignamos a un entero (Int32), el valor que se
asignará será: 1599413344.
Recuerda: Si quieres, generar números entre 1 y 6 (ambos incluidos),
debes usar algo como esto:
n = rnd.Next(1, 7)
Y para terminar, una nota de "compatibilidad" con Visual Basic 6.0, (o
para los que prefieran usar la función Rnd() de Visual Basic), lo más
parecido a esa función es el método NextDouble de la clase
Random, el cual
genera un número entre 0.0 y 1.0, es decir menor de 1.0 e igual o mayor que
0.0.
Esta función es útil cuando lo que necesitamos son números aleatorios con cifras decimales.
(29/Nov/18)
Échale un vistazo a esto: Números aleatorios con decimales en .NET
No hay proyectos que bajar, todo el código está
más abajo (tanto para VB como para C#).
Decirte también que en el código ese verás que uso un objeto del tipo
StringBuilder para ir almacenando los caracteres, para esas cosas en las que
se requiere concatenar (unir) caracteres, es mejor usar StringBuilder que
String.
Y esto es todo... espero que lo aquí comentado te sirva para aclarar las
cosas, y sobre todo, te sirva para cuando quieras generar números aleatorios
con los valores adecuados.
¡Que lo randomices bien!
Nos vemos.
Guillermo