Swift 4.2+ 播种随机数生成器

Swift 4.2+ seeding a random number generator

我正在尝试使用 Swift 4.2+ 和 Int.random() 函数生成种子随机数,但是没有允许随机数生成器种子的给定实现。据我所知,唯一的方法是创建一个符合 RandomNumberGenerator 协议的新随机数生成器。有没有人推荐更好的方法,或者符合 class 的具有播种功能的 RandomNumberGenerator 的实现,以及如何实现它?

此外,在寻找解决方案时,我曾多次看到 sranddrand 这两个函数,但从它很少被提及的情况来看,我不是确定使用它是否是错误的约定,我也找不到关于它们的任何文档。

我正在寻找最简单的解决方案,不一定是最安全或性能最快的解决方案(例如,使用外部库并不理想)。

更新: "seeded",我的意思是我要将一个种子传递给随机数生成器,这样如果我将同一个种子传递给两个随机数生成器不同的设备或在两个不同的时间,生成器将产生相同的数字。目的是我为应用程序随机生成数据,而不是将所有数据保存到数据库中,我想保存种子并在每次用户加载应用程序时使用该种子重新生成数据。

所以我使用了 Martin R 的建议,使用 GamePlayKitGKMersenneTwisterRandomSource 来制作一个符合 RandomNumberGenerator 协议的 class,我能够使用其中的一个实例功能如 Int.random():

import GameplayKit

class SeededGenerator: RandomNumberGenerator {
    let seed: UInt64
    private let generator: GKMersenneTwisterRandomSource
    convenience init() {
        self.init(seed: 0)
    }
    init(seed: UInt64) {
        self.seed = seed
        generator = GKMersenneTwisterRandomSource(seed: seed)
    }
    func next<T>(upperBound: T) -> T where T : FixedWidthInteger, T : UnsignedInteger {
        return T(abs(generator.nextInt(upperBound: Int(upperBound))))
    }
    func next<T>() -> T where T : FixedWidthInteger, T : UnsignedInteger {
        return T(abs(generator.nextInt()))
    }
}

用法:

// Make a random seed and store in a database
let seed = UInt64.random(in: UInt64.min ... UInt64.max)
var generator = Generator(seed: seed)
// Or if you just need the seeding ability for testing,
// var generator = Generator()
// uses a default seed of 0

let chars = ['a','b','c','d','e','f']
let randomChar = chars.randomElement(using: &generator)
let randomInt = Int.random(in: 0 ..< 1000, using: &generator)
// etc.

通过结合 GKMersenneTwisterRandomSource 的播种功能和标准库随机函数的简单性(例如 .randomElement() 用于数组和 .random() 用于 Int、Bool、Double 等)

这是 RPatel99 的答案的替代方案,它说明了 GKRandom 值的范围。

import GameKit

struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {

    mutating func next() -> UInt64 {
        // GKRandom produces values in [INT32_MIN, INT32_MAX] range; hence we need two numbers to produce 64-bit value.
        let next1 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
        let next2 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
        return next1 ^ (next2 << 32)
    }

    init(seed: UInt64) {
        self.gkrandom = GKMersenneTwisterRandomSource(seed: seed)
    }

    private let gkrandom: GKRandom
}

看起来像 Swift 对 RandomNumberGenerator.next(using:) changed in 2019 的实现。如果您的生成器的 next()->UInt64 实现没有在 UInt64 的域中统一生成值,这会影响 Collection.randomElement(using:) 并导致它总是 return 第一个元素。因此,此处提供的 GKRandom 解决方案存在问题,因为它的 next->Int 方法声明:

     * The value is in the range of [INT32_MIN, INT32_MAX].

这是一个对我有用的解决方案,使用 Swift 的 TensorFlow 中的 RNG 找到 here:


public struct ARC4RandomNumberGenerator: RandomNumberGenerator {
  var state: [UInt8] = Array(0...255)
  var iPos: UInt8 = 0
  var jPos: UInt8 = 0

  /// Initialize ARC4RandomNumberGenerator using an array of UInt8. The array
  /// must have length between 1 and 256 inclusive.
  public init(seed: [UInt8]) {
    precondition(seed.count > 0, "Length of seed must be positive")
    precondition(seed.count <= 256, "Length of seed must be at most 256")
    var j: UInt8 = 0
    for i: UInt8 in 0...255 {
      j &+= S(i) &+ seed[Int(i) % seed.count]
      swapAt(i, j)
    }
  }

  // Produce the next random UInt64 from the stream, and advance the internal
  // state.
  public mutating func next() -> UInt64 {
    var result: UInt64 = 0
    for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
      result <<= UInt8.bitWidth
      result += UInt64(nextByte())
    }
    print(result)
    return result
  }

  // Helper to access the state.
  private func S(_ index: UInt8) -> UInt8 {
    return state[Int(index)]
  }

  // Helper to swap elements of the state.
  private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
    state.swapAt(Int(i), Int(j))
  }

  // Generates the next byte in the keystream.
  private mutating func nextByte() -> UInt8 {
    iPos &+= 1
    jPos &+= S(iPos)
    swapAt(iPos, jPos)
    return S(S(iPos) &+ S(jPos))
  }
}

向我的同事 Samuel、Noah 和 Stephen 致敬,他们帮助我弄清了这个问题。

我最终使用 srand48()drand48() 为特定测试生成了带有种子的伪随机数。

class SeededRandomNumberGenerator : RandomNumberGenerator {

    let range: ClosedRange<Double> = Double(UInt64.min) ... Double(UInt64.max)

    init(seed: Int) {
        // srand48() — Pseudo-random number initializer
        srand48(seed)
    }

    func next() -> UInt64 {
        // drand48() — Pseudo-random number generator
        return UInt64(range.lowerBound + (range.upperBound - range.lowerBound) * drand48())
    }
    
}

因此,在生产中实现使用 SystemRandomNumberGenerator 但在测试套件中它使用 SeededRandomNumberGenerator.

示例:

let messageFixtures: [Any] = [
    "a string",
    ["some", ["values": 456]],
]

var seededRandomNumberGenerator = SeededRandomNumberGenerator(seed: 13)

func randomMessageData() -> Any {
    return messageFixtures.randomElement(using: &seededRandomNumberGenerator)!
}

// Always return the same element in the same order
randomMessageData() //"a string"
randomMessageData() //["some", ["values": 456]]
randomMessageData() //["some", ["values": 456]]
randomMessageData() //["some", ["values": 456]]
randomMessageData() //"a string"

Swift 5 的简化版本:

struct RandomNumberGeneratorWithSeed: RandomNumberGenerator {
    init(seed: Int) { srand48(seed) }
    func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
@State var seededGenerator = RandomNumberGeneratorWithSeed(seed: 123)
// when deployed used seed: Int.random(in: 0..<Int.max)

然后使用它:

let rand0to99 = Int.random(in: 0..<100, using: &seededGenerator)