¿Cómo puedo generar cadenas alfanuméricas aleatorias?

Resuelto KingNestor asked hace 15 años • 39 respuestas

¿Cómo puedo generar una cadena alfanumérica aleatoria de 8 caracteres en C#?

KingNestor avatar Aug 28 '09 06:08 KingNestor
Aceptado

Escuché que LINQ es el nuevo negro, así que aquí está mi intento de usar LINQ:

private static Random random = new Random();

public static string RandomString(int length)
{
    const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    return new string(Enumerable.Repeat(chars, length)
        .Select(s => s[random.Next(s.Length)]).ToArray());
}

(Nota: el uso de la Randomclase la hace inadecuada para cualquier tema relacionado con la seguridad , como la creación de contraseñas o tokens. Utilice la RNGCryptoServiceProviderclase si necesita un generador de números aleatorios potente).

dtb avatar Aug 27 '2009 23:08 dtb
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var stringChars = new char[8];
var random = new Random();

for (int i = 0; i < stringChars.Length; i++)
{
    stringChars[i] = chars[random.Next(chars.Length)];
}

var finalString = new String(stringChars);

No tan elegante como la solución Linq.

(Nota: el uso de la Randomclase la hace inadecuada para cualquier tema relacionado con la seguridad , como la creación de contraseñas o tokens. Utilice la RNGCryptoServiceProviderclase si necesita un generador de números aleatorios potente).

Dan Rigby avatar Aug 27 '2009 23:08 Dan Rigby

ACTUALIZADO para .NET 6. RNGCryptoServiceProvider está marcado como obsoleto. En su lugar, llame a RandomNumberGenerator.Create() . El código de la respuesta se actualizó en consecuencia.

ACTUALIZADO según los comentarios. La implementación original generaba ~1,95% del tiempo y los caracteres restantes ~1,56% del tiempo. La actualización genera todos los caracteres ~1,61% del tiempo.

COMPATIBILIDAD CON EL MARCO: .NET Core 3 (y plataformas futuras que admitan .NET Standard 2.1 o superior) proporciona un método criptográficamente sólido RandomNumberGenerator.GetInt32() para generar un número entero aleatorio dentro de un rango deseado.

A diferencia de algunas de las alternativas presentadas, esta es criptográficamente sólida .

using System;
using System.Security.Cryptography;
using System.Text;

namespace UniqueKey
{
    public class KeyGenerator
    {
        internal static readonly char[] chars =
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); 

        public static string GetUniqueKey(int size)
        {            
            byte[] data = new byte[4*size];
            using (var crypto = RandomNumberGenerator.Create())
            {
                crypto.GetBytes(data);
            }
            StringBuilder result = new StringBuilder(size);
            for (int i = 0; i < size; i++)
            {
                var rnd = BitConverter.ToUInt32(data, i * 4);
                var idx = rnd % chars.Length;

                result.Append(chars[idx]);
            }

            return result.ToString();
        }

        public static string GetUniqueKeyOriginal_BIASED(int size)
        {
            char[] chars =
                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
            byte[] data = new byte[size];
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                crypto.GetBytes(data);
            }
            StringBuilder result = new StringBuilder(size);
            foreach (byte b in data)
            {
                result.Append(chars[b % (chars.Length)]);
            }
            return result.ToString();
        }
    }
}

Basado en una discusión de alternativas aquí y actualizado/modificado según los comentarios a continuación.

Aquí hay un pequeño conjunto de pruebas que demuestra la distribución de caracteres en la salida antigua y actualizada. Para una discusión profunda sobre el análisis de la aleatoriedad , visite random.org.

using System;
using System.Collections.Generic;
using System.Linq;
using UniqueKey;

namespace CryptoRNGDemo
{
    class Program
    {

        const int REPETITIONS = 1000000;
        const int KEY_SIZE = 32;

        static void Main(string[] args)
        {
            Console.WriteLine("Original BIASED implementation");
            PerformTest(REPETITIONS, KEY_SIZE, KeyGenerator.GetUniqueKeyOriginal_BIASED);

            Console.WriteLine("Updated implementation");
            PerformTest(REPETITIONS, KEY_SIZE, KeyGenerator.GetUniqueKey);
            Console.ReadKey();
        }

