鍍金池/ 教程/ iOS/ 協(xié)議
特性(Attributes)
Access Control 權(quán)限控制的黑與白
基本運(yùn)算符(Basic Operators)
基礎(chǔ)部分(The Basics)
閉包(Closures)
擴(kuò)展
泛型參數(shù)(Generic Parameters and Arguments)
訪問(wèn)控制和 protected
語(yǔ)句(Statements)
模式(Patterns)
WWDC 里面的那個(gè)“大炮打氣球”
關(guān)于語(yǔ)言參考(About the Language Reference)
語(yǔ)法總結(jié)(Summary of the Grammar)
嵌套類型
類型(Types)
Swift 初見(jiàn)(A Swift Tour)
泛型
枚舉(Enumerations)
高級(jí)運(yùn)算符
繼承
析構(gòu)過(guò)程
關(guān)于 Swift(About Swift)
訪問(wèn)控制
類和結(jié)構(gòu)體
內(nèi)存安全
Swift 與 C 語(yǔ)言指針友好合作
協(xié)議
屬性(Properties)
可選類型完美解決占位問(wèn)題
錯(cuò)誤處理
字符串和字符(Strings and Characters)
聲明(Declarations)
自動(dòng)引用計(jì)數(shù)
Swift 里的值類型與引用類型
表達(dá)式(Expressions)
Swift 文檔修訂歷史
造個(gè)類型不是夢(mèng)-白話 Swift 類型創(chuàng)建
歡迎使用 Swift
詞法結(jié)構(gòu)(Lexical Structure)
集合類型(Collection Types)
下標(biāo)
方法(Methods)
可選鏈?zhǔn)秸{(diào)用
版本兼容性
類型轉(zhuǎn)換
構(gòu)造過(guò)程
The Swift Programming Language 中文版
函數(shù)(Functions)
Swift 教程
控制流(Control Flow)

協(xié)議


1.0 翻譯:geek5nan 校對(duì):dabing1022

2.0 翻譯+校對(duì):futantan

2.1 翻譯:小鐵匠 Linus 校對(duì):shanks

2.2 翻譯+校對(duì):SketchK

3.0 校對(duì):CMB,版本日期:2016-09-13

3.0.1 shanks,2016-11-13

4.1 翻譯+校對(duì):mylittleswift

本頁(yè)包含內(nèi)容:

協(xié)議 定義了一個(gè)藍(lán)圖,規(guī)定了用來(lái)實(shí)現(xiàn)某一特定任務(wù)或者功能的方法、屬性,以及其他需要的東西。類、結(jié)構(gòu)體或枚舉都可以遵循協(xié)議,并為協(xié)議定義的這些要求提供具體實(shí)現(xiàn)。某個(gè)類型能夠滿足某個(gè)協(xié)議的要求,就可以說(shuō)該類型遵循這個(gè)協(xié)議。

除了遵循協(xié)議的類型必須實(shí)現(xiàn)的要求外,還可以對(duì)協(xié)議進(jìn)行擴(kuò)展,通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)一部分要求或者實(shí)現(xiàn)一些附加功能,這樣遵循協(xié)議的類型就能夠使用這些功能。

協(xié)議語(yǔ)法

協(xié)議的定義方式與類、結(jié)構(gòu)體和枚舉的定義非常相似:

protocol SomeProtocol {
    // 這里是協(xié)議的定義部分
}

要讓自定義類型遵循某個(gè)協(xié)議,在定義類型時(shí),需要在類型名稱后加上協(xié)議名稱,中間以冒號(hào)(:)分隔。遵循多個(gè)協(xié)議時(shí),各協(xié)議之間用逗號(hào)(,)分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 這里是結(jié)構(gòu)體的定義部分
}

擁有父類的類在遵循協(xié)議時(shí),應(yīng)該將父類名放在協(xié)議名之前,以逗號(hào)分隔:

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 這里是類的定義部分
}

屬性要求

協(xié)議可以要求遵循協(xié)議的類型提供特定名稱和類型的實(shí)例屬性或類型屬性。協(xié)議不指定屬性是存儲(chǔ)型屬性還是計(jì)算型屬性,它只指定屬性的名稱和類型。此外,協(xié)議還指定屬性是可讀的還是可讀可寫(xiě)的。

如果協(xié)議要求屬性是可讀可寫(xiě)的,那么該屬性不能是常量屬性或只讀的計(jì)算型屬性。如果協(xié)議只要求屬性是可讀的,那么該屬性不僅可以是可讀的,如果代碼需要的話,還可以是可寫(xiě)的。

協(xié)議總是用 var 關(guān)鍵字來(lái)聲明變量屬性,在類型聲明后加上 { set get } 來(lái)表示屬性是可讀可寫(xiě)的,可讀屬性則用 { get } 來(lái)表示:

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

