介紹 5 個 Swift Extension 讓你輕鬆建立隨機數!


本篇原文(標題:5 Swift Extensions to Generate Randoms)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。

swift-random-number

有兩個行業經常會用到隨機數 (random number),就是遊戲行業 (games industry) 和加密貨幣行業 (cryptographic industry)。因此,我希望可以寫一篇有趣的文章,集合所有隨機值的範例程式碼!

相信大家都已經了解創建隨機數的程式碼,我就不在這裡詳述了,我們這次會更加深入地探討這個主題。很多 extension 都是加密安全的,不過 Apple 的 Man Page 都有警告,不同平台的加密效能可能會有所不同。

隨機字符 (Character)

除了 Int、Double、和 Bool 等資料型別外,我們也利用一個 extension 來創建隨機字符:

extension Character {
  static func returnQ(inq:Range<Int>) -> Int {
    var g = SystemRandomNumberGenerator()
    let c = Int.random(in: inq, using: &g)
    return c
  }
  static func randomCharacter() -> Character {
    let digits = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
    let c = returnQ(inq: 0..<digits.count)
    let r = digits.index(digits.startIndex, offsetBy: c)
    let d = String(digits[r])
    return Character(d)
  }
}

隨機字串 (String)

大家都知道,在 Swift 中字符與字串是不同的,字串包含一個或以上的字符。以下的範例程式碼就會回傳一個隨機字串:

extension String {
   static func random(of n: Int) -> String {
      let digits = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
      return String(Array(0..<n).map { _ in digits.randomElement()! })
   }
}

// let p = String.random(of:4)

利用自己提供的來源來創建隨機字串

接下來,讓我們來提供自己的字串,並回傳一個隨機字串。這裡需要用到兩個 extension,第一個需要字串的陣列,而第二個需要該字符。

extension String {
  static func random(among: [String]) -> String {
    assert(!among.isEmpty,"Empty Strings not supported")
    var g = SystemRandomNumberGenerator()
    let c = among.shuffled(using: &g)
    let y = String(c.first!)
    return y
  }
  
  static func random(within: String) -> String {
    assert(!within.isEmpty,"Empty Strings not supported")
    let c = Array(within)
    var g = SystemRandomNumberGenerator()
    let d = c.shuffled(using: &g)
    let e = d.first
    let y = String(e!)
    return y
  }
}

// let h = String.random(among:["a","b","c","d"])
// let i = String.random(within: "abcd")

以下是另一個做法,這需要字串的陣列:

extension String {
  static func randomString(list:[String]) -> String? {
    assert(!list.isEmpty,"Empty Lists not supported")
    return list.randomElement()!
  }
}
// var cities = ["London", "Paris", "Madrid", "Berlin", "Bern"]
// let r = String.randomString(list: cities)

一個獨特的隨機字串

當然,以上這些範例都可能會回傳同樣的字串,這樣可能無法符合部分人的要求。我們可以利用以下這個 extension,來刪除已經出現過的元素。

extension Int {
  static func returnQ(inq:Range<Int>) -> Int {
    var g = SystemRandomNumberGenerator()
    let c = Int.random(in: inq, using: &g)
    return c
  }
  
  static func randomPop(list:inout [String]) -> String {
    assert(!list.isEmpty,"Empty Lists not supported")
    let c = returnQ(inq: 0..<list.count)
    let foo = list.remove(at: c)
    return foo
  }
}
// var cities = ["London", "Paris", "Madrid", "Berlin", "Bern"]
// let s = String.randomPop(list: &cities)

在特定範圍內 創建一個不會重覆的整數

讓我們回到數字的例子,以下這個範例可以回傳在特定範圍內的一個整數,同時不會連續兩次回傳相同的數字。

extension Int {
    // function to ensure I don't get two random numbers that are the same next to each other
    static func random(in range: ClosedRange<Int>, excluding x: Int) -> Int {
        if range.contains(x) {
            let r = Int.random(in: Range(uncheckedBounds: (range.lowerBound, range.upperBound)))
            return r == x ? range.upperBound : r
        } else {
            return Int.random(in: range)
        }
    }
}

在特定範圍內 創建一個獨特的整數

這個範例會回傳一個特定範圍內的整數,並將每個顯示過的整數添加到一個 set 中,以避免回傳同樣的整數。在範圍內的數字全都顯示過之後,整個過程就會重置並重新開始。但是請注意,這是一種非確定性演算法 (non-deterministic algorithm),也就是說,特定範圍越大,回傳數字所需要的時間就會越長。

extension Int {
  static func randomV(in range: ClosedRange<Int>, excluding usedRange: inout Set<Int>) -> Int
  {
    if usedRange.count == range.upperBound - 1 {
      usedRange = []
    }
    var r = Int.random(in: Range(uncheckedBounds: (range.lowerBound, range.upperBound)))
    while usedRange.contains(r) {
      r = Int.random(in: Range(uncheckedBounds: (range.lowerBound, range.upperBound)))
    }
    if usedRange.count < range.upperBound - 1 {
      usedRange.insert(r)
    }
    return r
  }
}

