Menu

C# 随机数:Random.Next、范围、种子、洗牌与安全随机数

如何在 C# 中用 Random 类生成随机数:Next 与不包含的上限,指定范围内的随机 double,用种子得到可复现的结果,随机挑选元素,打乱列表,在循环里 new Random 的陷阱,以及密码学安全的随机数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

System.Random 类生成的是伪随机数:从一个种子出发、看起来随机的确定性序列。创建一个实例,然后调用它的方法:

示例输出:

1294270287
83
You rolled a 5
0.6060

再运行一次,数字就会变化。上限是不包含在内的:Next(1, 7) 可能返回 1、2、3、4、5 或 6,但永远不会返回 7。要得到 1 到 10(含 10)之间的数,应写 Next(1, 11)。如果传入的 max 小于 min,会抛出 ArgumentOutOfRangeException。

指定范围内的随机数

Next(min, max) 负责整数。要得到某个范围内的 double,可以对 NextDouble() 做缩放;对于价格这样的 decimal,则用整数来构造,这样结果就能具有你想要的精度:

示例输出:

23.6 C
72.42
heads
N
-3

rng.Next(100, 10000) / 100m 先随机选出一个以“分”为单位的整数,再除以一个 decimal,所以价格总是正好两位小数。如果对 double 做缩放,会得到 72.4183 这样的值。

种子:可复现的序列

传入一个整数种子,序列就可以重复。种子相同的两个 Random 对象会以相同的顺序生成完全相同的数:

输出:

16 29 91 99 91
16 29 91 99 91
38

需要测试、需要重新运行的模拟、游戏回放,以及程序化生成的关卡(“世界种子 2026”)时,用的就是种子。把你用过的种子记录下来,出现意外结果时就能在之后复现它。

Microsoft 并不保证同一个种子在每个 .NET 版本上都生成相同的序列。不要把用种子生成的结果当作数据保存(例如用固定种子生成客户 ID,并指望升级之后还能重新生成它们)。

随机挑选一个元素

随机索引就是 Next(0, count)。因为上限不包含在内,它总是一个合法的索引:

输出:

Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common     695
rare       260
legendary  45

加权抽取把 0 到 99 之间的数映射到若干个区间上,每个区间的大小就是权重:0 到 69 是普通,70 到 94 是稀有,95 到 99 是传说。抽取 1,000 次后,各自的次数会接近 700、250 和 50(这里是 695、260 和 45)。

打乱列表:Fisher-Yates 洗牌

要把列表打乱成随机顺序,就从末尾往前遍历,把每个元素和它本身或它之前的某个随机元素交换。这就是 Fisher-Yates 洗牌算法:速度快,而且每种排列出现的概率都相同。把它写成泛型方法,就能用于任意元素类型的数组和列表:

输出:

5 A 8 6 4 7 2 3
4 2 1 3 5

网上常见的两种捷径都更差。list.OrderBy(x => rng.Next()) 虽然能用,但它要排序,所以更慢,而且随机键可能会重复。用 rng.Next(items.Count) 代替 rng.Next(i + 1) 来交换,看起来差不多,却会让某些排列比其他排列更容易出现。在 .NET 8 及以后版本中,Random.Shared.Shuffle(array) 会原地正确地打乱数组,rng.GetItems(choices, 5) 会随机挑出五个元素(可重复)。

在循环里 new Random() 的陷阱

一个经典 bug 是每次需要随机数时都创建一个新的 Random:

// Don't do this
int RollDie()
{
    var rng = new Random();   // new instance on every call
    return rng.Next(1, 7);
}

在 .NET Framework 中,不带种子的 new Random() 使用系统时钟(Environment.TickCount),它每 10 到 16 毫秒才变化一次。在快速循环中调用时,创建出的实例种子相同,返回的数也相同,所以一个掷骰子的循环会打印出 4 4 4 4 4。.NET Core 和 .NET 5+ 会从一个共享的随机源为每个实例生成种子,掩盖了这个现象,但每生成一个数就创建一个对象仍然很浪费,而且这段代码一旦在旧项目或 Unity 中复用就会出问题。

解决办法是整个类只用一个实例,存放在字段中:

输出:

3 6 6 5 6 1 2 1

这里使用种子,只是为了让示例每次运行都打印相同的内容;在真正的游戏里,你会写 new Random()。

多线程与 Random.Shared

Random 不是线程安全的。如果两个线程同时对同一个实例调用 Next,它的内部状态可能会被破坏;典型的症状是这个实例从此以后只返回 0。可选的做法,从最简单的开始:

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 是合适的默认选择:它线程安全,不需要字段,而且种子是随机的。它不能指定种子,所以需要可复现时,请用你自己的 new Random(seed)。

密码学安全的随机数

System.Random 在设计上就是可预测的。任何知道种子或者看到足够多输出的人,都能算出后面的数。这对游戏和抽样来说没问题,但不能用于密码、重置令牌、会话 ID、密钥和抽奖。这些场景请使用 System.Security.Cryptography 中的 RandomNumberGenerator,它从操作系统的安全随机数生成器读取数据:

示例输出:

Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF

RandomNumberGenerator.GetInt32(.NET Core 3.0 及以后版本可用)会返回指定范围内无偏的整数。不要用 bytes[0] % 10 来生成安全验证码:取模会让某些数字比其他数字更容易出现。在 .NET 6 及以后版本中,RandomNumberGenerator.GetBytes(16) 会直接返回一个新数组,Convert.ToHexString(bytes) 可以把它格式化。如果只是需要唯一标识符而不在意保密,Guid.NewGuid() 更简单。

常见问题

C# 中如何生成随机数?

创建一个 Random 对象并调用 Next:var rng = new Random(); int roll = rng.Next(1, 7); 会得到 1 到 6。Next(max) 得到 0 到 max - 1,NextDouble() 得到一个大于等于 0.0、小于 1.0 的 double。在 .NET 6 及以后版本中,可以不创建对象,直接用 Random.Shared.Next(1, 7)。

Random.Next 的上限是包含在内的吗?

不是。Next(min, max) 返回的数大于等于 min,并且严格小于 max。要得到 1 到 10(含 10)之间的数,应写 Next(1, 11)。正因如此,Next(0, list.Count) 总是列表的一个合法索引。

为什么 new Random() 会生成相同的数?

在 .NET Framework 中,new Random() 用系统时钟作为种子,而系统时钟每隔几毫秒才变化一次,所以在快速循环中创建的多个实例会拿到相同的种子,生成相同的序列。.NET Core 和 .NET 5+ 会给每个实例不同的种子,但无论在哪个版本,解决办法都一样:只创建一个 Random 并重复使用它。

C# 中如何每次都得到相同的随机数?

给构造函数传入一个种子:new Random(42)。种子相同的两个实例会生成相同的序列,这让测试、模拟和程序化生成都可以复现。同一个种子生成的序列在不同的 .NET 版本之间可能不同,所以不要把用种子生成的结果当作永久数据保存。

System.Random 生成密码或令牌够安全吗?

不够。Random 是可预测的:任何知道它的状态或种子的人都能重现这些数。对于密码、令牌、密钥以及任何与安全相关的场景,请使用 System.Security.Cryptography.RandomNumberGenerator,例如 RandomNumberGenerator.GetInt32(0, 10) 或 RandomNumberGenerator.GetBytes(32)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始