为何在.NET Core源码中采用RandomNumberGenerator.Fill而非GetInt32实现随机恢复码字符生成?
我在浏览.NET Core源码时发现一段生成随机恢复码字符的代码,当NETCOREAPP标志为true时,实现大致如下:
private static char GetRandomRecoveryCodeChar() { // Based on RandomNumberGenerator implementation of GetInt32 uint range = (uint)AllowedChars.Length - 1; // Create a mask for the bits that we care about for the range. The other bits will be // masked away. uint mask = range; mask |= mask >> 1; mask |= mask >> 2; mask |= mask >> 4; mask |= mask >> 8; mask |= mask >> 16; Span<uint> resultBuffer = stackalloc uint[1]; uint result; do { RandomNumberGenerator.Fill(MemoryMarshal.AsBytes(resultBuffer)); result = mask & resultBuffer[0]; } while (result > range); return AllowedChars[(int)result]; }
我很好奇,为什么不采用下面这种看起来更简洁的实现方式?
private static char GetRandomRecoveryCodeChar() { int rInt = RandomNumberGenerator.GetInt32(0, AllowedChars.Length); return AllowedChars[rInt]; }
请问其中存在什么问题?
嘿,这个问题问得特别到位!其实背后主要有两个关键点:
1. 历史兼容性原因
RandomNumberGenerator.GetInt32(int minValue, int maxValue)这个便捷方法是在**.NET Core 3.0**才被引入的。而这段生成恢复码的代码,大概率是在更早的.NET Core版本中就已经存在了——当时还没有这个现成的API可以直接调用,所以开发人员只能手动实现这个无偏随机数的逻辑。
2. 逻辑本质是完全等价的
你仔细看这段手动实现的注释:// Based on RandomNumberGenerator implementation of GetInt32——它自己都说明了,这段代码就是照着GetInt32的底层逻辑写的!
为什么要这么麻烦?核心是为了避免模运算带来的随机偏斜:如果AllowedChars.Length不是2的整数次幂,直接用随机数 % 长度的方式,会导致某些字符被选中的概率略高于其他字符(因为随机数的范围可能无法被长度整除)。而这段代码通过生成掩码、循环过滤超出范围的结果,就是为了保证每个字符被选中的概率完全均匀,这和GetInt32内部的实现逻辑完全一致。
换句话说,现在如果用GetInt32来替代这段代码,功能上是完全等价的,而且代码更简洁。但源码里保留这段实现,要么是因为历史遗留没来得及更新,要么是为了兼容更早的.NET Core版本(不过既然已经有NETCOREAPP标志,可能更多是历史原因)。
内容的提问来源于stack exchange,提问作者Acerbic

