OneDayOneSwift[22] - Protocols

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

除了采納協(xié)議的類型必須實現(xiàn)的要求外,還可以對協(xié)議進(jìn)行擴展,通過擴展來實現(xiàn)一部分要求或者實現(xiàn)一些附加功能,這樣采納協(xié)議的類型就能夠使用這些功能。

協(xié)議語法

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

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

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

屬性要求(property requirements)

協(xié)議可以要求采納協(xié)議的類型提供特定名稱和類型的實例屬性或類型屬性。協(xié)議不指定屬性是存儲型屬性還是計算型屬性,它只指定屬性的名稱和類型。此外,協(xié)議還指定屬性是只讀的還是可讀可寫的。

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

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

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

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

如果協(xié)議要求未被完全滿足,在編譯時會報錯。

方法要求(Method requirements)

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

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) % 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”

Mutating 方法要求

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

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

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

構(gòu)造器要求

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

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

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

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

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

protocol SomeProtocol {
    init()
}

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

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因為采納協(xié)議,需要加上 required
    // 因為繼承自父類,需要加上 override
    required override init() {
        // 這里是構(gòu)造器的實現(xiàn)部分
    }
}

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

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

協(xié)議作為類型

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

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

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

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

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

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

委托(代理)模式 Delegation

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

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

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = [Int](count: finalSquare + 1, repeatedValue: 0)
        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)
    }
}

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
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}
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

通過擴展添加協(xié)議一致性

即便無法修改源代碼,依然可以通過擴展令已有類型采納并符合協(xié)議。擴展可以為已有類型添加屬性、方法、下標(biāo)以及構(gòu)造器,因此可以符合協(xié)議中的相應(yīng)要求。

ps: 通過擴展令已有類型采納并符合協(xié)議時,該類型的所有實例也會隨之獲得協(xié)議中定義的各項功能。

通過擴展采納并符合協(xié)議,和在原始定義中采納并符合協(xié)議的效果完全相同。協(xié)議名稱寫在類型名之后,以冒號隔開,然后在擴展的大括號內(nèi)實現(xiàn)協(xié)議要求的內(nèi)容。

通過擴展采納協(xié)議

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

ps: 即使?jié)M足了協(xié)議的所有要求,類型也不會自動采納協(xié)議,必須顯式地采納協(xié)議。

協(xié)議的繼承

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

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

類類型專屬協(xié)議

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

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

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

ps: 當(dāng)協(xié)議定義的要求需要采納協(xié)議的類型必須是引用語義而非值語義時,應(yīng)該采用類類型專屬協(xié)議。

協(xié)議合成

有時候需要同時采納多個協(xié)議,你可以將多個協(xié)議采用 protocol<SomeProtocol, AnotherProtocol> 這樣的格式進(jìn)行組合,稱為 協(xié)議合成(protocol composition)。你可以在 <> 中羅列任意多個你想要采納的協(xié)議,以逗號分隔。

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

ps: 協(xié)議合成并不會生成新的、永久的協(xié)議類型,而是將多個協(xié)議中的要求合成到一個只在局部作用域有效的臨時協(xié)議中。

檢查協(xié)議一致性

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

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

可選的協(xié)議要求

協(xié)議可以定義可選要求,采納協(xié)議的類型可以選擇是否實現(xiàn)這些要求。在協(xié)議中使用 optional 關(guān)鍵字作為前綴來定義可選要求。使用可選要求時(例如,可選的方法或者屬性),它們的類型會自動變成可選的。比如,一個類型為 (Int) -> String 的方法會變成 ((Int) -> String)?。需要注意的是整個函數(shù)類型是可選的,而不是函數(shù)的返回值

協(xié)議中的可選要求可通過可選鏈?zhǔn)秸{(diào)用來使用,因為采納協(xié)議的類型可能沒有實現(xiàn)這些可選要求。類似 someOptionalMethod?(someArgument) 這樣,你可以在可選方法名稱后加上 ? 來調(diào)用可選方法。

ps: 可選的協(xié)議要求只能用在標(biāo)記 @objc 特性的協(xié)議中。
該特性表示協(xié)議將暴露給 Objective-C 代碼,詳情參見Using Swift with Cocoa and Objective-C(Swift 2.1)。即使你不打算和 Objective-C 有什么交互,如果你想要指定可選的協(xié)議要求,那么還是要為協(xié)議加上 @obj 特性。
還需要注意的是,標(biāo)記 @objc 特性的協(xié)議只能被繼承自 Objective-C 類的類或者 @objc 類采納,其他類以及結(jié)構(gòu)體和枚舉均不能采納這種協(xié)議。

協(xié)議擴展,提供默認(rèn)實現(xiàn)

協(xié)議可以通過擴展來為采納協(xié)議的類型提供屬性、方法以及下標(biāo)的實現(xiàn)。通過這種方式,你可以基于協(xié)議本身來實現(xiàn)這些功能,而無需在每個采納協(xié)議的類型中都重復(fù)同樣的實現(xiàn),也無需使用全局函數(shù)。

可以通過協(xié)議擴展來為協(xié)議要求的屬性、方法以及下標(biāo)提供默認(rèn)的實現(xiàn)。如果采納協(xié)議的類型為這些要求提供了自己的實現(xiàn),那么這些自定義實現(xiàn)將會替代擴展中的默認(rèn)實現(xiàn)被使用。

ps: 通過協(xié)議擴展為協(xié)議要求提供的默認(rèn)實現(xiàn)和可選的協(xié)議要求不同。雖然在這兩種情況下,采納協(xié)議的類型都無需自己實現(xiàn)這些要求,但是通過擴展提供的默認(rèn)實現(xiàn)可以直接調(diào)用,而無需使用可選鏈?zhǔn)秸{(diào)用。

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

在擴展協(xié)議的時候,可以指定一些限制條件,只有采納協(xié)議的類型滿足這些限制條件時,才能獲得協(xié)議擴展提供的默認(rèn)實現(xiàn)。這些限制條件寫在協(xié)議名之后,使用 where 子句來描述.

extension CollectionType where Generator.Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joinWithSeparator(", ") + "]"
    }
}

ps: 如果多個協(xié)議擴展都為同一個協(xié)議要求提供了默認(rèn)實現(xiàn),而采納協(xié)議的類型又同時滿足這些協(xié)議擴展的限制條件,那么將會使用限制條件最多的那個協(xié)議擴展提供的默認(rèn)實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,480評論 2 379

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

  • 1、范型范型所解決的問題 函數(shù)、方法、類型:類,結(jié)構(gòu)體,枚舉,元組類型,協(xié)議參數(shù),返回值,成員函數(shù)參數(shù),成員屬性類...
    我是小胡胡123閱讀 853評論 0 1
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young閱讀 3,860評論 1 10
  • 132.轉(zhuǎn)換錯誤成可選值 通過轉(zhuǎn)換錯誤成一個可選值,你可以使用 try? 來處理錯誤。當(dāng)執(zhí)行try?表達(dá)式時,如果...
    無灃閱讀 1,274評論 0 3
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,072評論 6 13
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,825評論 18 139