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)。