在C#中生成随机数
[[academy-video-youtube({"vid": "GFLPncbEi-w", "start_time": "0", "title": "C# 中生成随机数", "creator": "Tim Corey", "length": "10m 05s"})]]
在C#中生成随机数看起来应该是一行代码,而且在许多情况下确实如此。 但语言提供了不止一种生成随机值的方法,一旦考虑到线程安全性、可重复性和用例,它们之间的差异就变得重要了。 选择错误的方法可能会在多线程代码中引入潜在的bug,或者让报告的缺陷无法重现。
在他的视频"在C#中生成随机数"中,Tim Corey详细讲解了经典的Random.Shared作为现代默认值。 我们将涵盖每种方法及其背后的原因,这样您就可以在不猜测的情况下选择正确的。 如果您曾经想知道为什么有多种方法来实现听起来如此简单的事情,这篇文章对此进行了分解。
设置演示
[0:11 - 0:59] Tim在运行于Visual Studio 2026(当前处于预览版)中的控制台应用程序中工作,并使用.NET 10。他指出这里演示的所有内容同样适用于.NET 9和Visual Studio 2022,因此您可以使用已经安装的任何工具来学习。
演示布局是一个for循环,在每次迭代时并排打印两个随机值。 并行运行两个输出使观察两个生成器是否独立运行或产生匹配结果变得更容易,这在种子值引入后变得重要。
经典Random类
[0:59 - 3:28] 生成随机整数在C#中的原始方法包括创建Random类的一个实例:
Random rng1 = new Random();
Random rng2 = new Random();Random rng1 = new Random();
Random rng2 = new Random();每个实例维护自身的内部状态。 调用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给出了一个实际的例子。 想象一下游戏在整个会话中生成随机的事件。 玩家报告了一个bug,但由于结果是随机的,所以难以重现。 如果游戏记录了该会话使用的种子,开发人员可以通过用同一个值初始化一个新的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类上的任何其他方法)并接收一个值,而无需担心对象生命周期或并发。
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就是您所需的一切。
示例提示:调用101。 这种一边界外的界限同样适用于带种子的实例。

