Swift Tour Learn (十一) -- Swift 語(yǔ)法(協(xié)議)

本章將會(huì)介紹

協(xié)議語(yǔ)法
屬性要求
方法要求(Method Requirements)
Mutating 方法要求
構(gòu)造器要求
協(xié)議作為類(lèi)型
委托(代理)模式
通過(guò)擴(kuò)展添加協(xié)議一致性
通過(guò)擴(kuò)展遵循協(xié)議
協(xié)議類(lèi)型的集合
協(xié)議的繼承
類(lèi)類(lèi)型專(zhuān)屬協(xié)議
協(xié)議合成
檢查協(xié)議一致性
可選的協(xié)議要求
協(xié)議擴(kuò)展

協(xié)議

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

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

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

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

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

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

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

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

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 這里是類(lèi)的定義部分
}
2.屬性要求

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

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

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

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

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

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

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

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 協(xié)議除了要求遵循協(xié)議的類(lèi)型提供 fullName 屬性外,并沒(méi)有其他特別的要求是整。這個(gè)協(xié)議表示肖揣,任何遵循 FullyNamed 的類(lèi)型,都必須有一個(gè)可讀的 String 類(lèi)型的實(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 類(lèi)型的存儲(chǔ)型屬性 fullName。這正好滿足了 FullyNamed 協(xié)議的要求秽晚,也就意味著 Person 結(jié)構(gòu)體正確地符合了協(xié)議瓦糟。(如果協(xié)議要求未被完全滿足,在編譯時(shí)會(huì)報(bào)錯(cuò)赴蝇。)

下面是一個(gè)更為復(fù)雜的類(lèi)菩浙,它適配并遵循了 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 類(lèi)把 fullName 屬性實(shí)現(xiàn)為只讀的計(jì)算型屬性。每一個(gè) Starship 類(lèi)的實(shí)例都有一個(gè)名為 name 的非可選屬性和一個(gè)名為 prefix 的可選屬性句伶。 當(dāng) prefix 存在時(shí)劲蜻,計(jì)算型屬性 fullName 會(huì)將 prefix 插入到 name 之前,從而為星際飛船構(gòu)建一個(gè)全名考余。

3.方法要求(Method Requirements)

協(xié)議可以要求遵循協(xié)議的類(lèi)型實(shí)現(xiàn)某些指定的實(shí)例方法或類(lèi)方法先嬉。這些方法作為協(xié)議的一部分,像普通方法一樣放在協(xié)議的定義中楚堤,但是不需要大括號(hào)和方法體疫蔓『茫可以在協(xié)議中定義具有可變參數(shù)的方法,和普通方法的定義方式相同衅胀。但是岔乔,不支持為協(xié)議中的方法的參數(shù)提供默認(rèn)值。

正如屬性要求中所述滚躯,在協(xié)議中定義類(lèi)方法的時(shí)候雏门,總是使用 static 關(guān)鍵字作為前綴。當(dāng)類(lèi)類(lèi)型遵循協(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é)議的類(lèi)型必須擁有一個(gè)名為 random症见, 返回值類(lèi)型為 Double 的實(shí)例方法。盡管這里并未指明呼胚,但是我們假設(shè)返回值在 [0.0,1.0) 區(qū)間內(nèi)。

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

如下所示,下邊是一個(gè)遵循并符合 RandomNumberGenerator 協(xié)議的類(lèi)呼盆。該類(lèi)實(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”
4.Mutating 方法要求

有時(shí)需要在方法中改變方法所屬的實(shí)例。例如访圃,在值類(lèi)型(即結(jié)構(gòu)體和枚舉)的實(shí)例方法中厨幻,將 mutating 關(guān)鍵字作為方法的前綴,寫(xiě)在 func 關(guān)鍵字之前腿时,表示可以在該方法中修改它所屬的實(shí)例以及實(shí)例的任意屬性的值况脆。

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

注意
實(shí)現(xiàn)協(xié)議中的 mutating 方法時(shí)徽鼎,若是類(lèi)類(lèi)型盛末,則不用寫(xiě) mutating 關(guān)鍵字。而對(duì)于結(jié)構(gòu)體和枚舉否淤,則必須寫(xiě) mutating 關(guān)鍵字悄但。

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

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

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

下面定義了一個(gè)名為 OnOffSwitch 的枚舉厢漩。這個(gè)枚舉在兩種狀態(tài)之間進(jìn)行切換膜眠,用枚舉成員 On 和 Off 表示。枚舉的 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
5.構(gòu)造器要求

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

protocol SomeProtocol {
    init(someParameter: Int)
}
  • 構(gòu)造器要求在類(lèi)中的實(shí)現(xiàn)

你可以在遵循協(xié)議的類(lèi)中實(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 修飾符可以確保所有子類(lèi)也必須提供此構(gòu)造器實(shí)現(xiàn),從而也能符合協(xié)議裹匙。

如果類(lèi)已經(jīng)被標(biāo)記為 final瑞凑,那么不需要在協(xié)議構(gòu)造器的實(shí)現(xiàn)中使用 required 修飾符,因?yàn)?final 類(lèi)不能有子類(lèi)概页。

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

protocol SomeProtocol {
    init()
}

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

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

協(xié)議還可以為遵循協(xié)議的類(lèi)型定義可失敗構(gòu)造器要求。

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

6.協(xié)議作為類(lèi)型

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

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

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

注意
協(xié)議是一種類(lèi)型,因此協(xié)議類(lèi)型的名稱應(yīng)與其他類(lèi)型(例如 Int稚虎,Double撤嫩,String)的寫(xiě)法相同,使用大寫(xiě)字母開(kāi)頭的駝峰式寫(xiě)法蠢终,例如(FullyNamed 和 RandomNumberGenerator)序攘。

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

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)茴她,用來(lái)代表桌游中擁有 N 個(gè)面的骰子。Dice 的實(shí)例含有 sides 和 generator 兩個(gè)屬性程奠,前者是整型丈牢,用來(lái)表示骰子有幾個(gè)面,后者為骰子提供一個(gè)隨機(jī)數(shù)生成器瞄沙,從而生成隨機(jī)點(diǎn)數(shù)己沛。

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

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

Dice 類(lèi)提供了一個(gè)名為 roll 的實(shí)例方法,用來(lái)模擬骰子的面值碱呼。它先調(diào)用 generator 的 random() 方法來(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
7.委托(代理)模式

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

下面的例子定義了兩個(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)型遵循泽台,用來(lái)追蹤 DiceGame 的游戲過(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)
    }
}

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

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

