iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
0
Software Development

Swift 菜鳥的30天系列 第 29

Day-29 Swift 語法(25) - 協定 Protocol

Protocol好長啊。大挑戰R
鐵人終於只剩一天了...


協定 Protocol

協定是 Swift 一個重要的特性,它會定義出為了完成某項任務或功能所需的方法、屬性,但是本身不會實作這些任務跟功能,而僅僅只是表達出該任務或功能的名稱。協定為方法、屬性、以及其他特定的任務需求或功能定義藍圖。協定可被 class、struct、或 enum 類型採納以提供所需功能的具體實現。滿足了協定中需求的任意類型都叫做遵循了該協定。

除了指定遵循類型必須實現的要求外,你可以擴展一個協定以實現其中的一些需求或實現一個符合類型的可以利用的附加功能。


協定語法

使用 protocol 關鍵字來定義協定:

protocol SomeProtocol {
    protocol 定義的內容
}

要讓自定義的類型遵循協定時,寫法類似繼承,一樣把協定名稱寫在類型名稱的冒號 (:) 後方,表示該類型採納這個協定。若要遵循多個協定則使用 (,) 分隔每個協定,如下面所示:

struct SomeStructure: SomeProtocol, AnotherProtocol {
    // struct 定義的內容
}

若 class 要繼承父類與遵循協定時,應該先將父類名稱寫在前面,接著才是協定名稱,一樣以逗號 (,) 分隔,如下面所示:

class SomeClass: SomeSuperclass, SomeProtocol, AnotherProtocol {
    // class 定義的內容
}

屬性要求

協議可以要求所有遵循該協議的類型提供特定名字和類型的實例屬性或類型屬性。協議並不會具體說明屬性是儲存屬性還是計算屬性,它只具體要求屬性有特定的名稱和類型。協議同時要求一個屬性必須明確是可讀的或可讀寫的。

若協議要求一個屬性為可讀寫的,那麼該屬性要求不能用常數存儲屬性或只讀計算屬性。若協議只要求屬性為可讀的,那麼任何種類的屬性都能滿足這個要求,而且如果你的程式碼需要的話,該屬性也可以是可寫的。

協定屬性要求定義為變數屬性,使用 var 關鍵字。使用 { get set } 寫在宣告後面來表示是可讀寫的屬性,使用 { get } 來表示可讀的屬性。

protocol SomeProtocol {
    var readOnly: Int { get } // 唯讀變數
    var readAndWritable: Int { get set } // 可讀寫變數
}

在協議中定義類型屬性時在前面加上 static 關鍵字。當 class 的實現使用 class 或 static 關鍵字宣告類型屬性要求時,這個規則仍然適用:

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

下面我們定義一個協定,包含一個唯讀的 String:

protocol FullyNamed {
    var fullName: String { get }
}

上面 FullyNamed 協議要求遵循的類型提供一個完全符合的名字。這個協議它只要求這個屬性必須為其提供一個全名。協議宣告了所有的 FullyNamed 類型必須有一個可讀實例屬性 fullName,為 String 類型。之後定義一個 struct 遵循上面的協議:

struct Person: FullyNamed {
    var fullName: String
}

let jeremy = Person(fullName: "Jeremy Xue")

每個 Person 的實例都有一個名為 fullName 的 String 儲存屬性。這符合了 FullyNamed 協議的要求,並且表示 Person 已經正確地遵循了該協議。若沒達成協議所定義的要求,那麼 Swift 這時會編輯錯誤。

下面我們再遵循上面的協議,創建一個 Class:

class ChineseName:FullyNamed {
    var lastName:String?
    var name: String
    init(name:String,lastName:String? = nil) {
        self.name = name
        self.lastName = lastName
    }
    var fullName: String{
        return name + " " + (lastName != nil ? lastName!:"")
    }
}

上面這個範例我們新增了兩個 String 屬性,其中 lastName 是可選的字串 String?,這兩個屬性透過初化器賦值給他們,遵循的 fullName 則接收這兩個屬性的結合,其中要是我們 lastName 為 nil 那麼就回傳一個空字串,如果有值就放在 name 後方(name + lastName),創建我們的全名。

https://ithelp.ithome.com.tw/upload/images/20180116/20107701frb5vcHE4T.png


方法要求

協議可以要求採納的類型實現指定的實例方法和類型方法。這些方法作為協議定義的一部分,編寫方式與實例和類型方法的方式相同,但是不需要大括號和方法的主體。允許變數擁有參數,與正常的方法使用同樣的規則。但在協議的定義中,方法參數不能定義默認值。

