Swift 4 泛型:如何在你的程式碼或App裡應用泛型

SwiftGG翻譯組發表於2019-02-27

原文連結:swift.gg/2018/08/28/…
作者:Andrew Jaffee
譯者:BigLuo
校對:numbbbbb,muhlenXi
定稿:CMB

問題 1:我能否寫一個 Swift 函式用於查詢在任意陣列中儲存的任意型別任何例項物件的位置\索引。

問題 2:我能否寫一個 Swift 函式用於確定在任意陣列中儲存的任意型別任何例項物件的型別。

我所說的 "任何型別",包括自定義型別,比如我們自己定義的 Class 型別。提示:我知道我能夠用 Swift Array 型別的內建方法,如 index 和 contains,但今天我將會用簡單程式碼例項來說明 Swift 泛型中的一些特性。

一般來說,我將泛型程式設計作如下定義:

… a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. This approach, pioneered by ML in 1973, permits writing common functions or types that differ only in the set of types on which they operate when used, thus reducing duplication.

是一種演算法機制為 types to-be-specified-later (型別確定滯後)的計算機程式設計風格,當具體的型別作為引數傳入後,該演算法機制會對型別進行例項化。這個方法由 "ML" 在 1973 年開創。可以用共有的函式和型別來表示一個型別集合從而來減少函式操作的重複。

特別的指出,來自蘋果Swift文件 關於"泛型"話題的說明:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

Generics are one of the most powerful features of Swift , and much of the Swift standard library is built with generic code. … For example, Swift ’s Array and Dictionary types are both generic collections. You can create an array that holds Int values, or an array that holds String values, or indeed an array for any other type that can be created in Swift . Similarly, you can create a dictionary to store values of any specified type, and there are no limitations on what that type can be. …

泛型編碼能讓你寫出符合需求、支援任意型別,靈活、可重用的函式。你能夠編寫避免重複和程式設計風格抽象、清晰、優雅的程式碼。

泛型是 Swift 中最強大的特性之一,大量的 Swift 標準庫使用了泛型編碼。例如, Swift 的陣列和字典都是泛型集合。你可以建立一個存有整型值或者字串值的陣列,有必要的話,還可以建立一個任何 Swift 支援型別的陣列。類似的,你也可以建立一個字典用於儲存任意指定型別的值。

我一直提倡構建可複用,簡潔,可維護的程式碼,對於 Swift 中的泛型,如果運用恰當,能某種程度上幫助我實現上面提到的效果。所以對於上面兩個問題,我的答案是 "YES"。

生活在一個特定型別編碼的世界

讓我們寫一個 Swift 的方法來說明在一個字串陣列中是否存在特定的一個字串:

func existsManual(item:String, inArray:[String]) -> Bool
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return true
    }
    else
    {
        return false
    }
}
複製程式碼

讓我們測試這個方法:

let strings = ["Ishmael", "Jacob", "Ezekiel"]
 
let nameExistsInArray = existsManual(item: "Ishmael", inArray: strings)
// returns true
 
let nameExistsInArray1 = existsManual(item: "Bubba", inArray: strings)
// returns false
複製程式碼

在建立了用於查詢 String 陣列的 existsManual 函式後。假如我決定想要一些類似的函式用於搜尋 IntegerFloat,和 Double 陣列 — 甚至用於查詢陣列中自定義類呢?我最終花費了寶貴的時間寫了很多做同樣事情的函式。我需要寫很多程式碼來實現。如果我發現了一個新的/更快的搜尋演算法呢?又如果在我的搜尋演算法有一個 bug 呢?我不得不改變我所有的查詢方法的版本。我發現這簡直是個複用地獄:

func existsManual(item:String, inArray:[String]) -> Bool
...
func existsManual(item:Int, inArray:[Int]) -> Bool
...
func existsManual(item:Float, inArray:[Float]) -> Bool
...
func existsManual(item:Double, inArray:[Double]) -> Bool
...
//  "Person"  is a custom class we'll create
//  "Person" 是我們將要建立的自定義的類
func existsManual(item:Person, inArray:[Person]) -> Bool
複製程式碼

問題

我們已經厭煩了活在一個處理型別的世界裡,不得不為每個我們想要查詢的陣列型別建立新的方法。終究這給我們帶來了大量的技術負債。由於現代軟體難以置信的複雜性,像你我這樣的開發者需要使用更好地實踐,更好的技術,更好的方法,用我們的神經元最大程度的控制這種混亂。據估計 Window 7 包含大約 4 千萬行程式碼而 macOS 10.4 (Tiger) 包含大約 8.5 千萬行程式碼,預估像這樣的系統潛在行為次數都是不可能的。

泛型的解決之道

(再次緊記學習泛型的目的,我們依舊假設 Swift 的陣列型別的內建的函式,indexcontains ,不存在。)

讓我們先嚐試寫這樣一個 Swift 函式,判斷 Swift 的標準型別(例如 StringIntegerFloatDouble)的一個特定例項是否存在於這個 Swift 標準型別的陣列中。怎麼做呢?

讓我們切換到 Swift 泛型,特別是泛型函式,型別引數,型別約束以及 Equatable 協議。在沒有定義任何術語前,我寫了一些程式碼,思考一下你看到的。

func exists<T: Equatable>(item: T, inArray: [T]) -> Bool
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return true
    }
    else
    {
        return false
    }
}
複製程式碼

讓我們測試下我新寫的泛型方法

let myFriends:[String] = ["John", "Dave", "Jim"]
 
let isOneOfMyFriends = exists(item: "Dave", inArray: myFriends)
// returns true
 
let isOneOfMyFriends1 = exists(item: "Laura", inArray: myFriends)
// returns false
 
let myNumbers:[Int] = [1,2,3,4,5,6]
 
let isOneOfMyNumbers = exists(item: 3, inArray: myNumbers)
// returns true
 
let isOneOfMyNumbers1 = exists(item: 0, inArray: myNumbers)
// returns false
 
let myNumbersFloat:[Float] = [1.0,2.0,3.0,4.0,5.0,6.0,]
 
let isOneOfMyFloatNumbers = exists(item: 3.0000, inArray: myNumbersFloat)
// returns true
複製程式碼

我新寫 exists 方法是一個泛型函式,這個方法“能正常工作在任何引數型別上”,此外,讓我們看看它的函式簽名。

func exists<T: Equatable >(item: T, inArray: [T]) -> Bool
複製程式碼

我們看到 那個 函式使用一個佔位符型別名字(名叫 T, 在這個案例)而不是真正的型別名(比如:IntStirng,或 Double)佔位符型別名沒有指定 T 必須是什麼,但他說明了 [item][inArray] 必須是相同的型別 T 無論 T 代表什麼,每當 [exists(_:_:)] 函式被呼叫時,真實的型別用於替代 T 被確定下來。

這個 exists 函式中的佔位符型別 T 被稱為型別引數

指定和命名了佔位符的型別,直接寫在函式名稱的後面,在一對尖括號之間(比如 )。

一旦你指定一個型別引數你可以用它來定義函式引數的型別(比如:[item] and [inArray] [exists(_:_:) 函式)或者作為函式返回值的型別,在任何條件下,當函式被呼叫的時候型別引數會被真實型別替代。

為了強化我們目前已經學到的,下面是一個 Swift 函式,該函式能夠找到儲存在陣列中任何型別例項的索引。

func find<T: Equatable>(item: T, inArray: [T]) -> Int?
{
    var index:Int = 0
    var found = false
    
    while (index < inArray.count && found == false)
    {
        if item == inArray[index]
        {
            found = true
        }
        else
        {
            index = index + 1
        }
    }
    
    if found
    {
        return index
    }
    else
    {
        return nil
    }
}
複製程式碼

讓我們測試下它

let myFriends:[String] = ["John", "Dave", "Jim", "Arthur", "Lancelot"]
 
let findIndexOfFriend = find(item: "John", inArray: myFriends)
// returns 0
 
let findIndexOfFriend1 = find(item: "Arthur", inArray: myFriends)
// returns 3
 
let findIndexOfFriend2 = find(item: "Guinevere", inArray: myFriends)
// returns nil
複製程式碼

關於 Equatable 協議

exists 函式中 <T: Equatable > 標註是什麼呢?它叫做型別約束,它規定了"那個型別引數必須繼承自一個具體的類,或者遵守一個特定的協議或是協議組合。我指定了 exists 函式引數,item: TinArray: [T], 必須是型別 T, 而型別 T 必須遵守協議 Equatable 協議,為什麼是這樣的呢?

所有的 Swift 內建型別已經被構建支援 Equatable 協議。來自 [Apple docs](developer.apple.com/documentati… Swift / Equatable): “遵守 Equatable 協議的型別進行相等比較,使用等於運算子(==)判斷相等,或者使用不等運算子(!=)判斷不等”。這就是為什麼我的泛型函式 "exists" 能夠在 Swift 的型別(如 StringIntegerFloatDouble)上正常工作。所有這些型別都定義了 ==!= 運算子。

自定義型別和泛型

假如我宣告瞭一個新的類叫做 “BasicPerson” 如下所示。我能用我的 exists" 函式來找出在陣列中是否有 "BasicPerson" 類的一個例項的型別麼?不行!為什麼不行?看看下面這個程式碼,我們接下來討論它:

class BasicPerson
{
    var name: String
    var weight: Int
    var sex: String
    
    init(weight: Int, name: String, sex: String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
}
 
let Jim = BasicPerson(weight: 180, name: "Jim Patterson", sex: "M")
let Sam = BasicPerson(weight: 120, name: "Sam Patterson", sex: "F")
let Sara = BasicPerson(weight: 115, name: "Sara Lewis", sex: "F")
 
let basicPersons = [Jim, Sam, Sara]
 
let isSamABasicPerson = exists(item: Sam, inArray: basicPersons)
複製程式碼

看到最後一行,因為它有一個編譯錯誤:

error: in argument type '[BasicPerson]', 'BasicPerson' does not conform to expected type 'Equatable'
let isSamABasicPerson = exists(item: Sam, inArray: basicPersons)
複製程式碼

image-20180813173212026

這很糟糕了, 在 "BasicPerson" 型別的陣列裡面,你不能使用 Swift 陣列的內建函式 indexcontains。(你必須定義一個閉包,每當你想使用那兩個方法 blah,blah,blah… 這個我就不提了。)

再次回到問題,為什麼報錯?

因為 "BasicPerson" 類沒有遵守 Equeatable 協議(這是一個提示,請看下文咯)

遵守 Equatable 協議

為了允許我的 "BasicPerson" 類是可以使用我的 "exists" 和 "find" 泛型方法,所有我需要做的是:

  • 讓類遵守 Equatable 協議
  • 過載類例項的 == 操作符

注意[這個](developer.apple.com/documentati… Swift / Equatable )"Swift 標準庫為所有遵循 Euqatable 協議的型別提供了不等於(!=) 操作符的實現。通過呼叫自定義的 == 函式獲取它的取反結果。

如果你對操作符過載不熟悉,我建議你閱讀這些主題,連結在這裡這裡的.相信我,你會想知道操作符過載的。

提示:我重新命名 "BasicPerson" 類為 "Person" 讓他們在相同的 Swift Playground 檔案能共存,接著我們來到 "Person" 類。

我將實現 == 操作符,所以它能比較 "Person" 類不同例項間的 "name", "weight", 和 "sex" 屬性。如果兩個 "Person" 類的例項有相同的的三個屬性。則他們是相等的。如果有一個屬性不同,則他們是不相等的(!=)。這就是為什麼我的 "Person" 類遵守了 Equatable 協議:

lass Person: Equatable 
{
    var name:String
    var weight:Int
    var sex:String
    
    init(weight: Int, name: String, sex: String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
    
    static func == (lhs: Person, rhs: Person) -> Bool
    {
        if lhs.weight == rhs.weight &&
            lhs.name == rhs.name &&
            lhs.sex == rhs.sex
        {
            return true
        }
        else
        {
            return false
        }
    }
}
複製程式碼

注意上面的 == 過載方法,這需要讓 "Person" 遵守 Equatable 協議。注意 == 過載方法中的 lhsrhs 引數。這是通用的,當過載操作符時,程式碼中等號兩邊的物件應該與引數中的物理位置一致,如:

lhs == rhs
left-hand side == right-hand side
複製程式碼

它實用嗎?

如果你跟隨著我的指南,你能建立像我寫的 "exists" 和 "find" 泛型函式用於任何你建立的新型別,如類或者結構體。讓你自定義的類和結構體集合型別遵守 Equatable 協議,像 Swift 裡面 Array 中的內建函式 indexcontains。他們確實有用:

let Joe = Person(weight: 180, name: "Joe Patterson", sex: "M")
let Pam = Person(weight: 120, name: "Pam Patterson", sex: "F")
let Sue = Person(weight: 115, name: "Sue Lewis", sex: "F")
let Jeb = Person(weight: 180, name: "Jeb Patterson", sex: "M")
let Bob = Person(weight: 200, name: "Bob Smith", sex: "M")
 
let myPeople:Array = [Joe, Pam, Sue, Jeb]
 
let indexOfOneOfMyPeople = find(item: Jeb, inArray: myPeople)
// returns 3 from custom generic function
// 返回 3 源自自定義泛型函式
 
let indexOfOneOfMyPeople1 = myPeople.index(of: Jeb)
// returns 3 from built-in Swift member function
// 返回 3 源自 Swift 內建成員函式
 
let isSueOneOfMyPeople = exists(item: Sue, inArray: myPeople)
// returns true from custom generic function
// 返回 true 源自自定義泛型函式
 
let isSueOneOfMyPeople1 = myPeople.contains(Sue)
// returns true from built-in Swift member function
// 返回 true 源自 Swift 內建成員函式
 
let indexOfBob = find(item: Bob, inArray: myPeople)
// returns nil from custom generic function
// 返回 nil 源自自定義泛型函式
 
let indexOfBob1 = myPeople.index(of: Bob)
// returns nil from built-in Swift member function
// 返回 nil 源自 Swift 內建成員函式
 
let isBobOneOfMyPeople1 = exists(item: Bob, inArray: myPeople)
// returns false from custom generic function
// 返回 false 源自自定義泛型函式
 
let isBobOneOfMyPeople2 = myPeople.contains(Bob)
// returns false from built-in Swift member function
// 返回 false 源自 Swift 內建成員函式
 
if Joe == Pam
{
    print("they're equal")
}
else
{
    print("they're not equal")
}
// returns "they're not equal"
複製程式碼

擴充套件閱讀

蘋果提示關於 Equatable 協議的更多好處:

Adding Equatable conformance to your custom types means that you can use more convenient APIs when searching for particular instances in a collection. Equatable is also the base protocol for the Hashable and Comparable protocols, which allow more uses of your custom type, such as constructing sets or sorting the elements of a collection.

讓你的自定義型別遵循 Equatable 協議意味著你可以使用許多系統提供的 API 來讓你在一個集合裡面查詢特定一個例項變得更加方便。

Equatable 協議也是 Hashable 協議和 Comparable 協議的基礎協議。這允許你使用更多的自定義型別,比如構建集合或者排序集合中的元素。

比如,如果你遵守了 comparable 協議,你能過載和使用 <><=>= 操作符,這真的很 Cool。

須知

想一下我們的 "Person" 類,假如我們有一些類似下文所示的例項:

let Joe = Person(weight: 180, name: "Joe Patterson", sex: "M")
let Pam = Person(weight: 120, name: "Pam Patterson", sex: "F")
let Sue = Person(weight: 115, name: "Sue Lewis", sex: "F")
let Jeb = Person(weight: 180, name: "Jeb Patterson", sex: "M")
let Bob = Person(weight: 200, name: "Bob Smith", sex: "M")
let Jan = Person(weight: 115, name: "Sue Lewis", sex: "F")
 
if Jan == Sue
{
    print("they're equal")
}
else
{
    print("they're not equal")
}
// returns "they're equal" for 2 different objects
// 返回 "they're equal" 對於兩個不同的物件 
複製程式碼

看最後一行,因為這些 "Person" 物件中 "Jan" 和 "Sue" 物件是絕對相等的。即使他們是兩個不同的例項物件。你的軟體好壞僅僅取決於你的設計。在資料庫的術語體系裡, "Person" 類集合中,你會需要一個"主鍵" — 或許在類的設計中,可以新增一個索引變數。比如一個社會安全碼、或者你熟知的其他的唯一值來保證 "Person" 類例項在集合 (Array) 中的唯一性,當然啦,你也可以使用 === 操作符。

享用吧!

相關文章