Случайные числа в 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