如同類型屬性要求的那樣,當協議中定義類型方法時,你要在它之前加上 static 關鍵字。即使在 class 實現時,類型方法要求使用 class 或 static 作為關鍵字前綴,前面的規則仍然適用:

protocol SomeProtocol {
    static func someTypeMethod()
}

這邊我舉一個簡單的費氏數列的方法來用在這個範例上,~~ 不知道為啥蘋果的範例是 Linear congruential generator (線性同餘法,LCG),原本想做一個類似的,但是看到眼花QQ ~~ :

下面我們定義了只有一個方法要求的協議:

protocol Fibonacci {
    func calculate() -> Int
}

再新增一個名為 FibonacciNum 的 class 遵循我們上面的 Fibonacci 的協定,calculate()方法也要新增,否則會報錯:

class FibonacciNum:Fibonacci{
    var a = 0
    var b = 1
    var total = 0
    
    func calculate() -> Int {
        total = a + b
        a = b
        b = total
        print(total)
        return total
    }
}

之後將它放到實例中測試,結果為下:

https://ithelp.ithome.com.tw/upload/images/20180116/20107701OoP6xCwQSO.png


異變方法要求

有時一個方法需要改變(或異變)其所屬的實例,在方法的 func 之前使用 mutating 表示在該方法可以改變其所屬的實例,以及該實例的所有屬性。

若你定義了一個協議的實例方法需求,想要異變任何採用了該協議的類型實例,只需在協議裡方法的定義當中使用 mutating 關鍵字。這允許 struct 和 enum 類型能採用相應協議並滿足方法要求。

如果將協議實例方法要求標記為 mutating,則在為該 class 編寫該方法的實現時,不需要加上 mutating 關鍵字。 mutating關鍵字僅用於 struct 和 enum。

下面我們編寫一個 Change 協議,其中 change() 的方法使用 mutating 關鍵字標記,來表示此方法在調用時會改變遵循該協議的實例的狀態:

protocol Change {
    mutating func change()
}

之後我們新增一個 enum ,有著上下左右的情況,並且在異變 change() 方法中新增一個 switch 並根據我們 DirectionChange 中的情況來進行不同的操作,我們裡面是將方向都做顛倒並印出現在的方向:

enum DirectionChange:Change {
    case up, down, left, right
    mutating func change() {
        switch self {
        case .up:
            self = .down
            print("方向顛倒,現在是下")
        case .down:
            self = .up
            print("方向顛倒,現在是上")
        case .left:
            self = .right
            print("方向顛倒,現在是右")
        case .right:
            self = .left
            print("方向顛倒,現在是左")
        }
    }
}

結果如下:

https://ithelp.ithome.com.tw/upload/images/20180116/20107701vLGmzzbg1I.png


初始化器要求

協議可以要求遵循協議的類型實現指定的初始化器。和一般的初始化器一樣,只用將初始化器寫在協議的定義中,不需要寫大括號,也就是初始化器的實體:

protocol SomeProtocol {
    init(someParameter: Int)
}

協議初始化器要求的類實現

你可以透過實現指定初始化器或便捷初始化器來使遵循該協議的類滿足協議的初始化器要求。在這兩種情況下,你都必須使用 required 關鍵字修飾初始化器的實現(如果 class 已被加上final,則不需要為其內的初始化器加上required,因為 final 類別不能再被子類別繼承):

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 初始化內容
    }
}

如果一個子類重寫了父類指定的初始化器,並且遵循協議實現了初始化器要求,那麼就要為這個初始化器的實現加上 required 和 override 兩個修飾符:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        
    }
}

class SomeSubClass:SomeSuperClass, SomeProtocol {
    required override init() {
        
    }
}

可失敗初始化器要求

協議可以為遵循該協議的類型定義可失敗的初始化器。遵循協議的類型可以使用一個可失敗 (init?) 的或不可失敗(init) 的初始化器滿足一個可失敗的初始化器要求。不可失敗初始化器要求可以使用一個不可失敗初始化器 (init)或隱式展開的可失敗初始化器 (init!) 滿足。


將協議作為類型

因為協議自身並不實現功能。但是所創建的協議都可以變為一個功能完備的類型在程式碼中使用。由於它是一個類型,你可以在很多其他類型可以使用的地方使用協議,像是:

  • 在函數、方法或者初始化器裡作為形式參數類型或者返回類型;
  • 作為常數、變數或者屬性的類型;
  • 作為數組、字典或者其他存儲器的元素的類型。

