Случайные числа в C#: генерация через Random и Random.Shared

Случайные числа в C#: генерация через Random и Random.Shared Полезное

Случайное число в C# проще всего получить через класс Random из пространства имен System: он выдает псевдослучайную последовательность, которой хватает для игр, тестов, симуляций и выборки. Для паролей, токенов и ключей нужен другой инструмент — RandomNumberGenerator из System.Security.Cryptography.

Ниже разберем, как сгенерировать число в нужном диапазоне, чем различаются конструкторы Random, почему один экземпляр нельзя делить между потоками и когда обычного генератора недостаточно. Все примеры даны под .NET 8; ожидаемый вывод показан рядом с кодом.

Быстрый старт: одно случайное число

Минимальная программа, которую можно скопировать и запустить. Она бросает кубик (целое от 1 до 6) и берет дробное число из полуинтервала от 0 до 1:

using System;

class Program
{
    static void Main()
    {
        Random rnd = new Random();
        int dice = rnd.Next(1, 7);       // целое от 1 до 6
        double unit = rnd.NextDouble();  // дробное [0.0, 1.0), 1.0 не входит

        Console.WriteLine($"Бросок кубика: {dice}");
        Console.WriteLine($"Дробное [0,1): {unit}");
    }
}

Вывод меняется от запуска к запуску, например:

Бросок кубика: 4
Дробное [0,1): 0.7302194564101349

Разделитель дробной части (точка или запятая) зависит от системной культуры: при русской локали NextDouble выведется через запятую. Чтобы вывод был стабильным, форматируйте число с CultureInfo.InvariantCulture.

Класс Random и его конструкторы

Random — это генератор псевдослучайных чисел. «Псевдо» значит, что числа выдает детерминированный алгоритм: он выглядит случайным, но полностью определяется начальным состоянием — сидом (seed). У класса два конструктора.

  • new Random() — без сида. Начальное состояние берется автоматически, каждый экземпляр дает свою последовательность.
  • new Random(int seed) — с явным сидом. Один и тот же сид всегда задает одну и ту же последовательность.

Важное уточнение про поведение без сида: в .NET Framework new Random() брал сид из системного таймера, поэтому два генератора, созданные в пределах ~15 мс, получали одинаковый сид и выдавали одинаковые числа. В .NET Core 2.0 и новее сид берется из потокобезопасного глобального источника, и эта коллизия ушла. С .NET 6 бессидовый Random вдобавок использует более быстрый и качественный алгоритм (xoshiro256**), а сеяный конструктор сохранил прежний алгоритм ради обратной совместимости вывода.

Методы генерации

Метод Что возвращает Границы
Next() неотрицательное целое [0, int.MaxValue)
Next(max) целое [0, max), max не входит
Next(min, max) целое [min, max), max не входит
NextInt64(...) 64-битное целое (long) как у Next, с .NET 6
NextDouble() дробное double [0.0, 1.0)
NextSingle() дробное float [0.0, 1.0), с .NET 6
NextBytes(buffer) заполняет массив или Span байтами 0-255 в каждый байт

Число в заданном диапазоне

Главная практическая задача — число из диапазона. Здесь легко ошибиться на границе: у Next(min, max) нижняя граница входит, а верхняя — нет. Значит, для кубика нужен Next(1, 7), а не Next(1, 6).

Random rnd = new Random();

int dice = rnd.Next(1, 7);      // 1..6 включительно
int percent = rnd.Next(101);    // 0..100 включительно
double price = rnd.NextDouble() * 10 + 5;  // дробное в [5, 15)

Типичная ошибка — забыть про исключенную верхнюю границу:

int dice = rnd.Next(1, 6);      // хотели 1..6, а получаем только 1..5

Результат: 6 не выпадет никогда, кубик «сломан». Исправление — поднять верхнюю границу на единицу: rnd.Next(1, 7). То же правило работает для NextInt64 и для RandomNumberGenerator.GetInt32, о котором ниже.

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

Сид нужен, когда результат должен повторяться: отладка, юнит-тесты, воспроизведение бага. Два генератора с одинаковым сидом внутри одного запуска выдают идентичные последовательности — это легко проверить:

using System;

class Program
{
    static void Main()
    {
        Random a = new Random(42);
        Random b = new Random(42);

        bool same = true;
        for (int i = 0; i < 1000; i++)
            if (a.Next() != b.Next())
                same = false;

        Console.WriteLine($"Последовательности совпали: {same}");
    }
}

Вывод:

Последовательности совпали: True

