Сформировать случайное число в диапазоне c. Генератор случайных чисел

Пожалуйста, приостановите работу AdBlock на этом сайте.

Иногда может возникнуть необходимость в генерации случайных чисел. Простой пример.

Пример: Определение победителя в конкурсе репостов.

Имеется список из 53 человек. Необходимо выбрать из них победителя. Если вы выберете его самостоятельно, то вас могут обвинить в предвзятости. Поэтому вы решили написать программу. Она будет работать следующим образом. Вы вводите количество участников N , после чего программа выводит одно число – номер победителя.

Как получить число от игрока, вам уже известно. А вот как заставить компьютер загадать случайное число? В этом уроке вы этому научитесь.

Функция rand().

Данная функция возвращает случайное целое число в диапазоне от нуля до RAND_MAX . RAND_MAX это специальная константа языка Си, в которой содержится максимальное целое число, которое может быть возвращено функцией rand() .

Функция rand() определена в заголовочном файле stdlib.h . Поэтому, если хотите использовать rand в своей программе, не забудьте подключить этот заголовочный файл. Константа RAND_MAX тоже определена в этом файле. Вы можете найти этот файл у себя на компьютере и посмотреть её значение.

Давайте посмотрим на эту функцию в действии. Запустим следующий код:

Листинг 1.

#include // чтобы пользоваться функцией printf #include // чтобы пользоваться функцией rand int main(void) { /* генерируем пять случайных целых чисел */ printf("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand()); }

Должно получиться что-то вроде этого.

Рис.1 Пять случайных чисел, сгенерированных функцийе rand

Но нам бы хотелось получить числа от 1 до 53 , а не всё подряд. Ниже описано несколько трюков, позволяющих наложить ограничения на функцию rand() .

Ограничить случайные числа сверху.

Кто в школе ждал момента, когда ему пригодится математика, приготовьтесь. Этот момент наступил. Чтобы ограничить сверху случайные числа, можно воспользоваться операцией получения остатка от деления, которую вы изучили в прошлом уроке. Наверное вы знаете, что остаток от деления на числа K всегда меньше числа K . Например, при делении на 4 могут получиться остатки 0, 1, 2 и 3 . Поэтому если вы хотите ограничить сверху случайные числа числом K , то просто возьмите остаток от деления на K . Вот так:

Листинг 2.

#include #include int main(void) { /* генерируем пять случайных целых чисел меньших 100 */ printf("%d\n", rand()%100); printf("%d\n", rand()%100); printf("%d\n", rand()%100); printf("%d\n", rand()%100); printf("%d\n", rand()%100); }


Рис.2 Пять случайных чисел меньше 100

Ограничить числа снизу.

Функция rand возвращает случайные числа из отрезка . А что если нам нужны только числа большие числа M (например, 1000 )? Как быть? Всё просто. Просто прибавим к тому, что вернула функция rand, наше значение M . Тогда если функция вернёт 0 , итоговый ответ будет M , если 2394 , то итоговый ответ будет M + 2394 . Этим действием мы как бы сдвигаем все числа на M единиц вперёд.

Задать границы функции rand сверху и снизу.

Например, получить числа от 80 до 100 . Кажется, нужно просто объединить два способа, которые приведены выше. Получим что-то вроде этого:

Листинг 3.

#include #include int main(void) { /* генерируем пять случайных целых чисел больших 80 и меньших 100 */ printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand()%100); }

Попробуйте запустить эту программу. Удивлены?

Да, такой способ работать не будет. Давайте прокрутим эту программу руками, чтобы убедиться в том, что мы допустили ошибку. Допустим rand() вернула число 143 . Остаток от деления на 100 равен 43 . Дальше 80 + 43 = 123 . Значит такой способ не работает. Подобная конструкция выдаст числа от 80 до 179 .

Давайте разберём по действиям наше выражение. rand()%100 может выдать числа от 0 до 99 включительно. Т.е. из отрезка .
Операция + 80 сдвигает наш отрезок на 80 единиц вправо. Получаем .
Как видим, проблема у нас заключается в правой границе отрезка, она сдвинута вправо на 79 единиц. Это наше исходное число 80 минус 1 . Давайте наведём порядок и сдвинем правую границу назад: 80 + rand()%(100 - 80 + 1) . Тогда всё должно сработать как надо.

