協(xié)議 定義了一個藍圖,規(guī)定了用來實現(xiàn)某一特定工作或者功能所必需的方法和屬性。類,結構體或枚舉類型都可以遵循協(xié)議,并提供具體實現(xiàn)來完成協(xié)議定義的方法和功能。任意能夠滿足協(xié)議要求的類型被稱為 遵循(conform) 這個協(xié)議。
除了遵循協(xié)議的類型必須實現(xiàn)那些指定的規(guī)定以外,還可以對協(xié)議進行擴展,實現(xiàn)一些特殊的規(guī)定或者一些附加的功能,使得遵循的類型能夠收益。
協(xié)議的語法
協(xié)議的定義方式與類,結構體,枚舉的定義非常相似。
protocol SomeProtocol {
// 協(xié)議內容
}
要使類遵循某個協(xié)議,需要在類型名稱后加上協(xié)議名稱,中間以冒號 : 分隔,作為類型定義的一部分。遵循多個協(xié)議時,各協(xié)議之間用逗號 , 分隔。
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 結構體內容
}
如果類在遵循協(xié)議的同時擁有父類,應該將父類名放在協(xié)議名之前,以逗號分隔。
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 類的內容
}
對屬性的規(guī)定
協(xié)議可以規(guī)定其 遵循者 提供特定名稱和類型的 實例屬性(instance property) 或 類屬性(type property) ,而不用指定是 存儲型屬性(stored property) 還是 計算型屬性(calculate property) 。此外還必須指明是只讀的還是可讀可寫的。
如果協(xié)議規(guī)定屬性是可讀可寫的,那么這個屬性不能是常量或只讀的計算屬性。如果協(xié)議只要求屬性是只讀的(gettable),那個屬性不僅可以是只讀的,如果你代碼需要的話,也可以是可寫的。
協(xié)議中的通常用var來聲明變量屬性,在類型聲明后加上 { set get } 來表示屬性是可讀可寫的,只讀屬性則用 {get } 來表示。
protocol SomeProtocol {
var mustBeSettable : Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在協(xié)議中定義類屬性(type property)時,總是使用 static 關鍵字作為前綴。當協(xié)議的遵循者是類時,可以使用class 或 static 關鍵字來聲明類屬性:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
如下所示,這是一個含有一個實例屬性要求的協(xié)議:
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed 協(xié)議除了要求協(xié)議的遵循者提供全名屬性外,對協(xié)議對遵循者的類型并沒有特別的要求。這個協(xié)議表示,任何遵循 FullyNamed 協(xié)議的類型,都具有一個可讀的 String 類型實例屬性 fullName 。下面是一個遵循 FullyNamed 協(xié)議的簡單結構體:
struct Person: FullyNamed{
var fullName: String
}
let john = Person(fullName: "John Appleseed")
//john.fullName 為 "John Appleseed"
這個例子中定義了一個叫做 Person 的結構體,用來表示具有名字的人。從第一行代碼中可以看出,它遵循了 FullyNamed 協(xié)議。Person 結構體的每一個實例都有一個 String 類型的存儲型屬性 fullName 。這正好滿足了 FullyNamed 協(xié)議的要求,也就意味著, Person 結構體完整的 遵循 了協(xié)議。(如果協(xié)議要求未被完全滿足,在編譯時會報錯)下面是一個更為復雜的類,它采用并遵循了 FullyNamed 協(xié)議:
class Starship: FullyNamed {
var prefix: String?
var name: Stringinit(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 屬性實現(xiàn)為只讀的計算型屬性。每一個 Starship 類的實例都有一個名為 name 的屬性和一個名為 prefix 的可選屬性。 當 prefix 存在時,將 prefix 插入到 name 之前來為Starship構建 fullName , prefix 不存在時,則將直接用 name 構建 fullName 。
對方法的規(guī)定
協(xié)議可以要求其遵循者實現(xiàn)某些指定的實例方法或類方法。這些方法作為協(xié)議的一部分,像普通的方法一樣放在協(xié)議的定義中,但是不需要大括號和方法體。可以在協(xié)議中定義具有可變參數(shù)的方法,和普通方法的定義方式相同。但是在協(xié)議的方法定義中,不支持參數(shù)默認值。
正如對屬性的規(guī)定中所說的,在協(xié)議中定義類方法的時候,總是使用 static 關鍵字作為前綴。當協(xié)議的遵循者是類的時候,你可以在類的實現(xiàn)中使用 class 或者 static 來實現(xiàn)類方法:
protocol SomeProtocol {
static func someTypeMethod()
}
下面的例子定義了含有一個實例方法的協(xié)議:
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator 協(xié)議要求其遵循者必須擁有一個名為 random , 返回值類型為 Double 的實例方法。盡管這里并未指明,但是我們假設返回值在[0,1)區(qū)間內。
RandomNumberGenerator 協(xié)議并不在意每一個隨機數(shù)是怎樣生成的,它只強調這里有一個隨機數(shù)生成器。如下所示,下邊的是一個遵循了 RandomNumberGenerator 協(xié)議的類。該類實現(xiàn)了一個叫做線性同余生成器(linearcongruential generator)的偽隨機數(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) % 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"
委托(代理)模式
委托是一種設計模式,它允許 類 或 結構體 將一些需要它們負責的功能 交由(或委托) 給其他的類型的實例。委托模式的實現(xiàn)很簡單: 定義協(xié)議來封裝那些需要被委托的函數(shù)和方法,使其 遵循者 擁有這些被委托的 函數(shù)和方法 。委托模式可以用來響應特定的動作或接收外部數(shù)據源提供的數(shù)據,而無需要知道外部數(shù)據源的類型信息。下面的例子是兩個基于骰子游戲的協(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é)議可以在任意含有骰子的游戲中實現(xiàn)。 DiceGameDelegate 協(xié)議可以用來追蹤 DiceGame 的游戲過程。如下所示, SnakesAndLadders 是 Snakes and Ladders游戲的新版本。新版本使用 Dice 作為骰子,并且實現(xiàn)了 DiceGame 和 DiceGameDelegate 協(xié)議,后者用來記錄游戲的過程:
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 協(xié)議,并且提供了相應的可讀的 dice 屬性和 play 實例方法。( dice 屬性在構造之后就不再改變,且協(xié)議只要求 dice 為只讀的,因此將 dice 聲明為常量屬性。)
游戲使用 SnakesAndLadders 類的 構造器(initializer) 初始化游戲。所有的游戲邏輯被轉移到了協(xié)議中的 play 方法, play 方法使用協(xié)議規(guī)定的 dice 屬性提供骰子搖出的值。
注意: delegate 并不是游戲的必備條件,因此 delegate 被定義為遵循 DiceGameDelegate 協(xié)議的可選屬性。因為 delegate 是可選值,因此在初始化的時候被自動賦值為 nil 。隨后,可以在游戲中為 delegate 設置適當?shù)闹怠?br>
DicegameDelegate 協(xié)議提供了三個方法用來追蹤游戲過程。被放置于游戲的邏輯中,即 play() 方法內。分別在游戲開始時,新一輪開始時,游戲結束時被調用。
因為 delegate 是一個遵循 DiceGameDelegate 的可選屬性,因此在 play() 方法中使用了 可選鏈 來調用委托方法。 若 delegate 屬性為 nil , 則delegate所調用的方法失效,并不會產生錯誤。若 delegate 不為 nil ,則方法能夠被調用
如下所示, 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
print("Rolled a \(diceRoll)")
}
func gameDidEnd(game: DiceGame) {
print("The game lasted for \(numberOfTurns) turns")
}
}
DiceGameTracker 實現(xiàn)了 DiceGameDelegate 協(xié)議規(guī)定的三個方法,用來記錄游戲已經進行的輪數(shù)。 當游戲開始時, numberOfTurns 屬性被賦值為0; 在每新一輪中遞增; 游戲結束后,輸出打印游戲的總輪數(shù)。
gameDidStart 方法從 game 參數(shù)獲取游戲信息并輸出。 game 在方法中被當做 DiceGame 類型而不是 SnakeAndLadders 類型,所以方法中只能訪問 DiceGame 協(xié)議中的成員。當然了,這些方法也可以在類型轉換之后調用。在上例代碼中,通過 is 操作符檢查 game 是否為 SnakesAndLadders 類型的實例,如果是,則打印出相應的內容。
無論當前進行的是何種游戲, game 都遵循 DiceGame 協(xié)議以確保 game 含有 dice 屬性,因此在 gameDidStart(_:) 方法中可以通過傳入的 game 參數(shù)來訪問 dice 屬性,進而打印出 dice 的 sides 屬性的值。
DiceGameTracker 的運行情況,如下所示:
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = trackergame.play()
// 開始一個新的Snakes and Ladders的游戲
// 游戲使用 6 面的骰子
// 翻轉得到 3
// 翻轉得到 5
// 翻轉得到 4
// 翻轉得到 5
// 游戲進行了 4 輪
文章摘自開發(fā)者手冊