Menu

Случайные числа в C#: Random.Next, диапазоны, seed, перемешивание и безопасный Random

Как генерировать случайные числа в C# с помощью класса Random: Next и его невключённая верхняя граница, случайные double в диапазоне, seed для воспроизводимых результатов, выбор случайного элемента, перемешивание списка, ловушка new Random в цикле и криптографически стойкие числа.

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

Класс System.Random генерирует псевдослучайные числа: детерминированную последовательность, которая выглядит случайной и начинается с seed (начального значения). Создайте один экземпляр и вызывайте его методы:

Пример вывода:

1294270287
83
You rolled a 5
0.6060

Запустите ещё раз, и числа изменятся. Верхняя граница не включается: Next(1, 7) может вернуть 1, 2, 3, 4, 5 или 6, но никогда 7. Для числа от 1 до 10 включительно пишите Next(1, 11). Если передать max меньше min, будет выброшено ArgumentOutOfRangeException.

Случайные числа в диапазоне

Next(min, max) подходит для целых чисел. Для double в диапазоне масштабируйте NextDouble(); для decimal, например цены, собирайте значение из целых чисел, чтобы результат имел нужную точность:

Пример вывода:

23.6 C
72.42
heads
N
-3

rng.Next(100, 10000) / 100m выбирает целое число центов и делит на decimal, поэтому у цены всегда ровно два знака после запятой. Масштабирование double дало бы значения вроде 72.4183.

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

Если передать целочисленный seed, последовательность станет повторяемой. Два объекта Random с одинаковым seed выдают в точности одинаковые числа в одинаковом порядке:

Вывод:

16 29 91 99 91
16 29 91 99 91
38

Seed нужен для тестов, симуляций, которые требуется перезапускать, повторов в играх и процедурно генерируемых уровней («world seed 2026»). Записывайте в лог использованный seed, и неожиданный результат можно будет воспроизвести позже.

Microsoft не обещает, что seed даёт одну и ту же последовательность во всех версиях .NET. Не сохраняйте такой вывод как данные (например, не генерируйте идентификаторы клиентов из фиксированного seed в расчёте получить их заново после обновления).

Выбор случайного элемента

Случайный индекс это Next(0, count), и он всегда допустим, потому что верхняя граница не включается:

Вывод:

Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common     695
rare       260
legendary  45

Взвешенный выбор отображает число от 0 до 99 на диапазоны, размеры которых равны весам: от 0 до 69 обычный, от 70 до 94 редкий, от 95 до 99 легендарный. За 1000 розыгрышей количества получаются около 700, 250 и 50 (здесь 695, 260 и 45).

Перемешивание списка: алгоритм Фишера-Йетса

Чтобы расположить список в случайном порядке, идите с конца и меняйте каждый элемент местами со случайным элементом на той же позиции или раньше. Это перемешивание Фишера-Йетса; оно быстрое, и все порядки равновероятны. Записанное как обобщённый метод, оно работает с массивами и списками элементов любого типа:

Вывод:

5 A 8 6 4 7 2 3
4 2 1 3 5

Два сокращения, которые встречаются в интернете, хуже. list.OrderBy(x => rng.Next()) работает, но выполняет сортировку, поэтому медленнее, а случайные ключи могут совпасть. Обмен с rng.Next(items.Count) вместо rng.Next(i + 1) выглядит похоже, но делает одни порядки вероятнее других. В .NET 8 и новее Random.Shared.Shuffle(array) правильно перемешивает массив на месте, а rng.GetItems(choices, 5) выбирает пять случайных элементов (с повторениями).

Ловушка new Random() в цикле

Классическая ошибка создавать новый Random каждый раз, когда нужно число:

// Don't do this
int RollDie()
{
    var rng = new Random();   // new instance on every call
    return rng.Next(1, 7);
}

В .NET Framework new Random() без seed использует системные часы (Environment.TickCount), которые меняются лишь раз в 10 до 16 миллисекунд. Вызовы в быстром цикле создают экземпляры с одинаковым seed и возвращают одно и то же число, поэтому цикл бросков кубика печатает 4 4 4 4 4. .NET Core и .NET 5+ инициализируют каждый экземпляр из общего случайного источника, что скрывает симптом, но создавать объект на каждое число всё равно расточительно, а код ломается, когда его переносят в старые проекты или Unity.

