在C#中生成随机数
在C#中生成随机数看起来应该是一行代码,而且在许多情况下确实如此。 但语言提供了不止一种生成随机值的方法,一旦考虑到线程安全性、可重复性和用例,它们之间的差异就变得重要了。 选择错误的方法可能会在多线程代码中引入潜在的bug,或者让报告的缺陷无法重现。
在他的影片 "Generating Random Numbers in C#" 中,Tim Corey 展示了经典的 Random 类,解释了为什么存在种子值,并介绍了 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();每个实例维护自身的内部状态。 调用 .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给出了一个实际的例子。 想象一下游戏在整个会话中生成随机的事件。 玩家报告了一个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.Shared 是由运行时管理的静态线程安全实例。您可以调用 .Next()(或 Random 类上的任何其他方法),无需担心对象生命周期或并发即可获得值。
Tim运行了两次演示以证明这一点。 第一次执行从94和91开始; 第二次以42和70开始。与具有种子的实例不同,Random.Shared 每次启动进程时都从不同的起始状态开始。 您不能设置种子,这意味着您无法通过此API产生可重复的序列。 这是一种权衡:以简洁性和安全性换取放弃确定性的重放。
除了 Random.Shared 还提供生成 double 值、填充字节数组和洗牌集合的方法。 对于大多数应用代码中您只需快速获得一个随机值而无需繁琐步骤的场合,这个单一的静态属性取代了您自己管理实例的样板代码。
选择正确的方法
[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。 这种一边界外的界限同样适用于带种子的实例。