在協(xié)議中定義類型屬性時(shí),總是使用 static 關(guān)鍵字作為前綴。當(dāng)類類型遵循協(xié)議時(shí),除了 static 關(guān)鍵字,還可以使用 class 關(guān)鍵字來(lái)聲明類型屬性:

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

如下所示,這是一個(gè)只含有一個(gè)實(shí)例屬性要求的協(xié)議:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 協(xié)議除了要求遵循協(xié)議的類型提供 fullName 屬性外,并沒(méi)有其他特別的要求。這個(gè)協(xié)議表示,任何遵循 FullyNamed 的類型,都必須有一個(gè)可讀的 String 類型的實(shí)例屬性 fullName。

下面是一個(gè)遵循 FullyNamed 協(xié)議的簡(jiǎn)單結(jié)構(gòu)體:

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

這個(gè)例子中定義了一個(gè)叫做 Person 的結(jié)構(gòu)體,用來(lái)表示一個(gè)具有名字的人。從第一行代碼可以看出,它遵循了 FullyNamed 協(xié)議。

Person 結(jié)構(gòu)體的每一個(gè)實(shí)例都有一個(gè) String 類型的存儲(chǔ)型屬性 fullName。這正好滿足了 FullyNamed 協(xié)議的要求,也就意味著 Person 結(jié)構(gòu)體正確地符合了協(xié)議。(如果協(xié)議要求未被完全滿足,在編譯時(shí)會(huì)報(bào)錯(cuò)。)

下面是一個(gè)更為復(fù)雜的類,它適配并遵循了 FullyNamed 協(xié)議:

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 是 "USS Enterprise"

Starship 類把 fullName 屬性實(shí)現(xiàn)為只讀的計(jì)算型屬性。每一個(gè) Starship 類的實(shí)例都有一個(gè)名為 name 的非可選屬性和一個(gè)名為 prefix 的可選屬性。 當(dāng) prefix 存在時(shí),計(jì)算型屬性 fullName 會(huì)將 prefix 插入到 name 之前,從而為星際飛船構(gòu)建一個(gè)全名。

方法要求

協(xié)議可以要求遵循協(xié)議的類型實(shí)現(xiàn)某些指定的實(shí)例方法或類方法。這些方法作為協(xié)議的一部分,像普通方法一樣放在協(xié)議的定義中,但是不需要大括號(hào)和方法體??梢栽趨f(xié)議中定義具有可變參數(shù)的方法,和普通方法的定義方式相同。但是,不支持為協(xié)議中的方法的參數(shù)提供默認(rèn)值。

正如屬性要求中所述,在協(xié)議中定義類方法的時(shí)候,總是使用 static 關(guān)鍵字作為前綴。當(dāng)類類型遵循協(xié)議時(shí),除了 static 關(guān)鍵字,還可以使用 class 關(guān)鍵字作為前綴:

protocol SomeProtocol {
    static func someTypeMethod()
}

下面的例子定義了一個(gè)只含有一個(gè)實(shí)例方法的協(xié)議:

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 協(xié)議要求遵循協(xié)議的類型必須擁有一個(gè)名為 random, 返回值類型為 Double 的實(shí)例方法。盡管這里并未指明,但是我們假設(shè)返回值是從0.0到(但不包括)1.0。

RandomNumberGenerator 協(xié)議并不關(guān)心每一個(gè)隨機(jī)數(shù)是怎樣生成的,它只要求必須提供一個(gè)隨機(jī)數(shù)生成器。

如下所示,下邊是一個(gè)遵循并符合 RandomNumberGenerator 協(xié)議的類。該類實(shí)現(xiàn)了一個(gè)叫做 線性同余生成器(linear congruential generator) 的偽隨機(jī)數(shù)算法。

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())")
// 打印 “Here's a random number: 0.37464991998171”
print("And another one: \(generator.random())")
// 打印 “And another one: 0.729023776863283”

異變方法要求

有時(shí)需要在方法中改變(或異變)方法所屬的實(shí)例。例如,在值類型(即結(jié)構(gòu)體和枚舉)的實(shí)例方法中,將 mutating 關(guān)鍵字作為方法的前綴,寫(xiě)在 func 關(guān)鍵字之前,表示可以在該方法中修改它所屬的實(shí)例以及實(shí)例的任意屬性的值。這一過(guò)程在在實(shí)例方法中修改值類型章節(jié)中有詳細(xì)描述。

如果你在協(xié)議中定義了一個(gè)實(shí)例方法,該方法會(huì)改變遵循該協(xié)議的類型的實(shí)例,那么在定義協(xié)議時(shí)需要在方法前加 mutating 關(guān)鍵字。這使得結(jié)構(gòu)體和枚舉能夠遵循此協(xié)議并滿足此方法要求。

注意

實(shí)現(xiàn)協(xié)議中的 mutating 方法時(shí),若是類類型,則不用寫(xiě) mutating 關(guān)鍵字。而對(duì)于結(jié)構(gòu)體和枚舉,則必須寫(xiě) mutating 關(guān)鍵字。