注意,delegate 并不是游戲的必備條件檐束,因此 delegate 被定義為 DiceGameDelegate 類(lèi)型的可選屬性辫秧。因?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 類(lèi)型的可選屬性廊移,因此在 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 類(lèi),它遵循了 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 類(lèi)型而不是 SnakeAndLadders 類(lèi)型,所以在gameDidStart(_:) 方法中只能訪問(wèn) DiceGame 協(xié)議中的內(nèi)容听皿。當(dāng)然了咕别,SnakeAndLadders 的方法也可以在類(lèi)型轉(zhuǎn)換之后調(diào)用。在上例代碼中写穴,通過(guò) is 操作符檢查 game 是否為 SnakesAndLadders 類(lèi)型的實(shí)例惰拱,如果是,則打印出相應(yīng)的消息。

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

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
8.通過(guò)擴(kuò)展添加協(xié)議一致性

即便無(wú)法修改源代碼,依然可以通過(guò)擴(kuò)展令已有類(lèi)型遵循并符合協(xié)議勾怒。擴(kuò)展可以為已有類(lèi)型添加屬性婆排、方法、下標(biāo)以及構(gòu)造器笔链,因此可以符合協(xié)議中的相應(yīng)要求段只。

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

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

protocol TextRepresentable {
    var textualDescription: String { get }
}

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

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

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

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

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

同樣陪踩,SnakesAndLadders 類(lèi)也可以通過(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”
9.通過(guò)擴(kuò)展遵循協(xié)議

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

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

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

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

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

10.協(xié)議類(lèi)型的集合

協(xié)議類(lèi)型可以在數(shù)組或者字典這樣的集合中使用,在協(xié)議類(lèi)型提到了這樣的用法列粪。下面的例子創(chuàng)建了一個(gè)元素類(lèi)型為 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

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

11.協(xié)議的繼承

協(xié)議能夠繼承一個(gè)或多個(gè)其他協(xié)議瘩蚪,可以在繼承的協(xié)議的基礎(chǔ)上增加新的要求泉懦。協(xié)議的繼承語(yǔ)法與類(lèi)的繼承相似,多個(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é)議的類(lèi)型在滿足該協(xié)議的要求時(shí)言沐,也必須滿足 TextRepresentable 協(xié)議的要求邓嘹。在這個(gè)例子中,PrettyTextRepresentable 協(xié)議額外要求遵循協(xié)議的類(lèi)型提供一個(gè)返回值為 String 類(lèi)型的 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 類(lèi)型同時(shí)也是 TextRepresentable 類(lèi)型跋涣,所以在 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:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
12.類(lèi)類(lèi)型專(zhuān)屬協(xié)議

你可以在協(xié)議的繼承列表中中燥,通過(guò)添加 class 關(guān)鍵字來(lái)限制協(xié)議只能被類(lèi)類(lèi)型遵循,而結(jié)構(gòu)體或枚舉不能遵循該協(xié)議塘偎。class 關(guān)鍵字必須第一個(gè)出現(xiàn)在協(xié)議的繼承列表中疗涉,在其他繼承的協(xié)議之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 這里是類(lèi)類(lèi)型專(zhuān)屬協(xié)議的定義部分
}