В общем случае если нам нужно получить числа из отрезка , то необходимо воспользоваться следующей конструкцией:
A + rand()%(B-A+1) .

Согласно этой формуле перепишем нашу последнюю программу:

Листинг 4.

#include #include int main(void) { /* генерируем пять случайных целых чисел из отрезка */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); }

Результат работы:


Рис.3 Случайные числа из диапазона

Ну вот, теперь вы можете решить исходную задачу урока. Сгенерировать число из отрезка . Или не можете?

Но прежде ещё немного полезной информации. Запустите последнюю программу три раза подряд и записывайте себе случайные числа, которые она генерирует. Заметили?

Функция srand().

Да, каждый раз появляются одни и те же одинаковые числа. «Так себе генератор!» – скажете вы. И будете не совсем правы. Действительно, генерируются всё время одинаковые числа. Но мы можем на это повлиять, для этого используется функция srand() , которая также определена в заголовочном файле stdlib.h . Она инициализирует генератор случайных чисел начальным числом.

Скомпилируйте и запустите несколько раз вот эту программу:

Листинг 5.

#include #include int main(void) { srand(2); /* генерируем пять случайных целых чисел из отрезка */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); }

Теперь поменяйте аргумент функции srand() на другое число (надеюсь вы ещё не забыли, что такое аргумент функции?) и снова скомпилируйте и запустите программу. Последовательность чисел должна измениться. Как только мы меняем аргумент в функции srand – меняется и последовательность. Не очень практично, не правда ли? Чтобы изменить последовательность, нужно перекомпилировать программу. Вот бы это число туда подставлялось автоматически.

И это можно сделать. Например, воспользуемся функцией time() , которая определена в заголовочном файле time.h . Данная функция, если ей в качестве аргумента передать NULL , возвращает количество секунд, прошедших c 1 января 1970 года . Вот посмотрите, как это делается.

Листинг 6.

#include #include #include // чтобы использовать функцию time() int main(void) { srand(time(NULL)); /* генерируем пять случайных целых чисел из отрезка */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); }

Вы спросите, а что такое NULL ? Резонный вопрос. А я вам пока отвечу, что это специальное зарезервированное слово такое. Могу ещё сказать, что им обозначает нулевой указатель, но т.к. это для вас никакой информации не несёт, то на данный момент рекомендую об этом не думать. А просто запомнить как некоторый хитрый трюк. В будущих уроках мы остановимся на этой штуке поподробнее.

В языках программирования обычно предусмотрены функции, позволяющие генерировать случайные числа в определенном по умолчанию диапазоне. На самом деле генерируются не случайные, а так называемые псевдослучайные числа; они выглядят случайно, но вычисляются по вполне конкретной формуле. Но для простоты далее мы все равно будем называть их случайными.

В языке программирования C получить случайное число можно с помощью функции rand() , которая входит в стандартную библиотеку языка. Эта функция не принимает никакие параметры.

Задание
Напишите программу, в которой целочисленной переменной присваивается результат выполнения функции rand() . Выведите значение переменной на экран.

Функция rand() возвращает целое число от 0 до значения присвоенного константе RAND_MAX. Значение RAND_MAX зависит от системы и определено в заголовочном файле stdlib.h. Так, например, оно может быть равно 32767 (двухбайтовое целое) или 2147483647 (четырехбайтовое целое).

Задание
Определите значение RAND_MAX в вашей системе. Для этого не забудьте подключить к файлу исходного кода заголовочный файл stdlib.h.

Код ниже выводит на экран 50 случайных чисел:

#include #include main () { char i; for (i = 1 ; i <= 50 ; i++ ) { printf ("%15d" , rand () ) ; if (i % 5 == 0 ) printf ("\n " ) ; } }

