iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
0
自我挑戰組

iOS 新手開發的大小事系列 第 16

Day 16: 協定 (Protocol) -1

前言

呼~終於過半了,介紹完協定的內容後,預計開始介紹一些 UIKit 的部分。協定 (Protocol) 的概念在 iOS 開發時是很重要的,也會與委任 (delegation) 有關,協定的強大之處在於在不提供實現的情況下形式化代碼不同部分之間的連結。這樣,就可以在代碼中構建剛性結構,而不必緊密耦合代碼的元件。

協定定義了適合特定任務或功能的方法、屬性和其他要求的藍圖。然後,該協定可以由類、結構或列舉採用,以提供這些要求的實際實現。滿足協定要求的任何型別都被稱為符合該協定。除了指定必須符合標準的型別的要求之外,還可以擴展協定以實現這些要求中的某些要求,或者實施符合標準的型別可以利用的其他功能。

協定語法 (Protocol Syntax)

用類、結構和列舉非常相似的方式定義協定:

protocol SomeProtocol {
    // protocol definition goes here
}

自定義型別表示它們採用特定的協定,方法是將協定名稱放在型別名稱之後,並用冒號分隔,以作為其定義的一部分。可以列出多個協定,並用逗號分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

如果類具有父類,請在其採用的任何協定之前列出該父類名稱,並以逗號開頭:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

屬性要求 (Property Requirements)

協定可以要求任何符合條件的型別來提供具有特定名稱和型別的實例屬性或型別屬性。協定沒有指定該屬性應該是儲存屬性還是計算屬性,它僅指定所需的屬性名稱和型別。該協定還指定每個屬性必須是可獲取的或是可獲取的和可設置的。

如果協定要求某個屬性必須是可獲取和可設置的,則該屬性的要求不能由常數儲存的屬性或唯讀的計算屬性來滿足。如果協定僅要求可獲取屬性,則可以通過任何型別的屬性來滿足該要求,並且如果該屬性對自己的代碼有用,則該屬性也可被設置為有效。

屬性要求始終宣告為變數屬性,並以 var 關鍵字為前綴。通過在型別宣告後寫 { get set } 來表示可獲取和可設置的屬性,通過寫 { get } 來表示可獲取的屬性。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

在協定中定義型別屬性要求時,請在其前面加上 static 關鍵字。即使在通過類實現型別屬性要求時可以使用 classstatic 關鍵字作為前綴,該規則也適用:

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

以下是一個具有單實例屬性要求的協定範例:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 協定需要一個符合標準的型別來提供完全限定的名稱。該協定未對一致性類型的性質作任何其他規定,僅指定型別必須能夠為其提供全名。該協定指出,任何 FullyNamed 型別都必須具有名為 fullNamegettable 實例屬性,該屬性的型別為 String。

這是採用並遵循 FullyNamed 協定的簡單結構的範例:

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

本範例定義了一個名為 Person 的結構,該結構代表一個特定的命名人。它聲明它採用 FullyNamed 協定作為其定義的第一行的一部分。每個 Person 實例都有一個名為 fullName 的儲存屬性,該屬性的型別為 String。這符合 FullyNamed 協定的單一要求,並且意味著 Person 已正確遵守該協定。(如果未滿足協定要求,Swift 將在編譯時報告錯誤。)

以下是一個更複雜的類,它也採用並遵循 FullyNamed 協定:

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

此類將屬性 fullName 要求實現為飛船的已計算的唯讀屬性。每個類 Starship 實例都儲存一個強制性名稱和一個可選前綴。屬性 fullName 使用前綴值(如果存在),並將其添加到 name 的前面用來為星艦創建全名。

方法要求

協定可能要求特定的實例方法和型別方法通過符合型別的方法來實現。這些方法以與普通實例和型別方法完全相同的方式寫為協定定義的一部分,但沒有大括號或方法主體。可變參數是允許的,但要遵循與常規方法相同的規則。但是,無法在協定的定義中為方法參數指定預設值。

與型別屬性要求一樣,在協定中定義型別方法要求時,請始終在其前面加上 static 關鍵字。即使型別方法要求在由類實現時以 classstatic 關鍵字為前綴也是如此:

protocol SomeProtocol {
    static func someTypeMethod()
}

以下範例定義了具有單實例方法要求的協定:

protocol RandomNumberGenerator {
    func random() -> Double
}