如下所示,Togglable 協(xié)議只要求實(shí)現(xiàn)一個(gè)名為 toggle 的實(shí)例方法。根據(jù)名稱的暗示,toggle() 方法將改變實(shí)例屬性,從而切換遵循該協(xié)議類型的實(shí)例的狀態(tài)。

toggle() 方法在定義的時(shí)候,使用 mutating 關(guān)鍵字標(biāo)記,這表明當(dāng)它被調(diào)用時(shí),該方法將會(huì)改變遵循協(xié)議的類型的實(shí)例:

protocol Togglable {
    mutating func toggle()
}

當(dāng)使用枚舉或結(jié)構(gòu)體來(lái)實(shí)現(xiàn) Togglable 協(xié)議時(shí),需要提供一個(gè)帶有 mutating 前綴的 toggle() 方法。

下面定義了一個(gè)名為 OnOffSwitch 的枚舉。這個(gè)枚舉在兩種狀態(tài)之間進(jìn)行切換,用枚舉成員 OnOff 表示。枚舉的 toggle() 方法被標(biāo)記為 mutating,以滿足 Togglable 協(xié)議的要求:

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 現(xiàn)在的值為 .on

構(gòu)造器要求

協(xié)議可以要求遵循協(xié)議的類型實(shí)現(xiàn)指定的構(gòu)造器。你可以像編寫(xiě)普通構(gòu)造器那樣,在協(xié)議的定義里寫(xiě)下構(gòu)造器的聲明,但不需要寫(xiě)花括號(hào)和構(gòu)造器的實(shí)體:

protocol SomeProtocol {
    init(someParameter: Int)
}

協(xié)議構(gòu)造器要求的類實(shí)現(xiàn)

你可以在遵循協(xié)議的類中實(shí)現(xiàn)構(gòu)造器,無(wú)論是作為指定構(gòu)造器,還是作為便利構(gòu)造器。無(wú)論哪種情況,你都必須為構(gòu)造器實(shí)現(xiàn)標(biāo)上 required 修飾符:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 這里是構(gòu)造器的實(shí)現(xiàn)部分
    }
}

使用 required 修飾符可以確保所有子類也必須提供此構(gòu)造器實(shí)現(xiàn),從而也能符合協(xié)議。

關(guān)于 required 構(gòu)造器的更多內(nèi)容,請(qǐng)參考必要構(gòu)造器。

注意

如果類已經(jīng)被標(biāo)記為 final,那么不需要在協(xié)議構(gòu)造器的實(shí)現(xiàn)中使用 required 修飾符,因?yàn)?final 類不能有子類。關(guān)于 final 修飾符的更多內(nèi)容,請(qǐng)參見(jiàn)防止重寫(xiě)

如果一個(gè)子類重寫(xiě)了父類的指定構(gòu)造器,并且該構(gòu)造器滿足了某個(gè)協(xié)議的要求,那么該構(gòu)造器的實(shí)現(xiàn)需要同時(shí)標(biāo)注 requiredoverride 修飾符:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 這里是構(gòu)造器的實(shí)現(xiàn)部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因?yàn)樽裱瓍f(xié)議,需要加上 required
    // 因?yàn)槔^承自父類,需要加上 override
    required override init() {
        // 這里是構(gòu)造器的實(shí)現(xiàn)部分
    }
}

可失敗構(gòu)造器要求

協(xié)議還可以為遵循協(xié)議的類型定義可失敗構(gòu)造器要求,詳見(jiàn)可失敗構(gòu)造器。

遵循協(xié)議的類型可以通過(guò)可失敗構(gòu)造器(init?)或非可失敗構(gòu)造器(init)來(lái)滿足協(xié)議中定義的可失敗構(gòu)造器要求。協(xié)議中定義的非可失敗構(gòu)造器要求可以通過(guò)非可失敗構(gòu)造器(init)或隱式解包可失敗構(gòu)造器(init!)來(lái)滿足。

協(xié)議作為類型

盡管協(xié)議本身并未實(shí)現(xiàn)任何功能,但是協(xié)議可以被當(dāng)做一個(gè)成熟的類型來(lái)使用。

協(xié)議可以像其他普通類型一樣使用,使用場(chǎng)景如下:

  • 作為函數(shù)、方法或構(gòu)造器中的參數(shù)類型或返回值類型
  • 作為常量、變量或?qū)傩缘念愋?/li>
  • 作為數(shù)組、字典或其他容器中的元素類型

注意

協(xié)議是一種類型,因此協(xié)議類型的名稱應(yīng)與其他類型(例如 Int,Double,String)的寫(xiě)法相同,使用大寫(xiě)字母開(kāi)頭的駝峰式寫(xiě)法,例如(FullyNamedRandomNumberGenerator)。

下面是將協(xié)議作為類型使用的例子:

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
    }
}