        static void PerformTest(int repetitions, int keySize, Func<int, string> generator)
        {
            Dictionary<char, int> counts = new Dictionary<char, int>();
            foreach (var ch in UniqueKey.KeyGenerator.chars) counts.Add(ch, 0);

            for (int i = 0; i < REPETITIONS; i++)
            {
                var key = generator(KEY_SIZE); 
                foreach (var ch in key) counts[ch]++;
            }

            int totalChars = counts.Values.Sum();
            foreach (var ch in UniqueKey.KeyGenerator.chars)
            {
                Console.WriteLine($"{ch}: {(100.0 * counts[ch] / totalChars).ToString("#.000")}%");
            }
        }
    }
}

Actualización 25/07/2022

Según una pregunta de los comentarios, tenía curiosidad por saber si la distribución es realmente aleatoria.

No soy estadístico, pero probablemente podría interpretar uno en la televisión. Si un estadístico de verdad quiere intervenir, sería muy bienvenido.

Hay 62 valores de salida posibles (A-Za-Z0-9) y int.MaxValuenúmeros utilizados para elegir un índice de matriz. int.MaxValue % 62es 1, por lo que un personaje será seleccionado 1 de cada 4 mil millones de veces más que los demás. Podríamos reducir aún más ese sesgo de selección rotando aleatoriamente la matriz de valores de salida antes de la indexación.

Una prueba T u otra medida estadística sería el mejor enfoque para determinar si hay sesgo en los resultados de salida, pero eso no es algo que pueda lograr en mi hora de almuerzo, así que les dejo con una modificación del código anterior que mide desviación de las expectativas. Tenga en cuenta que tiende a cero.

using System.Security.Cryptography;
using System.Text;

const int REPETITIONS = 1_000_000;
const int KEY_SIZE = 32;
int TASK_COUNT = Environment.ProcessorCount - 1;

var expectedPercentage = 100.0 / KeyGenerator.chars.Length;

var done = false;
var iterationNr = 1;
var totalRandomSymbols = 0L;

var grandTotalCounts = new Dictionary<char, long>();
foreach (var ch in KeyGenerator.chars) grandTotalCounts.Add(ch, 0);

while (!done)
{
    var experiments = Enumerable.Range(0, TASK_COUNT).Select(i => Task.Run(Experiment)).ToArray();
    Task.WaitAll(experiments);
    var totalCountsThisRun = experiments.SelectMany(e => e.Result)
        .GroupBy(e => e.Key)
        .Select(e => new { e.Key, Count = e.Select(_ => _.Value).Sum() })
        .ToDictionary(e => e.Key, e => e.Count);

    foreach (var ch in KeyGenerator.chars)
        grandTotalCounts[ch] += totalCountsThisRun[ch];

    var totalChars = grandTotalCounts.Values.Sum();
    totalRandomSymbols += totalChars;

    var distributionScores = KeyGenerator.chars.Select(ch =>
    new
    {
        Symbol = ch,
        OverUnder = (100.0 * grandTotalCounts[ch] / totalChars) - expectedPercentage

    });

    Console.WriteLine($"Iteration {iterationNr++}. Total random symbols: {totalRandomSymbols:N0}");
    foreach (var chWithValue in distributionScores.OrderByDescending(c => c.OverUnder))
    {
        Console.WriteLine($"{chWithValue.Symbol}: {chWithValue.OverUnder:#.00000}%");
    }

    done = Console.KeyAvailable;        
}

Dictionary<char, long> Experiment()
{
    var counts = new Dictionary<char, long>();
    foreach (var ch in KeyGenerator.chars) counts.Add(ch, 0);

    for (int i = 0; i < REPETITIONS; i++)
    {
        var key = KeyGenerator.GetUniqueKey(KEY_SIZE);
        foreach (var ch in key) counts[ch]++;
    }

    return counts;
}

public class KeyGenerator
{
    internal static readonly char[] chars =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();

    public static string GetUniqueKey(int size)
    {
        byte[] data = new byte[4 * size];
        using (var crypto = RandomNumberGenerator.Create())
        {
            crypto.GetBytes(data);
        }
        StringBuilder result = new StringBuilder(size);
        for (int i = 0; i < size; i++)
        {
            var rnd = BitConverter.ToUInt32(data, i * 4);
            var idx = rnd % chars.Length;

            result.Append(chars[idx]);
        }

        return result.ToString();
    }
}
Eric J. avatar Aug 27 '2009 23:08 Eric J.