Swift中協議的簡單介紹
前言
熟悉objective-c語言的同學們肯定對協議都不陌生,在Swift中蘋果將 protocol 這種語法發揚的更加深入和徹底。Swift中的 protocol 不僅能定義方法還能定義屬性,配合 extension 擴展的使用還能提供一些方法的默認實現,而且不僅類可以遵循協議,現在的枚舉和結構體也能遵循協議了。基于此本文從 1,協議中定義屬性和方法 , 2,協議的繼承、聚合、關聯類型 , 3,協議的擴展 , 4,Swift標準庫中常見的協議 , 5,為什么要使用協議 5個方面結合自身的學習經驗簡單介紹一下這種 “加強型 protocol ” 的使用,入門級、屬于學習總結,希望能給正在學習Swift的小伙伴們一點啟發。
協議中定義屬性和方法
-
協議的定義
協議為方法、屬性、以及其他特定的任務需求或功能定義藍圖。協議可被類、結構體、或枚舉類型采納以提供所需功能的具體實現。滿足了協議中需求的任意類型都叫做遵循了該協議(引自文檔官方文檔)。
Swift中定義一個協議和定義枚舉、結構體或者類的格式是相同的,使用 protocol 關鍵字:
//定義一個名字為學生協議
protocol Student {
}
-
這里 Student 是使用 protocol 關鍵字聲明的一個協議,和枚舉、結構體、類命名原則相似, Student 首字母大寫表示在以后的使用過程中很可能會將 Student 看作是一個類型使用。
-
協議中定義屬性
協議中定義屬性表示遵循該協議的類型具備了某種屬性,具體來說只能使用 var 關鍵字聲明并且必須明確規定該屬性是可讀的 get 、還是可讀可寫的 get set ,另外還可以通過關鍵字 static 聲明一個類型屬性。示例如下:
protocol Student {
//定義一個可讀可寫的 name 屬性
var name: String { get set }
//定義一個可讀的 birthPlace 屬性
var birthPlace: String { get }
//定義一個類屬性 record
static var qualification: String {get}
}
-
和定義方法一樣,我們只需要確定該屬性具體是什么類型并且添加對應的關鍵字,不需要具體的實現,更不能為他們賦上初始值(類似于計算屬性)。定義好屬性之后,我們就可以利用屬性來做點事情了。
struct Puple: Student {
static var qualification: String = "小學"
var name: String
var birthPlace: String = "北京"
}
var p1 = Puple(name: "小明", birthPlace: "上海")
-
定義一個 Puple 結構體遵循 Student 協議,該結構體中必須存在協議要求聲明的三個屬性 matrikelnummer 、 name 、 birthPlace , static 修飾的類型屬性必須被有初始值或者存在 get 、 set 方法。對于普通的實例屬性協議并不關心是計算型屬性還是存儲型屬性。實例中的屬性同樣可以被修改:
var p1 = Puple(name: "小明", birthPlace: "上海")
Puple.qualification = "中學"
看到這里有的同學可能有些疑問, birthPlace 、 qualification 明明只有 get 方法為什么卻可以修改賦值呢?其實協議中的“只讀”屬性修飾的是協議這種“類型“”的實例,例如下面的例子:
var s1: Student = p1
s1.birthPlace = "廣州"
雖然我們并不能像創建類的實例那樣直接創建協議的實例,但是我們可以通過“賦值”得到一個協議的實例。將 p1 的值賦值給 Student 類型的變量 s1 ,修改 s1 的 birthPlace 屬性時編譯器就會報錯: birthPlace 是一個只讀的屬性,不能被修改。如果 Puple 中還存在 Student 沒有的屬性,那么在賦值過程中 s1 將不會存在這樣的屬性,盡管這樣做的意義并不大,但是我們從中知道了協議中 get 、 set 的具體含義。
-
協議中定義方法
和objective-c類似,Swift中的協議可以定義類型方法或者實例方法,方法的參數不能有默認值(Swift認為默認值也是一種變相的實現),在遵守該協議的類型中具體實現方法的細節,通過類或實例調用:
protocol Student {
//類方法
static func study()
//實例方法
func changeName()
}
struct CollageStudent: Student {
//類方法實現
static func study() {
}
//實例方法實現
func changeName() {
}
}
//方法的調用
CollageStudent.study()
var c1 = CollageStudent()
c1.changeName()
注意:當我們在結構體中的方法修改到屬性的時候需要在方法前面加上關鍵字 mutating 表示該屬性能夠被修改(如果是類不需要添加 mutating 關鍵字),這樣的方法叫做: 異變方法 ,和 “在實例方法中修改值類型” 的處理是一樣的。
protocol Student {
mutating func changeName()
}
struct CollageStudent: Student {
mutating func changeName() {
self.name = "小明"
}
}
var c1 = CollageStudent()
c1.changeName()
-
協議中的初始化器
我們可以在協議中定義遵循協議的類型需要實現的指定初始化器(構造函數)或者便捷初始化器。
protocol Pet {
init(name: String)
}
class Cat: Pet {
var name: String = "Cat"
required init(name: String) {
self.name = name
}
}
Cat 由于遵循了 Pet 協議,應該用 required 關鍵字修飾初始化器的具體實現。
如果一個類既繼承了某個類,而且遵循了一個或多個協議,我們應該將父類放在最前面,然后依次用逗號排列。
class SomeClass: OneProtocol, TwoProtocol {
}
這是因為Swift中類的繼承是單一的,但是類可以遵守多個協議,因此為了突出其單一父類的特殊性,我們應該將繼承的父類放在最前面,將遵守的協議依次放在后面。
-
多個協議重名方法調用沖突
由于在Swift中并沒有規定不同的協議內方法不能重名(這樣的規定也是不合理的)。因此我們在自定義多個協議中方法重名的情況是可能出現的,比如有 TextOne 、 TextTwo 兩個協議,定義如下:
protocol TextOne {
func text() -> Int
}
protocol TextTwo {
func text() -> String
}
這兩個協議中的 text() 方法名相同返回值不同,如果存在一個類型 Person 同時遵守了 TextOne 和 TextTwo ,在 Person 實例調用方法的時候就會出現歧義。
struct Person: TextOne, TextTwo {
func text() -> Int {
return 10
}
func text() -> String {
return "hello"
}
}
let p1 = Person()
//嘗試調用返回值為Int的方法
let num = p1.text()
//嘗試調用返回值為String的方法
let string = p1.text()
上面的調用肯定是無法通過的,因為編譯器無法知道同名 text() 方法到底是哪個協議中的方法,那么出現這種情況的根本原因在于調用哪個協議的 text() 不確定,因此我們需要指定調用特定協議的 text() 方法,改進后的代碼如下:
//嘗試調用返回值為Int的方法
let num = (p1 as TextOne).text()
//嘗試調用返回值為String的方法
let string = (p1 as TextTwo).text()
也可以理解為在進行調用前將 p1 常量進行類型轉換。
協議的繼承、聚合、關聯類型
-
協議的繼承
協議可以繼承一個或者多個其他協議并且可以在它繼承的基礎之上添加更多要求。協議繼承的語法與類繼承的語法相似,選擇列出多個繼承的協議,使用逗號分隔:
protocol OneProtocol {
}
protocol TwoProtocol {
}
//定義一個繼承子OneProtocol 和 TwoProtocol協議的新的協議: ThreeProtocol
protocol ThreeProtocol: OneProtocol, TwoProtocol {
}
如上所示,任何遵守了 ThreeProtocol 協議的類型都應該同時實現 OneProtocol 和 TwoProtocol 的要求必須實現的方法或屬性(引自官方文檔,概念比較簡單)。
-
協議的聚合
日常開發中要求一個類型同時遵守多個協議是很常見的,除了使用協議的繼承外我們還可以使用形如 OneProtocol & TwoProtocol 的形式實現協議聚合(組合)復合多個協議到一個要求里。例如:
//協議聚合成臨時的類型
typealias Three = TwoProtocol & OneProtocol
//協議聚合成為參數的類型
func text(paramter: OneProtocol & TwoProtocol) {
}
一個很常見的例子:定義 text 函數的參數類型使用了協議的聚合,在這里我們并不關心 paramter 是什么類型的參數,只要它遵循這兩個要求的協議即可。
-
繼承和聚合在使用上的區別
善于思考的同學可以發現,要實現上面的 paramter參數的類型是遵守OneProtocol 和 TwoProtoco 的效果,完全可以使用協議的繼承,新定義一個協議 ThreeProtocol 繼承自 OneProtocol 和 TwoProtocol ,然后指定 paramter 參數的類型是 ThreeProtocol 類型。那么這兩種方法有何區別呢?首先 協議的繼承 是定義了一個全新的協議,我們是希望它能夠“大展拳腳”得到普遍使用。而 協議的聚合 不一樣,它并沒有定義新的固定協議類型,相反,它只是定義一個臨時的擁有所有聚合中協議要求組成的局部協議,很可能是“一次性需求”,使用 協議的聚合 保持了代碼的簡潔性、易讀性,同時去除了定義不必要的新類型的繁瑣,并且定義和使用的地方如此接近,見明知意,也被稱為 匿名協議聚合 。但是使用了 匿名協議聚合 能夠表達的信息就少了一些,所以需要開發者斟酌使用。
-
協議的檢查
如何檢查某個類型是否遵循了特定的協議?:使用關鍵字 is ,同時該運算符會返回一個Bool值用于判斷條件是否成立。
struct Person: OneProtocol {
}
let p1 = Person()
if (p1 is OneProtocol){ //可以理解為:p1 是一個遵守了OneProtocol協議類型的實例
print("yes")
}
如何讓定義的協議只能被類遵守?:使用關鍵字 class ,該關鍵字修飾之后表示協議只能被類遵守,如果有枚舉或結構體嘗試遵守會報錯。
//只能被類遵守的協議
protocol FourProtocol: class ,ThreeProtocol {
}
//此處報錯
struct Person: FourProtocol {
}
class Perple: FourProtocol {
}
-
關聯類型
協議的關聯類型指的是根據使用場景的變化,如果協議中某些屬性存在“邏輯相同的而類型不同”的情況,可以使用關鍵字 associatedtype 來為這些屬性的類型聲明“關聯類型”。
protocol WeightCalculable {
//為weight 屬性定義的類型別名
associatedtype WeightType
var weight: WeightType { get }
}
WeightCalculable 是一個“可稱重”協議, weight 屬性返回遵守該協議具體類型的實例的重量。這里我們使用 associatedtype 為該屬性的類型定義了一個別名 WeightType ,換言之在 WeightCalculable 中并不關心 weight 的類型是 Int 還是 Double 或者是其他類型,他只是簡單的告訴我們返回的類型是 WeightType ,至于 WeightType 到底是什么類型由遵守該協議的類中自己去定義。那么這樣做的好處是什么呢?
//定義手機結構體
struct MobilePhone: WeightCalculable {
typealias WeightType = Double
var weight: WeightType
}
let iPhone7 = MobilePhone(weight: 0.138)
//定義汽車結構體
struct Car: WeightCalculable {
typealias WeightType = Int
var weight: WeightType
}
let truck = Car(weight: 3000_000)
如上所示: MobilePhone 、 Car 類型都遵守了 WeightCalculable 協議,都能被稱重,但是手機由于結構精密、體型小巧,小數點后面的數字對于稱重來說是必不可少的,所以使用了 Double 類型,返回 0.138千克 即 138克 ,但是對于汽車這樣的龐然大物在稱重時如果還計較小數點后面的數字就顯得沒有意義了,所以使用 Int 類型,表示 3000千克 也就是 3噸 。
從上面的例子可以很好的看出由于 MobilePhone 、 Car 稱重時邏輯是一樣的,但是對于 weight 屬性的返回值要求不一樣,如果僅僅因為返回值類型的不同定義兩個類似的協議一個是 Int 類型另外一個是 Double 類型,這樣做顯然是重復的、不合適的。所以 associatedtype 在這種情況下就發揮出作用了,他讓開發者在遵守協議時根據需求去定義返回值的類型,而不是在協議中寫死。唯一要注意的是:一定要在遵守該協議的類型中使用 typealias 規定具體的類型。不然編譯器就報錯了。
協議的擴展
協議的擴展是協議中很重要的一部分內容,主要體現在以下兩個方面:
-
擴展協議的屬性和方法
我們通過一個常見的例子說明一下:
protocol Score {
var math: Int { get set}
var english: Int {get set}
func mathPercent() -> Double
}
首先定義一個 Score 協議,里面有兩個 Int 類型的屬性 math 、 english 和一個計算數學所占分數的比例的方法 mathPercent 。
struct Puple: Score {
var math: Int
var english: Int
func mathPercent() -> Double {
return Double(math) / Double(math + english)
}
}
定義 Puple 遵守該協議,實現了必要的屬性和方法。
let p1 = Puple(math: 90, english: 80)
s1.mathPercent()
通過上面的代碼可以計算出 s1 中數學所占的比例,但是設想一下如果還有很多個類似 Puple 結構體的類型都需要遵守該協議,都需要默認實現 mathPercent 方法計算出自己的數學分數所占的比例,還是按照上面的寫法代碼量就很大而且很冗雜了。問題的關鍵在于:任何遵守 Score 協議類型的 mathPercent 計算邏輯是不變的,而且需要默認實現。那么我們如何輕松的實現這樣的效果呢,答案是:為 Score 添加方法的擴展。
extension Score {
func mathPercent() -> Double {
return Double(math) / Double(math + english)
}
}
將 mathPercent 的具體實現寫在協議的擴展里面,就能為所有的遵守 Score 的類型提供 mathPercent 默認的實現。
struct CollageStudent: Score {
var math: Int
var english: Int
}
let c1 = CollageStudent(math: 80, english: 80)
c1.mathPercent()
如此就能起到“不實現 mathPercent 方法也能計算出數學所占分數的比例”的效果了。此語法在Swift中有一個專業術語叫做: default implementation 即默認實現。包括計算屬性和方法的默認實現,但是不支持存儲屬性,如果遵循類型給這個協議的要求提供了它自己的實現,那么它就會替代擴展中提供的默認實現。
通過這樣的語法,我們不僅能為自定義的協議提供擴展,還能為系統提供的協議添加擴展,例如,為 CustomStringConvertible 添加一個計算屬性默認實現的擴展:
extension CustomStringConvertible {
var customDescription: String {
return "YQ" + description
}
}
-
為存在的類型添加協議遵守
擴展一個已經存在的類型來采納和遵循一個新的協議,無需訪問現有類型的源代碼。擴展可以添加新的屬性、方法和下標到已經存在的類型,并且因此允許你添加協議需要的任何需要(引自文檔翻譯)。
簡單的來說我們可以對存在的類型(尤其是系統的類型)添加協議遵守。盡管這更像是對“類型的擴展”,但是官方文檔將這部分放在了協議的擴展中。
extension Double : CustomStringConvertible {
/// A textual representation of the value.
public var description: String { get }
}
上面的代碼就是Swift標準庫中對于 Double 類型添加的一個協議遵守。除了添加系統協議的遵守,我們還可以添加自定義的協議的遵守,其方法都是一樣的,這里就不太贅述了。
-
總結
通過協議的擴展提供協議中某些屬性和方法的默認實現,將公共的代碼和屬性統一起來極大的增加了代碼的復用,同時也增加了協議的靈活性和使用范圍,這樣的協議不僅僅是一系列接口的規范,還能提供相應的邏輯,是面向協議編程的基礎。
Swift標準庫中常見的協議
學習完協議的基礎語法,我們大致熟悉一下Swift標準庫中提供的協議。
55個標準庫協議
Swift標準庫為我們提供了55種協議,他們的命名很有特點,基本是以"Type"、“able”、“Convertible”結尾,分別表示該協議“可以被當做XX類型”、“具備某種能力或特性”、“能夠進行改變或變換”。因此在自定義協議時應該盡可能遵守蘋果的命名規則,便于開發人員之間的高效合作。下面介紹一下常見的幾種協議:
-
Equatable
Equatable 是和 比較相關 的協議,遵守該協議表示實例 能夠用于相等的比較 ,需要重載 == 運算符。
struct Student: Equatable {
var math: Int
var english: Int
}
//重載 == 運算符
func == (s1: Student, s2: Student) -> Bool {
return s1.math == s2.math && s1.english == s2.english
}
Student 遵守 Equatable 并且重載了 == 運算符后就能直接比較兩個學生的成績是否相等了。
let s1 = Student(math: 80, english: 60)
let s2 = Student(math: 70, english: 90)
s1 == s2 //false
值得注意的是,由于重載 == 運算符是遵守 Equatable 協議后要求我們實現的,因此重載方法應該緊跟在遵守該協議的類型定義后,中間不能有其他的代碼,否則就報錯了。
-
Comparable
Comparable 是和 比較相關 的第二個協議,遵守該協議表示實例 能夠進行比較 ,需要重載 < 運算符。
struct Student: Comparable{
var math: Int
var english: Int
}
//重載 < 運算符
func < (s1: Student, s2: Student) -> Bool {
return (s1.math + s1.english) < (s2.math + s2.english)
}
let s1 = Student(math: 80, english: 60)
let s2 = Student(math: 70, english: 90)
s1 < s2 //true
-
CustomStringConvertible
CustomStringConvertible 提供了一種 用文本表示一個對象或者結構 的方式,可以在任何遵守該協議的類型中自定義表示結構的文本,需要覆蓋 description 屬性。
struct Student: CustomStringConvertible{
var math: Int
var english: Int
var description: String {
return "Your math:" + String(math) + ", english:" + String(english)
}
}
let s1 = Student(math: 80, english: 60)
print(s1) // Your math:80, english:60
-
ExpressibleByArrayLiteral
ExpressibleByArrayLiteral 提供了 使用數組文本初始化的類型 的能力,具體來說使用逗號分隔的值、實例、字面值列表,方括號以創建數組文本。遵守該協議需要實現 init(arrayLiteral elements: Person.Element...) 方法。
struct Person: ExpressibleByArrayLiteral {
var name: String = ""
var job: String = ""
typealias Element = String
init(arrayLiteral elements: Person.Element...) {
if elements.count == 2 {
name = elements[0]
job = elements[1]
}
}
}
let p1: Person = ["jack", "teacher"]
print(p1.name) //jack
print(p1.job) //teacher
上面的代碼用到了之前關聯類型,通過遵守ExpressibleByArrayLiteral,現在的Person就可以使用數組直接創建實例了。類似的協議還有ExpressibleByDictionaryLiteral、ExpressibleByStringLiteral、ExpressibleByBooleanLiteral 、ExpressibleByIntegerLiteral等等,相信大家通過名稱就能大概猜出具體作用,由于篇幅有限這里就不再贅述了。
為什么要使用協議
-
協議可以作為類型使用
協議作為一種類型是蘋果在Swift中提出的,并且在官方文檔中還為我們具體指出了可以將協議當做類型使用的場景:
1,在函數、方法或者初始化器里作為形式參數類型或者返回類型;
2,作為常量、變量或者屬性的類型;
3,作為數組、字典或者其他存儲器的元素的類型。
-
協議可以解決面向對象中一些棘手的問題
寵物類圖
如圖所示的類結構圖中 麻雀 在寵物類圖中的位置顯得比較尷尬,之所以尷尬是因為 麻雀 作為一種鳥類,應該繼承 鳥 ,但是如果繼承了 鳥 ,就相當于默認了 麻雀 是一種 寵物 ,這顯然是不和邏輯的。解決此問題的一般方法如下:
寵物類圖
乍一看好像解決了這樣的問題,但是仔細想由于Swift只支持單繼承, 麻雀 沒有繼承 鳥 類就無法體現 麻雀 作為一種鳥擁有的特性和方法(比如飛翔),如果此時出現一個新的 飛機 類,雖然 飛機 和 寵物 之間沒有任何聯系,但是 飛機 和 鳥 是由很多共同特性的(比如飛翔),這樣的特性該如何體現呢?答案還是新建一個類成為 動物 和 飛機 的父類。面向對象就是這樣一層一層的向上新建父類最終得到一個“超級父類”在OC和Swift中是 NSObject ,盡管問題得到了解決,但是 麻雀 與 鳥 、 飛機 與 鳥 之間的共性并沒有得到很好的體現。而協議的出現正是為了解決這類問題。
寵物類圖
實際上寵物類圖中包括 動物 、 鳥 、 飛機 等類之間的關系就應該是如上圖所示的繼承關系。使用協議將“寵物”、“飛翔”等關系看作是一種特性,或者是從另一個維度描述這種類別,更重要的是使用協議并不會打破原有類別之間繼承的父子關系。和飛翔相關的代碼統一放在 Flyable 中,需要“飛翔”這種能力遵守該協議;和寵物相關的代碼統一放在 PetType 中,需要成為寵物遵守該協議。這些協議靈活多變結合原有的面向對象類之間固有的繼承關系,完美的描述了這個世界。這幅包含了協議的寵物類圖是本人在學習中印象最深刻的,分享出來與大家共勉。
Swift中的協議更多的時候是在描述某種屬性,是否應該將“寵物”設計成一個類或者是一個協議,應該根據當前項目的需求。如果你的世界沒有 麻雀 、 飛機 ,那么將“寵物”設計成一個類也是未嘗不可甚至是非常合理的,這點需要我們仔細思考。
學習使用協議就不得不提到通過協議語法延伸出來的 面向協議編程范式 ,蘋果提出Swift是一門支持面向協議編程的語言,甚至提倡我們通過協議、結構體代替部分類的使用,可見協議這種語法以及面向協議編程思想在Swift中是多么的重要。
-
文章最后
以上就是本人前段時間學習心得,示例代碼在Swift3.0語法下都是編譯通過的,知識點比較雜,部分描述引自官方的文檔,另外協議作為一種強大的語法肯定還有很多值得我們去探索,本文列出的知識點僅僅涵括了部分內容。如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來;喜歡我的文章,可以關注我以此促進交流學習。
來自:http://www.cocoachina.com/swift/20161219/18376.html