例子中定義了一個(gè) Dice 類,用來(lái)代表桌游中擁有 N 個(gè)面的骰子。Dice 的實(shí)例含有 sidesgenerator 兩個(gè)屬性,前者是整型,用來(lái)表示骰子有幾個(gè)面,后者為骰子提供一個(gè)隨機(jī)數(shù)生成器,從而生成隨機(jī)點(diǎn)數(shù)。

generator 屬性的類型為 RandomNumberGenerator,因此任何遵循了 RandomNumberGenerator 協(xié)議的類型的實(shí)例都可以賦值給 generator,除此之外并無(wú)其他要求。

Dice 類還有一個(gè)構(gòu)造器,用來(lái)設(shè)置初始狀態(tài)。構(gòu)造器有一個(gè)名為 generator,類型為 RandomNumberGenerator 的形參。在調(diào)用構(gòu)造方法創(chuàng)建 Dice 的實(shí)例時(shí),可以傳入任何遵循 RandomNumberGenerator 協(xié)議的實(shí)例給 generator。

Dice 類提供了一個(gè)名為 roll 的實(shí)例方法,用來(lái)模擬骰子的面值。它先調(diào)用 generatorrandom() 方法來(lái)生成一個(gè) [0.0,1.0) 區(qū)間內(nèi)的隨機(jī)數(shù),然后使用這個(gè)隨機(jī)數(shù)生成正確的骰子面值。因?yàn)?generator 遵循了 RandomNumberGenerator 協(xié)議,可以確保它有個(gè) random() 方法可供調(diào)用。

下面的例子展示了如何使用 LinearCongruentialGenerator 的實(shí)例作為隨機(jī)數(shù)生成器來(lái)創(chuàng)建一個(gè)六面骰子:

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

委托

委托是一種設(shè)計(jì)模式,它允許類或結(jié)構(gòu)體將一些需要它們負(fù)責(zé)的功能委托給其他類型的實(shí)例。委托模式的實(shí)現(xiàn)很簡(jiǎn)單:定義協(xié)議來(lái)封裝那些需要被委托的功能,這樣就能確保遵循協(xié)議的類型能提供這些功能。委托模式可以用來(lái)響應(yīng)特定的動(dòng)作,或者接收外部數(shù)據(jù)源提供的數(shù)據(jù),而無(wú)需關(guān)心外部數(shù)據(jù)源的類型。

下面的例子定義了兩個(gè)基于骰子游戲的協(xié)議:

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame 協(xié)議可以被任意涉及骰子的游戲遵循。DiceGameDelegate 協(xié)議可以被任意類型遵循,用來(lái)追蹤 DiceGame 的游戲過(guò)程。

如下所示,SnakesAndLadders控制流 章節(jié)引入的蛇梯棋游戲的新版本。新版本使用 Dice 實(shí)例作為骰子,并且實(shí)現(xiàn)了 DiceGameDiceGameDelegate 協(xié)議,后者用來(lái)記錄游戲的過(guò)程:

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = [Int](repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

關(guān)于這個(gè)蛇梯棋游戲的詳細(xì)描述請(qǐng)參閱 中斷(Break)。

這個(gè)版本的游戲封裝到了 SnakesAndLadders 類中,該類遵循了 DiceGame 協(xié)議,并且提供了相應(yīng)的可讀的 dice 屬性和 play() 方法。( dice 屬性在構(gòu)造之后就不再改變,且協(xié)議只要求 dice 為可讀的,因此將 dice 聲明為常量屬性。)

游戲使用 SnakesAndLadders 類的 init() 構(gòu)造器來(lái)初始化游戲。所有的游戲邏輯被轉(zhuǎn)移到了協(xié)議中的 play() 方法,play() 方法使用協(xié)議要求的 dice 屬性提供骰子搖出的值。

注意,delegate 并不是游戲的必備條件,因此 delegate 被定義為 DiceGameDelegate 類型的可選屬性。因?yàn)?delegate 是可選值,因此會(huì)被自動(dòng)賦予初始值 nil。隨后,可以在游戲中為 delegate 設(shè)置適當(dāng)?shù)闹怠?/p>

DicegameDelegate 協(xié)議提供了三個(gè)方法用來(lái)追蹤游戲過(guò)程。這三個(gè)方法被放置于游戲的邏輯中,即 play() 方法內(nèi)。分別在游戲開(kāi)始時(shí),新一輪開(kāi)始時(shí),以及游戲結(jié)束時(shí)被調(diào)用。

因?yàn)?delegate 是一個(gè) DiceGameDelegate 類型的可選屬性,因此在 play() 方法中通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)調(diào)用它的方法。若 delegate 屬性為 nil,則調(diào)用方法會(huì)優(yōu)雅地失敗,并不會(huì)產(chǎn)生錯(cuò)誤。若 delegate 不為 nil,則方法能夠被調(diào)用,并傳遞 SnakesAndLadders 實(shí)例作為參數(shù)。

