C#

Случайные числа в C#: System.Random от Next до Shuffle

Введение

В программах часто требуется реализовать случайность. В качестве примеров можно привести случайный урон, сдачу карт или случайное расположение объектов в играх, генерацию криптографических ключей, подготовку тестовых данных, случайный выбор сервера из пула для балансировки. В C# для генерации случайных чисел есть класс System.Random, о котором мы сегодня и поговорим.

Обратите внимание: Класс Random генерирует псевдослучайную последовательность. Это значит, что числа выглядят случайными, но вычисляются по определенному алгоритму. Применение псевдослучайных последовательностей в криптографии несет риски безопасности. Такие генераторы являются предсказуемыми, и при определенных условиях (например, знании начального значения) можно вычислить все сгенерированные числа. Если вам нужны случайные последовательности для криптографических целей, используйте класс System.Security.Cryptography.RandomNumberGenerator.

var rnd = System.Security.Cryptography.RandomNumberGenerator.Create();
var randomBytes = new byte[32];
rnd.GetBytes(randomBytes);

System.Random

Чтобы начать генерировать случайные числа, необходимо создать экземпляр генератора. После этого для получения каждого нового числа вызывается соответствующий метод. Вот простейший пример:

// Создаем экземпляр класса Random
var rnd = new Random();

// Симуляция подбрасывания монеты
var isHeads = rnd.Next(2) == 0; // 0 - орел, 1 - решка
Console.WriteLine($"Подбрасывание монеты: {(isHeads ? "Орел" : "Решка")}");

Метод Next возвращает случайное целое число int. Оно будет больше или равно 0, но строго меньше верхнего предела, указанного в качестве аргумента. Есть перегрузка этого метода, которая принимает два аргумента – нижнюю и верхнюю границы диапазона, в который должно попадать случайное число. По схожему принципу работает метод NextInt64, который генерирует целое число long.

Методы NextSingle и NextDouble умеют генерировать случайные числа с плавающей запятой – float и double соответственно. Эти методы не имеют параметров и результат всегда больше или равно 0 и меньше 1.

var rnd = new Random();

// Генерация случайного времени задержки
var time1 = rnd.GetDouble();
await Task.Delay((int)(time1 * 1000));

// Генерация случайного времени задержки (1.5-4.0 секунды)
var time2 = 1.5 + rnd.GetDouble() * 2.5;
await Task.Delay((int)(time2 * 1000));

В примере выше мы использовали выражение 1.5 + random.GetDouble() * 2.5, чтобы расширить диапазон результата и получить случайное число от 1.5 до 4. Общая формула выглядит так: minValue + random.GetDouble() * (maxValue - minValue)

Для задач, требующих случайных двоичных данных (как генерация токенов или уникальных идентификаторов), используйте NextBytes. Метод принимает массив байт и заполняет его случайными значениями. Например, это отлично подойдет для создания случайного имени файла.

var rnd = new Random();

var randomBytes = new byte[8];
rnd.NextBytes(randomBytes);
var fileName = Convert.ToHexString(randomBytes).ToLowerInvariant();

File.WriteAllText($"{fileName}.txt", "Hi!");

Примечание: Подобный код используется настолько часто, что в .NET 10 у класса Random появились два новых метода – GetHexString и GetString.

Когда требуется выбрать несколько случайных элементов из списка, используйте GetItems. Метод принимает коллекцию-источник и количество элементов, возвращая массив случайно выбранных значений.

var rnd = new Random();
var colors = new[] { "красный", "зеленый", "синий", "желтый", "фиолетовый", "оранжевый" };

// Выбор 3 цветов
string[] selectedColors = rnd.GetItems(colors, 3);
Console.WriteLine($"Выбранные цвета: {string.Join(", ", selectedColors)}");

Когда требуется изменить порядок элементов в коллекции (например, перемешать карты в колоде или ответы в тесте), используйте Shuffle. В отличие от методов, возвращающих новую коллекцию, Shuffle работает напрямую с исходными данными, что экономит память и повышает производительность.

var rnd = new Random();

var question = "Самый большой океан?";
var answers = new[] { "Атлантический", "Индийский", "Тихий", "Северный Ледовитый" };
rnd.Shuffle(answers);

Console.WriteLine(question);
Console.WriteLine($"a) {answers[0]} b) {answers[1]}");
Console.WriteLine($"c) {answers[2]} d) {answers[3]}");

Воспроизводимость последовательности

Бывают случаи, когда нужно генерировать одну и ту же случайную последовательность при каждом запуске программы. Например – вы хотите воспроизвести редкую ошибку, которая возникает при определенной последовательности “случайных” событий. Для такого сценария можно воспользоваться перегрузкой конструктора Random, которая принимает значение seed. Seed – это начальное значение, которое полностью определяет все последующие числа в “случайной” последовательности. Это значит, что если в конструкторе двух разных экземпляров Random указать одинаковый seed, эти экземпляры Random будут генерировать одинаковые последовательности чисел.

// Два генератора с одинаковым seed...
var rnd1 = new Random(42);
var rnd2 = new Random(42);

// ...будут вести себя одинаково:
int num1 = rnd1.Next(100); // Допустим, 71
int num2 = rnd2.Next(100); // И это тоже будет 71!

// Их следующие числа также совпадут
int next1 = rnd1.Next(50); // Например, 12
int next2 = rnd2.Next(50); // Тоже 12!

System.Random.Shared

В .NET 6 в класс Random было добавлено статическое свойство Shared. Это было сделано из-за того, что класс Random не потокобезопасный. Если вам нужна потокобезопасная генерация случайных чисел и нет необходимости контролировать seed, то используйте Random.Shared. Вот простой пример:

// Запускаем параллельно
Parallel.For(0, 10, x =>
{
    // Используем Shared для генерации случайных чисел
    var value = Random.Shared.Next();
    Console.WriteLine(value);
});

Результат будет ожидаемый – набор случайных чисел.

Что не так с потокобезопасностью этого класса? В .NET до 6 версии в случае одновременного доступа, Random возвращает 0 (см. пост на Stack Overflow). Вот пример наивного использования:

// Это не безопасно, не делайте так
var rng = new Random();

// Запускаем параллельно
Parallel.For(0, 10, x =>
{
    var value = rng.Next(); // Получаем случайное число, но часто будет 0
    Console.WriteLine(value);
});

Можно было бы создавать экземпляр Random в каждом таске, но это не потокобезопасно в .NET Framework. Кроме того, повышается нагрузка на Garbage Collector.

Заключение

Класс Random в C# предоставляет простой и эффективный инструмент для генерации псевдослучайных чисел в большинстве типовых сценариев: от игровой механики до тестовых данных и балансировки нагрузки. Его богатый API с методами Next, NextDouble, GetItems и Shuffle, покрывает основные потребности разработчиков.

Однако важно помнить о ключевых ограничениях:

  • Random генерирует псевдослучайные последовательности, которые не подходят для криптографии.
  • Конструктор с параметром seed позволяет воспроизводить последовательности, что незаменимо для отладки и тестирования.
  • Экземпляры класса не являются потокобезопасными. Для многопоточных сценариев в .NET 6 и выше следует использовать статическое свойство Random.Shared.

Выбирайте Random для задач, не требующих криптографической стойкости, а для безопасности применяйте RandomNumberGenerator. Правильное использование этого класса сделает ваш код предсказуемым в тестах, эффективным в работе и действительно случайным для пользователя.

Ссылки

https://learn.microsoft.com/ru-ru/dotnet/api/system.random

https://andrewlock.net/building-a-thread-safe-random-implementation-for-dotnet-framework