在以上例子中,協(xié)議 SomeClassOnlyProtocol 只能被類(lèi)類(lèi)型遵循吟秩。如果嘗試讓結(jié)構(gòu)體或枚舉類(lèi)型遵循該協(xié)議咱扣,則會(huì)導(dǎo)致編譯錯(cuò)誤。

當(dāng)協(xié)議定義的要求需要遵循協(xié)議的類(lèi)型必須是引用語(yǔ)義而非值語(yǔ)義時(shí)涵防,應(yīng)該采用類(lèi)類(lèi)型專(zhuān)屬協(xié)議闹伪。

13.協(xié)議合成

有時(shí)候需要同時(shí)遵循多個(gè)協(xié)議,你可以將多個(gè)協(xié)議采用 SomeProtocol & AnotherProtocol 這樣的格式進(jìn)行組合,稱為 協(xié)議合成(protocol composition)祭往。你可以羅列任意多個(gè)你想要遵循的協(xié)議伦意,以與符號(hào)(&)分隔。

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

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 類(lèi)型的 name 屬性。Aged 協(xié)議包含 Int 類(lèi)型的 age 屬性已骇。Person 結(jié)構(gòu)體遵循了這兩個(gè)協(xié)議离钝。

wishHappyBirthday(to:) 函數(shù)的參數(shù) celebrator 的類(lèi)型為 Named & Aged。這意味著它不關(guān)心參數(shù)的具體類(lèi)型褪储,只要參數(shù)符合這兩個(gè)協(xié)議即可卵渴。

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

注意
協(xié)議合成并不會(huì)生成新的碘橘、永久的協(xié)議類(lèi)型,而是將多個(gè)協(xié)議中的要求合成到一個(gè)只在局部作用域有效的臨時(shí)協(xié)議中吱肌。

14.檢查協(xié)議一致性

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

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

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

protocol HasArea {
    var area: Double { get }
}

如下所示月杉,Circle 類(lèi)和 Country 類(lèi)都遵循了 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 類(lèi)把 area 屬性實(shí)現(xiàn)為基于存儲(chǔ)型屬性 radius 的計(jì)算型屬性刃跛。Country 類(lèi)則把 area 屬性實(shí)現(xiàn)為存儲(chǔ)型屬性。這兩個(gè)類(lèi)都正確地符合了 HasArea 協(xié)議苛萎。

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

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

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

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