В теле цикла осуществляется переход на новую строку после каждых выведенных на экран пяти чисел. Для этого используется выражение, в котором находится остаток от деления i на 5, результат сравнивается с 0. Чтобы после первого числа не происходил переход на новую строку, iсначала присваивается единица, а не ноль (т.к. 0 делится на 5 без остатка).

Задание
Спишите код, приведенный выше. Выполните программу несколько раз, при этом обратите внимание, разные ли результаты вы получаете от выполнения к выполнению.

Вы должны были заметить, что при каждом запуске программы числа остаются одинаковыми. Даже если вы перекомпилируете программу, результат не изменится. Данный эффект связан с тем, что начальное (инициализирующее) число, которое подставляется в формулу вычисления первого и последующих псевдослучайных чисел, для каждой системы всегда одно и то же. Однако это начальное число можно изменить с помощью функции srand() , которой в качестве параметра передается любое целое число. Понятно, что если вы зададите конкретный аргумент для функции, например, srand(1000) , то от вызова к вызову программы числа будут также одни и те же. Хотя и не те, что были бы без srand() . Поэтому появляется проблема, как сделать так, чтобы аргумент для srand() был тоже случайным? Получается замкнутый круг.

Задание
Переделайте программу, выводящую на экран 50 случайных чисел так, чтобы сначала у пользователя запрашивалось любое целое число с помощью scanf() , которое передавалось бы в функцию srand() .

Пользователь программы сам может задавать инициализирующее значение. Но чаще всего это не является полноценным выходом из ситуации. Поэтому инициализирующее значение привязывают к какому-либо процессу, протекающему в операционной системе, например, к часам. Время (учитывая не только время суток, но и дату) никогда не бывает одинаковым. Значит значение для srand() , преобразованное в целое из системного времени, будет различным.

Текущее время можно узнать с помощью функции time() , прототип которой описан в файле time.h. Передав time() в качестве параметра NULL, мы получим целое число, которое можно передать в srand() :
srand(time(NULL));

Задание
Переделайте вашу программу так, чтобы инициализирующее значение зависело от системного времени.

Получение целых случайных чисел в заданных диапазонах

Функция rand() выдает случайное число от 0 до значения RAND_MAX. Что делать, если требуется получать случайные числа в иных диапазонах, например, от 100 до 999?

Сначала рассмотрим более простую ситуацию: получить случайные числа от 0 до 5. Если любое целое число попытаться разделить на 5 нацело, то можно получить как 0 (когда число делится на 5 без остатка), так и 1, 2, 3, 4. Например, rand() вернула число 283. Применяя к этому числу операцию нахождения остатка от деления на 5, получим 3. Т.е. выражение rand() % 5 дает любое число в диапазоне ? Логично предположить, что следует найти остаток от деления на 6. При этом более грамотным будет следующее рассуждение: надо находить остаток от деления на размер диапазона. В данном случае он равен шести значениям: 0, 1, 2, 3, 4, 5. Чтобы найти размер диапазона надо из допустимого максимума вычесть допустимый минимум и прибавить единицу: max - min + 1. Будьте внимательны: если, например, требуется, чтобы указанный в задаче максимум не входил в диапазон, то единицу прибавлять не надо или надо вычитать единицу из максимума.

Задание
Напишите программу, выдающую 50 случайных чисел от 0 до 99 включительно.

Итак, мы знаем формулу получения длины диапазона: max - min + 1. Если требуется получить число от 6 до 10 включительно, то длина диапазона будет равна 10 - 6 + 1 = 5. Выражение rand()% 5 даст любое число от 0 до 4 включительно. Но нам надо от 6 до 10. В таком случае достаточно к полученному случайному остатку прибавить 6, т.е. минимум. Другими словами, надо выполнить сдвиг. Действительно для приведенного примера:

  • если остаток был равен 0, то добавляя 6, получаем 6;
  • остаток 1, добавляем 6, получаем 7;
  • остаток 4, прибавляем 6, получаем 10;
  • остатка больше 4 не может быть.

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

Rand() % длина_диапазона + сдвиг

где длина_диапазона вычисляется как b - a + 1, сдвиг является значением a.

