Avatar ·

Генератор случайных чисел C# (Random) всегда возвращает одно и то же число в цикле

Всем привет! Я новичок в C# и пытаюсь сгенерировать список случайных чисел в цикле for. Но у меня проблема: каждое число в списке одинаковое! Вот мой код: ```csharp using System; using System.Collections.Generic; class Program { static void Main() { List numbers = new List(); for (int i = 0; i < 10; i++) { Random rand = new Random(); int num = rand.Next(1, 100); numbers.Add(num); } foreach (int n in numbers) { Console.WriteLine(n); } } } ``` Я ожидал увидеть 10 разных случайных чисел от 1 до 99, но вместо этого получаю что-то вроде: ``` 45 45 45 45 45 45 45 45 45 45 ``` Хотя иногда число меняется, если я перезапускаю программу, но в рамках одного запуска все числа одинаковые. Я перепробовал разные seed-значения, менял диапазон — ничего не помогает. Почему так происходит? Как правильно генерировать разные случайные числа в цикле? Я читал, что Random использует системное время, но почему тогда числа не меняются, ведь время-то идет? Заранее спасибо за помощь! <<>>

Почему Random возвращает одинаковые числа?

Вы столкнулись с классической проблемой использования класса Random в C#. Основная причина — конструктор Random() без параметров использует текущее системное время в качестве начального зерна (seed). Внутри цикла for каждый новый экземпляр Random создается настолько быстро, что системные часы не успевают измениться. В результате все объекты Random получают одно и то же зерно и генерируют одинаковую последовательность чисел.

На современных многоядерных процессорах, где цикл выполняется за микросекунды, эта проблема проявляется особенно часто. Даже если вы добавите Thread.Sleep(1) — это не гарантия, так как тик системного таймера (около 15 мс в Windows) намного больше.

Правильное решение: один экземпляр Random на всё

Самый простой и эффективный способ — создать один экземпляр Random до цикла и использовать его многократно:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Создаем ОДИН генератор
        Random rand = new Random();
        List<int> numbers = new List<int>();
        
        for (int i = 0; i < 10; i++)
        {
            // Используем один и тот же объект
            int num = rand.Next(1, 100);
            numbers.Add(num);
        }
        
        foreach (int n in numbers)
        {
            Console.WriteLine(n);
        }
    }
}

Теперь вы получите 10 разных случайных чисел. Почему это работает? Потому что один экземпляр Random хранит внутреннее состояние и при каждом вызове Next() обновляет его, переходя к следующему псевдослучайному числу в последовательности.

Важные рекомендации для многопоточности

Если вы работаете в многопоточном приложении, то один общий Random может привести к проблемам — он не потокобезопасен. В таких случаях используйте:

  • ThreadLocal<Random> — создает отдельный экземпляр для каждого потока
  • lock — синхронизация доступа к общему Random
  • Random.Shared (начиная с .NET 6) — потокобезопасный статический генератор

Пример с Random.Shared:

for (int i = 0; i < 10; i++)
{
    int num = Random.Shared.Next(1, 100);
    numbers.Add(num);
}

Когда создавать новый Random?

Единственный случай, когда стоит создавать новый экземпляр — если вам нужно воспроизводимое поведение (например, для тестов). Тогда передайте конкретное зерно в конструктор:

Random rand = new Random(42); // Всегда будет одна и та же последовательность

Или если вы генерируете числа в разных, независимых частях программы с большими временными промежутками — но даже тогда лучше использовать один статический экземпляр.

Запомните главное правило: один объект Random на логическую единицу работы. Не создавайте его в цикле, не создавайте при каждом вызове метода — вынесите на уровень класса или метода.

<<>>

Альтернативный подход: использование криптостойкого генератора

Если вам нужна настоящая случайность (например, для генерации паролей или токенов), стандартный Random не подходит — он псевдослучайный и предсказуемый. В таких случаях используйте System.Security.Cryptography.RandomNumberGenerator:

using System.Security.Cryptography;

public static int GetCryptographicRandom(int min, int max)
{
    // Создаем массив из 4 байт (32 бита)
    byte[] randomBytes = new byte[4];
    RandomNumberGenerator.Fill(randomBytes);
    
    // Преобразуем в беззнаковое целое
    uint randomUint = BitConverter.ToUInt32(randomBytes, 0);
    
    // Масштабируем в нужный диапазон
    return (int)(min + (randomUint % (max - min)));
}

// Использование:
for (int i = 0; i < 10; i++)
{
    int num = GetCryptographicRandom(1, 100);
    Console.WriteLine(num);
}

Преимущества криптостойкого генератора:

  • Абсолютно непредсказуемая последовательность
  • Не зависит от системного времени
  • Можно безопасно использовать в многопоточных приложениях
  • Не требует создания одного экземпляра — статические методы

Недостаток: работает медленнее обычного Random, поэтому для игр или симуляций он избыточен. Используйте его только там, где безопасность критична.