確定性演算法

以下這個範例會回傳 Set 內的整數,同時將顯示過的整數添加到另一個 Set 中。這與上一個範例十分相似,唯一不同的是這個範例是確定性演算法,它會按時間複雜度 (time complexity) O(n) 來隨機選數字。雖然特定範圍越大,回傳數字所需要的時間還是會越長,但以下程式碼不同的是,我們可以肯定執行時間會隨著 set 的大小按比例增加。

extension Int {
    static func randomX(randomSet:Set<Int>, working usedRange: inout Set<Int>) -> Int {
    if usedRange.isEmpty {
      usedRange = randomSet
    }
    let r = usedRange.first!
    usedRange.remove(at: usedRange.startIndex)
    usedRange = Set(usedRange.shuffled())
    return r
  }
}

當然,在這 3 個方法中,我們不止可以使用整數,還可以使用 Double、Float,或是一些字串方法,搭配同樣的結果或限制,來把它們變成模板。

GameKit

你知道 GameKit 也有自己創建隨機數程式碼範例嗎?不過這些隨機數只適合遊戲行業,對加密行業則不是太適合。以下的範例會創建一個 0 到 20 之間的隨機數。Apple 在 Man Page 也提過,如果想確保每次運行程式碼都可以回傳一個隨機數,我們就需要利用 arc4.drop(1024) 刪掉起始值 (initial value)。

let arc4 = GKARC4RandomSource()<br>let t = arc4.nextInt(upperBound: 20)

但是對於遊戲行業來說,隨機是非常重要的,我們可以在這裡多做一步,調用以下範例程式碼,來創建一個更隨機的隨機數。不這,它所需要的時間比前一個範例多一點。

let mersenne = GKMersenneTwisterRandomSource()<br>mersenne.nextInt(upperBound: 20)

以下這個方法可以比前兩個範例運行得更快,不過它當然也有壞處,就是隨機性會比較低。

let arc4b = GKLinearCongruentialRandomSource()<br>let u = arc4.nextInt(upperBound: 20)

不過,以擲骰子為例,我們也可能不斷得到相同的值;但我們不想偽隨機數生成器中發生這樣的情況。所以,如果我們想要讓生成器更真實,就可以使用 distribution。從以下範例可見,ShuffledDistribution 可以減少相同值出現的次數。

let rand = GKShuffledDistribution(forDieWithSideCount: 6)<br>let distribution = GKRandomDistribution(randomSource: rand, lowestValue: 0, highestValue: 20)

而以下這個範例,我們用了 GaussianDistribution,如此一來,結果就會以鐘形曲線 (bell curve) 分佈。也就是說,特定範圍內中間的值出現次數會是最高或最低。

let rand2 = GKGaussianDistribution(forDieWithSideCount: 6)<br>let distribution2 = GKRandomDistribution(randomSource: rand2, lowestValue: 0, highestValue: 20)

隨機數並不隨機

值得一提的是,GameKit 隨機數其實並不隨機。以下的範例程式碼有 2 個 seed,第 1 個 seed 是用來創建一個相同的隨機數序列;這是用來作測試的。而當我們使用第 2 個 seed 時,每次都會得到一個不同的隨機數序列。

let seed1 = " ".data(using: .utf8)
let seed2 = UInt32(Date().timeIntervalSince1970).description.data(using: .utf8)
let arc4c = GKARC4RandomSource(seed: seed1!)
print(arc4c.nextInt(upperBound: 20))
print(arc4c.nextInt(upperBound: 20))
print(arc4c.nextInt(upperBound: 20))
print(arc4c.nextInt(upperBound: 20))

安全隨機數 (Secure Random Number)

最後,我必須要跟大家分享這段程式碼,這是我從這篇文章中找到的。這個範例程式碼可以創建一個隨機整數,讓我們用於加密程式碼中。如其他理智的開發者一樣,我們可以使用 CryptKit,而不需要自己編寫程式碼。

func secureRandomInt() throws -> Int? {
    let count = MemoryLayout<Int>.size
    var bytes = [Int8](repeating: 0, count: count)

    // Fill bytes with secure random data
    let status = SecRandomCopyBytes(
        kSecRandomDefault,
        count,
        &bytes
    )
    
    // A status of errSecSuccess indicates success
    if status == errSecSuccess {
        // Convert bytes to Int
        let int = bytes.withUnsafeBytes { pointer in
            return pointer.load(as: Int.self)
        }

        return int
    }
    else {
        // Handle error
    }
    return nil
}

這篇文章到此為止,希望你在文章中找到或學到一些有用的知識。

本篇原文(標題:5 Swift Extensions to Generate Randoms)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。

作者簡介:Mark Lucking,編程資歷超過 35 年,熱愛使用及學習 Swift/iOS 開發,定期在 Better ProgrammingThe StartUpMac O’ClockLevel Up Coding、及其它平台上發表文章。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This