Klasa System.Random generuje liczby pseudolosowe: deterministyczny ciąg, który wygląda na losowy i zaczyna się od ziarna (seed). Utwórz jedną instancję i wywołuj jej metody:
Przykładowy wynik:
1294270287
83
You rolled a 5
0.6060
Uruchom ponownie, a liczby się zmienią. Górna granica jest wyłączona: Next(1, 7) może zwrócić 1, 2, 3, 4, 5 lub 6, ale nigdy 7. Dla liczby od 1 do 10 włącznie napisz Next(1, 11). Przekazanie max mniejszego niż min rzuca ArgumentOutOfRangeException.
Liczby losowe w zakresie
Next(min, max) obsługuje liczby całkowite. Dla double w zakresie przeskaluj NextDouble(); dla decimal, na przykład ceny, zbuduj wartość z liczb całkowitych, żeby wynik miał pożądaną precyzję:
Przykładowy wynik:
23.6 C
72.42
heads
N
-3
rng.Next(100, 10000) / 100m losuje całkowitą liczbę centów i dzieli przez decimal, więc cena ma zawsze dokładnie dwa miejsca po przecinku. Skalowanie double dałoby wartości w rodzaju 72.4183.
Ziarno: powtarzalne ciągi
Przekazanie ziarna w postaci liczby całkowitej sprawia, że ciąg jest powtarzalny. Dwa obiekty Random z tym samym ziarnem dają dokładnie te same liczby w tej samej kolejności:
Wynik:
16 29 91 99 91
16 29 91 99 91
38
Ziarna przydają się w testach, symulacjach, które trzeba powtórzyć, powtórkach w grach i proceduralnie generowanych poziomach ("ziarno świata 2026"). Zapisuj w logach użyte ziarno, a zaskakujący wynik da się później odtworzyć.
Microsoft nie obiecuje, że ziarno da ten sam ciąg w każdej wersji .NET. Nie zapisuj wyników z ziarnem jako danych (na przykład generując identyfikatory klientów ze stałego ziarna w oczekiwaniu, że po aktualizacji da się je wygenerować ponownie).
Losowanie elementu
Losowy indeks to Next(0, count), który zawsze jest poprawnym indeksem, bo górna granica jest wyłączona:
Wynik:
Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common 695
rare 260
legendary 45
Losowanie z wagami odwzorowuje liczbę od 0 do 99 na przedziały, których rozmiary są wagami: od 0 do 69 to common, od 70 do 94 rare, od 95 do 99 legendary. Przy 1000 losowaniach liczniki lądują w okolicy 700, 250 i 50 (tutaj 695, 260 i 45).
Tasowanie listy: Fisher-Yates
Żeby ułożyć listę w losowej kolejności, przejdź od końca i zamieniaj każdy element z losowym elementem na tej samej lub wcześniejszej pozycji. To tasowanie Fishera-Yatesa; jest szybkie, a każde ułożenie jest równie prawdopodobne. Zapisane jako metoda generyczna działa dla tablic i list o dowolnym typie elementów:
Wynik:
5 A 8 6 4 7 2 3
4 2 1 3 5
Dwa skróty, które zobaczysz w internecie, są gorsze. list.OrderBy(x => rng.Next()) działa, ale sortuje, więc jest wolniejsze, a losowe klucze mogą się powtórzyć. Zamiana z rng.Next(items.Count) zamiast rng.Next(i + 1) wygląda podobnie, ale sprawia, że niektóre ułożenia są bardziej prawdopodobne od innych. W .NET 8 i nowszych Random.Shared.Shuffle(array) poprawnie tasuje tablicę w miejscu, a rng.GetItems(choices, 5) losuje pięć elementów (z powtórzeniami).
Pułapka new Random() w pętli
Klasyczny błąd to tworzenie nowego Random za każdym razem, gdy potrzebna jest liczba:
// Don't do this
int RollDie()
{
var rng = new Random(); // new instance on every call
return rng.Next(1, 7);
}
W .NET Framework new Random() bez ziarna używa zegara systemowego (Environment.TickCount), który zmienia się tylko co 10 do 16 milisekund. Wywołania w szybkiej pętli tworzą instancje z tym samym ziarnem i zwracają tę samą liczbę, więc pętla rzutów kostką wypisuje 4 4 4 4 4. .NET Core i .NET 5+ dają każdej instancji ziarno ze wspólnego losowego źródła, co ukrywa objaw, ale tworzenie obiektu na każdą liczbę nadal jest marnotrawstwem, a kod psuje się po ponownym użyciu w starszych projektach albo w Unity.
Rozwiązaniem jest jedna instancja na klasę, przechowywana w polu:
Wynik:
3 6 6 5 6 1 2 1
Ziarno jest tu tylko po to, żeby przykład za każdym razem wypisywał to samo; w prawdziwej grze napisałbyś new Random().
Wątki i Random.Shared
Random nie jest bezpieczny wątkowo. Jeśli dwa wątki wywołają jednocześnie Next na tej samej instancji, jej stan wewnętrzny może się uszkodzić; znany objaw to instancja, która od tej pory zwraca 0. Możliwości, od najprostszej:
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 to właściwy wybór domyślny w nowym kodzie: jest bezpieczny wątkowo, nie wymaga pola i ma losowe ziarno. Nie da się mu nadać ziarna, więc gdy potrzebujesz powtarzalności, użyj własnego new Random(seed).
Kryptograficznie bezpieczne liczby losowe
System.Random jest z założenia przewidywalny. Każdy, kto zna ziarno albo zobaczy wystarczająco dużo wyników, może obliczyć resztę. To w porządku w grach i przy próbkowaniu, ale źle przy hasłach, tokenach resetu, identyfikatorach sesji, kluczach i losowaniach loterii. Do nich używaj RandomNumberGenerator z System.Security.Cryptography, który czyta z bezpiecznego generatora systemu operacyjnego:
Przykładowy wynik:
Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF
RandomNumberGenerator.GetInt32 (dostępne od .NET Core 3.0) zwraca nieobciążoną liczbę całkowitą z zakresu. Nie buduj bezpiecznych kodów przez bytes[0] % 10: modulo sprawia, że niektóre cyfry są bardziej prawdopodobne od innych. W .NET 6 i nowszych RandomNumberGenerator.GetBytes(16) zwraca od razu nową tablicę, a Convert.ToHexString(bytes) ją formatuje. Dla unikalnych identyfikatorów, przy których tajność nie ma znaczenia, prostszy jest Guid.NewGuid().
Najczęściej zadawane pytania
Jak wylosować liczbę w C#?
Utwórz jeden obiekt Random i wywołaj Next: var rng = new Random(); int roll = rng.Next(1, 7); daje od 1 do 6. Next(max) daje od 0 do max - 1, a NextDouble() daje double od 0.0 do 1.0, bez 1.0. W .NET 6 i nowszych możesz pominąć obiekt i użyć Random.Shared.Next(1, 7).
Czy górna granica Random.Next jest włączona?
Nie. Next(min, max) zwraca liczbę większą lub równą min i ściśle mniejszą od max. Dla liczby od 1 do 10 włącznie napisz Next(1, 11). Dzięki temu Next(0, list.Count) jest zawsze poprawnym indeksem listy.
Dlaczego new Random() daje te same liczby?
W .NET Framework new Random() bierze ziarno z zegara systemowego, który zmienia się tylko co kilka milisekund, więc kilka instancji utworzonych w szybkiej pętli dostaje to samo ziarno i ten sam ciąg. .NET Core i .NET 5+ dają każdej instancji inne ziarno, ale rozwiązanie jest wszędzie takie samo: utwórz jeden Random i używaj go wielokrotnie.
Jak za każdym razem dostać te same liczby losowe w C#?
Przekaż ziarno do konstruktora: new Random(42). Dwie instancje z tym samym ziarnem dają ten sam ciąg, dzięki czemu testy, symulacje i generowanie proceduralne są powtarzalne. Ciąg dla danego ziarna może różnić się między wersjami .NET, więc nie zapisuj wyników z ziarnem jako trwałych danych.
Czy System.Random jest wystarczająco bezpieczny do haseł lub tokenów?
Nie. Random jest przewidywalny: każdy, kto pozna jego stan lub ziarno, może odtworzyć liczby. Do haseł, tokenów, kluczy i wszystkiego, co związane z bezpieczeństwem, używaj System.Security.Cryptography.RandomNumberGenerator, na przykład RandomNumberGenerator.GetInt32(0, 10) albo RandomNumberGenerator.GetBytes(32).