iOS App 程式開發

Swift 4 Generics: 如何在程式碼及 iOS App 中應用泛型

Swift 4 Generics: 如何在程式碼及 iOS App 中應用泛型
Swift 4 Generics: 如何在程式碼及 iOS App 中應用泛型
In: iOS App 程式開發, Swift 程式語言

問題一:我可以撰寫一個 Swift 函式,來找出存放在任意陣列裡、某個任意型別特定實例的索引或位置嗎?

問題二:我可以撰寫一個 Swift 函式,來確認某個任意型別的特定實例,是否存在於任意陣列裡?

所謂「任意」型別,是指包含了我自己定義的型別 (像 Class)。附註:我知道可以使用 Swift Array 型別內建的函式:index 以及 contains,但這次我會使用簡單的程式碼來說明 Swift 泛型 (Generics) 的一些要點。

總括來說,我會涵蓋 泛型程式設計 (Generic Programming) 的內容,它是:

… 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 官方文件中,關於「泛型」 的描述:

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 函式,來確認某個特定的字串是否存在於一個字串陣列中:

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

在建立了 "existsManual" 函式來搜尋 String 陣列之後,如果我想要類似的函式來搜尋 IntegerFloatDouble 陣列、甚至是自定類別的陣列,我可能要花時間撰寫許多函式、利用更多的程式碼去做同一件事。假如我找到更新或更快的搜尋演算法呢?又假如我在我的搜尋演算法中發現 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
func existsManual(item:Person, inArray:[Person]) -> Bool

問題所在

在這個我們與型別緊密相連的世界,我們必須為每個想要搜尋的陣列建立新函式,結果就會產生大量的技術債。由於現代軟體非常複雜,像我們這種開發者需要使用最好的實踐、最好的技術、最好的方法、並最大限度地利用我們的智慧來控制這混亂的情況。據估計,Windows 7 含有大約四千萬行的程式碼,而 macOS 10.4 (Tiger) 則大約有八千五百萬行。在計算上來說,我們不可能估算到這些系統可能的行為數量。

泛型前來救援

(請記住我們學習泛型的目的,所以要假設 Swift Array 型別內建的 indexcontains 都不存在)

讓我們先試著寫一個 Swift 函式,來確認在同一標準型別如 StringIntegerFloatDouble 的陣列裡,是否有個特定的 Swift 標準型別的實例。那麼該如何撰寫呢?

我們先來看看 Swift 泛型,特別是其中的 泛型函式 (Generic Functions)型別參數 (Type Parameters)型別限制 (Type Constraints) 段落介紹,以及 Equatable 協定。看看下面的程式碼,我暫時不作任何解釋,讓你思考一下:

func exists(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

我新寫的 "exist" 函式就是一個 泛型函式,它可以用在任何一個類型上。此外,看看這個函式的開頭:

func exists(item:T, inArray:[T]) -> Bool

我們可以看到我的函式使用了一個佔位符型別 (在這個例子中叫作 T),而不是一個實際的型別 (像是 IntString 或是 Double)。佔位符型別並不是說 T 一定要是什麼型別,而是指[item]及 [inArray]兩者必須是相同的 T 型別,不論 T 代表什麼。用來取代 T 的實際型別是在每次呼叫 [exists(_:_:)] 時決定的。

"exists" 函式裡的佔位符型別 T 就是所謂的型別參數,而官方文件的解釋是:

"specify and name a placeholder type, and are written immediately after the function's name, between a pair of matching angle brackets (such as <T>).

Once you specify a type parameter, you can use it to define the type of a function's parameters (such as the [item] and [inArray] parameters of the [exists(_:_:)] function), or as the function's return type, or as a type annotation within the body of the function. In each case, the type parameter is replaced with an actual type whenever the function is called."

為了加強我們至今所學的,這裡有一個 Swift 函式,可以找出存放在任意陣列裡、某個任意型別特定實例的索引或位置:

func find(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 協定。 蘋果官方文件 中提到:「符合 Equatable 協定的型別都可以使用等於運算符 (==) 來比較是否相等,或是使用不等於運算符 (!=)比較是否不相等。」所有 Swift 型別如 StringIntegerFloat、和 Double 都遵循 Equatable 協定,因此我的泛型函式 "exists" 能於這些型別起作用。

自定型別與泛型

假設我建立一個新的類別叫做 "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)
                                                   ^

Swift generics

更糟的是,你也不能在 "BasicPerson" 型別的陣列中,使用 Swift Array 型別內建的函式 indexcontains。 (每次你想要使用這兩個方法,就必須要定義一個閉包,接著還有很多步驟,我就不說下去了。)

那麼,為什麼呢?

因為 "BasicPerson" 類別並不符合 Equatable 協定。(這是這篇文章餘下內容的一個提示唷!😉)

符合 Equatable

為了讓我的 "BasicPerson" 類別可以在 “exists" 及 "find" 泛型函式中運作,我只需要做兩件事:

  • 讓類別調用 Equatable 協定;
  • 為類別實例撰寫 == 運算符的多型 (overload) 函式。

要注意的是根據官方文件,「標準函式庫為任何 Equatable 型別提供不等式 (!=) 的實作,它是藉由呼叫 == 函式然後回傳否定結果。」

如果你不太熟悉運算符的多型概念,我建議你閱讀這篇這篇文章。相信我,你會想要了解更多運算符的多型概念。

注意: 我重新命名 "BasicPerson" 類別為 "Person",讓它們可以同時存在於同一個 Swift Playground 中。從這之後,我會改為參照 "Person" 類別。

我將實作 == 運算符,好讓它可以比較兩個 "Person" 類別實例的 "name"、"weight" 及 "sex" 屬性。如果兩個 "Person" 類別實例三個屬性都相同,即它們就相等;如果任何一個屬性不相同,就代表它們兩個不相等 (!=)。以下是 "Person" 類別如何調用 Equatable 協定:

class 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 協定類別及結構的 Array 型別集合上,使用 Swift 內建的函式如 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

let indexOfOneOfMyPeople1 = myPeople.index(of: Jeb)
// returns 3 from built-in Swift member function

let isSueOneOfMyPeople = exists(item: Sue, inArray: myPeople)
// returns true from custom generic function

let isSueOneOfMyPeople1 = myPeople.contains(Sue)
// returns true from built-in Swift member function

let indexOfBob = find(item: Bob, inArray: myPeople)
// returns nil from custom generic function

let indexOfBob1 = myPeople.index(of: Bob)
// returns nil from built-in Swift member function

let isBobOneOfMyPeople1 = exists(item: Bob, inArray: myPeople)
// returns false from custom generic function

let isBobOneOfMyPeople2 = myPeople.contains(Bob)
// returns false from built-in Swift member function

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.

例如,如果你調用了 Comparable 協定,你可以撰寫及使用 <><=、和 >= 運算符的多型函式。很酷吧!

注意

想想關於 "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

看看最後一行的註解,儘管 "Person" 的物件 "Jan" 及 "Sue" 是不同的類別實例,但兩者在技術上來說是相等的。你的軟體並沒什麼問題,你只需要在 "Person" 類別的集合中加入一個「主鍵」 (Primary Key) ——你或許可以添加一個 GUID 變數到類別設計中,或是社會安全碼,或其他可以保證在 "Person" 類別實例集合 (陣列) 中獨特的數值;或者,你也可以使用 ===

希望你喜歡這篇教學!

譯者簡介:楊敦凱-目前於科技公司擔任 iOS Developer,工作之餘開發自有 iOS App同時關注網路上有趣的新玩意、話題及科技資訊。平時的興趣則是與自身專業無關的歷史、地理、棒球。來信請寄到:[email protected]

原文Swift 4 Generics: How to Apply Them in Your Code and iOS Apps

作者
Andrew Jaffee
熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。