Menu

Losowanie liczb w C#: Random.Next, zakresy, ziarno, tasowanie i bezpieczne liczby

Jak losować liczby w C# za pomocą klasy Random: Next i jej wyłączna górna granica, losowe liczby double w zakresie, ziarno dla powtarzalnych wyników, losowanie elementu, tasowanie listy, pułapka new Random w pętli i kryptograficznie bezpieczne liczby.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