如下示例定義了 DiceGameTracker 類,它遵循了 DiceGameDelegate 協(xié)議:

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTracker 實(shí)現(xiàn)了 DiceGameDelegate 協(xié)議要求的三個(gè)方法,用來(lái)記錄游戲已經(jīng)進(jìn)行的輪數(shù)。當(dāng)游戲開(kāi)始時(shí),numberOfTurns 屬性被賦值為 0,然后在每新一輪中遞增,游戲結(jié)束后,打印游戲的總輪數(shù)。

gameDidStart(_:) 方法從 game 參數(shù)獲取游戲信息并打印。game 參數(shù)是 DiceGame 類型而不是 SnakeAndLadders 類型,所以在 gameDidStart(_:) 方法中只能訪問(wèn) DiceGame 協(xié)議中的內(nèi)容。當(dāng)然了,SnakeAndLadders 的方法也可以在類型轉(zhuǎn)換之后調(diào)用。在上例代碼中,通過(guò) is 操作符檢查 game 是否為 SnakesAndLadders 類型的實(shí)例,如果是,則打印出相應(yīng)的消息。

無(wú)論當(dāng)前進(jìn)行的是何種游戲,由于 game 符合 DiceGame 協(xié)議,可以確保 game 含有 dice 屬性。因此在 gameDidStart(_:) 方法中可以通過(guò)傳入的 game 參數(shù)來(lái)訪問(wèn) dice 屬性,進(jìn)而打印出 dicesides 屬性的值。

DiceGameTracker 的運(yùn)行情況如下所示:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

在擴(kuò)展里添加協(xié)議遵循

即便無(wú)法修改源代碼,依然可以通過(guò)擴(kuò)展令已有類型遵循并符合協(xié)議。擴(kuò)展可以為已有類型添加屬性、方法、下標(biāo)以及構(gòu)造器,因此可以符合協(xié)議中的相應(yīng)要求。詳情請(qǐng)?jiān)?a href="./20_Extensions.html">擴(kuò)展章節(jié)中查看。

注意

通過(guò)擴(kuò)展令已有類型遵循并符合協(xié)議時(shí),該類型的所有實(shí)例也會(huì)隨之獲得協(xié)議中定義的各項(xiàng)功能。

例如下面這個(gè) TextRepresentable 協(xié)議,任何想要通過(guò)文本表示一些內(nèi)容的類型都可以實(shí)現(xiàn)該協(xié)議。這些想要表示的內(nèi)容可以是實(shí)例本身的描述,也可以是實(shí)例當(dāng)前狀態(tài)的文本描述:

protocol TextRepresentable {
    var textualDescription: String { get }
}

可以通過(guò)擴(kuò)展,令先前提到的 Dice 類遵循并符合 TextRepresentable 協(xié)議:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

通過(guò)擴(kuò)展遵循并符合協(xié)議,和在原始定義中遵循并符合協(xié)議的效果完全相同。協(xié)議名稱寫(xiě)在類型名之后,以冒號(hào)隔開(kāi),然后在擴(kuò)展的大括號(hào)內(nèi)實(shí)現(xiàn)協(xié)議要求的內(nèi)容。

現(xiàn)在所有 Dice 的實(shí)例都可以看做 TextRepresentable 類型:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”

同樣,SnakesAndLadders 類也可以通過(guò)擴(kuò)展遵循并符合 TextRepresentable 協(xié)議:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// 打印 “A game of Snakes and Ladders with 25 squares”

有條件地遵循協(xié)議

泛型類型可能只在某些情況下滿足一個(gè)協(xié)議的要求,比如當(dāng)類型的泛型形式參數(shù)遵循對(duì)應(yīng)協(xié)議時(shí)。你可以通過(guò)在擴(kuò)展類型時(shí)列出限制讓泛型類型有條件地遵循某協(xié)議。在你采納協(xié)議的名字后面寫(xiě)泛型 where 分句。更多關(guān)于泛型 where 分句,見(jiàn)泛型 Where 分句。

下面的擴(kuò)展讓 Array 類型只要在存儲(chǔ)遵循 TextRepresentable 協(xié)議的元素時(shí)就遵循 TextRepresentable 協(xié)議。

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// 打印 "[A 6-sided dice, A 12-sided dice]"

在擴(kuò)展里聲明采納協(xié)議

當(dāng)一個(gè)類型已經(jīng)符合了某個(gè)協(xié)議中的所有要求,卻還沒(méi)有聲明采納該協(xié)議時(shí),可以通過(guò)空擴(kuò)展體的擴(kuò)展采納該協(xié)議:

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

從現(xiàn)在起,Hamster 的實(shí)例可以作為 TextRepresentable 類型使用:

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”

注意

即使?jié)M足了協(xié)議的所有要求,類型也不會(huì)自動(dòng)遵循協(xié)議,必須顯式地遵循協(xié)議。

協(xié)議類型的集合

協(xié)議類型可以在數(shù)組或者字典這樣的集合中使用,在協(xié)議類型提到了這樣的用法。下面的例子創(chuàng)建了一個(gè)元素類型為 TextRepresentable 的數(shù)組:

let things: [TextRepresentable] = [game, d12, simonTheHamster]

如下所示,可以遍歷 things 數(shù)組,并打印每個(gè)元素的文本表示:

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

thingTextRepresentable 類型而不是 Dice,DiceGame,Hamster 等類型,即使實(shí)例在幕后確實(shí)是這些類型中的一種。由于 thingTextRepresentable 類型,任何 TextRepresentable 的實(shí)例都有一個(gè) textualDescription 屬性,所以在每次循環(huán)中可以安全地訪問(wèn) thing.textualDescription。

協(xié)議的繼承

協(xié)議能夠繼承一個(gè)或多個(gè)其他協(xié)議,可以在繼承的協(xié)議的基礎(chǔ)上增加新的要求。協(xié)議的繼承語(yǔ)法與類的繼承相似,多個(gè)被繼承的協(xié)議間用逗號(hào)分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 這里是協(xié)議的定義部分
}

如下所示,PrettyTextRepresentable 協(xié)議繼承了 TextRepresentable 協(xié)議:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

例子中定義了一個(gè)新的協(xié)議 PrettyTextRepresentable,它繼承自 TextRepresentable 協(xié)議。任何遵循 PrettyTextRepresentable 協(xié)議的類型在滿足該協(xié)議的要求時(shí),也必須滿足 TextRepresentable 協(xié)議的要求。在這個(gè)例子中,PrettyTextRepresentable 協(xié)議額外要求遵循協(xié)議的類型提供一個(gè)返回值為 String 類型的 prettyTextualDescription 屬性。

如下所示,擴(kuò)展 SnakesAndLadders,使其遵循并符合 PrettyTextRepresentable 協(xié)議:

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

上述擴(kuò)展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 協(xié)議,并提供了協(xié)議要求的 prettyTextualDescription 屬性。每個(gè) PrettyTextRepresentable 類型同時(shí)也是 TextRepresentable 類型,所以在 prettyTextualDescription 的實(shí)現(xiàn)中,可以訪問(wèn) textualDescription 屬性。然后,拼接上了冒號(hào)和換行符。接著,遍歷數(shù)組中的元素,拼接一個(gè)幾何圖形來(lái)表示每個(gè)棋盤(pán)方格的內(nèi)容:

  • 當(dāng)從數(shù)組中取出的元素的值大于 0 時(shí),用 表示。
  • 當(dāng)從數(shù)組中取出的元素的值小于 0 時(shí),用 表示。
  • 當(dāng)從數(shù)組中取出的元素的值等于 0 時(shí),用 表示。

任意 SankesAndLadders 的實(shí)例都可以使用 prettyTextualDescription 屬性來(lái)打印一個(gè)漂亮的文本描述:

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

類專屬的協(xié)議

你通過(guò)添加 AnyObject 關(guān)鍵字到協(xié)議的繼承列表,就可以限制協(xié)議只能被類類型采納(以及非結(jié)構(gòu)體或者非枚舉的類型)。

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 這里是類專屬協(xié)議的定義部分
}

在以上例子中,協(xié)議 SomeClassOnlyProtocol 只能被類類型采納。如果嘗試讓結(jié)構(gòu)體或枚舉類型采納 SomeClassOnlyProtocol,則會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。

注意

當(dāng)協(xié)議定義的要求需要遵循協(xié)議的類型必須是引用語(yǔ)義而非值語(yǔ)義時(shí),應(yīng)該采用類類型專屬協(xié)議。關(guān)于引用語(yǔ)義和值語(yǔ)義的更多內(nèi)容,請(qǐng)查看結(jié)構(gòu)體和枚舉是值類型類是引用類型。

協(xié)議合成

要求一個(gè)類型同時(shí)遵循多個(gè)協(xié)議是很有用的。你可以使用協(xié)議組合來(lái)復(fù)合多個(gè)協(xié)議到一個(gè)要求里。協(xié)議組合行為就和你定義的臨時(shí)局部協(xié)議一樣擁有構(gòu)成中所有協(xié)議的需求。協(xié)議組合不定義任何新的協(xié)議類型。

協(xié)議組合使用 SomeProtocol & AnotherProtocol 的形式。你可以列舉任意數(shù)量的協(xié)議,用和符號(hào)(&)分開(kāi)。除了協(xié)議列表,協(xié)議組合也能包含類類型,這允許你標(biāo)明一個(gè)需要的父類。

下面的例子中,將 NamedAged 兩個(gè)協(xié)議按照上述語(yǔ)法組合成一個(gè)協(xié)議,作為函數(shù)參數(shù)的類型:

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// 打印 “Happy birthday Malcolm - you're 21!”

Named 協(xié)議包含 String 類型的 name 屬性。Aged 協(xié)議包含 Int 類型的 age 屬性。Person 結(jié)構(gòu)體采納了這兩個(gè)協(xié)議。

