Полное руководство по System.Random | Кодементор

Проверить первоначальная версия этой статьи в моем блоге разработчиков.

Генерировать случайное число должно быть легко, верно? Давным-давно, когда я начал изучать C#, у меня был опыт работы с PHP. PHP rand было легко, как и следовало ожидать. Мне потребовалось некоторое время, чтобы понять, что такое пример под «Случайным», и это все еще сильно сбивает с толку младших, так что вот моя попытка безобидного руководства по генерации случайных чисел в C#.

Я всегда верил в понимание «почему», а не простое копирование «как», но не стесняйтесь переходить к следующему разделу, если вы просто хотите «скопировать код».

Понимание семени

На мгновение забудьте, что вы знаете о System.Random и думать об этом как о IReadOnlyDictionary<int, IEnumerable<int>> в котором для каждого целочисленного ключа существует бесконечная последовательность неотрицательных случайных целочисленных значений. Например:

1 -> 534011718, 237820880, 1002897798, 1657007234, ...
2 -> 1655911537, 867932563, 356479430, 2115372437, ...
3 -> 630327709, 1498044246, 426253993, 1203643911, ...
4 -> 1752227528, 2128155929, 1211126341, 884619196, ...
...

Эти последовательности являются константамикаждый раз, когда вы запускаете программу, вы будете получать одни и те же числа. Как бесконечная последовательность может быть константой? Ну, сама последовательность не постоянна, но она создается математическим путем с заданным аргументом (то, что мы назвали ключом словаря выше). Фактическая используемая функция — это деталь реализации, которая может различаться в зависимости от фреймворка и версии. В текущей статье docs.microsoft.com говорится:

Выбранные числа не являются полностью случайными, поскольку для их выбора используется математический алгоритм, но они достаточно случайны для практических целей. Текущая реализация класса Random основана на модифицированной версии алгоритма вычитания случайных чисел Дональда Кнута. Для получения дополнительной информации см. DE Knuth. Искусство компьютерного программирования, том 2: получисловые алгоритмы. Аддисон-Уэсли, Рединг, Массачусетс, третье издание, 1997 г.

Этот аргумент словарного ключа/функции — это то, что мы называем SEED.

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

Что, если мы просто возьмем одну из этих последовательностей и используем ее в качестве rand()?

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

Хорошо, тогда что?

Каждый раз мы должны случайным образом выбирать, какую последовательность мы будем использовать. Подожди, а мы не пишем генератор случайных чисел? Ну, очевидно, мы не можем использовать свои собственные, поэтому нам нужно получить некоторую переменную из нашей системы, которая постоянно меняется, чтобы мы случайно не получили одну и ту же последовательность несколько раз.

Удивительно трудно найти достаточно случайный переменная в компьютерной системе на самом деле. Есть два основных решения:

  1. Используйте показания нескольких датчиков (температура процессора, вход микрофона, движение мыши, переменные процессора), объедините их и используйте это число. Это действительно безопасный как это непредсказуемо, но чтение всех входных данных медленный. Вот как RNGCryptoServiceProvider работ, что выходит за рамки данной статьи.
  2. Используйте системное время. Это действительно быстро, хотя и несколько предсказуемо, но это не является большой проблемой для большинства случаев использования. Это режим работы по умолчанию для System.Random.

Так что же System.Random делать именно?

Есть два конструктора System.Random. Есть тот, который принимает integer как семя. Он «выбирает» последовательность, принадлежащую этому семени, и каждый последующий вызов Next, NextDoubleи т. д. вернет следующее число в последовательности.

Конструктор без параметров:

public Random()
      : this(Environment.TickCount)
    {}

Подвох в том, что Environment.TickCount изменяется только каждые 10-16 мс, поэтому если вы создадите новый экземпляр за короткий промежуток времени, вы получите ту же последовательность чисел. (См. примеры.)

Ладно, хватит теории. Перейдем к части «что и как».

Основное использование

Делать:

Создать Random экземпляр один раз и использовать его несколько раз.

Random r = new Random();
int a = r.Next();
int b = r.Next();
int c = r.Next();

Не:

new а Random экземпляр и использовать его на месте.

int a = new Random().Next();
int b = new Random().Next();
int c = new Random().Next();

По указанным выше причинам это в 99% случаев приведет к a, b а также c быть одним и тем же числом.

Использование класса (однопоточный)

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

Делать:

class SomeClass{
  
  private readonly Random _rng = new Random();
  
  
  public ReturnType SomeMethod(){
    int randomNumber = _rng.Next();
  }
}

Не

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

Поточно-безопасное использование

Это немного усложняет ситуацию, потому что System.Random не является потокобезопасным.

Конечно, мы можем заблокировать инстанс, но блокировку следует использовать в крайнем случае, так как она может существенно повлиять на производительность, и в этом случае мы можем добиться большего. Совершенно не обязательно использовать один и тот же Random instance в каждом потоке, мы можем сделать по одному для каждого.

  private ThreadLocal<Random> _tlRng = new ThreadLocal<Random>(() => new Random());
  
  
  int randomNumber = _tlRng.Value.Next();

В большинстве случаев этого достаточно, но вы можете подумать: «Подождите, а не существует ли шанс, что несколько потоков инициализируют _tlRng в то же время, что приводит к совпадающим значениям?». Есть. Если вы действительно хотите пройти лишнюю милю, вы можете включить дополнительное значение в начальное значение, которое увеличивается каждый раз при создании экземпляра.

  private int _seedCount = 0;
  private ThreadLocal<Random> _tlRng = new ThreadLocal<Random>(() => new Random(GenerateSeed()));
  
  private static int GenerateSeed(){
    
    return (int) ((DateTime.Now.Ticks << 4) + (Interlocked.Increment(ref _seedCount)));
  }

Это обертывает наш System.Random руководство. Надеюсь, я рассмотрел все варианты использования, если я этого не сделал, или у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне. И, как всегда, разбирайтесь во внутренностях того, что вы пишете, а не просто копируйте и вставляйте код отсюда, из StackOverflow или откуда угодно.

Также проверьте Почему System.Random не следует использовать для критического с точки зрения безопасности кода.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *