Swift 4 泛型:如何在你的代碼或App裏應用泛型

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

問題 1:我可否寫一個 Swift 函數用於查找在任意數組中存儲的任意類型任何實例對象的位置\索引。算法

問題 2:我可否寫一個 Swift 函數用於肯定在任意數組中存儲的任意類型任何實例對象的類型。數據庫

我所說的 "任何類型",包括自定義類型,好比咱們本身定義的 Class 類型。提示:我知道我可以用 Swift Array 類型的內置方法,如 index 和 contains,但今天我將會用簡單代碼實例來講明 Swift 泛型中的一些特性。express

通常來講,我將泛型編程做以下定義:編程

… 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.swift

是一種算法機制爲 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. …app

泛型編碼能讓你寫出符合需求、支持任意類型,靈活、可重用的函數。你可以編寫避免重複和編程風格抽象、清晰、優雅的代碼。

泛型是 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) 中的惟一性,固然啦,你也可使用 === 操做符。

享用吧!

相關文章
相關標籤/搜索