在 C# 中生成隨機數
在C#中生成隨機數似乎應該是單行代碼,而在許多情況下確實是如此。 但是,這種語言提供了多種方法來生成隨機值,當考慮到執行緒安全性、可重現性和使用情況時,它們之間的差異很重要。 選擇錯誤的方法會在多執行緒代碼中引入微妙的錯誤,或者使報告的缺陷無法重現。
在他的影片<"在C#中生成隨機數"中,Tim Corey 步驟講解了經典 Random 類,解釋為什麼種子值存在,並介紹 Random.Shared 作為現代默認值。 我們將介紹每種方法及其背後的原因,讓您在選擇時不必猜測。 如果您曾經想知道為什麼有多種方法來解決聽起來如此簡單的問題,這篇文章將為您分解。
設定演示環境
[0:11 - 0:59] Tim 在 Visual Studio 2026(目前為預覽版)上運行的控制台應用程式中工作,使用 .NET 10。他指出,這裡演示的所有內容同樣適用於 .NET 9 和 Visual Studio 2022,因此您可以使用已安裝的工具跟隨操作。
演示佈局是一個 for 循環,在每次迭代中並排打印兩個隨機值。 並行運行的兩個輸出更容易觀察是否兩個生成器獨立運行或產生匹配結果,當種子值出現時這一區分變得重要。
經典的隨機類別
[0:59 - 3:28] 生成C# 中的隨機整數的原始方法涉及創建 Random 類的實例:
Random rng1 = new Random();
Random rng2 = new Random();
Random rng1 = new Random();
Random rng2 = new Random();
每個實例都維護自己的內部狀態。 在任一上調用 .Next(1, 101) 會產生一個介於 1 至 100 之間的整數。Tim 強調一個讓新手感到困惑的細節:最小值是包含的,但最大值是排除的。如果您想要從 1 到 100 的值,您應當傳遞 1 和 101,而不是 1 和 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);
運行應用程式確認兩個實例產生不同的序列。 該結果直觀,但當兩個實例共享相同的起始點時,結果又不同。
關於這種方法的一個重要警告:個別的 Random 實例不是執行緒安全的。 如果您的應用程式進行並行處理且多個執行緒訪問同一實例,內部狀態可能會損壞,產生零值或重複值。 安全的做法是每個執行緒創建一個實例。 這種限制是該語言後來推出更好替代方案的原因之一。
種子值與可重現的序列
[3:28 - 6:00] 然後 Tim 向兩個構造函數傳遞一個顯式種子:
Random rng1 = new Random(25);
Random rng2 = new Random(25);
Random rng1 = new Random(25);
Random rng2 = new Random(25);
輸出發生劇烈變化。 現在兩個生成器生成相同的序列:79、16、25、90、50、41,等等。 如果您不知道種子,這些數字仍然是不可預測的,但考慮到相同的起始值,進程是確定性的。
為什麼有人會想要這樣? Tim 給出了一個實際例子。 想像一個在整個會話中生成隨機事件的遊戲。 玩家報告一個錯誤,但重現它似乎不可能,因為結果是隨機的。 如果遊戲記錄了該會話使用的種子,開發者可以使用相同的值初始化新的 Random 實例以重現確切的決策鏈。 相同的邏輯適用於單元測試場景,您需要一致的輸出來對隨機行為編寫可靠的斷言。
種子實例給您可控的隨機性:一個看似不可預測但可以按需重播的序列。 這種能力是接受種子的經典 Random 構造函數尚未棄用的原因,儘管現在存在更簡單的API。
Random.Shared:現代默認
[7:36 - 9:01] 從 .NET 6 開始,大多數隨機數生成的推薦方法是 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);
不涉及實例化。 Random.Shared 是由執行時管理的靜態、執行緒安全實例。您調用 .Next()(或 Random 類的其他任何方法),然後接收一個值,而不必擔心對象的生命週期或並發性。
Tim 執行演示兩次以證明該點。 首次執行以 94 和 91 開始; 第二次以 42 和 70 開始。與帶種子的實例不同,Random.Shared 每次啟動過程時都從不同的起始狀態中提取。 您無法設置種子,這意味著您無法通過此 API 生成可重現的序列。 這就是取捨:簡單和安全以交換放棄確定性重播。
除了 Random.Shared 還公開方法來生成雙精度數、填充字節陣列和洗牌集合。 對於大多數應用程式代碼,您需要快速隨機值而不需要繁文縟節,這個單一的靜態屬性取代了管理您自己的實例樣板。
選擇正確的方法
[9:01 - 9:30] Tim 以一個簡明的決策框架結束。 對於日常隨機性(選擇值、洗牌列表、選擇隨機元素),Random.Shared 是正確的選擇。 它不需要設置,處理並發,並在執行緒之間正確運行。
當您需要可重複的輸出系列時,無論是用於調試、測試還是模擬重播,請創建一個具有已知種子的專用 Random 實例。 請記住,這些實例不安全,不能跨執行緒共享。
對於涉及安全性的任何事情(令牌、密鑰、密碼鹽值),兩種方法都不合適。 Tim 指引觀眾到 System.Security.Cryptography 中的加密庫,這些庫生成的值不僅是隨機的,而且對預測具有抗性。
總結:簡單的API,有意義的差異
[9:30 - 9:50] 這個主題具有欺騙性的是所需的代碼很少。 一行代碼即可通過任何這些方法生成隨機數。 複雜性不在於語法,而在於理解每種方法提供的保證:執行緒安全、可重現性或加密強度。
結論
[9:50 - 10:05] 總結:Random.Shared 涵蓋大多數需求,不需要設置,並內置於執行緒安全性中。 帶種子的 Random 實例允許您在調試或測試需要時重現特定序列。 加密生成器屬於安全敏感代碼中,預測性是一個漏洞而不是功能。
下次您在 C# 中尋找隨機數時,決策取決於一個問題:您是否需要稍後重播此序列? 如果答案是否定的,Random.Shared 就是您所需的全部。
示例提示:調用 Random.Shared.Next(min, max) 時,註意到 max 是排除的。1 到 100 的範圍需要傳遞 1 和 101。 這種邊界偏差也適用於帶種子的實例。
