Генерация случайных чисел в C#
Случайное целое число в C# получают методом Next класса Random: например, Random.Shared.Next(1, 101) вернёт число от 1 до 100. В статье разобраны диапазоны, дробные и длинные числа, повторяющиеся последовательности, потокобезопасность и безопасная генерация для паролей и токенов.

Содержание
- Быстрый способ получить случайное число
- Перегрузки метода Next
- Почему числа повторяются: ошибка с new Random в цикле
- Seed: повторяемая последовательность
- Случайные числа в многопоточной программе
- Уникальные случайные числа без повторов
- Случайный элемент коллекции
- Выбор с разными вероятностями
- Криптографически стойкие числа
- Случайные числа в Unity
- Типичные ошибки
- Частые вопросы
Быстрый способ получить случайное число
Начиная с .NET 6 у класса Random есть готовый общий экземпляр Random.Shared. Его не нужно создавать, он потокобезопасен и подходит для большинства задач: игр, тестовых данных, выбора случайного элемента.
int a = Random.Shared.Next(); // от 0 до int.MaxValue - 1
int b = Random.Shared.Next(10); // от 0 до 9
int c = Random.Shared.Next(1, 101); // от 1 до 100 включительно
double d = Random.Shared.NextDouble(); // от 0.0 до 1.0 (1.0 не входит)
Console.WriteLine($"{a} {b} {c} {d}");
Если проект собирается под .NET Framework или более старую версию, Random.Shared недоступен. Тогда создают один экземпляр new Random() и повторно используют его, как описано ниже.
Перегрузки метода Next
Метод Next имеет три формы. Главное правило одно: нижняя граница входит в диапазон, верхняя нет.
| Вызов | Диапазон результата |
|---|---|
Next() | От 0 до int.MaxValue, не включая максимум |
Next(max) | От 0 до max - 1 |
Next(min, max) | От min до max - 1 |
NextInt64(min, max) | Длинное число от min до max - 1 |
NextDouble() | Дробное от 0 до 1, единица не включается |
NextSingle() | Число типа float от 0 до 1 |
NextBytes(byte[]) | Заполняет массив случайными байтами |
Диапазон с включённой верхней границей
Чтобы получить число от 1 до 10 включительно, верхнюю границу увеличивают на единицу. Для игральной кости это выглядит так:
int dice = Random.Shared.Next(1, 7); // 1, 2, 3, 4, 5 или 6
int percent = Random.Shared.Next(0, 101); // от 0 до 100
Частая ошибка начинающих - написать Next(1, 6) и удивиться, что шестёрка никогда не выпадает. Метод исключает верхнюю границу, поэтому нужен вызов Next(1, 7).
Дробное число в диапазоне
Для дробей диапазон растягивают умножением. Так получают, например, температуру от -10.5 до 35.0 или координату на экране.
static double NextDouble(Random rnd, double min, double max) =>
min + rnd.NextDouble() * (max - min);
double temperature = NextDouble(Random.Shared, -10.5, 35.0);
Console.WriteLine(Math.Round(temperature, 1));
Почему числа повторяются: ошибка с new Random в цикле
Самая известная проблема связана с созданием Random внутри цикла. В .NET Framework генератор инициализировался текущим временем с точностью до миллисекунд, и все экземпляры, созданные в течение одной миллисекунды, выдавали одинаковую последовательность. В .NET Core и новее начальное значение берётся из системного источника, и эта проблема почти ушла, но привычка создавать генератор в цикле остаётся плохой: это лишние выделения памяти.
// плохо: новый генератор на каждой итерации
for (int i = 0; i < 10; i++)
{
var rnd = new Random();
Console.WriteLine(rnd.Next(1, 11));
}
// хорошо: один экземпляр на все вызовы
var shared = Random.Shared;
for (int i = 0; i < 10; i++)
Console.WriteLine(shared.Next(1, 11));
Если вам нужно хранить генератор в поле класса, создайте его один раз как static readonly или используйте Random.Shared. Ту же ошибку часто допускают, когда создают Random внутри метода, который вызывается из цикла.
Seed: повторяемая последовательность
Конструктор принимает начальное значение (seed). Два генератора с одинаковым seed выдают одну и ту же последовательность. Это полезно в тестах, симуляциях и играх, где нужно воспроизвести уровень по номеру.
var r1 = new Random(12345);
var r2 = new Random(12345);
for (int i = 0; i < 3; i++)
Console.WriteLine($"{r1.Next(100)} {r2.Next(100)}"); // пары одинаковы
Важно: при заданном seed алгоритм остаётся стабильным, но между версиями .NET его реализация может меняться, поэтому не стоит хранить такие последовательности как долгосрочные данные. А вот для обычного кода без seed используется другой, более быстрый алгоритм.
Случайные числа в многопоточной программе
Экземпляр new Random() не потокобезопасен. При одновременном доступе из нескольких потоков внутреннее состояние портится, и генератор может начать выдавать нули. Решений три:
- использовать
Random.Shared, который уже защищён от этой проблемы; - завести отдельный генератор на каждый поток через
[ThreadStatic]илиThreadLocal<Random>; - блокировать доступ через
lock, что медленнее.
var results = new int[1000];
Parallel.For(0, results.Length, i =>
{
results[i] = Random.Shared.Next(1, 7);
});
Об общей схеме работы с потоками можно прочитать в материалах блога по многопоточности, а для обычных консольных программ Random.Shared достаточно.
Уникальные случайные числа без повторов
Задача "вытащить 6 разных чисел из 49" возникает в лотереях и розыгрышах. Нельзя просто генерировать числа в цикле и надеяться на удачу, лучше перемешать набор и взять начало. Начиная с .NET 8 для этого есть метод Random.Shuffle.
int[] pool = Enumerable.Range(1, 49).ToArray();
Random.Shared.Shuffle(pool); // .NET 8 и новее
int[] ticket = pool.Take(6).OrderBy(x => x).ToArray();
Console.WriteLine(string.Join(", ", ticket));
В более старых версиях применяют алгоритм Фишера-Йетса или вариант с HashSet, пока набор не достигнет нужного размера. Подробный разбор шаффла есть в статье как перемешать список в C#.
Случайный элемент коллекции
Чтобы выбрать случайный элемент массива или списка, достаточно получить случайный индекс. В .NET 8 появился метод GetItems, который выбирает несколько элементов с повторами.
string[] colors = { "красный", "зелёный", "синий", "жёлтый" };
string one = colors[Random.Shared.Next(colors.Length)];
string[] three = Random.Shared.GetItems(colors, 3); // .NET 8, возможны повторы
Console.WriteLine(one);
Console.WriteLine(string.Join(", ", three));
Выбор с разными вероятностями
Иногда варианты должны выпадать неравномерно: редкий предмет в игре или баннер с повышенным шансом. Для этого суммируют веса и смотрят, в какой отрезок попало случайное число.
static string WeightedPick(Dictionary<string, int> weights)
{
int total = weights.Values.Sum();
int roll = Random.Shared.Next(total);
foreach (var (name, weight) in weights)
{
if (roll < weight) return name;
roll -= weight;
}
throw new InvalidOperationException();
}
var loot = new Dictionary<string, int>
{
["Обычный"] = 70, ["Редкий"] = 25, ["Легендарный"] = 5
};
Console.WriteLine(WeightedPick(loot));
Криптографически стойкие числа
Класс Random предсказуем: зная несколько выданных значений, можно восстановить состояние генератора. Для паролей, токенов сессии, ключей и кодов подтверждения он не подходит. Используйте RandomNumberGenerator из пространства имён System.Security.Cryptography.
using System.Security.Cryptography;
int pin = RandomNumberGenerator.GetInt32(100000, 1000000); // шестизначный код
byte[] key = RandomNumberGenerator.GetBytes(32); // 256-битный ключ
string token = Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
Console.WriteLine($"{pin} {token}");
Метод GetInt32 доступен с .NET Core 3.0 и учитывает смещение распределения, поэтому результат равномерный. Работает такой генератор медленнее, чем Random, но для безопасности это оправданная цена.
| Задача | Что использовать |
|---|---|
| Игры, симуляции, тестовые данные | Random.Shared |
| Воспроизводимая последовательность | new Random(seed) |
| Пароли, токены, коды, ключи | RandomNumberGenerator |
| Перемешивание коллекции | Random.Shuffle (.NET 8) или Фишер-Йетс |
| Многопоточный код | Random.Shared или генератор на поток |
Случайные числа в Unity
В играх на Unity обычно используют собственный класс UnityEngine.Random. У него метод Range(min, max) для целых чисел исключает верхнюю границу, а для дробных, наоборот, включает. Не смешивайте его с System.Random в одном файле без явного указания пространства имён, иначе компилятор сообщит о неоднозначности.
Типичные ошибки
- Создание нового
Randomпри каждом вызове метода. - Ожидание включённой верхней границы в
Next(min, max). - Использование
Randomдля паролей и токенов. - Общий экземпляр
new Random()в нескольких потоках. - Расчёт случайного индекса через
Next() % n: из-за остатка небольшой перекос в вероятностях остаётся, лучше вызыватьNext(n).
Частые вопросы
Как получить случайное число от 1 до 100 в C#?
Вызовите Random.Shared.Next(1, 101). Верхняя граница не входит в диапазон, поэтому указывается 101.
Как получить случайное дробное число?
Используйте NextDouble(), который возвращает число от 0 до 1. Для произвольного диапазона умножьте результат на ширину диапазона и прибавьте минимум.
Почему Random выдаёт одинаковые числа?
Обычно причина в том, что генератор создаётся заново рядом по времени или используется один и тот же seed. Заведите один экземпляр или используйте Random.Shared, и последовательность станет разнообразной.
Можно ли использовать Random для паролей?
Нет. Для паролей, токенов и ключей применяйте RandomNumberGenerator, так как обычный Random можно предсказать.
Как получить случайные числа без повторов?
Создайте массив всех допустимых значений, перемешайте его через Random.Shuffle или алгоритм Фишера-Йетса и возьмите нужное количество первых элементов.
Как выбрать случайный элемент из списка?
Получите случайный индекс методом Next(list.Count) и обратитесь к элементу по индексу. Для нескольких элементов с повторами в .NET 8 есть GetItems.





