Swift 程式語言

Swift開發指南:Protocols與Protocol Extensions的使用心法

Swift開發指南:Protocols與Protocol Extensions的使用心法
Swift開發指南:Protocols與Protocol Extensions的使用心法
In: Swift 程式語言

歡迎來到Swift的protocols(協定)和protocols導向的編程教程,在本文中,我們將討論什麼是protocols,以及如何使用它們達到POP(protocol oriented programming:協定導向編程)開發。

我們將首先解釋什麼是protocol,關注protocol和class/structures之間的關鍵差異。接下來,我們將透過範例比較使用協定和類別繼承的差異,展示每種方法的優缺點。之後,我們將討論抽象化(abstraction)和多型(polymorphism),這些物件導向和協定導向編程中的重要概念。然後討論協定擴展(protocol extensions),這是Swift的一個特性,允許開發者替protocols提供預設值和擴增實作,最後,探討如何將OOP搭配POP來解決編程中的各種問題。

注意:本教程預設讀者具有Swift編程語言的基礎知識,不會侷限iOS,macOS或watchOS這些平台,相反的,文章中將專注於Swift,教你可以在任何環境中使用的Swift技能。然而,在開始前確實掌握Swift基本概念是很重要的,接下來,一起進入本文重點吧!
編者提醒:如果您剛接觸iOS編程和Swift,請參閱我們的Swift入門指南

什麼是Protocol?

如果讀者對protocols還是相當陌生,你的第一個問題最可能會想問protocol到底是什麼?

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

The Swift Programming Language Guide by Apple

因此,根據Swift語言的創建者所示,protocols是定義一組其他類型所需功能的好方法。當我想到協定時,我認為協定提供類型可以的資訊,Classes和structs則提供物件的資訊,協定則提供物件將會執行的動作。

例如,你可能有一個名為str的變量,其類型為String。身為一個開發人員,你應該知道str代表String,如果我們定義了一個名為StringProtocol的協定,它具有所有的String的API,我們可以擴展任何類型去遵循StringProtocol(意思是滿足其所有要求),如此一來,即可以使用該對象,讓它就像是一個String,儘管我們不知道它是什麼!如果看起來像一隻鴨子,游泳像一隻鴨子,叫聲像一隻鴨子,那就是一隻鴨子。我們新的StringProtocol可以告訴那些遵守它協定的類型能夠做什麼,且不需要知道這些類型的資訊。

協定是抽象的,可以使用協定來替你的程式碼抽象化,現在你知道什麼協定是什麼,以及它們在做什麼,讓我們看看如何在實作中使用它們。

Protocols vs. Subclassing

物件導向編程中最常見的設計模式之一稱為類別繼承(Subclassing)。它允許你在類別之間定義父/子關係:

class MyClass { }
class MySubclass: MyClass { }

在上述關係中,MySubclassMyClass的子類。MySubclass將自動繼承所有的MyClass功能,包括屬性、函數、初始化函式等,這意味著所有的MyClass的成員將自動讓MySubclass使用:

class MyClass {
    var myProperty: String {
        return "property"
    }
    
    func myFunction() -> String {
        return "function"
    }
    
    init(string: String) {
        print("Initializing an instance of MyClass with \(string)!")
    }
}

class MySubclass: MyClass() { }

let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MyClass with hello, world!"
print(s.myProperty) //prints "property"
print(s.myFunction()) //prints "function"

子類也可以覆寫override它們父類的函式,代表他們可以用自己的實作代替:

class MySubclass: MyClass() { 
    override var myProperty: String {
        return "overriden property"
    }
    
    override func myFunction() -> String {
        return "overriden function"
    }
    
    override init(string: String) {
        print("Initializing an instance of MySubclass with \(string)!")
    }
}

let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MySubclass with hello, world!"
print(s.myProperty) //prints "overriden property"
print(s.myFunction()) //prints "overridden function"

正如你所看到的,這是一個非常強大的設計模式。它允許開發人員在class之間創建緊密的關係。但是,儘管類別繼承很實用,卻仍無法解決我們在構建應用程式時遇到的每個問題。請看下列範例:

class Animal {
    func makeSound() { }
}

我們剛剛定義了一個Animal的類別,在我們的應用程式中代表不同種類的動物。Cool!我們這邊有一個makeSound()函式,來表示我們的動物擁有的技能。但是有一個問題,動物的聲音都不一樣,那我們可以在makeSound()功能中做什麼?在實作上,我們可能會這樣做:

class Animal {
    func makeSound() { fatalError("Implement me!") }
}

它為Animal中定義的函數添加了一個fatalError,讓Animal成為一個抽象基類,抽象基類只能通過子類實例化。現在,我們可以繼承Animal並定義我們自己的動物:

class Dog: Animal {
    override func makeSound() { print("Woof!") }
}

很好!現在我們有一個Dog的類別,接著我們可以這樣做:

let rex = Dog()
rex.makeSound() //prints "Woof!"

如果我們忘記覆寫makeSound()會發生什麼事? 或者如果我們嘗試直接實例化一個Animal?讓我們來實驗看看:

let tim = Animal()
tim.makeSound() //CRASH
class Cat: Animal { }
let ginger = Cat()
ginger.makeSound() //CRASH

所以,這是一個使用子類化的時候不太理想的情況,我們很常看到這樣的情況。例如,看一下UITableViewDataSourceUITableViewDelegate。我們不能使用類別繼承,因為沒有很好的方法來定義tableview的delegate/data的預設行為。實際上,只要沒有在superclass中預設實作行為,子類繼承就會出錯,讓我們回顧一下我們的animal範例,但是改成使用協定:

protocol Sound {
    func makeSound() 
}

很好,我們定義了一個名為Sound的protocol,它指定使用者必須有一個makeSound()函式,我們只關心它是否符合我們對Sound協定的要求,哪個物件使用它並不重要,讓我們看看這在實踐中如何運作:

struct Dog: Sound {
    func makeSound() {
        print("Woof")
    }
}

struct Tree: Sound {
    func makeSound() {
        print("Susurrate")
    }
}

struct iPhone: Sound {
    func makeSound() {
        print("Ring")
    }
}

這裡Sound是一個協定,我們可以擴展任何類型以符合Sound這個protocol,就像我們在上面的例子中所做的那樣。雖然狗是動物,但樹木和iPhone並不是,如果我們從Animal中將它們進行子類化,其實並不是那麼合理。然而,在使用Sound協定的情況下,這並不重要,對於希望能發出聲音的對象,唯一要求是採用協定並實現所需的方法。

抽象化與協定擴展

我們學到在某些情況下protocol如何替代子類,但是我們來探討另一種用法:抽象化!我們知道協定允許我們定義其他類型可以遵循的預設功能,那讓我們看看,如果使用這種能力來抽像化類型資訊會發生什麼。其實,我們已經看到了一個例子,通過使用Sound協定,替樹木和iPhone添加了像動物一樣的能力!

但是,我們是否可將一些邏輯上相關的類型,卻不是一個繼承共同的父類別,通過單一共通接口使用它們呢?這邊如果讀者不太了解,可以想像一下Swift中的各種數字類型,我們有DoubleFloatInt及其各種類型(Int8Int16等)和UInt及其各種類型,你在算術運算過程中是否嘗試過結合它們? 例如,你有沒有嘗試將一個IntFloat相除,或者將一個DoubleUInt相除?請看看下面這段代碼,它在Swift中無法編譯:

let x: Float = 1.2345
let y: Double = 1.3579
let q = x / y

雖然Swift標準函式庫將各種數值類型定義為各自獨立的類型,但對於我們來說,所有數值類型都適用同一個邏輯:數字

FloatDoubleIntUInt對於我們來說都是數字,我們能否提供一個協定給Swift中的各種數值類型採用,通過一個共同接口Number來使用它們,來看看這是不是可以採取的方法,我們首先定義一個名為Number的協定:

protocol Number {
    var floatValue: Float { get } // the { get } means that the variable must be read only
}

我們這個新的protocol有一個遵循條件:floatValue。從它的宣告可以看到,floatValue是一個變量,它接受其底層類型並將其轉換為Float。因此,我們已經定義了一個Number協定,其中包含一個floatValue宣告要求,這就意味著,任何遵循floatValue的有效實作,我們就當它是一個數字。Cool!現在,我們如何將此協定應用於Swift中的既有類型?

答案就是使用extension,Swift的Extension允許我們擴展類型,它可能是原本我們自己定義的,也可能是既有原生的類型,讓我們來看看:

extension Float: Number {
    var floatValue: Float {
        return self
    }
}

extension Double: Number {
    var floatValue: Float {
        return Double(self)
    }
}

//repeat for Int and UInt

感謝我們的extensions,每個DoubleFloatIntUInt 在我們的應用程序中,現在也都是一個Number。 我們現在可以這樣使用它們:

let x: Double = 1.2345
let f = x.floatValue

很酷,對吧?讓我們做最後一件事來完成我們的Number類型:定義Number的特定運算符,它接受Number的實例(instances)做為參數,並返回Float

//MARK: operator definitions
public func +(lhs: Number, rhs: Number) -> Float {
    return lhs.floatValue + rhs.floatValue
}

public func -(lhs: Number, rhs: Number) -> Float {
    return lhs.floatValue - rhs.floatValue
}

//repeat for * and /

由於我們巧妙地使用protocols和extensions,我們現在可以混用Swift中的數字類型,使用它們來執行算術運算:

let x: Double = 1.2345
ley y: Int = 5
let q = x / y //compiles properly

我們剛剛學到了一個很棒的方式來使用協定:抽象和擴展。通過擴展功能,我們可以定義protocols,然後修改現有的Swift標準庫類型去遵循這個協定,使它們比現有的更強大。

雖然你可能沒有注意到,但是這種方法也啟用了一些名為polymorphism(多型)的東西。多型不是特定於協定導向的編程(多型其實是我們一直與OOP一起使用的東西),它讓我們回到熟悉的物件導向編程原理,在此將其運用在協定導向編程(POP)的環境。在我們的例子中,我們可以使IntFloatDoubleUInt成為Number的實例,同時仍保留他們原來的功能!

在OOP中,當類別繼承其他父類別時就會碰上多型,允許它們的物件能能夠身兼父類別與自身類別的特性,POP更進一步,使我們能夠使用幾行代碼即可在整個APP中應用多型原則。使用協定,我們不需要讓DoubleFloat等類別去繼承其他父類別,我們只需要擴展它們,即可為這些類型的每一個實例添加功能,雖然這是一個非常抽象的概念,但它非常有用,在應用程序中應用多型原理的能力將有助於你編寫安全,強大和乾淨的程式碼。

協定擴展與多重繼承

現在,讀者應該已經了解協定的概念,知道它們到底是什麼,以及它們在代碼中提供的好處。很好!但是我們還沒有談論到協定的另外一個重要特點。這是Swift會從物件導向編程語演進到協議導向編程的關鍵,畢竟,我們在Objective C中也有protocols。那麼為什麼Objective C不曾考慮POP,Swift卻開始使用呢?答案在於protocol extension。 就像我們在上一節中看到的那樣,我們可以在Swift中擴展classes和structs以向它們添加功能。然而,通過protocols讓extensions更加強大,因為它們允許你為協定提供預設功能,
這意味著你可以宣告具有自動滿足要求的protocols。

基本上,協定擴展允許開發者保留子類(繼承)的最佳功能之一,同時獲得協定的所有最佳功能。 我們再回到我們的animal範例吧!

想像一下,我們在一個新的世界裡,每一個動物都有自己獨特的聲音(如:woof,meow,moo等)。但我們先設定一個通用的聲音,這裡將預設的通用聲音為”Wow” 現在可以為我們的Sound協定創建一個extension:

extension Sound {
    func makeSound() {
        print("Wow")
    }
}

現在,若是我們需要宣告一個動物類型可以參考下列格式:

extension MyType: Sound { }

除了添加符合協定要求的宣告外,我們不需要在做其他事,但也可以覆寫預設的功能(就像classes一樣):

extension Snake: Sound {
    func makeSound() {
        print("hiss")
    }
}

所以,這就是使用協定擴展的方法。但等一下,這看起來很像類別繼承,我們正在為我們的功能定義預設值,提供它的預設的實作方法,然後可以選擇性地覆寫它。為什麼我們在這樣的情況下使用協定,而不是子類化?

答案是多重繼承。當你定義一個類別時,它可以有0個或1個父類別,但不能同時拿兩個父類別去定義一個子類別,換句話說,就是無法從兩個父類別繼承功能,但是協定沒有這個限制,物件可以應需求去使用多個所需的協定,繼承所有協定的預設功能。此外,類別可以有選擇地覆寫從協定繼承的功能,就像類別一樣,如下所示:

protocol Sound {
    func makeSound()
}

extension Sound {
    func makeSound() {
        print("Wow")
    }
}

protocol Flyable {
    func fly()
}

extension Flyable {
    func fly() {
        print("✈️")
    }
}

class Airplane: Flyable { }
class Pigeon: Sound, Flyable { }
class Penguin: Sound { }


let pigeon = Pigeon()
pigeon.fly()  // prints ✈️
pigeon.makeSound() // prints Wow

在我們的範例中,我們定義了兩個protocols,分別是SoundFlyable。我們已經知道了什麼是Sound,但是現在我們知道有能夠使用fly()的東西是Flyable協定。然後,我們定義了一個名為Airplane的類別,假設這架飛機沒有任何聲音,所以Airplane只繼承Flyable,並且繼承了它附帶的預設功能。

相反的,企鵝不能飛,所以我們的Penguin類別採用Sound協定,但是由於它不能飛,因此,Penguin不會繼承Flyable

上面範例比較有趣的一點在Pigeon(鴿子)鴿子不僅可以發出聲音而且也會飛。我們的Pigeon類別自動繼承了飛行的能力和發出聲音的能力。如果SoundFlyable被定義為類別,則Pigeon只能從其中一個繼承功能,而不是兩者。擴展可以說是protocols最有用的功能之一,因為它們允許class和struct能繼承多個其他類型的功能,這是傳統的class/subclass結構無法實現的,無論它的設計如何巧妙。

通過組合protocols和protocol extensions,我們可以使用我們最喜歡的OOP功能(繼承),同時獲得protocols的所有附加優點。協定更安全,更易於使用,並且保持我們的類別結構簡單。此外,使用協定允許我們同時繼承多個parents。很酷,對吧?

Protocols和OOP

我們在本教程中學到了很多關於協定的內容。但是,記住一件很重要的事情,沒有一個編程模式可以解決你在開發上的每一個問題。自從iOS開始以來,我們已經看到物件導向編程、響應式編程、協定導向編程以及無數的其他編程範例。

在應用程式中使用多種編程模式是相當盛行的,只侷限在單一設計模式容易陷入困境,你必須記住,做為一名軟體工程師,你有很多工具可供你使用,只使用一個是不明智的。協定和協定導向編程不是替代OOP的。相反的,它們是用來補充其他編程的不足。當你使用Swift構建下一個應用程序、網站或後端服務時,請記住不要陷入試圖使單一編程方法成為聖杯的陷阱,你必須具有適應性和靈活性,分析每種情況,以確定正確的解決方案。

我希望讀者可以從本教程中獲得很多有價值的知識,如果你喜歡,請不吝與你的朋友和社群分享。謝謝!

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文A Beginner’s Guide to Protocols and Protocol Extensions in Swift

作者
Pranjal Satija
現時為高中生,喜歡在課餘時間創作App,享受把創作App的經驗與人分享。除此之外還喜歡滑雪、高爾夫球、足球,與朋友在一起。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。