随机数生成一直是程序员要面临的大问题之一,在高中电脑课堂上我们就知道,由 CPU 时钟,进程和线程所构建出的世界中,是没有真正的随机的。在给定一个随机种子后,使用某些神奇的算法我们可以得到一组伪随机的序列。

arc4random 是一个非常优秀的随机数算法,并且在 Swift 中也可以使用。它会返回给我们一个任意整数,我们想要在某个范围里的数的话,可以做模运算 (%) 取余数就行了。但是有个陷阱..

这是错误代码

let diceFaceCount = 6
let randomRoll = Int(arc4random()) % diceFaceCount + 1
print(randomRoll)

其实在 iPhone 5s 上完全没有问题,但是在 iPhone 5 或者以下的设备中,有时候 程序会崩溃...请注意这个“有时候”..

最让程序员郁闷的事情莫过于程序有时候会崩溃而有时候又能良好运行。还好这里的情况比较简单,聪明的我们马上就能指出原因。其实 Swift 的 Int 是和 CPU 架构有关的:在 32 位的 CPU 上 (也就是 iPhone 5 和前任们),实际上它是 Int32,而在 64 位 CPU (iPhone 5s 及以后的机型) 上是 Int64arc4random 所返回的值不论在什么平台上都是一个 UInt32,于是在 32 位的平台上就有一半几率在进行 Int 转换时越界,时不时的崩溃也就不足为奇了。

这种情况下,一种相对安全的做法是使用一个 arc4random 的改良版本:

func arc4random_uniform(_: UInt32) -> UInt32

这个改良版本接受一个 UInt32 的数字 n 作为输入,将结果归一化到 0 到 n - 1 之间。只要我们的输入不超过 Int 的范围,就可以避免危险的转换:

let diceFaceCount: UInt32 = 6
let randomRoll = Int(arc4random_uniform(diceFaceCount)) + 1
print(randomRoll)

最佳实践当然是为创建一个 Range 的随机数的方法,这样我们就能在之后很容易地复用,甚至设计类似与 Randomable 这样的接口了:

func randomInRange(range: Range<Int>) -> Int {
    let count = UInt32(range.endIndex - range.startIndex)
    return  Int(arc4random_uniform(count)) + range.startIndex
}

for _ in 0...100 {
    print(randomInRange(1...6))
}