Поэтому в тестах фиксируют сид (new Random(42)), чтобы «случайные» данные были одинаковыми при каждом прогоне. Оговорка: конкретные числа воспроизводимы в пределах одной версии .NET; между мажорными версиями рантайма алгоритм сеяного генератора Microsoft менять не гарантирует, поэтому одинаковый сид может дать другую серию на другой версии .NET. В боевом коде, где нужна настоящая непредсказуемость, сид, наоборот, не задают.

Потокобезопасность и Random.Shared

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

// Плохо: новый Random на каждой итерации
for (int i = 0; i < 3; i++)
{
    Random r = new Random();
    Console.WriteLine(r.Next(100));
}

В .NET Framework это давало повтор одного значения, потому что генераторы получали одинаковый сид из таймера:

41
41
41

Исправление — один экземпляр на всю работу, а для многопоточного кода — статическое свойство Random.Shared (появилось в .NET 6), которое потокобезопасно по определению:

// Хорошо: общий потокобезопасный генератор
int x = Random.Shared.Next(1, 7);
double y = Random.Shared.NextDouble();

Random.Shared не требует создавать объект и корректно работает из разных потоков, поэтому в новом коде для несекретных задач это вариант по умолчанию.

Криптостойкие числа: RandomNumberGenerator

Random предсказуем: зная или подобрав сид, можно восстановить всю последовательность. Для паролей, токенов, соли и ключей это недопустимо — там нужен RandomNumberGenerator из System.Security.Cryptography. Класс абстрактный, но у него есть готовые статические методы, и создавать экземпляр не нужно.

using System;
using System.Security.Cryptography;

class Program
{
    static void Main()
    {
        // криптостойкое целое из [0, 100)
        int secure = RandomNumberGenerator.GetInt32(0, 100);

        // 16 случайных байтов, например для токена
        byte[] token = RandomNumberGenerator.GetBytes(16);

        Console.WriteLine($"Число: {secure}");
        Console.WriteLine($"Токен: {Convert.ToHexString(token)}");
    }
}

Вывод непредсказуем и различается при каждом запуске, например:

Число: 73
Токен: 9F3A1C08B7E24D5106AABF77CC13920E

Старый класс RNGCryptoServiceProvider использовать не нужно: он помечен устаревшим (предупреждение SYSLIB0023) и заменен статическими методами RandomNumberGenerator. Криптогенератор надежнее, но медленнее обычного Random, поэтому брать его стоит именно под задачи безопасности, а не под игры и симуляции.

Критерий Random / Random.Shared RandomNumberGenerator
Назначение игры, симуляции, выборка, тесты токены, пароли, ключи, соль
Предсказуемость предсказуем при известном сиде криптостойкий, непредсказуем
Скорость быстрый медленнее
Потоки Random.Shared — да, обычный экземпляр — нет статические методы потокобезопасны
Namespace System System.Security.Cryptography

Выводы

  • Случайное число в C# дает класс Random: Next(min, max) — для целого в диапазоне (верхняя граница не входит), NextDouble() — для дробного из [0, 1).
  • Конструктор без сида дает новую последовательность; new Random(seed) повторяет одну и ту же — это для тестов и отладки.
  • Экземпляр Random не потокобезопасен и не должен пересоздаваться в цикле; в .NET 6+ для общего доступа есть Random.Shared.
  • Для паролей, токенов и ключей нужен криптостойкий RandomNumberGenerator, а не Random; устаревший RNGCryptoServiceProvider не применять.

Где применяется и что учить дальше

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

Освойте тему на практике

Если хочется освоить C# системно — от синтаксиса и типов до работы с библиотеками .NET и практических задач, посмотрите курс C# Basic. Оценить формат и уровень до старта помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.

Смежные темы: Генерируем случайные числа в JavaScript, Квадрат числа в математике и программировании.

FAQ

Как перемешать список случайным образом? Пройдите массив с конца к началу и на каждом шаге меняйте текущий элемент с элементом на случайной позиции из Random.Shared.Next(0, i + 1) — это алгоритм Фишера-Йейтса. Сортировка «по случайному ключу» дает смещенный результат, так делать не стоит.

Можно ли получить случайное дробное число в произвольном диапазоне? Да: Random.Shared.NextDouble() дает значение из [0, 1), а линейное преобразование NextDouble() * (max - min) + min переносит его в нужный полуинтервал [min, max).

Почему нельзя брать Random для генерации пароля? Потому что последовательность Random восстанавливается по сиду, а сид угадывается или подбирается. Пароль или токен, построенный так, предсказуем; для этого есть криптостойкий RandomNumberGenerator.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)