> 1.0 翻譯:[geek5nan](https://github.com/geek5nan)?校對:[dabing1022](https://github.com/dabing1022)
>
> 2.0 翻譯+校對:[futantan](https://github.com/futantan)
本頁包含內容:
[TOC=2]
`協議`定義了一個藍圖,規定了用來實現某一特定工作或者功能所必需的方法和屬性。類,結構體或枚舉類型都可以遵循協議,并提供具體實現來完成協議定義的方法和功能。任意能夠滿足協議要求的類型被稱為`遵循(conform)`這個協議。
## 協議的語法
協議的定義方式與類,結構體,枚舉的定義非常相似。
~~~
protocol SomeProtocol {
// 協議內容
}
~~~
要使類遵循某個協議,需要在類型名稱后加上協議名稱,中間以冒號`:`分隔,作為類型定義的一部分。遵循多個協議時,各協議之間用逗號`,`分隔。
~~~
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 結構體內容
}
~~~
如果類在遵循協議的同時擁有父類,應該將父類名放在協議名之前,以逗號分隔。
~~~
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 類的內容
}
~~~
## 對屬性的規定
協議可以規定其`遵循者`提供特定名稱和類型的`實例屬性(instance property)`或`類屬性(type property)`,而不指定是`存儲型屬性(stored property)`還是`計算型屬性(calculate property)`。此外還必須指明是只讀的還是可讀可寫的。
如果協議規定屬性是可讀可寫的,那么這個屬性不能是常量或只讀的計算屬性。如果協議只要求屬性是只讀的(gettable),那個屬性不僅可以是只讀的,如果你代碼需要的話,也可以是可寫的。
協議中的通常用var來聲明屬性,在類型聲明后加上`{ set get }`來表示屬性是可讀可寫的,只讀屬性則用`{ get }`來表示。
~~~
protocol SomeProtocol {
var mustBeSettable : Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
~~~
在協議中定義類屬性(type property)時,總是使用`static`關鍵字作為前綴。當協議的遵循者是類時,可以使用`class`或`static`關鍵字來聲明類屬性,但是在協議的定義中,仍然要使用`static`關鍵字。
~~~
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
~~~
如下所示,這是一個含有一個實例屬性要求的協議。
~~~
protocol FullyNamed {
var fullName: String { get }
}
~~~
`FullyNamed`協議除了要求協議的遵循者提供fullName屬性外,對協議對遵循者的類型并沒有特別的要求。這個協議表示,任何遵循`FullyNamed`協議的類型,都具有一個可讀的`String`類型實例屬性`fullName`。
下面是一個遵循`FullyNamed`協議的簡單結構體。
~~~
struct Person: FullyNamed{
var fullName: String
}
let john = Person(fullName: "John Appleseed")
//john.fullName 為 "John Appleseed"
~~~
這個例子中定義了一個叫做`Person`的結構體,用來表示具有名字的人。從第一行代碼中可以看出,它遵循了`FullyNamed`協議。
`Person`結構體的每一個實例都有一個叫做`fullName`,`String`類型的存儲型屬性。這正好滿足了`FullyNamed`協議的要求,也就意味著,`Person`結構體完整的`遵循`了協議。(如果協議要求未被完全滿足,在編譯時會報錯)
下面是一個更為復雜的類,它采用并遵循了`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"
~~~
Starship類把`fullName`屬性實現為只讀的計算型屬性。每一個`Starship`類的實例都有一個名為`name`的屬性和一個名為`prefix`的可選屬性。 當`prefix`存在時,將`prefix`插入到`name`之前來為Starship構建`fullName`,`prefix`不存在時,則將直接用`name`構建`fullName`。
## 對方法的規定
協議可以要求其遵循者實現某些指定的實例方法或類方法。這些方法作為協議的一部分,像普通的方法一樣放在協議的定義中,但是不需要大括號和方法體。可以在協議中定義具有可變參數的方法,和普通方法的定義方式相同。但是在協議的方法定義中,不支持參數默認值。
正如對屬性的規定中所說的,在協議中定義類方法的時候,總是使用`static`關鍵字作為前綴。當協議的遵循者是類的時候,雖然你可以在類的實現中使用`class`或者`static`來實現類方法,但是在協議中聲明類方法,仍然要使用`static`關鍵字。
~~~
protocol SomeProtocol {
static func someTypeMethod()
}
~~~
下面的例子定義了含有一個實例方法的協議。
~~~
protocol RandomNumberGenerator {
func random() -> Double
}
~~~
`RandomNumberGenerator`協議要求其遵循者必須擁有一個名為`random`, 返回值類型為`Double`的實例方法。盡管這里并未指明,但是我們假設返回值在[0,1)區間內。
`RandomNumberGenerator`協議并不在意每一個隨機數是怎樣生成的,它只強調這里有一個隨機數生成器。
如下所示,下邊的是一個遵循了`RandomNumberGenerator`協議的類。該類實現了一個叫做_線性同余生成器(linear congruential generator)_的偽隨機數算法。
~~~
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方法的規定
有時需要在方法中改變它的實例。例如,值類型(結構體,枚舉)的實例方法中,將`mutating`關鍵字作為函數的前綴,寫在`func`之前,表示可以在該方法中修改它所屬的實例及其實例屬性的值。這一過程在[在實例方法中修改值類型](http://wiki.jikexueyuan.com/project/swift/chapter2/11_Methods.html#modifying_value_types_from_within_instance_methods)章節中有詳細描述。
如果你在協議中定義了一個方法旨在改變遵循該協議的實例,那么在協議定義時需要在方法前加`mutating`關鍵字。這使得結構和枚舉遵循協議并滿足此方法要求。
> 注意:
> 用類實現協議中的`mutating`方法時,不用寫`mutating`關鍵字;用結構體,枚舉實現協議中的`mutating`方法時,必須寫`mutating`關鍵字。
如下所示,`Togglable`協議含有名為`toggle`的實例方法。根據名稱推測,`toggle()`方法將通過改變實例屬性,來切換遵循該協議的實例的狀態。
`toggle()`方法在定義的時候,使用`mutating`關鍵字標記,這表明當它被調用時該方法將會改變協議遵循者實例的狀態。
~~~
protocol Togglable {
mutating func toggle()
}
~~~
當使用`枚舉`或`結構體`來實現`Togglable`協議時,需要提供一個帶有`mutating`前綴的`toggle`方法。
下面定義了一個名為`OnOffSwitch`的枚舉類型。這個枚舉類型在兩種狀態之間進行切換,用枚舉成員`On`和`Off`表示。枚舉類型的`toggle`方法被標記為`mutating`以滿足`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 現在的值為 .On
~~~
## 對構造器的規定
協議可以要求它的遵循者實現指定的構造器。你可以像書寫普通的構造器那樣,在協議的定義里寫下構造器的聲明,但不需要寫花括號和構造器的實體:
~~~
protocol SomeProtocol {
init(someParameter: Int)
}
~~~
### 協議構造器規定在類中的實現
你可以在遵循該協議的類中實現構造器,并指定其為類的指定構造器(designated initializer)或者便利構造器(convenience initializer)。在這兩種情況下,你都必須給構造器實現標上"required"修飾符:
~~~
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
//構造器實現
}
}
~~~
使用`required`修飾符可以保證:所有的遵循該協議的子類,同樣能為構造器規定提供一個顯式的實現或繼承實現。
關于`required`構造器的更多內容,請參考[必要構造器](http://wiki.jikexueyuan.com/project/swift/chapter2/14_Initialization.html#required_initializers)。
> 注意
> 如果類已經被標記為`final`,那么不需要在協議構造器的實現中使用`required`修飾符。因為final類不能有子類。關于`final`修飾符的更多內容,請參見[防止重寫](http://wiki.jikexueyuan.com/project/swift/chapter2/13_Inheritance.html#preventing_overrides)。
如果一個子類重寫了父類的指定構造器,并且該構造器遵循了某個協議的規定,那么該構造器的實現需要被同時標示`required`和`override`修飾符
~~~
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 構造器的實現
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因為遵循協議,需要加上"required"; 因為繼承自父類,需要加上"override"
required override init() {
// 構造器實現
}
}
~~~
### 可失敗構造器的規定
可以通過給協議`Protocols`中添加[可失敗構造器](http://wiki.jikexueyuan.com/project/swift/chapter2/14_Initialization.html#failable_initializers)來使遵循該協議的類型必須實現該可失敗構造器。
如果在協議中定義一個可失敗構造器,則在遵頊該協議的類型中必須添加同名同參數的可失敗構造器或非可失敗構造器。如果在協議中定義一個非可失敗構造器,則在遵循該協議的類型中必須添加同名同參數的非可失敗構造器或隱式解析類型的可失敗構造器(`init!`)。
## 協議類型
盡管協議本身并不實現任何功能,但是協議可以被當做類型來使用。
協議可以像其他普通類型一樣使用,使用場景:
* 作為函數、方法或構造器中的參數類型或返回值類型
* 作為常量、變量或屬性的類型
* 作為數組、字典或其他容器中的元素類型
> 注意
> 協議是一種類型,因此協議類型的名稱應與其他類型(Int,Double,String)的寫法相同,使用大寫字母開頭的駝峰式寫法,例如(`FullyNamed`和`RandomNumberGenerator`)
如下所示,這個示例中將協議當做類型來使用
~~~
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個面的骰子。`Dice`的實例含有`sides`和`generator`兩個屬性,前者是整型,用來表示骰子有幾個面,后者為骰子提供一個隨機數生成器。
`generator`屬性的類型為`RandomNumberGenerator`,因此任何遵循了`RandomNumberGenerator`協議的類型的實例都可以賦值給`generator`,除此之外,無其他要求。
`Dice`類中也有一個構造器(initializer),用來進行初始化操作。構造器中含有一個名為`generator`,類型為`RandomNumberGenerator`的形參。在調用構造方法時創建`Dice`的實例時,可以傳入任何遵循`RandomNumberGenerator`協議的實例給generator。
`Dice`類也提供了一個名為`roll`的實例方法用來模擬骰子的面值。它先使用`generator`的`random()`方法來創建一個[0,1)區間內的隨機數,然后使用這個隨機數生成正確的骰子面值。因為generator遵循了`RandomNumberGenerator`協議,因而保證了`random`方法可以被調用。
下面的例子展示了如何使用`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
~~~
## 委托(代理)模式
委托是一種設計模式,它允許`類`或`結構體`將一些需要它們負責的功能`交由(委托)`給其他的類型的實例。委托模式的實現很簡單: 定義協議來封裝那些需要被委托的函數和方法, 使其`遵循者`擁有這些被委托的`函數和方法`。委托模式可以用來響應特定的動作或接收外部數據源提供的數據,而無需要知道外部數據源的類型信息。
下面的例子是兩個基于骰子游戲的協議:
~~~
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`協議可以在任意含有骰子的游戲中實現。`DiceGameDelegate`協議可以用來追蹤`DiceGame`的游戲過程
如下所示,`SnakesAndLadders`是`Snakes and Ladders`([Control Flow](http://wiki.jikexueyuan.com/project/swift/chapter2/05_Control_Flow.html)章節有該游戲的詳細介紹)游戲的新版本。新版本使用`Dice`作為骰子,并且實現了`DiceGame`和`DiceGameDelegate`協議,后者用來記錄游戲的過程:
~~~
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)
}
}
~~~
這個版本的游戲封裝到了`SnakesAndLadders`類中,該類遵循了`DiceGame`協議,并且提供了相應的可讀的`dice`屬性和`play`實例方法。(`dice`屬性在構造之后就不再改變,且協議只要求`dice`為只讀的,因此將`dice`聲明為常量屬性。)
游戲使用`SnakesAndLadders`類的`構造器(initializer)`初始化游戲。所有的游戲邏輯被轉移到了協議中的`play`方法,`play`方法使用協議規定的`dice`屬性提供骰子搖出的值。
注意:`delegate`并不是游戲的必備條件,因此`delegate`被定義為遵循`DiceGameDelegate`協議的可選屬性。因為`delegate`是可選值,因此在初始化的時候被自動賦值為`nil`。隨后,可以在游戲中為`delegate`設置適當的值。
`DicegameDelegate`協議提供了三個方法用來追蹤游戲過程。被放置于游戲的邏輯中,即`play()`方法內。分別在游戲開始時,新一輪開始時,游戲結束時被調用。
因為`delegate`是一個遵循`DiceGameDelegate`的可選屬性,因此在`play()`方法中使用了`可選鏈`來調用委托方法。 若`delegate`屬性為`nil`, 則delegate所調用的方法失效,并不會產生錯誤。若`delegate`不為`nil`,則方法能夠被調用
如下所示,`DiceGameTracker`遵循了`DiceGameDelegate`協議
~~~
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")
}
}
~~~
`DiceGameTracker`實現了`DiceGameDelegate`協議規定的三個方法,用來記錄游戲已經進行的輪數。 當游戲開始時,`numberOfTurns`屬性被賦值為0; 在每新一輪中遞增; 游戲結束后,輸出打印游戲的總輪數。
`gameDidStart`方法從`game`參數獲取游戲信息并輸出。`game`在方法中被當做`DiceGame`類型而不是`SnakeAndLadders`類型,所以方法中只能訪問`DiceGame`協議中的成員。當然了,這些方法也可以在類型轉換之后調用。在上例代碼中,通過`is`操作符檢查`game`是否為?`SnakesAndLadders`類型的實例,如果是,則打印出相應的內容。
無論當前進行的是何種游戲,`game`都遵循`DiceGame`協議以確保`game`含有`dice`屬性,因此在`gameDidStart(_:)`方法中可以通過傳入的`game`參數來訪問`dice`屬性,進而打印出`dice`的`sides`屬性的值。
`DiceGameTracker`的運行情況,如下所示:
~~~
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
~~~
## 在擴展中添加協議成員
即便無法修改源代碼,依然可以通過擴展(Extension)來擴充已存在類型(_譯者注: 類,結構體,枚舉等_)。擴展可以為已存在的類型添加屬性,方法,下標腳本,協議等成員。詳情請在[擴展](http://wiki.jikexueyuan.com/project/swift/chapter2/21_Extensions.html)章節中查看。
> 注意
> 通過擴展為已存在的類型遵循協議時,該類型的所有實例也會隨之添加協議中的方法
例如`TextRepresentable`協議,任何想要表示一些文本內容的類型都可以遵循該協議。這些想要表示的內容可以是類型本身的描述,也可以是當前內容的版本:
~~~
protocol TextRepresentable {
func asText() -> String
}
~~~
可以通過擴展,為上一節中提到的`Dice`增加類遵循`TextRepresentable`協議的功能
~~~
extension Dice: TextRepresentable {
func asText() -> String {
return "A \(sides)-sided dice"
}
}
~~~
現在,通過擴展使得`Dice`類型遵循了一個新的協議,這和`Dice`類型在定義的時候聲明為遵循`TextRepresentable`協議的效果相同。在擴展的時候,協議名稱寫在類型名之后,以冒號隔開,在大括號內寫明新添加的協議內容。
現在所有`Dice`的實例都遵循了`TextRepresentable`協議:
~~~
let d12 = Dice(sides: 12,generator: LinearCongruentialGenerator())
print(d12.asText())
// 輸出 "A 12-sided dice"
~~~
同樣`SnakesAndLadders`類也可以通過`擴展`的方式來遵循`TextRepresentable`協議:
~~~
extension SnakesAndLadders: TextRepresentable {
func asText() -> String {
return "A game of Snakes and Ladders with \(finalSquare) squares"
}
}
print(game.asText())
// 輸出 "A game of Snakes and Ladders with 25 squares"
~~~
## 通過擴展補充協議聲明
當一個類型已經實現了協議中的所有要求,卻沒有聲明為遵循該協議時,可以通過擴展(空的擴展體)來補充協議聲明:
~~~
struct Hamster {
var name: String
func asText() -> String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
~~~
從現在起,`Hamster`的實例可以作為`TextRepresentable`類型使用
~~~
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.asText())
// 輸出 "A hamster named Simon"
~~~
> 注意
> 即使滿足了協議的所有要求,類型也不會自動轉變,因此你必須為它做出顯式的協議聲明
## 集合中的協議類型
協議類型可以在集合使用,表示集合中的元素均為協議類型,下面的例子創建了一個類型為`TextRepresentable`的數組:
~~~
let things: [TextRepresentable] = [game,d12,simonTheHamster]
~~~
如下所示,`things`數組可以被直接遍歷,并打印每個元素的文本表示:
~~~
for thing in things {
print(thing.asText())
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
~~~
`thing`被當做是`TextRepresentable`類型而不是`Dice`,`DiceGame`,`Hamster`等類型。因此能且僅能調用`asText`方法
## 協議的繼承
協議能夠繼承一個或多個其他協議,可以在繼承的協議基礎上增加新的內容要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號分隔:
~~~
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 協議定義
}
~~~
如下所示,`PrettyTextRepresentable`協議繼承了`TextRepresentable`協議
~~~
protocol PrettyTextRepresentable: TextRepresentable {
func asPrettyText() -> String
}
~~~
例子中定義了一個新的協議`PrettyTextRepresentable`,它繼承自`TextRepresentable`協議。任何遵循`PrettyTextRepresentable`協議的類型在滿足該協議的要求時,也必須滿足`TextRepresentable`協議的要求。在這個例子中,`PrettyTextRepresentable`協議要求其遵循者提供一個返回值為`String`類型的`asPrettyText`方法。
如下所示,擴展`SnakesAndLadders`,讓其遵循`PrettyTextRepresentable`協議:
~~~
extension SnakesAndLadders: PrettyTextRepresentable {
func asPrettyText() -> String {
var output = asText() + ":\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
}
}
~~~
上述擴展使得`SnakesAndLadders`遵循了`PrettyTextRepresentable`協議,并為每個`SnakesAndLadders`類型提供了了協議要求的`asPrettyText()`方法。每個`PrettyTextRepresentable`類型同時也是`TextRepresentable`類型,所以在`asPrettyText`的實現中,可以調用`asText()`方法。之后在每一行加上換行符,作為輸出的開始。然后遍歷數組中的元素,輸出一個幾何圖形來表示遍歷的結果:
* 當從數組中取出的元素的值大于0時,用`▲`表示
* 當從數組中取出的元素的值小于0時,用`▼`表示
* 當從數組中取出的元素的值等于0時,用`○`表示
任意`SankesAndLadders`的實例都可以使用`asPrettyText()`方法。
~~~
print(game.asPrettyText())
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
~~~
## 類專屬協議
你可以在協議的繼承列表中,通過添加`class`關鍵字,限制協議只能適配到類(class)類型。(結構體或枚舉不能遵循該協議)。該`class`關鍵字必須是第一個出現在協議的繼承列表中,其后,才是其他繼承協議。
~~~
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// class-only protocol definition goes here
}
~~~
在以上例子中,協議`SomeClassOnlyProtocol`只能被類(class)類型適配。如果嘗試讓結構體或枚舉類型適配該協議,則會出現編譯錯誤。
> 注意
> 當協議想要定義的行為,要求(或假設)它的遵循類型必須是引用語義而非值語義時,應該采用類專屬協議。關于引用語義,值語義的更多內容,請查看[結構體和枚舉是值類型](http://wiki.jikexueyuan.com/project/swift/chapter2/09_Classes_and_Structures.html#structures_and_enumerations_are_value_types)和[類是引用類型](http://wiki.jikexueyuan.com/project/swift/chapter2/09_Classes_and_Structures.html#classes_are_reference_types)。
## 協議合成
有時候需要同時遵循多個協議。你可以將多個協議采用`protocol<SomeProtocol, AnotherProtocol>`這樣的格式進行組合,稱為`協議合成(protocol composition)`。你可以在`<>`中羅列任意多個你想要遵循的協議,以逗號分隔。
下面的例子中,將`Named`和`Aged`兩個協議按照上述的語法組合成一個協議:
~~~
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!
~~~
`Named`協議包含`String`類型的`name`屬性;`Aged`協議包含`Int`類型的`age`屬性。`Person`結構體`遵循`了這兩個協議。
`wishHappyBirthday`函數的形參`celebrator`的類型為`protocol<Named,Aged>`。可以傳入任意`遵循`這兩個協議的類型的實例。
上面的例子創建了一個名為`birthdayPerson`的`Person`實例,作為參數傳遞給了`wishHappyBirthday(_:)`函數。因為`Person`同時遵循這兩個協議,所以這個參數合法,函數將輸出生日問候語。
> 注意
> `協議合成`并不會生成一個新協議類型,而是將多個協議合成為一個臨時的協議,超出范圍后立即失效。
## 檢驗協議的一致性
你可以使用`is`和`as`操作符來檢查是否遵循某一協議或強制轉化為某一類型。檢查和轉化的語法和之前相同(_詳情查看[類型轉換](http://wiki.jikexueyuan.com/project/swift/chapter2/20_Type_Casting.html)_):
* `is`操作符用來檢查實例是否`遵循`了某個`協議`
* `as?`返回一個可選值,當實例`遵循`協議時,返回該協議類型;否則返回`nil`
* `as`用以強制向下轉型,如果強轉失敗,會引起運行時錯誤。
下面的例子定義了一個`HasArea`的協議,要求有一個`Double`類型可讀的`area`:
~~~
protocol HasArea {
var area: Double { get }
}
~~~
如下所示,定義了`Circle`和`Country`類,它們都遵循了`HasArea`協議
~~~
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`實現為基于`存儲型屬性`radius的`計算型屬性`,`Country`類則把`area`實現為`存儲型屬性`。這兩個類都`遵循`了`HasArea`協議。
如下所示,Animal是一個沒有實現`HasArea`協議的類
~~~
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
~~~
`Circle`,`Country`,`Animal`并沒有一個相同的基類,然而,它們都是類,它們的實例都可以作為`AnyObject`類型的變量,存儲在同一個數組中:
~~~
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
~~~
`objects`數組使用字面量初始化,數組包含一個`radius`為2的`Circle`的實例,一個保存了英國面積的`Country`實例和一個`legs`為4的`Animal`實例。
如下所示,`objects`數組可以被迭代,對迭代出的每一個元素進行檢查,看它是否遵循了`HasArea`協議:
~~~
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
~~~
當迭代出的元素遵循`HasArea`協議時,通過`as?`操作符將其`可選綁定(optional binding)`到`objectWithArea`常量上。`objectWithArea`是`HasArea`協議類型的實例,因此`area`屬性是可以被訪問和打印的。
`objects`數組中元素的類型并不會因為強轉而丟失類型信息,它們仍然是`Circle`,`Country`,`Animal`類型。然而,當它們被賦值給`objectWithArea`常量時,則只被視為`HasArea`類型,因此只有`area`屬性能夠被訪問。
## 對可選協議的規定
協議可以含有可選成員,其`遵循者`可以選擇是否實現這些成員。在協議中使用`optional`關鍵字作為前綴來定義可選成員。
可選協議在調用時使用`可選鏈`,因為協議的遵循者可能沒有實現可選內容,詳細內容在[可空鏈式調用](http://wiki.jikexueyuan.com/project/swift/chapter2/17_Optional_Chaining.html)章節中查看。
像`someOptionalMethod?(someArgument)`這樣,你可以在可選方法名稱后加上`?`來檢查該方法是否被實現。可選方法和可選屬性都會返回一個`可選值(optional value)`,當其不可訪問時,`?`之后語句不會執行,并整體返回`nil`
> 注意
> 可選協議只能在含有`@objc`前綴的協議中生效。且`@objc`的協議只能被`類`遵循
> 這個前綴表示協議將暴露給Objective-C代碼,詳情參見`Using Swift with Cocoa and Objective-C`。即使你不打算和Objective-C有什么交互,如果你想要指明協議包含可選屬性,那么還是要加上`@obj`前綴
下面的例子定義了一個叫`Counter`的整數加法類,它使用外部的數據源來實現每次的增量。數據源是兩個可選屬性,在`CounterDataSource`協議中定義:
~~~
@objc protocol CounterDataSource {
optional func incrementForCount(count: Int) -> Int
optional var fixedIncrement: Int { get }
}
~~~
`CounterDataSource`含有`incrementForCount`可選方法和`fiexdIncrement`可選屬性,它們使用了不同的方法來從數據源中獲取合適的增量值。
> 注意
> `CounterDataSource`中的屬性和方法都是可選的,因此可以在類中聲明都不實現這些成員,盡管技術上允許這樣做,不過最好不要這樣寫。
`Counter`類含有`CounterDataSource?`類型的可選屬性`dataSource`,如下所示:
~~~
@objc 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`來存儲當前的值。該類同時定義了一個`increment`方法,每次調用該方法的時候,將會增加`count`的值。
`increment()`方法首先試圖使用`incrementForCount(_:)`方法來得到每次的增量。`increment()`方法使用可選鏈來嘗試調用`incrementForCount(_:)`,并將當前的`count`值作為參數傳入。
這里使用了兩種可選鏈方法。由于`dataSource`可能為`nil`,因此在`dataSource`后邊加上了`?`標記來表明只在`dataSource`非空時才去調用`incrementForCount`方法。即使`dataSource`存在,但是也無法保證其是否實現了`incrementForCount`方法,因此在`incrementForCount`方法后邊也加有`?`標記。
調用`incrementForCount`方法在上述兩種情形都有可能失敗,所以返回值為_可選_`Int`類型。雖然在`CounterDataSource`中,`incrementForCount`被定義為一個非可選`Int`(non-optional),但是這里我們仍然需要返回_可選_`Int`類型。
在調用`incrementForCount`方法后,`Int`型`可選值`通過`可選綁定(optional binding)`自動拆包并賦值給常量`amount`。如果可選值確實包含一個數值,這表示`delegate`和方法都存在,之后便將`amount`加到`count`上,增加操作完成。
如果沒有從`incrementForCount(_:)`獲取到值,可能是`dataSource`為nil,或者它并沒有實現`incrementForCount`方法——那么`increment()`方法將試圖從數據源的`fixedIncrement`屬性中獲取增量。`fixedIncrement`也是一個可選型,所以在屬性名的后面添加`?`來試圖取回可選屬性的值。和之前一樣,返回值為可選型。
`ThreeSource`實現了`CounterDataSource`協議,它實現來可選屬性`fixedIncrement`,每次返回值`3`:
~~~
@objc class ThreeSource: CounterDataSource {
let fixedIncrement = 3
}
~~~
可以使用`ThreeSource`的實例作為`Counter`實例的數據源:
~~~
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
~~~
上述代碼新建了一個`Counter`實例;將它的數據源設置為`TreeSource`實例;調用`increment()`4次。和你預想的一樣,每次在調用的時候,`count`的值增加3.
下面是一個更為復雜的數據源`TowardsZeroSource`,它將使得最后的值變為0:
~~~
class TowardsZeroSource: CounterDataSource {
func incrementForCount(count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
~~~
`TowardsZeroSource`實現了`CounterDataSource`協議中的`incrementForCount(_:)`方法,以`count`參數為依據,計算出每次的增量。如果`count`已經為0,方法返回0,這表示之后不會再有增量。
你可以配合使用`TowardsZeroSource`實例和`Counter`實例來從`-4`增加到`0`.一旦增加到`0`,數值便不會再有變動。
在下面的例子中,將從`-4`增加到`0`。一旦結果為`0`,便不在增加:
~~~
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
~~~
## 協議擴展
使用擴展協議的方式可以為遵循者提供方法或屬性的實現。通過這種方式,可以讓你無需在每個遵循者中都實現一次,無需使用全局函數,你可以通過擴展協議的方式進行定義。
例如,可以擴展`RandomNumberGenerator`協議,讓其提供`randomBool()`方法。該方法使用協議中要求的`random()`方法來實現:
~~~
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
~~~
通過擴展協議,所有協議的遵循者,在不用任何修改的情況下,都自動得到了這個擴展所增加的方法。
~~~
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"
~~~
### 提供默認實現
可以通過協議擴展的方式來為協議規定的屬性和方法提供默認的實現。如果協議的遵循者對規定的屬性和方法提供了自己的實現,那么遵循者提供的實現將被使用。
> 注意
> 通過擴展協議提供的協議實現和可選協議規定有區別。雖然協議遵循者無需自己實現,通過擴展提供的默認實現,可以不是用可選鏈調用。
例如,`PrettyTextRepresentable`協議,繼承了`TextRepresentable`協議,可以為其提供一個默認的`asPrettyText()`方法來簡化返回值
~~~
extension PrettyTextRepresentable {
func asPrettyText() -> String {
return asText()
}
}
~~~
### 為協議擴展添加限制條件
在擴展協議的時候,可以指定一些限制,只有滿足這些限制的協議遵循者,才能獲得協議擴展提供的屬性和方法。這些限制寫在協議名之后,使用`where`關鍵字來描述限制情況。([Where語句](http://wiki.jikexueyuan.com/project/swift/chapter2/23_Generics.html#where_clauses))。:
例如,你可以擴展`CollectionType`協議,但是只適用于元素遵循`TextRepresentable`的情況:
~~~
extension CollectionType where Generator.Element : TextRepresentable {
func asList() -> String {
return "(" + ", ".join(map({$0.asText()})) + ")"
}
}
~~~
`asList()`方法將每個元素以`asText()`的方式表示,最后以逗號分隔鏈接起來。
現在我們來看`Hamster`,它遵循`TextRepresentable`:
~~~
let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]
~~~
因為`Array`遵循`CollectionType`協議,數組的元素又遵循`TextRepresentable`協議,所以數組可以使用`asList()`方法得到數組內容的文本表示:
~~~
print(hamsters.asList())
// 輸出 "(A hamster named Murray, A hamster named Morgan, A hamster named Maurice)"
~~~
> 注意
> 如果有多個協議擴展,而一個協議的遵循者又同時滿足它們的限制,那么將會使用所滿足限制最多的那個擴展。
- 介紹
- 歡迎使用 Swift
- 關于 Swift
- Swift 初見
- Swift 版本歷史記錄
- Swift1.0 發布內容
- Swift 教程
- 基礎部分
- 基本運算符
- 字符串和字符
- 集合類型
- 控制流
- 函數
- 閉包
- 枚舉
- 類和結構體
- 屬性
- 方法
- 下標腳本
- 繼承
- 構造過程
- 析構過程
- 自動引用計數
- 可選鏈
- 錯誤處理
- 類型轉換
- 嵌套類型
- 擴展
- 協議
- 泛型
- 權限控制
- 高級操作符
- 語言參考
- 關于語言參考
- 詞法結構
- 類型
- 表達式
- 語句
- 聲明
- 特性
- 模式
- 泛型參數
- 語法總結
- 蘋果官方Blog官方翻譯
- Access Control 權限控制的黑與白
- 造個類型不是夢-白話Swift類型創建
- WWDC里面的那個“大炮打氣球”
- Swift與C語言指針友好合作
- 引用類型和值類型的恩怨
- 訪問控制和Protected
- 可選類型完美解決占位問題