В эту формулу также вписываются случаи, когда необходимо получить случайное число от 0 до N, т.е. они являются ее частными случаями.

Задание
Выведите на экран ряд случайных чисел, принадлежащих диапазону от 100 до 299 включительно.

С таким же успехом можно получать случайные отрицательные числа. Действительно, если диапазон задан как [-35, -1], то его длина будет равна -1 - (-35) + 1 = 35, что соответствует действительности; выражение получения случайного числа будет выглядеть так:

rand() % 35 - 35

Так, если остаток от деления составил 0, то мы получим -35, а если 34, то -1. Остальные остатки дадут значения в промежутке от -35 до -1.

Задание
Выведите на экран ряд случайных чисел, принадлежащих диапазону от -128 до 127 включительно.

Получение вещественных случайных чисел

Ситуация с вещественными числами выглядит несколько по-иному. Во-первых, мы не можем получить остаток от деления, если делимое или делитель дробное число. Во вторых при вычислении длины диапазона нельзя прибавлять единицу.

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

Если разделить случайное число, преобразованное к вещественному типу, которое выдала функция rand() , на значение константы RAND_MAX, то получится вещественное случайное число от 0 до 1. Теперь, если это число умножить на длину диапазона, то получится число, лежащее в диапазоне от 0 до значения длины диапазона. Далее если прибавить к нему смещение к минимальной границе, то число благополучно впишется в требуемый диапазон. Таким образом формула для получения случайного вещественного числа выглядит так:

(float) rand() / RAND_MAX * (max - min) + min

Задание
Заполните массив случайными числами в диапазоне от 0.51 до 1.00. Выведите значение элементов массива на экран.

Равновероятные случайные числа

Функция rand() генерирует любое случайное число от 0 до RAND_MAX с равной долей вероятности. Другими словами, у числа 100 есть такой же шанс выпасть, как и у числа 25876.

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

#include #include #define N 500 main () { int i; int arr[ 5 ] = { 0 } ; srand (time (NULL) ) ; for (i= 0 ; i < N; i++ ) switch (rand () % 5 ) { case 0 : arr[ 0 ] ++; break ; case 1 : arr[ 1 ] ++; break ; case 2 : arr[ 2 ] ++; break ; case 3 : arr[ 3 ] ++; break ; case 4 : arr[ 4 ] ++; break ; } for (i= 0 ; i < 5 ; i++ ) printf ("%d - %.2f%%\n " , i, ((float ) arr[ i] / N) * 100 ) ; }

В приведенной программе массив из пяти элементов сначала заполняется нулями. Случайные числа генерируются от 0 до 4 включительно. Если выпадает число 0, то увеличивается значение первого элемента массива, если число 1, то второго, и т.д. В конце на экран выводится процент выпадения каждого из чисел.

Очень часто в программах возникает необходимость использования случайных чисел - от заполнения массива до криптографии. Для получения последовательности случайных чисел в языке C# имеется класс Random . Этот класс предусматривает два конструктора:

  • Random () - инициализирует экземпляр класса Random с помощью начального значения, зависящего от текущего времени. Как известно, время может быть представлено в тиках - 100-наносекундных импульсах, начиная с 1 января 0001 года. И значение времени в тиках представляет собой 64-битное целое число, которое и будет использоваться для инициализации экземпляра генератора случайных чисел.
  • Random (Int32 ) - инициализирует экземпляр класса Random с помощью указанного начального значения. Такая инициализация генератора случайных чисел может быть удобна на этапе отладки программы, поскольку в этом случае при каждом запуске программы будут генерироваться одни и те же "случайные" числа.
Основным методом данного класса является метод Next() , позволяющий получить случайное число и имеющий ряд перегрузок:
  • Next() - возвращает случайное целое неотрицательное число формата Int32 .
  • Next(Int32 ) - возвращает случайное целое неотрицательное число, которое меньше указанного значения.
  • Next(Int32 min, Int32 max) - возвращает случайное целое число в указанном диапазоне. При этом должно соблюдаться условие min
