Генератор случайных чисел C# (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, поэтому для игр или симуляций он избыточен. Используйте его только там, где безопасность критична.