wishHappyBirthday(to:) 函數(shù)的參數(shù) celebrator 的類型為 Named & Aged, 這意味著“任何同時(shí)遵循 Named 和 Aged 的協(xié)議”。它不關(guān)心參數(shù)的具體類型,只要參數(shù)符合這兩個(gè)協(xié)議即可。

上面的例子創(chuàng)建了一個(gè)名為 birthdayPersonPerson 的實(shí)例,作為參數(shù)傳遞給了 wishHappyBirthday(to:) 函數(shù)。因?yàn)?Person 同時(shí)符合這兩個(gè)協(xié)議,所以這個(gè)參數(shù)合法,函數(shù)將打印生日問(wèn)候語(yǔ)。

這里有一個(gè)例子:將 Location 類和前面的 Named 協(xié)議進(jìn)行組合:

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

beginConcert(in:) 方法接受一個(gè)類型為 Location & Named 的參數(shù),這意味著“任何 Location 的子類,并且遵循 Named 協(xié)議”。例如,City 就滿足這樣的條件。

將 birthdayPerson 傳入 beginConcert(in:) 函數(shù)是不合法的,因?yàn)?Person 不是一個(gè) Location 的子類。就像,如果你新建一個(gè)類繼承與 Location,但是沒(méi)有遵循 Named 協(xié)議,你用這個(gè)類的實(shí)例去調(diào)用 beginConcert(in:) 函數(shù)也是不合法的。

檢查協(xié)議一致性

你可以使用類型轉(zhuǎn)換中描述的 isas 操作符來(lái)檢查協(xié)議一致性,即是否符合某協(xié)議,并且可以轉(zhuǎn)換到指定的協(xié)議類型。檢查和轉(zhuǎn)換到某個(gè)協(xié)議類型在語(yǔ)法上和類型的檢查和轉(zhuǎn)換完全相同:

  • is 用來(lái)檢查實(shí)例是否符合某個(gè)協(xié)議,若符合則返回 true,否則返回 false。
  • as? 返回一個(gè)可選值,當(dāng)實(shí)例符合某個(gè)協(xié)議時(shí),返回類型為協(xié)議類型的可選值,否則返回 nil
  • as! 將實(shí)例強(qiáng)制向下轉(zhuǎn)換到某個(gè)協(xié)議類型,如果強(qiáng)轉(zhuǎn)失敗,會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤。

下面的例子定義了一個(gè) HasArea 協(xié)議,該協(xié)議定義了一個(gè) Double 類型的可讀屬性 area

protocol HasArea {
    var area: Double { get }
}

如下所示,Circle 類和 Country 類都遵循了 HasArea 協(xié)議:

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Circle 類把 area 屬性實(shí)現(xiàn)為基于存儲(chǔ)型屬性 radius 的計(jì)算型屬性。Country 類則把 area 屬性實(shí)現(xiàn)為存儲(chǔ)型屬性。這兩個(gè)類都正確地符合了 HasArea 協(xié)議。

如下所示,Animal 是一個(gè)未遵循 HasArea 協(xié)議的類:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle,CountryAnimal 并沒(méi)有一個(gè)共同的基類,盡管如此,它們都是類,它們的實(shí)例都可以作為 AnyObject 類型的值,存儲(chǔ)在同一個(gè)數(shù)組中:

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

objects 數(shù)組使用字面量初始化,數(shù)組包含一個(gè) radius2Circle 的實(shí)例,一個(gè)保存了英國(guó)國(guó)土面積的 Country 實(shí)例和一個(gè) legs4Animal 實(shí)例。

如下所示,objects 數(shù)組可以被迭代,并對(duì)迭代出的每一個(gè)元素進(jìn)行檢查,看它是否符合 HasArea 協(xié)議:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

當(dāng)?shù)龅脑胤?HasArea 協(xié)議時(shí),將 as? 操作符返回的可選值通過(guò)可選綁定,綁定到 objectWithArea 常量上。objectWithAreaHasArea 協(xié)議類型的實(shí)例,因此 area 屬性可以被訪問(wèn)和打印。

objects 數(shù)組中的元素的類型并不會(huì)因?yàn)閺?qiáng)轉(zhuǎn)而丟失類型信息,它們?nèi)匀皇?CircleCountry,Animal 類型。然而,當(dāng)它們被賦值給 objectWithArea 常量時(shí),只被視為 HasArea 類型,因此只有 area 屬性能夠被訪問(wèn)。

可選的協(xié)議要求

協(xié)議可以定義可選要求,遵循協(xié)議的類型可以選擇是否實(shí)現(xiàn)這些要求。在協(xié)議中使用 optional 關(guān)鍵字作為前綴來(lái)定義可選要求??蛇x要求用在你需要和 Objective-C 打交道的代碼中。協(xié)議和可選要求都必須帶上 @objc 屬性。標(biāo)記 @objc 特性的協(xié)議只能被繼承自 Objective-C 類的類或者 @objc 類遵循,其他類以及結(jié)構(gòu)體和枚舉均不能遵循這種協(xié)議。

使用可選要求時(shí)(例如,可選的方法或者屬性),它們的類型會(huì)自動(dòng)變成可選的。比如,一個(gè)類型為 (Int) -> String 的方法會(huì)變成 ((Int) -> String)?。需要注意的是整個(gè)函數(shù)類型是可選的,而不是函數(shù)的返回值。

協(xié)議中的可選要求可通過(guò)可選鏈?zhǔn)秸{(diào)用來(lái)使用,因?yàn)樽裱瓍f(xié)議的類型可能沒(méi)有實(shí)現(xiàn)這些可選要求。類似 someOptionalMethod?(someArgument) 這樣,你可以在可選方法名稱后加上 ? 來(lái)調(diào)用可選方法。詳細(xì)內(nèi)容可在可選鏈?zhǔn)秸{(diào)用章節(jié)中查看。

下面的例子定義了一個(gè)名為 Counter 的用于整數(shù)計(jì)數(shù)的類,它使用外部的數(shù)據(jù)源來(lái)提供每次的增量。數(shù)據(jù)源由 CounterDataSource 協(xié)議定義,包含兩個(gè)可選要求:

@objc protocol CounterDataSource {
    @objc optional func incrementForCount(count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

CounterDataSource 協(xié)議定義了一個(gè)可選方法 increment(forCount:) 和一個(gè)可選屬性 fiexdIncrement,它們使用了不同的方法來(lái)從數(shù)據(jù)源中獲取適當(dāng)?shù)脑隽恐怠?/p>

注意

嚴(yán)格來(lái)講,CounterDataSource 協(xié)議中的方法和屬性都是可選的,因此遵循協(xié)議的類可以不實(shí)現(xiàn)這些要求,盡管技術(shù)上允許這樣做,不過(guò)最好不要這樣寫(xiě)。

Counter 類含有 CounterDataSource? 類型的可選屬性 dataSource,如下所示:

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.incrementForCount?(count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

Counter 類使用變量屬性 count 來(lái)存儲(chǔ)當(dāng)前值。該類還定義了一個(gè) increment 方法,每次調(diào)用該方法的時(shí)候,將會(huì)增加 count 的值。

increment() 方法首先試圖使用 increment(forCount:) 方法來(lái)得到每次的增量。increment() 方法使用可選鏈?zhǔn)秸{(diào)用來(lái)嘗試調(diào)用 increment(forCount:),并將當(dāng)前的 count 值作為參數(shù)傳入。

這里使用了兩層可選鏈?zhǔn)秸{(diào)用。首先,由于 dataSource 可能為 nil,因此在 dataSource 后邊加上了 ?,以此表明只在 dataSource 非空時(shí)才去調(diào)用 increment(forCount:) 方法。其次,即使 dataSource 存在,也無(wú)法保證其是否實(shí)現(xiàn)了 increment(forCount:) 方法,因?yàn)檫@個(gè)方法是可選的。因此,increment(forCount:) 方法同樣使用可選鏈?zhǔn)秸{(diào)用進(jìn)行調(diào)用,只有在該方法被實(shí)現(xiàn)的情況下才能調(diào)用它,所以在 increment(forCount:) 方法后邊也加上了 ?。

調(diào)用 increment(forCount:) 方法在上述兩種情形下都有可能失敗,所以返回值為 Int? 類型。雖然在 CounterDataSource 協(xié)議中,increment(forCount:) 的返回值類型是非可選 Int。另外,即使這里使用了兩層可選鏈?zhǔn)秸{(diào)用,最后的返回結(jié)果依舊是單層的可選類型。關(guān)于這一點(diǎn)的更多信息,請(qǐng)查閱連接多層可選鏈?zhǔn)秸{(diào)用

在調(diào)用 increment(forCount:) 方法后,Int? 型的返回值通過(guò)可選綁定解包并賦值給常量 amount。如果可選值確實(shí)包含一個(gè)數(shù)值,也就是說(shuō),數(shù)據(jù)源和方法都存在,數(shù)據(jù)源方法返回了一個(gè)有效值。之后便將解包后的 amount 加到 count 上,增量操作完成。

如果沒(méi)有從 increment(forCount:) 方法獲取到值,可能由于 dataSourcenil,或者它并沒(méi)有實(shí)現(xiàn) increment(forCount:) 方法,那么 increment() 方法將試圖從數(shù)據(jù)源的 fixedIncrement 屬性中獲取增量。fixedIncrement 是一個(gè)可選屬性,因此屬性值是一個(gè) Int? 值,即使該屬性在 CounterDataSource 協(xié)議中的類型是非可選的 Int。

下面的例子展示了 CounterDataSource 的簡(jiǎn)單實(shí)現(xiàn)。ThreeSource 類遵循了 CounterDataSource 協(xié)議,它實(shí)現(xiàn)了可選屬性 fixedIncrement