А также методы
  • NextBytes(Byte ) - заполняет элементы указанного массива байтов случайными числами.
  • NextDouble() - возвращает случайное число с плавающей запятой, в диапазоне )
    break ; // совпадение найдено, элемент не подходит
    }
    if (j == i)
    { // совпадение не найдено
    a[i] = num; // сохраняем элемент
    i++; // переходим к следующему элементу
    }
    }
    for (int i = 0; i < 100; i++)
    {

    if (i % 10 == 9)
    Console .WriteLine();
    }
    Console .ReadKey();
    }
    }
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    using System;
    namespace MyProgram
    {
    class Program
    {
    static void Main(string args)
    {
    Random rnd = new Random ();
    int a = new int ; // массив элементов
    int count = new int ; // массив количества генераций
    a = rnd.Next(0, 101);
    int c = 0; // счетчик количества генераций
    count = 1; // a генерируется только 1 раз
    for (int i = 1; i < 100;)
    {
    int num = rnd.Next(0, 101);
    c++; // сгенерировали элемент еще один раз
    int j;
    for (j = 0; j < i; j++)
    {
    if (num == a[j])
    break ;
    }
    if (j == i)
    {
    a[i] = num; i++;
    count[i] = c; c = 0; // сохраняем количество генераций
    }
    }
    // Вывод значений элементов
    Console .WriteLine("Значения элементов" );
    for (int i = 0; i < 100; i++)
    {
    Console .Write("{0,4} " , a[i]);
    if (i % 10 == 9)
    Console .WriteLine();
    }
    Console .WriteLine();
    // Вывод количества генераций
    Console .WriteLine("Количество генераций элементов" );
    int sum = 0;
    for (int i = 0; i < 100; i++)
    {
    sum += count[i];
    Console .Write("{0,4} " , count[i]);
    if (i % 10 == 9)
    Console .WriteLine();
    }
    Console .WriteLine("Общее количество генераций - {0}" , sum);
    Console .ReadKey();
    }
    }
    }

    void Main(string args)
    {
    Random rnd = new Random ();
    int a = new int ;
    for (int i = 0; i < 100; i++)
    a[i] = i;
    for (int i = 0; i < 50; i++)
    {
    int i1 = rnd.Next(0, 100); // первый индекс
    int i2 = rnd.Next(0, 100); // второй индекс
    // обмен значений элементов с индексами i1 и i2
    int temp = a;
    a = a;
    a = temp;
    }
    Console .WriteLine("Значения элементов" );
    for (int i = 0; i < 100; i++)
    {
    Console .Write("{0,4} " , a[i]);
    if (i % 10 == 9)
    Console .WriteLine();
    }
    Console .ReadKey();
    }
    }
    }

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

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

    На помощь нам приходит функция стандартной библиотеки языка Си (не C++) rand() .

    int rand (void);

    Она генерирует псевдослучайное целое число на интервале значений от 0 до RAND_MAX . Последнее является константой, которая варьируется в зависимости от реализации языка, но в большинстве случаев составляет 32767.
    А что если нам нужны случайные числа от 0 до 9? Типичный выход из ситуации — использование операции деления по модулю.

    Если нам нужны числа от 1 (а не от 0) до 9, то можно прибавить единичку…

    Идея такая: генерируем случайное число от 0 до 8, и после прибавления 1 оно превращается в случайное число от 1 до 9.

    И последнее, самое печальное.
    К сожалению, функция rand() генерирует псевдослучайные числа, т.е. числа, которые кажутся случайными, но на самом деле являются последовательностью значений, вычисленных по хитрому алгоритму, в качестве параметра принимающему так называемое зерно (seed). Т.е. сгенерированные функцией rand() числа будут зависеть от значения, которое имеет зерно в момент ее вызова. А зерно всегда устанавливается компилятором в значение 1. Иными словами, последовательность чисел будет хоть и псевдослучайной, но всегда одинаковой.
    А это не то, что нам надо.

    Исправить ситуацию помогает функция srand() .

    void srand (unsigned int seed);

    Она устанавливает зерно равным значению параметра, с которым была вызвана. И последовательность чисел тоже будет другая.

    Но проблема осталась. Как сделать случайным зерно, ведь от него все зависит?
    Типичный выходит из ситуации — использование функции time() .

    time_t time (time_t* timer);

    Она тоже досталась в наследство от языка Си и, будучи вызвана с нулевым указателем в качестве параметра, возвращает количество секунд, прошедших с 1 января 1970 года. Нет, это не шутка.

    Теперь значение этой функции мы можем передать в функцию srand() (при этом выполняется неявное приведение типа), и у нас будет замечательное случайное зерно.
    И числа будут замечательные и неповторяющиеся.

    Для использования функций rand() и srand() нужно подключить заголовочный файл , а для использования time() — файл .

    Вот полноценный пример.

    #include
    #include
    #include

    using namespace std;

    int main()
    {
    cout << "10 random numbers (1..100): " << endl;
    srand(time(NULL));
    for(int i=0;i<10;i++) cout << rand() % 100 + 1 << " ";
    cin.get();
    return 0;
    }

    Перевод статьи Random numbers широко известного в узких кругах Джона Скита. Остановился на этой статье, так как в своё время сам столкнулся с описываемой в ней проблемой.

    Просматривая темы по .NET и C# на сайте StackOverflow, можно увидеть бесчисленное множество вопросов с упоминанием слова «random», в которых, по сути, поднимается один и тот же извечный и «неубиваемый» вопрос: почему генератор случайных чисел System.Random «не работает» и как это «исправить». Данная статья посвящена рассмотрению данной проблемы и способов её решения.

    Постановка проблемы

    На StackOverflow, в новостных группах и рассылках все вопросы по теме «random» звучат примерно так:
    Я использую Random.Next для генерации нескольких случайных чисел, но метод возвращает одно и то же число при его множественных вызовах. Число меняется при каждом запуске приложения, однако в рамках одного выполнения программы оно постоянное.

    В качестве примера кода приводится примерно следующее:
    // Плохой код! Не использовать! for (int i = 0; i < 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
    Итак, что здесь неправильно?

    Объяснение

    Класс Random не является истинным генератором случайных чисел, он содержит генератор псевдо случайных чисел. Каждый экземпляр класса Random содержит некоторое внутреннее состояние, и при вызове метода Next (или NextDouble , или NextBytes) метод использует это состояние для возврата числа, которое будет казаться случайным. После этого внутреннее состояние меняется таким образом, чтобы при следующем вызове метода Next он возвратил другое кажущееся-случайным число, отличное от возвращённого ранее.

    Все «внутренности» работы класса Random полностью детерминистичны . Это значит, что если вы возьмёте несколько экземпляров класса Random с одинаковым начальным состоянием, которое задаётся через параметр конструктора seed , и для каждого экземпляра вызовите определённые методы в одинаковом порядке и с одинаковыми параметрами, то в конце вы получите одинаковые результаты.

    Так что ж плохого в вышеприведённом коде? Плохо то, что мы используем новый экземпляр класса Random внутри каждой итерации цикла. Конструктор Random, не принимающий параметров , принимает значение текущей даты и времени как seed (начальное состояние). Итерации в цикле «прокрутятся» настолько быстро, что системное время «не успеет измениться» по их окончании; таким образом, все экземпляры Random получат в качестве начального состояния одинаковое значение и поэтому возвратят одинаковое псевдослучайное число.

    Как это исправить?

    Есть немало решений проблемы, каждое со своими плюсами и минусами. Мы рассмотрим несколько из них.
    Использование криптографического генератора случайных чисел
    .NET содержит абстрактный класс RandomNumberGenerator , от которого должны наследоваться все реализации криптографических генераторов случайных чисел (далее - криптоГСЧ). Одну из таких реализаций содержит и.NET - встречайте класс RNGCryptoServiceProvider . Идея криптоГСЧ в том, что даже если он всё так же является генератором псевдослучайных чисел, он обеспечивает достаточно сильную непредсказуемость результатов. RNGCryptoServiceProvider использует несколько источников энтропии, которые фактически являются «шумами» в вашем компьютере, и генерируемую им последовательность чисел очень тяжело предсказать. Более того, «внутрикомпьютерный» шум может использоваться не только в качестве начального состояния, но и между вызовами следующих случайных чисел; таким образом, даже зная текущее состояние класса, этого не хватит для вычисления как следующих чисел, которые будут сгенерированы в будущем, так и тех, которые были сгенерированы ранее. Вообще-то точное поведение зависит от реализации. Помимо этого, Windows может использовать специализированное аппаратное обеспечение, являющееся источником «истинных случайностей» (например, это может быть датчик распада радиоактивного изотопа) для генерации ещё более защищённых и надёжных случайных чисел.

    Сравним это с ранее рассматриваемым классом Random. Предположим, вы вызвали Random.Next(100) десять раз и сохранили результаты. Если вы имеете достаточно вычислительной мощи, то можете сугубо на основании этих результатов вычислить начальное состояние (seed), с которым был создан экземпляр Random, предсказать следующие результаты вызова Random.Next(100) и даже вычислить результаты предыдущих вызовов метода. Такое поведение катастрофически неприемлемо, если вы используете случайные числа для обеспечения безопасности, в финансовых целях и т.д. КриптоГСЧ работают существенно медленнее класса Random, но генерируют последовательность чисел, каждое из которых более независимо и непредсказуемо от значений остальных.

    В большинстве случаев более низкая производительность не является препятствием - им является плохой API. RandomNumberGenerator создан для генерации последовательностей байтов - и всё. Сравните это с методами класса Random, где есть возможность получения случайного целого числа, дробного числа, а также набора байтов. Ещё одно полезное свойство - возможность получения случайного числа в указанном диапазоне. Сравните эти возможности с массивом случайных байтов, который выдаёт RandomNumberGenerator. Исправить ситуацию можно, создав свою оболочку (враппер) вокруг RandomNumberGenerator, которая будет преобразовывать случайные байты в «удобный» результат, однако это решение нетривиально.

    Тем не менее, в большинстве случаев «слабость» класса Random вполне подходит, если вы сможете решить проблему, описанную в начале статьи. Посмотрим, что здесь можно сделать.

    Используйте один экземпляр класса Random при множественных вызовах
    Вот он, корень решения проблемы - использовать лишь один экземпляр Random при создании множества случайных чисел посредством Random.Next. И это очень просто - посмотрите, как можно изменить вышеприведённый код:
    // Этот код будет получше Random rng = new Random(); for (int i = 0; i < 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
    Теперь в каждой итерации будут разные числа, … но это ещё не всё. Что будет, если мы вызовем этот блок кода два раза подряд? Правильно, мы создадим два экземпляра Random с одинаковым начальным значением и получим два одинаковых набора случайных чисел. В каждом наборе числа будут различаться, однако между собой эти наборы будут равны.

    Есть два способа решения проблемы. Во-первых, мы можем использовать не экземплярное, а статическое поле, содержащее экземпляр Random, и тогда вышеприведённый кусок кода создаст лишь один экземпляр, и будет его использовать, вызываясь сколько угодно раз. Во-вторых, мы можем вообще убрать оттуда создание экземпляра Random, переместив его «повыше», в идеале - на самый «верх» программы, где будет создан единичный экземпляр Random, после чего он будет передаваться во все места, где нужны случайные числа. Это отличная идея, которая красиво выражается зависимостями, но она будет работать до тех пор, пока мы используем лишь один поток.

    Потокобезопасность

    Класс Random не потокобезопасен. Учитывая то, как мы любим создавать один экземпляр и использовать его по всей программе на протяжении всего времени её выполнения (синглтон, привет!), отсутствие потокобезопасности становится реальной занозой. Ведь если мы используем один экземпляр одновременно в нескольких потоках, то есть вероятность обнуления его внутреннего состояния, и если это произойдёт, то с этого момента экземпляр станет бесполезным.

    Снова-таки, здесь есть два пути решения проблемы. Первый путь по-прежнему предполагает использование одного экземпляра, однако на этот раз с использованием блокировки ресурса посредством монитора. Для этого необходимо создать оболочку вокруг Random, которая будет оборачивать вызов его методов в оператор lock , обеспечивающий эксклюзивный доступ к экземпляру для вызывающей стороны. Этот путь плох тем, что снижает производительность в многопоточно-интенсивных сценариях.

    Другой путь, который я опишу ниже - использование по одному экземпляру на каждый поток. Единственное, нам нужно удостовериться, что при создании экземпляров мы используем разные начальные значения (seed), а потому мы не можем использовать конструкторы по умолчанию. Во всём остальном этот путь относительно прямолинеен.

    Безопасный провайдер

    К счастью, новый обобщённый класс ThreadLocal , появившийся в.NET 4, очень сильно облегчает написание провайдеров, обеспечивающих по одному экземпляру на поток. Просто нужно в конструктор ThreadLocal передать делегат, который будет ссылаться на получение значения собственно нашего экземпляра. В данном случае я решил использовать единственное начальное значение (seed), инициализируя его при помощи Environment.TickCount (именно так действует конструктор Random без параметров). Далее полученное количество тиков инкрементируется каждый раз, когда нам нужно получить новый экземпляр Random для отдельного потока.

    Нижепредставленный класс полностью статический и содержит лишь один публичный (открытый) метод GetThreadRandom. Этот метод сделан методом, а не свойством, в основном из-за удобства: благодаря этому все классы, которым нужен экземпляр Random, будут зависеть от Func (делегат, указывающий на метод, не принимающий параметров и возвращающий значение типа Random), а не от самого класса Random. Если тип предназначен для работы в одном потоке, он может вызвать делегат для получения единого экземпляра Random и после чего использовать его повсюду; если же тип должен работать в многопоточных сценариях, он может вызывать делегат каждый раз, когда ему требуется генератор случайных чисел. Нижеприведенный класс создаст столько экземпляров класса Random, сколько есть потоков, и каждый экземпляр будет стартовать с различного начального значения. Если нам нужно использовать провайдер случайных чисел как зависимость в других типах, мы можем сделать так: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . Ну а вот и сам код:
    using System; using System.Threading; public static class RandomProvider { private static int seed = Environment.TickCount; private static ThreadLocal randomWrapper = new ThreadLocal(() => new Random(Interlocked.Increment(ref seed))); public static Random GetThreadRandom() { return randomWrapper.Value; } }
    Достаточно просто, не правда ли? Всё потому, что весь код направлен на выдачу правильного экземпляра Random. После того, как экземпляр создан и возвращён, совершенно неважно, что вы будете с ним делать дальше: все дальнейшие выдачи экземпляров совершенно не зависят от текущего. Конечно, клиентский код имеет лазейку для злонамеренного неправильного использования: он может получить один экземпляр Random и передать его в другие потоки вместо вызова в тех, других потоках, нашего RandomProvider.

    Проблемы с дизайном интерфейса

    Одна проблема всё равно остаётся: мы используем слабо защищённый генератор случайных чисел. Как упоминается ранее, существует намного более безопасная во всех отношениях версия ГСЧ в RandomNumberGenerator, реализация которого находится в классе RNGCryptoServiceProvider. Однако его API достаточно сложно использовать в стандартных сценариях.

    Было бы очень приятно, если бы провайдеры ГСЧ в фреймворке имели отдельные «источники случайности». В таком случае мы могли бы иметь единый простой и удобный API, который бы поддерживался как небезопасной-но-быстрой реализацией, так и безопасной-но-медленной. Что-ж, мечтать не вредно. Возможно, подобный функционал появится в следующих версиях.NET Framework. Возможно, кто-то не из Microsoft предложит свою реализацию адаптера. (К сожалению, я не буду этим кем-то… правильная реализация подобной задумки удивительно сложна.) Вы также можете создать свой класс, отнаследовав его от Random и переопределив методы Sample и NextBytes, однако неясно, как именно они должны работать, и даже собственная реализация Sample может быть намного сложнее, нежели кажется. Может быть, в следующий раз…