Generowanie Losowych Liczb w C#
Generowanie losowych liczb w C# wydaje się powinno być jednowierszowe, i w wielu przypadkach tak jest. Jednak język oferuje więcej niż jeden sposób na generowanie losowych wartości, a różnice między nimi mają znaczenie, gdy uwzględniasz bezpieczeństwo wątków, odtwarzalność i przypadek użycia. Wybranie niewłaściwego podejścia może wprowadzić subtelne błędy w kodzie wielowątkowym lub uczynić zgłoszony defekt niemożliwym do odtworzenia.
W swoim wideo "Generating Random Numbers in C#" Tim Corey omawia klasyczną klasę Random, wyjaśnia, dlaczego istnieją wartości zalążka i przedstawia Random.Shared jako nowoczesny domyślny model. Omówimy każde podejście wraz z uzasadnieniem za nim, abyś mógł wybrać odpowiednie, nie zgadując. Jeśli kiedykolwiek zastanawiałeś się, dlaczego istnieje tyle dróg do czegoś, co wydaje się tak proste, ten artykuł wyjaśnia to.
Konfigurowanie Dema
[0:11 - 0:59] Tim pracuje w aplikacji konsolowej działającej na Visual Studio 2026 (obecnie w wersji podglądowej) z .NET 10. Zaznacza, że wszystko, co tu demonstruje, obejmuje równie dobrze .NET 9 i Visual Studio 2022, więc możesz podążać za tym z dowolnymi narzędziami, które już masz zainstalowane.
Układ demonstracji to pętla for, która drukuje dwie losowe wartości obok siebie w każdej iteracji. Posiadanie dwóch wyników wykonujących się równolegle ułatwia obserwację, czy oba generatory działają niezależnie, czy też zwracają zgodne wyniki, rozróżnienie, które nabiera znaczenia, gdy pojawiają się wartości nasion.
Klasyczna Klasa Random
[0:59 - 3:28] Oryginalne podejście do generowania losowych całkowitych w C# polega na stworzeniu instancji klasy Random:
Random rng1 = new Random();
Random rng2 = new Random();
Random rng1 = new Random();
Random rng2 = new Random();
Każda instancja utrzymuje swój własny wewnętrzny stan. Wywołanie .Next(1, 101) na obu z nich produkuje liczbę całkowitą pomiędzy 1 a 100. Tim podkreśla szczegół, który może zmylić nowicjuszy: minimalna wartość jest włącznie, ale maksymalna jest wyłączona. Jeśli chcemy wartości od 1 do 100, należy użyć 1 i 101, a nie 1 i 100.
int output1 = rng1.Next(1, 101);
int output2 = rng2.Next(1, 101);
int output1 = rng1.Next(1, 101);
int output2 = rng2.Next(1, 101);
Uruchomienie aplikacji potwierdza, że obie instancje produkują różne sekwencje. Ten wynik wydaje się intuicyjny, ale co się stanie, gdy obie instancje dzielą ten sam punkt wyjścia, mówi zupełnie inna historia.
Jedno ważne zastrzeżenie dotyczące tego podejścia: poszczególne instancje Random nie są bezpieczne dla wątków. Jeśli Twoja aplikacja wykonuje przetwarzanie równoległe i wiele wątków uzyskuje dostęp do tej samej instancji, wewnętrzny stan może zostać uszkodzony, generując zera lub powtarzające się wartości. Bezpieczną praktyką jest utworzenie jednej instancji na wątek. To ograniczenie jest jednym z powodów, dla których język wprowadził później lepszą alternatywę.
Wartości Nasion i Odtwarzalne Sekwencje
[3:28 - 6:00] Następnie Tim podaje jawne nasienie do obu konstruktorów:
Random rng1 = new Random(25);
Random rng2 = new Random(25);
Random rng1 = new Random(25);
Random rng2 = new Random(25);
Wynik zmienia się dramatycznie. Oba generatory teraz produkują identyczne sekwencje: 79, 16, 25, 90, 50, 41 i tak dalej. Liczby nadal są nieprzewidywalne indywidualnie, jeśli nie znasz nasienia, ale dany ten sam punkt wyjścia, progresja jest deterministyczna.
Czemu ktoś powinien tego chcieć? Tim daje praktyczny przykład. Wyobraź sobie grę, która generuje losowe zdarzenia w trakcie sesji. Gracz zgłasza błąd, ale odtworzenie go wydaje się niemożliwe, ponieważ wyniki były zrandomizowane. Jeśli gra loguje użyty w tej sesji zalążek, programista może odtworzyć dokładny ciąg decyzji, inicjalizując nową instancję Random z tą samą wartością. Ta sama logika dotyczy scenariuszy testów jednostkowych, gdzie potrzebujesz spójnych wyjść, aby napisać wiarygodne asercje przeciwko zrandomizowanemu zachowaniu.
Instancje z nasieniami dają kontrolowaną losowość: sekwencję wyglądającą nieprzewidywanie, ale mogącą być odtwarzaną na żądanie. Ta zdolność jest powodem, dla którego klasyczny konstruktor Random akceptujący zalążek nie został usunięty, mimo iż istnieje teraz prostsze API.
Random.Shared: Nowoczesny Domyślny
[7:36 - 9:01] Począwszy od .NET 6, zalecane podejście dla większości generacji losowych liczb to Random.Shared:
int output1 = Random.Shared.Next(1, 101);
int output2 = Random.Shared.Next(1, 101);
int output1 = Random.Shared.Next(1, 101);
int output2 = Random.Shared.Next(1, 101);
Nie jest wymagane tworzenie instancji. Random.Shared to statyczna, bezpieczna dla wątków instancja zarządzana przez czas wykonywania. Wywołujesz .Next() (lub którąkolwiek z innych metod klasy Random) i otrzymujesz wartość, nie martwiąc się o czas życia obiektu czy współbieżność.
Tim uruchamia demo dwa razy, aby to udowodnić. Pierwsze wykonanie zaczyna się od 94 i 91; drugi zaczyna się od 42 i 70. W przeciwieństwie do instancji z zalążkiem, Random.Shared korzysta z innego stanu początkowego przy każdym uruchomieniu procesu. Nie możesz ustawić nasienia, co oznacza, że nie możesz wygenerować odtwarzalnej sekwencji za pomocą tego API. To jest kompromis: prostota i bezpieczeństwo w zamian za rezygnację z deterministycznego odtworzenia.
Poza .Next(), Random.Shared udostępnia metody generowania liczb zmiennoprzecinkowych, wypełniania tablic bajtowych i mieszania kolekcji. Dla zdecydowanej większości kodu aplikacji, gdzie potrzebujesz szybkiej losowej wartości bez ceremonii, ta pojedyncza statyczna właściwość zastępuje złożoność zarządzania własnymi instancjami.
Wybór Właściwego Podejścia
[9:01 - 9:30] Tim kończy zwięzłym frameworkiem decyzyjnym. Do codziennej losowości (wybór wartości, mieszanie listy, wybieranie losowego elementu) Random.Shared jest właściwym rozwiązaniem. Nie wymaga żadnej konfiguracji, obsługuje współbieżność i działa poprawnie między wątkami.
Gdy potrzeba powtarzalnej serii wyników, czy to do debugowania, testowania, czy symulacji, należy utworzyć dedykowaną instancję Random z znanym zalążkiem. Pamiętaj, że te instancje nie są bezpieczne do współdzielenia między wątkami.
A dla wszystkiego, co wymaga bezpieczeństwa (tokeny, klucze, sole hasła), żadne z tych podejść nie jest odpowiednie. Tim kieruje widzów do kryptograficznych bibliotek w System.Security.Cryptography, które produkują wartości nie tylko losowe, ale też odporne na przewidywanie.
Podsumowanie: Proste API, Znaczące Różnice
[9:30 - 9:50] To co czyni ten temat mylnym to jak mało kodu potrzeba. Jedna linia może wygenerować losową liczbę za pomocą któregokolwiek z tych podejść. Kompleksowość nie tkwi w składni, ale w zrozumieniu, jakie gwarancje dostarcza każda metoda: bezpieczeństwo wątków, odtwarzalność czy siła kryptograficzna.
Wnioski
[9:50 - 10:05] Podsumowując: Random.Shared pokrywa większość potrzeb bez jakiejkolwiek konfiguracji i z wbudowanym bezpieczeństwem wątkowym. Instancje Random z zalążkiem pozwalają na odtworzenie konkretnej sekwencji, gdy wymaga tego debugowanie lub testowanie. Generator kryptograficzny należy do kodu wrażliwego na bezpieczeństwo, w którym przewidywalność jest wrażliwością, a nie cechą.
Następnym razem, gdy sięgniesz po losową liczbę w C#, decyzja sprowadza się do jednego pytania: czy musisz później odtworzyć tę sekwencję? Jeśli odpowiedź brzmi nie, Random.Shared jest wszystkim, czego potrzebujesz.
Przykładowa wskazówka: Podczas wywoływania Random.Shared.Next(min, max) pamiętaj, że max jest wykluczające. Przedział od 1 do 100 wymaga przekazania 1 i 101. Ta niedokładność jednego dotyczy także instancji z nasieniem.
Obejrzyj pełny film na jego Kanale YouTube i zdobądź więcej wiedzy na temat podstaw C#.
