協(xié)議 定義了一個藍圖,規(guī)定了用來實現(xiàn)某一特定任務或者功能的方法、屬性,以及其他需要的東西。類、結(jié)構體或枚舉都可以遵循協(xié)議,并為協(xié)議定義的這些要求提供具體實現(xiàn)。某個類型能夠滿足某個協(xié)議的要求,就可以說該類型遵循這個協(xié)議。
除了遵循協(xié)議的類型必須實現(xiàn)的要求外,還可以對協(xié)議進行擴展,通過擴展來實現(xiàn)一部分要求或者實現(xiàn)一些附加功能,這樣遵循協(xié)議的類型就能夠使用這些功能。
協(xié)議的定義方式與類、結(jié)構體和枚舉的定義非常相似:
protocol SomeProtocol {
// 這里是協(xié)議的定義部分
}
要讓自定義類型遵循某個協(xié)議,在定義類型時,需要在類型名稱后加上協(xié)議名稱,中間以冒號(:
)分隔。遵循多個協(xié)議時,各協(xié)議之間用逗號(,
)分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 這里是結(jié)構體的定義部分
}
若是一個類擁有父類,應該將父類名放在遵循的協(xié)議名之前,以逗號分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 這里是類的定義部分
}
協(xié)議可以要求遵循協(xié)議的類型提供特定名稱和類型的實例屬性或類型屬性。協(xié)議不指定屬性是存儲屬性還是計算屬性,它只指定屬性的名稱和類型。此外,協(xié)議還指定屬性是可讀的還是可讀可寫的。
如果協(xié)議要求屬性是可讀可寫的,那么該屬性不能是常量屬性或只讀的計算型屬性。如果協(xié)議只要求屬性是可讀的,那么該屬性不僅可以是可讀的,如果代碼需要的話,還可以是可寫的。
協(xié)議總是用 var
關鍵字來聲明變量屬性,在類型聲明后加上 { set get }
來表示屬性是可讀可寫的,可讀屬性則用 { get }
來表示:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在協(xié)議中定義類型屬性時,總是使用 static
關鍵字作為前綴。當類類型遵循協(xié)議時,除了 static
關鍵字,還可以使用 class
關鍵字來聲明類型屬性:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
如下所示,這是一個只含有一個實例屬性要求的協(xié)議:
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed
協(xié)議除了要求遵循協(xié)議的類型提供 fullName
屬性外,并沒有其他特別的要求。這個協(xié)議表示,任何遵循 FullyNamed
的類型,都必須有一個可讀的 String
類型的實例屬性 fullName
。
下面是一個遵循 FullyNamed
協(xié)議的簡單結(jié)構體:
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName 為 "John Appleseed"
這個例子中定義了一個叫做 Person
的結(jié)構體,用來表示一個具有名字的人。從第一行代碼可以看出,它遵循了 FullyNamed
協(xié)議。
Person
結(jié)構體的每一個實例都有一個 String
類型的存儲型屬性 fullName
。這正好滿足了 FullyNamed
協(xié)議的要求,也就意味著 Person
結(jié)構體正確地遵循了協(xié)議。(如果協(xié)議要求未被完全滿足,在編譯時會報錯。)
下面是一個更為復雜的類,它采納并遵循了 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
作為只讀的計算屬性來實現(xiàn)。每一個 Starship
類的實例都有一個名為 name
的非可選屬性和一個名為 prefix
的可選屬性。 當 prefix
存在時,計算屬性 fullName
會將 prefix
插入到 name
之前,從而得到一個帶有 prefix
的 fullName
。
協(xié)議可以要求遵循協(xié)議的類型實現(xiàn)某些指定的實例方法或類方法。這些方法作為協(xié)議的一部分,像普通方法一樣放在協(xié)議的定義中,但是不需要大括號和方法體??梢栽趨f(xié)議中定義具有可變參數(shù)的方法,和普通方法的定義方式相同。但是,不支持為協(xié)議中的方法提供默認參數(shù)。
正如屬性要求中所述,在協(xié)議中定義類方法的時候,總是使用 static
關鍵字作為前綴。即使在類實現(xiàn)時,類方法要求使用 class
或 static
作為關鍵字前綴,前面的規(guī)則仍然適用:
protocol SomeProtocol {
static func someTypeMethod()
}
下面的例子定義了一個只含有一個實例方法的協(xié)議:
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator
協(xié)議要求遵循協(xié)議的類型必須擁有一個名為 random
, 返回值類型為 Double
的實例方法。盡管這里并未指明,但是我們假設返回值是從 0.0
到(但不包括)1.0
。
RandomNumberGenerator
協(xié)議并不關心每一個隨機數(shù)是怎樣生成的,它只要求必須提供一個隨機數(shù)生成器。
如下所示,下邊是一個遵循并符合 RandomNumberGenerator
協(xié)議的類。該類實現(xiàn)了一個叫做 線性同余生成器(linear congruential 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).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”
有時需要在方法中改變(或異變)方法所屬的實例。例如,在值類型(即結(jié)構體和枚舉)的實例方法中,將 mutating
關鍵字作為方法的前綴,寫在 func
關鍵字之前,表示可以在該方法中修改它所屬的實例以及實例的任意屬性的值。這一過程在 在實例方法中修改值類型 章節(jié)中有詳細描述。
如果你在協(xié)議中定義了一個實例方法,該方法會改變遵循該協(xié)議的類型的實例,那么在定義協(xié)議時需要在方法前加 mutating
關鍵字。這使得結(jié)構體和枚舉能夠遵循此協(xié)議并滿足此方法要求。
注意
實現(xiàn)協(xié)議中的
mutating
方法時,若是類類型,則不用寫mutating
關鍵字。而對于結(jié)構體和枚舉,則必須寫mutating
關鍵字。
如下所示,Togglable
協(xié)議只定義了一個名為 toggle
的實例方法。顧名思義,toggle()
方法將改變實例屬性,從而切換遵循該協(xié)議類型的實例的狀態(tài)。
toggle()
方法在定義的時候,使用 mutating
關鍵字標記,這表明當它被調(diào)用時,該方法將會改變遵循協(xié)議的類型的實例:
protocol Togglable {
mutating func toggle()
}
當使用枚舉或結(jié)構體來實現(xiàn) Togglable
協(xié)議時,需要提供一個帶有 mutating
前綴的 toggle()
方法。
下面定義了一個名為 OnOffSwitch
的枚舉。這個枚舉在兩種狀態(tài)之間進行切換,用枚舉成員 On
和 Off
表示。枚舉的 toggle()
方法被標記為 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
協(xié)議可以要求遵循協(xié)議的類型實現(xiàn)指定的構造器。你可以像編寫普通構造器那樣,在協(xié)議的定義里寫下構造器的聲明,但不需要寫花括號和構造器的實體:
protocol SomeProtocol {
init(someParameter: Int)
}
你可以在遵循協(xié)議的類中實現(xiàn)構造器,無論是作為指定構造器,還是作為便利構造器。無論哪種情況,你都必須為構造器實現(xiàn)標上 required
修飾符:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 這里是構造器的實現(xiàn)部分
}
}
使用 required
修飾符可以確保所有子類也必須提供此構造器實現(xiàn),從而也能遵循協(xié)議。
關于 required
構造器的更多內(nèi)容,請參考 必要構造器。
注意
如果類已經(jīng)被標記為
final
,那么不需要在協(xié)議構造器的實現(xiàn)中使用required
修飾符,因為final
類不能有子類。關于final
修飾符的更多內(nèi)容,請參見 防止重寫。
如果一個子類重寫了父類的指定構造器,并且該構造器滿足了某個協(xié)議的要求,那么該構造器的實現(xiàn)需要同時標注 required
和 override
修飾符:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 這里是構造器的實現(xiàn)部分
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因為遵循協(xié)議,需要加上 required
// 因為繼承自父類,需要加上 override
required override init() {
// 這里是構造器的實現(xiàn)部分
}
}
協(xié)議還可以為遵循協(xié)議的類型定義可失敗構造器要求,詳見 可失敗構造器。
遵循協(xié)議的類型可以通過可失敗構造器(init?
)或非可失敗構造器(init
)來滿足協(xié)議中定義的可失敗構造器要求。協(xié)議中定義的非可失敗構造器要求可以通過非可失敗構造器(init
)或隱式解包可失敗構造器(init!
)來滿足。
盡管協(xié)議本身并未實現(xiàn)任何功能,但是協(xié)議可以被當做一個功能完備的類型來使用。協(xié)議作為類型使用,有時被稱作「存在類型」,這個名詞來自「存在著一個類型 T,該類型遵循協(xié)議 T」。
協(xié)議可以像其他普通類型一樣使用,使用場景如下:
注意
協(xié)議是一種類型,因此協(xié)議類型的名稱應與其他類型(例如
Int
,Double
,String
)的寫法相同,使用大寫字母開頭的駝峰式寫法,例如(FullyNamed
和RandomNumberGenerator
)。
下面是將協(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
}
}
例子中定義了一個 Dice
類,用來代表桌游中擁有 N 個面的骰子。Dice
的實例含有 sides
和 generator
兩個屬性,前者是整型,用來表示骰子有幾個面,后者為骰子提供一個隨機數(shù)生成器,從而生成隨機點數(shù)。
generator
屬性的類型為 RandomNumberGenerator
,因此任何遵循了 RandomNumberGenerator
協(xié)議的類型的實例都可以賦值給 generator
,除此之外并無其他要求。并且由于其類型是 RandomNumberGenerator
,在 Dice
類中與 generator
交互的代碼,必須適用于所有 generator
實例都遵循的方法。這句話的意思是不能使用由 generator
底層類型提供的任何方法或?qū)傩?。但是你可以通過向下轉(zhuǎn)型,從協(xié)議類型轉(zhuǎn)換成底層實現(xiàn)類型,比如從父類向下轉(zhuǎn)型為子類。請參考 向下轉(zhuǎn)型。
Dice
類還有一個構造器,用來設置初始狀態(tài)。構造器有一個名為 generator
,類型為 RandomNumberGenerator
的形參。在調(diào)用構造方法創(chuàng)建 Dice
的實例時,可以傳入任何遵循 RandomNumberGenerator
協(xié)議的實例給 generator
。
Dice
類提供了一個名為 roll
的實例方法,用來模擬骰子的面值。它先調(diào)用 generator
的 random()
方法來生成一個 [0.0,1.0)
區(qū)間內(nèi)的隨機數(shù),然后使用這個隨機數(shù)生成正確的骰子面值。因為 generator
遵循了 RandomNumberGenerator
協(xié)議,可以確保它有個 random()
方法可供調(diào)用。
下面的例子展示了如何使用 LinearCongruentialGenerator
的實例作為隨機數(shù)生成器來創(chuàng)建一個六面骰子:
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
委托是一種設計模式,它允許類或結(jié)構體將一些需要它們負責的功能委托給其他類型的實例。委托模式的實現(xiàn)很簡單:定義協(xié)議來封裝那些需要被委托的功能,這樣就能確保遵循協(xié)議的類型能提供這些功能。委托模式可以用來響應特定的動作,或者接收外部數(shù)據(jù)源提供的數(shù)據(jù),而無需關心外部數(shù)據(jù)源的類型。
下面的例子定義了兩個基于骰子游戲的協(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é)議可以被任意類型遵循,用來追蹤 DiceGame
的游戲過程。為了防止強引用導致的循環(huán)引用問題,可以把協(xié)議聲明為弱引用,更多相關的知識請看 類實例之間的循環(huán)強引用,當協(xié)議標記為類專屬可以使 SnakesAndLadders
類在聲明協(xié)議時強制要使用弱引用。若要聲明類專屬的協(xié)議就必須繼承于 AnyObject
,更多請看 類專屬的協(xié)議。
如下所示,SnakesAndLadders
是 控制流 章節(jié)引入的蛇梯棋游戲的新版本。新版本使用 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 = Array(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)
}
}
關于這個蛇梯棋游戲的詳細描述請參閱 中斷(Break)。
這個版本的游戲封裝到了 SnakesAndLadders
類中,該類遵循了 DiceGame
協(xié)議,并且提供了相應的可讀的 dice
屬性和 play()
方法。( dice
屬性在構造之后就不再改變,且協(xié)議只要求 dice
為可讀的,因此將 dice
聲明為常量屬性。)
游戲使用 SnakesAndLadders
類的 init()
構造器來初始化游戲。所有的游戲邏輯被轉(zhuǎn)移到了協(xié)議中的 play()
方法,play()
方法使用協(xié)議要求的 dice
屬性提供骰子搖出的值。
注意,delegate
并不是游戲的必備條件,因此 delegate
被定義為 DiceGameDelegate
類型的可選屬性。因為 delegate
是可選值,因此會被自動賦予初始值 nil
。隨后,可以在游戲中為 delegate
設置適當?shù)闹?。因?DiceGameDelegate
協(xié)議是類專屬的,可以將 delegate
聲明為 weak
,從而避免循環(huán)引用。
DicegameDelegate
協(xié)議提供了三個方法用來追蹤游戲過程。這三個方法被放置于游戲的邏輯中,即 play()
方法內(nèi)。分別在游戲開始時,新一輪開始時,以及游戲結(jié)束時被調(diào)用。
因為 delegate
是一個 DiceGameDelegate
類型的可選屬性,因此在 play()
方法中通過可選鏈式調(diào)用來調(diào)用它的方法。若 delegate
屬性為 nil
,則調(diào)用方法會優(yōu)雅地失敗,并不會產(chǎn)生錯誤。若 delegate
不為 nil
,則方法能夠被調(diào)用,并傳遞 SnakesAndLadders
實例作為參數(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
實現(xiàn)了 DiceGameDelegate
協(xié)議要求的三個方法,用來記錄游戲已經(jīng)進行的輪數(shù)。當游戲開始時,numberOfTurns
屬性被賦值為 0
,然后在每新一輪中遞增,游戲結(jié)束后,打印游戲的總輪數(shù)。
gameDidStart(_:)
方法從 game
參數(shù)獲取游戲信息并打印。game
參數(shù)是 DiceGame
類型而不是 SnakeAndLadders
類型,所以在 gameDidStart(_:)
方法中只能訪問 DiceGame
協(xié)議中的內(nèi)容。當然了,SnakeAndLadders
的方法也可以在類型轉(zhuǎn)換之后調(diào)用。在上例代碼中,通過 is
操作符檢查 game
是否為 SnakesAndLadders
類型的實例,如果是,則打印出相應的消息。
無論當前進行的是何種游戲,由于 game
遵循 DiceGame
協(xié)議,可以確保 game
含有 dice
屬性。因此在 gameDidStart(_:)
方法中可以通過傳入的 game
參數(shù)來訪問 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
即便無法修改源代碼,依然可以通過擴展令已有類型遵循并符合協(xié)議。擴展可以為已有類型添加屬性、方法、下標以及構造器,因此可以符合協(xié)議中的相應要求。詳情請在 擴展 章節(jié)中查看。
注意
通過擴展令已有類型遵循并符合協(xié)議時,該類型的所有實例也會隨之獲得協(xié)議中定義的各項功能。
例如下面這個 TextRepresentable
協(xié)議,任何想要通過文本表示一些內(nèi)容的類型都可以實現(xiàn)該協(xié)議。這些想要表示的內(nèi)容可以是實例本身的描述,也可以是實例當前狀態(tài)的文本描述:
protocol TextRepresentable {
var textualDescription: String { get }
}
可以通過擴展,令先前提到的 Dice
類可以擴展來采納和遵循 TextRepresentable
協(xié)議:
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
通過擴展遵循并采納協(xié)議,和在原始定義中遵循并符合協(xié)議的效果完全相同。協(xié)議名稱寫在類型名之后,以冒號隔開,然后在擴展的大括號內(nèi)實現(xiàn)協(xié)議要求的內(nèi)容。
現(xiàn)在所有 Dice
的實例都可以看做 TextRepresentable
類型:
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”
同樣,SnakesAndLadders
類也可以通過擴展來采納和遵循 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é)議的要求,比如當類型的泛型形式參數(shù)遵循對應協(xié)議時。你可以通過在擴展類型時列出限制讓泛型類型有條件地遵循某協(xié)議。在你采納協(xié)議的名字后面寫泛型 where
分句。更多關于泛型 where
分句,見 泛型 Where 分句。
下面的擴展讓 Array
類型只要在存儲遵循 TextRepresentable
協(xié)議的元素時就遵循 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]"
當一個類型已經(jīng)遵循了某個協(xié)議中的所有要求,卻還沒有聲明采納該協(xié)議時,可以通過空的擴展來讓它采納該協(xié)議:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
從現(xiàn)在起,Hamster
的實例可以作為 TextRepresentable
類型使用:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”
注意
即使?jié)M足了協(xié)議的所有要求,類型也不會自動遵循協(xié)議,必須顯式地遵循協(xié)議。
協(xié)議類型可以在數(shù)組或者字典這樣的集合中使用,在 協(xié)議類型 提到了這樣的用法。下面的例子創(chuàng)建了一個元素類型為 TextRepresentable
的數(shù)組:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
如下所示,可以遍歷 things
數(shù)組,并打印每個元素的文本表示:
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
類型而不是 Dice
,DiceGame
,Hamster
等類型,即使實例在幕后確實是這些類型中的一種。由于 thing
是 TextRepresentable
類型,任何 TextRepresentable
的實例都有一個 textualDescription
屬性,所以在每次循環(huán)中可以安全地訪問 thing.textualDescription
。
協(xié)議能夠繼承一個或多個其他協(xié)議,可以在繼承的協(xié)議的基礎上增加新的要求。協(xié)議的繼承語法與類的繼承相似,多個被繼承的協(xié)議間用逗號分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 這里是協(xié)議的定義部分
}
如下所示,PrettyTextRepresentable
協(xié)議繼承了 TextRepresentable
協(xié)議:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
例子中定義了一個新的協(xié)議 PrettyTextRepresentable
,它繼承自 TextRepresentable
協(xié)議。任何遵循 PrettyTextRepresentable
協(xié)議的類型在滿足該協(xié)議的要求時,也必須滿足 TextRepresentable
協(xié)議的要求。在這個例子中,PrettyTextRepresentable
協(xié)議額外要求遵循協(xié)議的類型提供一個返回值為 String
類型的 prettyTextualDescription
屬性。
如下所示,擴展 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
}
}
上述擴展令 SnakesAndLadders
遵循了 PrettyTextRepresentable
協(xié)議,并提供了協(xié)議要求的 prettyTextualDescription
屬性。每個 PrettyTextRepresentable
類型同時也是 TextRepresentable
類型,所以在 prettyTextualDescription
的實現(xiàn)中,可以訪問 textualDescription
屬性。然后,拼接上了冒號和換行符。接著,遍歷數(shù)組中的元素,拼接一個幾何圖形來表示每個棋盤方格的內(nèi)容:
0
時,用 ▲
表示。0
時,用 ▼
表示。0
時,用 ○
表示。任意 SankesAndLadders
的實例都可以使用 prettyTextualDescription
屬性來打印一個漂亮的文本描述:
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
你通過添加 AnyObject
關鍵字到協(xié)議的繼承列表,就可以限制協(xié)議只能被類類型采納(以及非結(jié)構體或者非枚舉的類型)。
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// 這里是類專屬協(xié)議的定義部分
}
在以上例子中,協(xié)議 SomeClassOnlyProtocol
只能被類類型采納。如果嘗試讓結(jié)構體或枚舉類型采納 SomeClassOnlyProtocol
,則會導致編譯時錯誤。
注意
當協(xié)議定義的要求需要遵循協(xié)議的類型必須是引用語義而非值語義時,應該采用類類型專屬協(xié)議。關于引用語義和值語義的更多內(nèi)容,請查看 結(jié)構體和枚舉是值類型 和 類是引用類型。
要求一個類型同時遵循多個協(xié)議是很有用的。你可以使用協(xié)議組合來復合多個協(xié)議到一個要求里。協(xié)議組合行為就和你定義的臨時局部協(xié)議一樣擁有構成中所有協(xié)議的需求。協(xié)議組合不定義任何新的協(xié)議類型。
協(xié)議組合使用 SomeProtocol & AnotherProtocol
的形式。你可以列舉任意數(shù)量的協(xié)議,用和符號(&
)分開。除了協(xié)議列表,協(xié)議組合也能包含類類型,這允許你標明一個需要的父類。
下面的例子中,將 Named
和 Aged
兩個協(xié)議按照上述語法組合成一個協(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é)構體采納了這兩個協(xié)議。
wishHappyBirthday(to:)
函數(shù)的參數(shù) celebrator
的類型為 Named & Aged
, 這意味著“任何同時遵循 Named 和 Aged 的協(xié)議”。它不關心參數(shù)的具體類型,只要參數(shù)遵循這兩個協(xié)議即可。
上面的例子創(chuàng)建了一個名為 birthdayPerson
的 Person
的實例,作為參數(shù)傳遞給了 wishHappyBirthday(to:)
函數(shù)。因為 Person
同時遵循這兩個協(xié)議,所以這個參數(shù)合法,函數(shù)將打印生日問候語。
這里有一個例子:將 Location 類和前面的 Named 協(xié)議進行組合:
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)
// 打印 "Hello, Seattle!"
beginConcert(in:)
函數(shù)接受一個類型為 Location & Named
的參數(shù),這意味著“任何 Location 的子類,并且遵循 Named 協(xié)議”。在這個例子中,City 就滿足這樣的條件。
將 birthdayPerson 傳入 beginConcert(in:)
函數(shù)是不合法的,因為 Person 不是 Location 的子類。同理,如果你新建一個類繼承于 Location,但是沒有遵循 Named 協(xié)議,而用這個類的實例去調(diào)用 beginConcert(in:)
函數(shù)也是非法的。
你可以使用 類型轉(zhuǎn)換 中描述的 is
和 as
操作符來檢查協(xié)議一致性,即是否遵循某協(xié)議,并且可以轉(zhuǎn)換到指定的協(xié)議類型。檢查和轉(zhuǎn)換協(xié)議的語法與檢查和轉(zhuǎn)換類型是完全一樣的:
is
用來檢查實例是否遵循某個協(xié)議,若遵循則返回 true
,否則返回 false
;as?
返回一個可選值,當實例遵循某個協(xié)議時,返回類型為協(xié)議類型的可選值,否則返回 nil
;as!
將實例強制向下轉(zhuǎn)換到某個協(xié)議類型,如果強轉(zhuǎn)失敗,將觸發(fā)運行時錯誤。下面的例子定義了一個 HasArea
協(xié)議,該協(xié)議定義了一個 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
屬性實現(xiàn)為基于存儲型屬性 radius
的計算型屬性。Country
類則把 area
屬性實現(xiàn)為存儲型屬性。這兩個類都正確地遵循了 HasArea
協(xié)議。
如下所示,Animal
是一個未遵循 HasArea
協(xié)議的類:
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle
,Country
,Animal
并沒有一個共同的基類,盡管如此,它們都是類,它們的實例都可以作為 AnyObject
類型的值,存儲在同一個數(shù)組中:
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
objects
數(shù)組使用字面量初始化,數(shù)組包含一個 radius
為 2
的 Circle
的實例,一個保存了英國國土面積的 Country
實例和一個 legs
為 4
的 Animal
實例。
如下所示,objects
數(shù)組可以被迭代,并對迭代出的每一個元素進行檢查,看它是否遵循 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
當?shù)龅脑刈裱?HasArea
協(xié)議時,將 as?
操作符返回的可選值通過可選綁定,綁定到 objectWithArea
常量上。objectWithArea
是 HasArea
協(xié)議類型的實例,因此 area
屬性可以被訪問和打印。
objects
數(shù)組中的元素的類型并不會因為強轉(zhuǎn)而丟失類型信息,它們?nèi)匀皇?Circle
,Country
,Animal
類型。然而,當它們被賦值給 objectWithArea
常量時,只被視為 HasArea
類型,因此只有 area
屬性能夠被訪問。
協(xié)議可以定義可選要求,遵循協(xié)議的類型可以選擇是否實現(xiàn)這些要求。在協(xié)議中使用 optional
關鍵字作為前綴來定義可選要求。可選要求用在你需要和 Objective-C 打交道的代碼中。協(xié)議和可選要求都必須帶上 @objc
屬性。標記 @objc
特性的協(xié)議只能被繼承自 Objective-C 類的類或者 @objc
類遵循,其他類以及結(jié)構體和枚舉均不能遵循這種協(xié)議。
使用可選要求時(例如,可選的方法或者屬性),它們的類型會自動變成可選的。比如,一個類型為 (Int) -> String
的方法會變成 ((Int) -> String)?
。需要注意的是整個函數(shù)類型是可選的,而不是函數(shù)的返回值。
協(xié)議中的可選要求可通過可選鏈式調(diào)用來使用,因為遵循協(xié)議的類型可能沒有實現(xiàn)這些可選要求。類似 someOptionalMethod?(someArgument)
這樣,你可以在可選方法名稱后加上 ?
來調(diào)用可選方法。詳細內(nèi)容可在 可選鏈式調(diào)用 章節(jié)中查看。
下面的例子定義了一個名為 Counter
的用于整數(shù)計數(shù)的類,它使用外部的數(shù)據(jù)源來提供每次的增量。數(shù)據(jù)源由 CounterDataSource
協(xié)議定義,它包含兩個可選要求:
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
CounterDataSource
協(xié)議定義了一個可選方法 increment(forCount:)
和一個可選屬性 fiexdIncrement
,它們使用了不同的方法來從數(shù)據(jù)源中獲取適當?shù)脑隽恐怠?/p>
注意
嚴格來講,
CounterDataSource
協(xié)議中的方法和屬性都是可選的,因此遵循協(xié)議的類可以不實現(xiàn)這些要求,盡管技術上允許這樣做,不過最好不要這樣寫。
Counter
類含有 CounterDataSource?
類型的可選屬性 dataSource
,如下所示:
class Counter {
var count = 0
var dataSource: CounterDataSource?
func increment() {
if let amount = dataSource?.increment?(forCount: count) {
count += amount
} else if let amount = dataSource?.fixedIncrement {
count += amount
}
}
}
Counter
類使用變量屬性 count
來存儲當前值。該類還定義了一個 increment
方法,每次調(diào)用該方法的時候,將會增加 count
的值。
increment()
方法首先試圖使用 increment(forCount:)
方法來得到每次的增量。increment()
方法使用可選鏈式調(diào)用來嘗試調(diào)用 increment(forCount:)
,并將當前的 count
值作為參數(shù)傳入。
這里使用了兩層可選鏈式調(diào)用。首先,由于 dataSource
可能為 nil
,因此在 dataSource
后邊加上了 ?
,以此表明只在 dataSource
非空時才去調(diào)用 increment(forCount:)
方法。其次,即使 dataSource
存在,也無法保證其是否實現(xiàn)了 increment(forCount:)
方法,因為這個方法是可選的。因此,increment(forCount:)
方法同樣使用可選鏈式調(diào)用進行調(diào)用,只有在該方法被實現(xiàn)的情況下才能調(diào)用它,所以在 increment(forCount:)
方法后邊也加上了 ?
。
調(diào)用 increment(forCount:)
方法在上述兩種情形下都有可能失敗,所以返回值為 Int?
類型。雖然在 CounterDataSource
協(xié)議中,increment(forCount:)
的返回值類型是非可選 Int
。另外,即使這里使用了兩層可選鏈式調(diào)用,最后的返回結(jié)果依舊是單層的可選類型。關于這一點的更多信息,請查閱 連接多層可選鏈式調(diào)用。
在調(diào)用 increment(forCount:)
方法后,Int?
型的返回值通過可選綁定解包并賦值給常量 amount
。如果可選值確實包含一個數(shù)值,也就是說,數(shù)據(jù)源和方法都存在,數(shù)據(jù)源方法返回了一個有效值。之后便將解包后的 amount
加到 count
上,增量操作完成。
如果沒有從 increment(forCount:)
方法獲取到值,可能由于 dataSource
為 nil
,或者它并沒有實現(xiàn) increment(forCount:)
方法,那么 increment()
方法將試圖從數(shù)據(jù)源的 fixedIncrement
屬性中獲取增量。fixedIncrement
是一個可選屬性,因此屬性值是一個 Int?
值,即使該屬性在 CounterDataSource
協(xié)議中的類型是非可選的 Int
。
下面的例子展示了 CounterDataSource
的簡單實現(xiàn)。ThreeSource
類遵循了 CounterDataSource
協(xié)議,它實現(xiàn)了可選屬性 fixedIncrement
,每次會返回 3
:
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
可以使用 ThreeSource
的實例作為 Counter
實例的數(shù)據(jù)源:
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
上述代碼新建了一個 Counter
實例,并將它的數(shù)據(jù)源設置為一個 ThreeSource
的實例,然后調(diào)用 increment()
方法 4
次。按照預期預期一樣,每次調(diào)用都會將 count
的值增加 3
.
下面是一個更為復雜的數(shù)據(jù)源 TowardsZeroSource
,它將使得最后的值變?yōu)?0
:
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
實現(xiàn)了 CounterDataSource
協(xié)議中的 increment(forCount:)
方法,以 count
參數(shù)為依據(jù),計算出每次的增量。如果 count
已經(jīng)為 0
,此方法將返回 0
,以此表明之后不應再有增量操作發(fā)生。
你可以使用 TowardsZeroSource
實例將 Counter
實例來從 -4
增加到 0
。一旦增加到 0
,數(shù)值便不會再有變動:
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
協(xié)議可以通過擴展來為遵循協(xié)議的類型提供屬性、方法以及下標的實現(xiàn)。通過這種方式,你可以基于協(xié)議本身來實現(xiàn)這些功能,而無需在每個遵循協(xié)議的類型中都重復同樣的實現(xiàn),也無需使用全局函數(shù)。
例如,可以擴展 RandomNumberGenerator
協(xié)議來提供 randomBool()
方法。該方法使用協(xié)議中定義的 random()
方法來返回一個隨機的 Bool
值:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
通過協(xié)議擴展,所有遵循協(xié)議的類型,都能自動獲得這個擴展所增加的方法實現(xiàn)而無需任何額外修改:
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”
協(xié)議擴展可以為遵循協(xié)議的類型增加實現(xiàn),但不能聲明該協(xié)議繼承自另一個協(xié)議。協(xié)議的繼承只能在協(xié)議聲明處進行指定。
可以通過協(xié)議擴展來為協(xié)議要求的方法、計算屬性提供默認的實現(xiàn)。如果遵循協(xié)議的類型為這些要求提供了自己的實現(xiàn),那么這些自定義實現(xiàn)將會替代擴展中的默認實現(xiàn)被使用。
注意
通過協(xié)議擴展為協(xié)議要求提供的默認實現(xiàn)和可選的協(xié)議要求不同。雖然在這兩種情況下,遵循協(xié)議的類型都無需自己實現(xiàn)這些要求,但是通過擴展提供的默認實現(xiàn)可以直接調(diào)用,而無需使用可選鏈式調(diào)用。
例如,PrettyTextRepresentable
協(xié)議繼承自 TextRepresentable
協(xié)議,可以為其提供一個默認的 prettyTextualDescription
屬性來簡單地返回 textualDescription
屬性的值:
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
在擴展協(xié)議的時候,可以指定一些限制條件,只有遵循協(xié)議的類型滿足這些限制條件時,才能獲得協(xié)議擴展提供的默認實現(xiàn)。這些限制條件寫在協(xié)議名之后,使用 where
子句來描述,正如 泛型 Where 子句 中所描述的。
例如,你可以擴展 Collection
協(xié)議,適用于集合中的元素遵循了 Equatable
協(xié)議的情況。通過限制集合元素遵循 Equatable
協(xié)議, 作為標準庫的一部分, 你可以使用 ==
和 !=
操作符來檢查兩個元素的等價性和非等價性。
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
如果集合中的所有元素都一致,allEqual()
方法才返回 true
。
看看兩個整數(shù)數(shù)組,一個數(shù)組的所有元素都是一樣的,另一個不一樣:
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
由于數(shù)組遵循 Collection
而且整數(shù)遵循 Equatable
,equalNumbers
和 differentNumbers
都可以使用 allEqual()
方法。
print(equalNumbers.allEqual())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"
注意
如果一個遵循的類型滿足了為同一方法或?qū)傩蕴峁崿F(xiàn)的多個限制型擴展的要求, Swift 會使用最匹配限制的實現(xiàn)。