Исправление: один экземпляр на класс, хранящийся в поле:

Вывод:

3 6 6 5 6 1 2 1

Seed здесь нужен только для того, чтобы пример при каждом запуске печатал одно и то же; в настоящей игре вы бы написали new Random().

Потоки и Random.Shared

Random не потокобезопасен. Если два потока одновременно вызывают Next у одного экземпляра, его внутреннее состояние может испортиться; известный симптом это экземпляр, который с этого момента возвращает 0. Варианты, от самого простого:

static class Dice
{
    // .NET 6 and later: a thread-safe shared instance
    public static int RollShared() => Random.Shared.Next(1, 7);

    // Any version: one instance per thread
    [ThreadStatic] private static Random _local;
    private static Random Local => _local ?? (_local = new Random(Guid.NewGuid().GetHashCode()));
    public static int RollPerThread() => Local.Next(1, 7);

    // Any version: a lock around one shared instance
    private static readonly Random _rng = new Random();
    private static readonly object _sync = new object();
    public static int RollLocked()
    {
        lock (_sync) { return _rng.Next(1, 7); }
    }
}

Random.Shared это правильный выбор по умолчанию в новом коде: он потокобезопасен, не требует поля и инициализируется случайно. Задать ему seed нельзя, поэтому когда нужна воспроизводимость, используйте собственный new Random(seed).

Криптографически стойкие случайные числа

System.Random предсказуем по замыслу. Любой, кто знает seed или видит достаточно выходных значений, может вычислить остальные. Для игр и выборок это нормально, а для паролей, токенов сброса, идентификаторов сессий, ключей и лотерейных розыгрышей неправильно. Для них используйте RandomNumberGenerator из System.Security.Cryptography, который читает из защищённого генератора операционной системы:

Пример вывода:

Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF

RandomNumberGenerator.GetInt32 (доступен в .NET Core 3.0 и новее) возвращает несмещённое целое число в диапазоне. Не собирайте защищённые коды через bytes[0] % 10: остаток от деления делает одни цифры вероятнее других. В .NET 6 и новее RandomNumberGenerator.GetBytes(16) сразу возвращает новый массив, а Convert.ToHexString(bytes) форматирует его. Для уникальных идентификаторов, где секретность не важна, проще Guid.NewGuid().

Часто задаваемые вопросы

Как сгенерировать случайное число в C#?

Создайте один объект Random и вызывайте Next: var rng = new Random(); int roll = rng.Next(1, 7); даёт число от 1 до 6. Next(max) даёт от 0 до max - 1, а NextDouble() даёт double от 0.0 включительно до 1.0 не включительно. В .NET 6 и новее можно обойтись без объекта и использовать Random.Shared.Next(1, 7).

Включается ли верхняя граница в Random.Next?

Нет. Next(min, max) возвращает число, большее или равное min и строго меньшее max. Для числа от 1 до 10 включительно пишите Next(1, 11). Благодаря этому Next(0, list.Count) всегда даёт допустимый индекс списка.

Почему new Random() даёт одинаковые числа?

В .NET Framework new Random() получает seed от системных часов, которые меняются лишь раз в несколько миллисекунд, поэтому несколько экземпляров, созданных в быстром цикле, получают одинаковый seed и одинаковую последовательность. .NET Core и .NET 5+ инициализируют каждый экземпляр по-разному, но исправление везде одно: создайте один Random и используйте его повторно.

Как получать одни и те же случайные числа при каждом запуске в C#?

Передайте seed в конструктор: new Random(42). Два экземпляра с одинаковым seed выдают одинаковую последовательность, что делает тесты, симуляции и процедурную генерацию воспроизводимыми. Последовательность для заданного seed может отличаться между версиями .NET, поэтому не храните такой вывод как постоянные данные.

Достаточно ли System.Random для паролей или токенов?

Нет. Random предсказуем: любой, кто узнает его состояние или seed, может воспроизвести числа. Для паролей, токенов, ключей и всего, что связано с безопасностью, используйте System.Security.Cryptography.RandomNumberGenerator, например RandomNumberGenerator.GetInt32(0, 10) или RandomNumberGenerator.GetBytes(32).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