有兩個行業經常會用到隨機數 (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()
let t = arc4.nextInt(upperBound: 20)
但是對於遊戲行業來說,隨機是非常重要的,我們可以在這裡多做一步,調用以下範例程式碼,來創建一個更隨機的隨機數。不這,它所需要的時間比前一個範例多一點。
let mersenne = GKMersenneTwisterRandomSource()
mersenne.nextInt(upperBound: 20)
以下這個方法可以比前兩個範例運行得更快,不過它當然也有壞處,就是隨機性會比較低。
let arc4b = GKLinearCongruentialRandomSource()
let u = arc4.nextInt(upperBound: 20)
不過,以擲骰子為例,我們也可能不斷得到相同的值;但我們不想偽隨機數生成器中發生這樣的情況。所以,如果我們想要讓生成器更真實,就可以使用 distribution。從以下範例可見,ShuffledDistribution
可以減少相同值出現的次數。
let rand = GKShuffledDistribution(forDieWithSideCount: 6)
let distribution = GKRandomDistribution(randomSource: rand, lowestValue: 0, highestValue: 20)
而以下這個範例,我們用了 GaussianDistribution
,如此一來,結果就會以鐘形曲線 (bell curve) 分佈。也就是說,特定範圍內中間的值出現次數會是最高或最低。
let rand2 = GKGaussianDistribution(forDieWithSideCount: 6)
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
}
這篇文章到此為止,希望你在文章中找到或學到一些有用的知識。