該協定 RandomNumberGenerator 要求任何符合條件的型別都具有一個稱為 random 的實例方法,該方法在每次調用時都會返回 Double 值。儘管未將其指定為協定的一部分,但假定此值為從 0.0 到(但不包括)1.0 之間的數字。 RandomNumberGenerator 協定對如何生成每個隨機數沒有任何假設,它只是要求生成器提供生成新隨機數的標準方法。 這是採用並符合 RandomNumberGenerator 協定的類的實現。此類實現稱為線性同餘生成器的偽隨機數生成器算法:

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

變異方法要求 (Mutating Method Requirements)

有時,方法有必要修改(或變異)它所屬的實例。對於基於值型別(即結構和列舉)的實例方法,請將關鍵字 mutating 放在方法的關鍵字 func 之前,用來表示允許該方法修改其所屬的實例以及該實例的任何屬性。

如果定義了一個協定實例方法要求,旨在對採用該協定的任何型別的實例進行突變,請將該方法標記為 mutating 關鍵字,作為協定定義的一部分。這使結構和列舉可以採用協定並滿足該方法要求。

如果將協定實例方法的要求標記為 mutating,則在為類編寫該方法的實現時無需編寫關鍵字 mutating。關鍵字 mutating 僅由結構和列舉使用。

下面的範例定義了一個名為 Togglable 的協定,該協定定義了一個名為 toggle 的單實例方法需求。顧名思義,toggle() 方法用於切換或反轉任何符合型別的狀態,通常是通過修改該型別的屬性來實現的。

toggle() 方法在 Togglable 協定定義中標有關鍵字 mutating,以表明該方法在被調用時會改變符合實例的狀態:

protocol Togglable {
    mutating func toggle()
}

如果為某個結構或列舉實現 Togglable 協定,則該結構或列舉可以通過提供 toggle() 方法的實現(也標記為變異)來符合該協定。下面的飯例定義了一個名為 OnOffSwitch 的列舉。此列舉在兩個狀態之間切換,由打開和關閉的列舉情況指示。列舉的切換實現標記為變異,以符合 Togglable 協定的要求:

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

協定作為型別 (Protocols as Types)

協定本身實際上並未實現任何功能。但是,您可以將協定用作代碼中的完整型別。使用協定作為型別有時也稱為存在型別 (existential type),它來自短語 “存在型別 T,使得 T 符合協定”。

可以在允許使用其他型別的許多地方使用協定,包括:

  • 作為函數,方法或初始化程序中的參數型別或返回型別
  • 作為常量,變數或屬性的型別
  • 作為數組,字典或其他容器中項目的型別

因為協定是型別,所以其名稱以大寫字母開頭(例如 FullyNamedRandomNumberGenerator),以匹配 Swift 中其他型別的名稱(例如 Int、String 和 Double)。

以下是當作型別的協定的範例:

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

此範例定義了一個稱為 Dice 的新類,該類代表用於棋盤遊戲的 n 面骰子。骰子實例具有一個稱為 sides 的整數屬性,該屬性表示它們具有多少側;還有一個名為 generator 的屬性,該屬性提供一個隨機數生成器,從中創建骰子擲骰值。

屬性 generator 的型別為 RandomNumberGenerator。因此,可以將其設置為採用 RandomNumberGenerator 協定的任何型別的實例。分配給該屬性的實例不需要任何其他操作,只是該實例必須採用 RandomNumberGenerator 協定。由於其型別為 RandomNumberGenerator,因此類 Dice 中的代碼只能以適用於所有符合此協定的生成器的方式與生成器進行相互作用。這代表著它不能使用由生成器的基礎型別定義的任何方法或屬性。但是,可以像從向下轉換中討論的那樣,從協定型別向下轉換為基礎型別的方式,就像從父類向下轉換為子類的方式一樣。

Dice 還具有一個初始化程序,用於設置其初始狀態。此初始化程序具有一個稱為 generator 的參數,該參數的型別也為 RandomNumberGenerator。初始化新的 Dice 實例時,可以在該參數中傳遞任何符合型別的值。骰子提供了一個實例方法 roll,該方法返回 1 到骰子邊數之間的整數值。此方法調用生成器的 random() 方法來創建介於 0.0 和 1.0 之間的新隨機數,並使用該隨機數來創建正確範圍內的骰子擲骰值。由於已知 Generator 會採用 RandomNumberGenerator,因此可以確保使用 random() 方法進行調用。

下面介紹瞭如何使用類 Dice 創建一個具有 LinearCongruentialGenerator 實例作為其隨機數生成器的六面骰子:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

上一篇
Day 15: 自動引用計數 (Automatic Reference Counting) -3
下一篇
Day 17: 協定 (Protocol) -2
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言