objects 數(shù)組使用字面量初始化凹蜂,數(shù)組包含一個(gè) radius 為 2 的 Circle 的實(shí)例,一個(gè)保存了英國(guó)國(guó)土面積的 Country 實(shí)例和一個(gè) legs 為 4 的 Animal 實(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 常量上趴乡。objectWithArea 是 HasArea 協(xié)議類(lèi)型的實(shí)例对省,因此 area 屬性可以被訪問(wèn)和打印。

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

15.可選的協(xié)議要求

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

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

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

下面的例子定義了一個(gè)名為 Counter 的用于整數(shù)計(jì)數(shù)的類(lèi)痛倚,它使用外部的數(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é)議的類(lèi)可以不實(shí)現(xiàn)這些要求颠区,盡管技術(shù)上允許這樣做削锰,不過(guò)最好不要這樣寫(xiě)。

Counter 類(lèi)含有 CounterDataSource? 類(lèi)型的可選屬性 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 類(lèi)使用變量屬性 count 來(lái)存儲(chǔ)當(dāng)前值器贩。該類(lèi)還定義了一個(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? 類(lèi)型剂邮。雖然在 CounterDataSource 協(xié)議中,increment(forCount:) 的返回值類(lèi)型是非可選 Int乐埠。另外抗斤,即使這里使用了兩層可選鏈?zhǔn)秸{(diào)用囚企,最后的返回結(jié)果依舊是單層的可選類(lèi)型。關(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:) 方法獲取到值锯蛀,可能由于 dataSource 為 nil灭衷,或者它并沒(méi)有實(shí)現(xiàn) increment(forCount:) 方法,那么 increment() 方法將試圖從數(shù)據(jù)源的 fixedIncrement 屬性中獲取增量旁涤。fixedIncrement 是一個(gè)可選屬性翔曲,因此屬性值是一個(gè) Int? 值,即使該屬性在 CounterDataSource 協(xié)議中的類(lèi)型是非可選的 Int劈愚。

下面的例子展示了 CounterDataSource 的簡(jiǎn)單實(shí)現(xiàn)瞳遍。ThreeSource 類(lèi)遵循了 CounterDataSource 協(xié)議,它實(shí)現(xiàn)了可選屬性 fixedIncrement菌羽,每次會(huì)返回 3:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

可以使用 ThreeSource 的實(shí)例作為 Counter 實(shí)例的數(shù)據(jù)源:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

上述代碼新建了一個(gè) Counter 實(shí)例掠械,并將它的數(shù)據(jù)源設(shè)置為一個(gè) ThreeSource 的實(shí)例,然后調(diào)用 increment() 方法四次注祖。和預(yù)期一樣猾蒂,每次調(diào)用都會(huì)將 count 的值增加 3.

下面是一個(gè)更為復(fù)雜的數(shù)據(jù)源 TowardsZeroSource,它將使得最后的值變?yōu)?0:

@objc class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

TowardsZeroSource 實(shí)現(xiàn)了 CounterDataSource 協(xié)議中的 increment(forCount:) 方法氓轰,以 count 參數(shù)為依據(jù)婚夫,計(jì)算出每次的增量浸卦。如果 count 已經(jīng)為 0署鸡,此方法返回 0,以此表明之后不應(yīng)再有增量操作發(fā)生限嫌。

你可以使用 TowardsZeroSource 實(shí)例將 Counter 實(shí)例來(lái)從 -4 增加到 0靴庆。一旦增加到 0,數(shù)值便不會(huì)再有變動(dòng):

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
16.協(xié)議擴(kuò)展

協(xié)議可以通過(guò)擴(kuò)展來(lái)為遵循協(xié)議的類(lèi)型提供屬性怒医、方法以及下標(biāo)的實(shí)現(xiàn)炉抒。通過(guò)這種方式,你可以基于協(xié)議本身來(lái)實(shí)現(xiàn)這些功能稚叹,而無(wú)需在每個(gè)遵循協(xié)議的類(lèi)型中都重復(fù)同樣的實(shí)現(xiàn)焰薄,也無(wú)需使用全局函數(shù)拿诸。

例如,可以擴(kuò)展 RandomNumberGenerator 協(xié)議來(lái)提供 randomBool() 方法塞茅。該方法使用協(xié)議中定義的 random() 方法來(lái)返回一個(gè)隨機(jī)的 Bool 值:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

通過(guò)協(xié)議擴(kuò)展亩码,所有遵循協(xié)議的類(lèi)型,都能自動(dòng)獲得這個(gè)擴(kuò)展所增加的方法實(shí)現(xiàn)野瘦,無(wú)需任何額外修改:

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean: \(generator.randomBool())")
// 打印 “And here's a random Boolean: true”

提供默認(rèn)實(shí)現(xiàn)

可以通過(guò)協(xié)議擴(kuò)展來(lái)為協(xié)議要求的屬性描沟、方法以及下標(biāo)提供默認(rèn)的實(shí)現(xiàn)。如果遵循協(xié)議的類(lèi)型為這些要求提供了自己的實(shí)現(xiàn)鞭光,那么這些自定義實(shí)現(xiàn)將會(huì)替代擴(kuò)展中的默認(rèn)實(shí)現(xiàn)被使用吏廉。

注意
通過(guò)協(xié)議擴(kuò)展為協(xié)議要求提供的默認(rèn)實(shí)現(xiàn)和可選的協(xié)議要求不同。雖然在這兩種情況下惰许,遵循協(xié)議的類(lèi)型都無(wú)需自己實(shí)現(xiàn)這些要求席覆,但是通過(guò)擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)可以直接調(diào)用,而無(wú)需使用可選鏈?zhǔn)秸{(diào)用汹买。

例如娜睛,PrettyTextRepresentable 協(xié)議繼承自 TextRepresentable 協(xié)議,可以為其提供一個(gè)默認(rèn)的 prettyTextualDescription 屬性卦睹,只是簡(jiǎn)單地返回 textualDescription 屬性的值:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

為協(xié)議擴(kuò)展添加限制條件

在擴(kuò)展協(xié)議的時(shí)候畦戒,可以指定一些限制條件,只有遵循協(xié)議的類(lèi)型滿足這些限制條件時(shí)结序,才能獲得協(xié)議擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)障斋。這些限制條件寫(xiě)在協(xié)議名之后,使用 where 子句來(lái)描述徐鹤,正如Where子句中所描述的垃环。

