泛型範圍的用法

做者:Ole Begemann,原文連接,原文日期:2016-10-13
譯者:Cwift;校對:walkingway;定稿:CMBhtml

我在前面的文章中提到過,Swift 中有兩個基礎的區間(Range)類型:RangeClosedRange,而且這兩個類型不能互相轉換。這使得編寫一個同時適用於兩種區間類型的函數變得很困難。git

昨天,swift-users 的郵件列表中有人問了一個具體的問題:假設你寫了一個名爲 random 的函數,它接受一個整數的區間,並返回一個該範圍中的隨機值:github

import Darwin //在 Linux 上也能夠用 Glibc

func random(from range: Range<Int>) -> Int {
    let distance = range.upperBound - range.lowerBound
    let rnd = arc4random_uniform(UInt32(distance))
    return range.lowerBound + Int(rnd)
}

你可使用一個半開的區間調用這個函數:算法

let random1 = random(from: 1..<10)

可是你不能傳入一個閉合的區間:swift

let random2 = random(from: 1...9) // error

太差勁了,什麼是最好的解決方案?數組

重載

一個方案是重載這個 random 隨機函數,給 random 函數家族增長一個新成員,新的隨機函數接受一個 ClosedRange 類型的參數。而後在內部經過調用原有的函數實現其功能:app

func random(from range: ClosedRange<Int>) -> Int {
    return random(from: range.lowerBound ..< range.upperBound+1)
}

若是輸入範圍的上限爲 Int.max,這個方法將失敗,由於這樣的範圍沒法生成一個對應的半開區間。然而,不過在咱們的例子中這個問題無傷大雅,由於 arc4random_uniform 函數只能處理 32 位的整數,程序不會被邊界問題所影響,由於早在轉換成 UInt32 的時候就 crash 掉了。dom

可計數區間是可以轉換的

因爲這個特定的例子只涉及整數區間,所以咱們還有另外一個方案。基於整數的區間都是能夠計數的,可計數的區間對應的半開區間 CountableRange 和閉合區間 CountableClosedRange 能夠互相轉換。因此咱們能夠把參數改成 CountableRangeide

func random(from range: CountableRange<Int>) -> Int {
    // 相同的實現
    ...
}

如今咱們能夠向函數中傳入閉合的區間了,可是首先要把參數顯式地轉換成 CountableRange,這種方式不是很好(也不夠直觀):函數

// 用法與以前相同
let random3 = random(from: 1..<10)
// 須要顯式轉換類型
let random4 = random(from: CountableRange(1...9))

如今你可能會想,沒問題,讓咱們來重載閉合區間的運算符 ... ,返回一個半開的區間,就像這樣(我從當前版本的標準庫中拷貝了這份聲明,僅僅把返回類型從 CountableClosedRange 更改成 CountableRange):

func ...<Bound>(minimum: Bound, maximum: Bound) -> CountableRange<Bound>
    where Bound: _Strideable & Comparable, Bound.Stride: SignedInteger {
    return CountableRange(uncheckedBounds: (lower: minimum, upper: maximum.advanced(by: 1)))
}

這樣作能夠解決咱們以前的問題,不幸的是這種作法帶來了新的問題,由於當沒有顯式地註明類型信息時,像 1...9 這樣的表達式的類型是模棱兩可的——編譯器沒法決定使用哪一個重載。因此這也不是一個好方案。

經過識別基本接口編寫通用代碼

我寫這篇文章是由於霍曼·梅爾在郵件列表上提出了一個很是好的建議:若是在這個算法中,區間不是最佳的抽象呢?咱們是否能夠考慮更高層的抽象?

讓咱們嘗試從隨機函數中篩選出基本接口,即所需實現的最小功能集合:

  • 它須要一種有效的方法來計算輸入序列的下限和上限之間的距離。

  • 它須要一種有效的方式來檢索輸入序列的第 n 個元素以便將其返回,其中 n 是從其長度的下界計算獲得的隨機距離。

霍曼注意到兩個可計數的區間類型都遵照協議 RandomAccessCollection ,共享該公共協議的一致性。事實上,RandomAccessCollection 提供了咱們想要的基本接口:可隨機訪問的集合保證了所需開銷時間都是恆定的,而這些開銷主要集中在測量索引間距離以及訪問任意索引所指向的元素。

所以,把 random 函數定義成 RandomAccessCollection 中的方法(這裏用到 numericCasts 是由於不一樣集合類型的 IndexDistance 是不一樣的):

extension RandomAccessCollection {
    func random() -> Iterator.Element? {
        guard count > 0 else { return nil }
        let offset = arc4random_uniform(numericCast(count))
        let i = index(startIndex, offsetBy: numericCast(offset))
        return self[i]
    }
}

如今兩種區間類型均可以使用了:

(1..<10).random()
(1...9).random()

這個方案甚至比最初的立意更好,咱們能夠從任意可隨機存取的數組中獲取一個隨機元素:

let people = ["David", "Chris", "Joe", "Jordan", "Tony"]
let winner = people.random()

結論

如今咱們已經知道了在類型系統中區分半開區間和閉合區間很不直觀。若是你被區間所困擾,解決問題的最好辦法是接受現實而後提供兩個重載,即使這意味着你必須寫一些重複代碼。

但代價是代碼的通用性變差了。即便如今不須要你的算法來處理其餘的數據類型,但當你在正確的抽象層次上實現算法時,也會迫使你考慮算法所須要的基本接口。反過來講,代碼的讀者經過基本接口的頭文件類型,能夠更方便地梳理算法所涉及的複雜類型關係——就聲明來講,RandomAccessCollection 協議比遵照協議的具體類型(例如 CountableRange)具備更少的方法和屬性。

即使如此,不要過分地抽象。對泛型參數作不少約束後,泛型代碼可能更難閱讀,尤爲是當你編寫的應用程序不向第三方提供公共 API 的時候。花費太多時間來構建完美的抽象,而不去作真正的工做是咱們很容易犯的錯誤。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索