Swift 4.2+ 种子随机数生成器

30

我正在尝试使用Swift 4.2+的Int.random()函数生成种子随机数,但是没有给出可实现随机数生成器种子的实现方式。据我所知,唯一的方法是创建一个符合RandomNumberGenerator协议的新随机数生成器。是否有更好的实现方式或符合功能要求的支持种子的RandomNumberGenerator类的实现方法以及如何实现它?

此外,在寻找解决方案时,我看到了两个函数sranddrand,但从提及频率来看,我不确定使用它是否存在不良惯例,并且我也找不到任何关于它们的文档。

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

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


也许如果您告诉我们您想要实现什么,那么“seed/seeded”这个词在网络上的解释可能会有很大不同。 - Joakim Danielson
如果您使用SystemRandomNumberGenerator,种子将自动为您生成:https://developer.apple.com/documentation/swift/systemrandomnumbergenerator - Andreas Oetjen
2
你必须使用 RandomNumberGenerator 吗?GamePlayKit 有各种随机数生成器,例如请参见 https://dev59.com/W6_la4cB1Zd3GeqPzuBU#53355215。 - Martin R
@AndreasOetjen 我需要能够按照自己的条件传递种子,请查看问题中的更新。 - RPatel99
对Martin R的建议使用GamePlaykit随机数生成器表示赞同。他们有确定性生成器,允许生成的数字可再现。MartinR应该将该评论升级为答案。我记得在gameplaykit的WWDC视频中提到了这个功能。 - Darrell Root
显示剩余3条评论
6个回答

13

所以我使用了Martin R的建议,使用GamePlayKitGKMersenneTwisterRandomSource来创建一个符合随机数生成器协议的类,在像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()和用于 Int、Bool、Double 等的.random())的简单性相结合,使我能够灵活地实现所需。


1
我想知道上述生成器产生的值范围是否存在问题,因为GKRandom(因此GKMersenneTwisterRandomSource)的文档记录了在[INT32_MIN,INT32_MAX]范围内产生值。请参见下面的替代变体来解决这个问题。 - Grigory Entin
就像@GrigoryEntin所说的那样。即使在64位平台上,使用abs()也会将范围限制在一半,因此例如next<UInt64>()永远不会返回范围(Int64.max + 1)...(UInt64.max)中的任何内容。我怀疑在64位平台上,next<UInt32>()会产生很多溢出错误。 - David Moles
奇怪。我尝试了这段代码,如果我尝试像Double.random(in:0...1,using:&gen)这样的东西,就会得到一个无限循环。 在我的iPhone和Mac上都发生了。它一直在重复调用“next”,但从未返回随机的Double。 - Rob N
@RobN,你解决了这个问题吗?我也遇到了同样的问题。 - Tobias
1
@Tobias 好久不见了,但我认为是这样的。尝试像Grigory Entin的答案一样,只覆盖协议的next() -> UInt64方法。我不确定为什么这个答案要覆盖通用扩展方法。 - Rob N

13

这里提供了一个与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
}

1
我已经尝试过这种方法,但是 next(upperBound:) 函数给出的分布很奇怪,直到我使用 & 0xFFFF_FFFF 掩码对 next1next2 进行处理,只保留最低有效位。 - David Moles
1
(我猜GKMersenneTwisterRandomSource在64位平台上返回完整的有符号Int64范围?) - David Moles
1
@DavidMoles 我在 macOS 上使用 GKMersenneTwisterRandomSource 进行了检查。它不会给我任何超过 0x7FFF_FFFF(考虑符号)的东西,因此它看起来不是 64 位的。至于用 0xFFFF_FFFF 进行屏蔽 - 我相信这确实有意义,因为“一半时间” next1 将从负 Int32 生成,这意味着它将具有所有高 32 位设置,并且 next1 | (next2 << 32) 不会更改这些高位(即生成的数字将在高 32 位处有 0xFFFF_FFFF)。另一种选择可能是在 next1 ^ (next2 << 32) 中使用 'xor' 而不是 'or'。 - Grigory Entin
4
@DavidMoles 我进行了小规模测试,使用 next1 | (next2 << 32) 得到的结果有一半时间是在上半部分看到了 0xffffffff。而使用 next1 ^ (next2 << 32) 则看起来是随机的。我会更新回答,谢谢! - Grigory Entin

7
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)
// use fixed seed for testing, when deployed use Int.random(in: 0..<Int.max)

然后使用它:
let rand0to99 = Int.random(in: 0..<100, using: &seededGenerator)

这个在我使用 Bool.random(using:) 时没有起作用。请参考我的回答:https://dev59.com/c1QJ5IYBdhLWcg3wAA1S#76233303 - Heath Borders

2

看起来Swift的RandomNumberGenerator.next(using:)在2019年有所改变。这会影响Collection.randomElement(using:),如果你的生成器的next()->UInt64实现不能在UInt64域中均匀地产生值,则导致它始终返回第一个元素。因此,这里提供的GKRandom解决方案存在问题,因为它的next->Int方法说明:

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

这是我使用 Swift 的 TensorFlow 中的 RNG 找到的解决方案,在这里可以找到。

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帮助我彻底搞清楚了这个问题。

1

我最终使用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"

这对我使用 Bool.random(using:) 不起作用。请参考我的回答:https://dev59.com/c1QJ5IYBdhLWcg3wAA1S#76233303 - Heath Borders

0

srand48 的实现在我尝试使用 Bool.random(using:) 时并不起作用。它们会产生:

var randomNumberGenerator = RandomNumberGeneratorWithSeed(seed:69)
for _ in 0..<100 {
  print("\(Bool.random(using: &randomNumberGenerator))")
}
true
true
false
false
true
true
false
false
true
true
...

然而,在Swift论坛中,我发现了Nate Cook的一个帖子post,其中包含了一个公共领域算法的Swift实现,在我的上述测试中看起来更随机(没有明显的模式存在)。
// This is a fixed-increment version of Java 8's SplittableRandom generator.
// It is a very fast generator passing BigCrush, with 64 bits of state.
// See http://dx.doi.org/10.1145/2714064.2660195 and
// http://docs.oracle.com/javase/8/docs/api/java/util/SplittableRandom.html
//
// Derived from public domain C implementation by Sebastiano Vigna
// See http://xoshiro.di.unimi.it/splitmix64.c
public struct SplitMix64: RandomNumberGenerator {
    private var state: UInt64

    public init(seed: UInt64) {
        self.state = seed
    }

    public mutating func next() -> UInt64 {
        self.state &+= 0x9e3779b97f4a7c15
        var z: UInt64 = self.state
        z = (z ^ (z &>> 30)) &* 0xbf58476d1ce4e5b9
        z = (z ^ (z &>> 27)) &* 0x94d049bb133111eb
        return z ^ (z &>> 31)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接