例如,你可以擴(kuò)展 CollectionType 協(xié)議返敬,但是只適用于集合中的元素遵循了 TextRepresentable 協(xié)議的情況:

extension Collection where Iterator.Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}

textualDescription 屬性返回整個(gè)集合的文本描述遂庄,它將集合中的每個(gè)元素的文本描述以逗號(hào)分隔的方式連接起來(lái),包在一對(duì)方括號(hào)中劲赠。

現(xiàn)在我們來(lái)看看先前的 Hamster 結(jié)構(gòu)體涛目,它符合 TextRepresentable 協(xié)議,同時(shí)這里還有個(gè)裝有 Hamster 的實(shí)例的數(shù)組:

let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]

因?yàn)?Array 符合 CollectionType 協(xié)議凛澎,而數(shù)組中的元素又符合 TextRepresentable 協(xié)議霹肝,所以數(shù)組可以使用 textualDescription 屬性得到數(shù)組內(nèi)容的文本表示:

print(hamsters.textualDescription)
// 打印 “[A hamster named Murray, A hamster named Morgan, A hamster named Maurice]”

注意
如果多個(gè)協(xié)議擴(kuò)展都為同一個(gè)協(xié)議要求提供了默認(rèn)實(shí)現(xiàn),而遵循協(xié)議的類(lèi)型又同時(shí)滿足這些協(xié)議擴(kuò)展的限制條件塑煎,那么將會(huì)使用限制條件最多的那個(gè)協(xié)議擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)沫换。

17.協(xié)議總結(jié)
協(xié)議

protocol FullyNamed {
    var fullName: String { get }
}
// 該協(xié)議表示任何遵循FullyNamed的類(lèi)型,都必須有一個(gè)可讀的String類(lèi)型的實(shí)例屬性fullName

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

// 屬性要求
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")
print(ncc1701.fullName)

// 方法要求
protocol RandomNumberGenerator {
    func random() -> Double
}

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())")
print("Another random number is: \(generator.random())")

// mutating 方法要求
protocol Togglable {
    mutating func toggle()
}

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()
print(lightSwitch)

// 構(gòu)造器要求在類(lèi)中的實(shí)現(xiàn)
protocol SomeProtocol {
    init()
}

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

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

// 協(xié)議作為類(lèi)型
class Dice {
    let sides: Int
    let generate: RandomNumberGenerator
    // generate屬性的類(lèi)型是RandomNumberGenerator垮兑,因此任何遵循了RandomNumberGenerator協(xié)議的類(lèi)型的實(shí)例都可以賦值給generate,除此之外并無(wú)其他要求
    init(sides: Int, generate: RandomNumberGenerator) {
        self.sides = sides
        self.generate = generate
    }
    func roll() -> Int {
        return Int(generate.random() * Double(sides)) + 1
    }
}
var d6 = Dice(sides: 6, generate: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}

// 委托代理模式
protocol DiceGame {
    var dice: Dice { get }
    func play()
}

protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStratNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

class SnakeAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generate: 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, didStratNewTurnWithDiceRoll: 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)
    }
}

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

let tracker = DiceGameTracker()
let game = SnakeAndLadders()
game.delegate = tracker
game.play()

// 通過(guò)擴(kuò)展添加協(xié)議一致性
protocol TextRepresentable {
    var textualDescription: String { get }
}
extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}
let d12 = Dice(sides: 12, generate: LinearCongruentialGenerator())
print(d12.textualDescription)

extension SnakeAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)

// 通過(guò)擴(kuò)展遵循協(xié)議 當(dāng)一個(gè)類(lèi)型已經(jīng)符合某個(gè)協(xié)議中的所有要求漱挎,卻還沒(méi)有聲明遵循該協(xié)議時(shí)甥角,可以通過(guò)空擴(kuò)展體的擴(kuò)展來(lái)遵循該協(xié)議
struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
// 空擴(kuò)展體
extension Hamster: TextRepresentable{}
let simonTheHamster = Hamster(name: "Simon")
let somethingRextRepresentable: TextRepresentable = simonTheHamster
print(somethingRextRepresentable.textualDescription)