我們下面定一個簡單的例子,先新增一個只有要求一個方法的協議 SomeProtocol,之後新增一個 SomeClass 來遵循這個協議,並在 msg() 方法中回傳一個 String ,最後,我們再新增一個 AnotherClass 裡面有一個屬性類型為 SomeProtocol 的常數 msg,並透過初始化器賦值給 msg :

protocol SomeProtocol {
    func msg() -> String
}

class SomeClass:SomeProtocol {
    func msg() -> String {
        return "協議作為類型"
    }
}

class AnotherClass {
    let msg:SomeProtocol
    init(msg:SomeProtocol) {
        self.msg = msg
    }
}

之後我們先新增一個 SomeClass 的實例 SomeInstance,之後我們新增一個 AnotherClass 的實例 AnotherInstance , 在初始化時我們使用 SomeInstance 作為他 SomeProtocol 類型的參數傳入,因為任何遵循 SomeProtocol 協定的實例,都可以被當做 SomeProtocol 類型:

let SomeInstance = SomeClass()
let AnotherInstance = AnotherClass(msg: SomeInstance)

結果如下:

https://ithelp.ithome.com.tw/upload/images/20180117/20107701bwRiqQ4bQx.png


委託

委託是一個允許 class 或者 struct 委託它們自身的某些責任給其他類型實例的設計模式,這個設計模式通過定義一個封裝了委託責任的協議來實現,比如遵循了協議的類型來保證提供被委託的功能。委任可以用來回應特定的動作或是接收外部資料,而不需要知道外部資料的類型。

我們使用上次的費氏數列來操作這個委託例子,首先我們建立兩個各有一個方法需求的協定,並新增了 a、b、c 三個變數,再來新增兩個 class,其中 Fibonacci 遵循 Calculate 協定;FibonacciValue 則是遵循 NewValue :

protocol Calculate {
    func result()
}

protocol NewValue {
    func newValue()
}

var a = 0
var b = 1
var c = 0

class Fibonacci:Calculate {
    var delegate:NewValue?
    
    func result() {
        c = a + b
        print(c)
        delegate?.newValue()
    }

}

class FibonacciValue:NewValue {
    func newValue() {
        a = b
        b = c
    }
}

let fib = Fibonacci()
let fibValue = FibonacciValue()
fib.delegate = fibValue

fib.result()

在 Fibonacci 的 class 中我們有宣告一個變數他的類型為 NewValue? 為可選類型,因為不是一定需要委託。所以 delegate 會先初始化為 nil 之後,再將其設置為負責其他動作的另一個類型的實例,delegate?.NewValue 再將其他動作委任給另一個類型的實體實作,接著我們將它分配給兩個實例,其中 fib.delegate 屬性設為委任的 fibValue 實例,之後我們便能透過 fib.result() 來計算費氏數列:

https://ithelp.ithome.com.tw/upload/images/20180117/201077016w7yaV8gs8.png


使用擴展聲明採納協議

你也可以讓擴展遵循協定,這樣就可以在不修改原始程式碼的情況下,讓已存在的類型經由擴展來遵循一個協定。當已存在類型經由擴展遵循協定時,這個型別的所有實體也會隨之獲得協定中定義的功能。我們用上面費氏數列的例子,讓他遵循一個新的協定:

protocol Value {
    func value()
}

extension Fibonacci:Value {
    func value() {
        print("現在 a 的值為:\(a)")
        print("現在 b 的值為:\(b)")
    }
}

之後我們便能透過 fib.value() 方法來查看目前 a 和 b 的值為何:
https://ithelp.ithome.com.tw/upload/images/20180117/20107701IgU6ycikgp.png


使用擴展宣告採納協議

如果一個類型已經遵循了協議的所有要求,但是還沒有宣告它採用這個協議,你可以透過一個空的擴展來讓它採納這個協議:

protocol SomeProtocol {
    var name: String {get set}
}
// 定義一個 class 滿足了 SomeProtocol 的要求,但尚未採用它
class SomeClass {
    var name = "Jeremy"
}
// 透過擴展宣告採用協議
extension SomeClass:SomeProtocol {}

注意類型不會因為滿足協定需求就自動採用協議,必須顯式地宣告類型採用了哪個協議。


上一篇
Day-28 Swift 語法(24) - 擴展 Extensions
下一篇
Day-30 Swift 語法(26) - 最後の協定
系列文
Swift 菜鳥的30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0

這個機制感覺跟 .NET 的 Interface 很像

我要留言

立即登入留言