// 協(xié)議類(lèi)型的集合
let things: [TextRepresentable] = [d12, game, simonTheHamster]
for thing in things {
    print(thing.textualDescription)
}

// 協(xié)議的繼承
protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

extension SnakeAndLadders: 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
    }
}
print(game.prettyTextualDescription)

// 協(xié)議合成
protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person2: 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 = Person2(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)

// 檢查協(xié)議的一致性
protocol HasArea {
    var area: Double { get }
}

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) {
        self.radius = radius
    }
}
// Circle類(lèi)把a(bǔ)rea屬性實(shí)現(xiàn)為基于存儲(chǔ)型屬性radius的計(jì)算屬性

class Country: HasArea {
    var area: Double
    init(area: Double) {
        self.area = area
    }
}
// Country類(lèi)則把a(bǔ)rea屬性實(shí)現(xiàn)為存儲(chǔ)型屬性

class Animal {
    var legs: Int
    init(legs: Int) {
        self.legs = legs
    }
}
// Animal類(lèi)沒(méi)有遵循HasArea的協(xié)議

let objects: [AnyObject] = [Circle(radius: 2.0), Country(area: 243_610), Animal(legs: 4)]
for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}

// 可選的協(xié)議要求 optional
// 可選要求用在需要和Object-C打交道的代碼中,并且協(xié)議和要求需帶上@objc屬性
@objc protocol CounterDataSource {
    // 定義可選方法
    @objc optional func incrementForCount(count: Int) -> Int
    // 定義可選屬性
    @objc optional var fixedIncrement: Int { get }
}

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

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}

@objc class TowardsZeroSource: NSObject, CounterDataSource {
    func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }

    }
}
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}

// 協(xié)議擴(kuò)展
extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}
let generator2 = LinearCongruentialGenerator()
print("Here's a random number: \(generator2.random())")
print("And here's a random Boolean: \(generator2.randomBool())")

// 提供默認(rèn)實(shí)現(xiàn) 如果遵循協(xié)議的類(lèi)型提供了自己的實(shí)現(xiàn)识樱,那么這些自定義的實(shí)現(xiàn)將會(huì)替代擴(kuò)展中的默認(rèn)實(shí)現(xiàn)被使用
extension PrettyTextRepresentable {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

// 為協(xié)議擴(kuò)展添加限制條件
extension Collection where Iterator.Element: TextRepresentable {
    var textualDescription: String {
        let itemAsText = self.map { $0.textualDescription }
        return "[" + itemAsText.joined(separator: ", ") + "]"
    }
}

let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]

print(hamsters.textualDescription)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嗤无,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子怜庸,更是在濱河造成了極大的恐慌当犯,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,331評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件割疾,死亡現(xiàn)場(chǎng)離奇詭異嚎卫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)宏榕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)拓诸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人麻昼,你說(shuō)我怎么就攤上這事奠支。” “怎么了抚芦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,755評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵倍谜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我叉抡,道長(zhǎng)尔崔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,528評(píng)論 1 296
  • 正文 為了忘掉前任褥民,我火速辦了婚禮季春,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘消返。我一直安慰自己载弄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,526評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布侦副。 她就那樣靜靜地躺著侦锯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秦驯。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,166評(píng)論 1 308
  • 那天挣棕,我揣著相機(jī)與錄音坷牛,去河邊找鬼墙牌。 笑死片酝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的题篷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,768評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼厅目,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼番枚!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起损敷,我...
    開(kāi)封第一講書(shū)人閱讀 39,664評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤葫笼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拗馒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體路星,經(jīng)...
    沈念sama閱讀 46,205評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,290評(píng)論 3 340
  • 正文 我和宋清朗相戀三年诱桂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了洋丐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,435評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挥等,死狀恐怖友绝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肝劲,我是刑警寧澤九榔,帶...
    沈念sama閱讀 36,126評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站涡相,受9級(jí)特大地震影響哲泊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜催蝗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,804評(píng)論 3 333
  • 文/蒙蒙 一切威、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丙号,春花似錦先朦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,276評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至怀薛,卻和暖如春刺彩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工创倔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗡害,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,818評(píng)論 3 376
  • 正文 我出身青樓畦攘,卻偏偏與公主長(zhǎng)得像霸妹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子知押,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,442評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容