協(xié)議定義了適合特定任務(wù)或功能塊的方法、屬性和其他需求的藍圖肥印。然后伴箩,類入愧、結(jié)構(gòu)或枚舉可以采用該協(xié)議來提供這些需求的實際實現(xiàn)江咳。任何滿足協(xié)議要求的類型都被稱為符合該協(xié)議政恍。
除了指定符合類型必須實現(xiàn)的需求之外纲仍,您還可以擴展協(xié)議來實現(xiàn)其中的一些需求善镰,或者實現(xiàn)符合類型可以利用的其他功能。
Protocol Syntax 協(xié)議語法
定義協(xié)議的方式與類松忍、結(jié)構(gòu)和枚舉非常類似:
protocol SomeProtocol {
// protocol definition goes here
}
自定義類型聲明它們采用特定的協(xié)議拥诡,方法是將協(xié)議的名稱放在類型名稱之后绞蹦,用冒號分隔椅野,作為定義的一部分终畅。可以列出多個協(xié)議鳄橘,并用逗號分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
如果一個類有一個超類,在它所采用的任何協(xié)議之前列出超類的名稱芒炼,后跟逗號:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
Property Requirements (協(xié)議中的)屬性需求
協(xié)議可以要求任何符合規(guī)范的類型提供實例屬性或具有特定名稱和類型的類型屬性瘫怜。協(xié)議沒有指定該屬性是存儲屬性還是計算屬性——它只指定了所需的屬性名稱和類型。協(xié)議還指定了每個屬性必須是可獲取的還是可獲取并可設(shè)置的本刽。
如果協(xié)議要求屬性是可讀寫的鲸湃,則不能通過常量存儲屬性或只讀計算屬性來滿足該屬性要求。如果協(xié)議只要求一個屬性是只讀的子寓,那么任何類型的屬性都可以滿足這個要求暗挑,如果這個屬性對您自己的代碼有用,那么它也是設(shè)置的斜友。
屬性要求總是聲明為變量屬性炸裆,前面加上var關(guān)鍵字。Gettable和settable屬性在類型聲明之后通過寫入{get set}表示鲜屏,Gettable屬性通過寫入{get}表示烹看。
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在協(xié)議中定義靜態(tài)關(guān)鍵字時,請始終使用它們作為類型屬性需求的前綴洛史。即使類型屬性要求可以在類實現(xiàn)時以class或static關(guān)鍵字作為前綴惯殊,該規(guī)則仍然適用:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
下面是一個協(xié)議的例子,它只需要一個實例屬性:
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed協(xié)議需要符合要求的類型來提供全名也殖。協(xié)議沒有指定關(guān)于符合類型的任何其他性質(zhì)—它只指定類型必須能夠為自己提供一個全名土思。協(xié)議規(guī)定,任何FullyNamed類型都必須具有一個名為fullName的可獲取實例屬性,該屬性的類型為String己儒。
下面是一個簡單結(jié)構(gòu)的例子崎岂,它采用并符合完全命名的協(xié)議:
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
本例定義了一個名為Person的結(jié)構(gòu),它表示一個特定的已命名Person址愿。它聲明它采用FullyNamed協(xié)議作為其定義的第一行的一部分该镣。
Person的每個實例都有一個名為fullName的存儲屬性,其類型為String响谓。這符合FullyNamed 協(xié)議的單個要求损合,并且意味著該人員已經(jīng)正確地符合該協(xié)議。(如果沒有滿足協(xié)議要求娘纷,Swift會在編譯時報告錯誤嫁审。)
下面是一個更復(fù)雜的類,它也采用并符合 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 is "USS Enterprise"
該類將fullName屬性要求實現(xiàn)為Starship實例的計算只讀屬性赖晶。每個Starship類實例都存儲一個強制名稱和一個可選前綴律适。fullName屬性如果存在,則使用前綴值遏插,并將其前置到名稱的開頭捂贿,以創(chuàng)建starship的全名。
Method Requirements (協(xié)議中)方法需求
協(xié)議可以要求特定的實例方法和類型方法通過符合類型來實現(xiàn)胳嘲。這些方法作為協(xié)議定義的一部分編寫厂僧,其方式與普通實例和類型方法完全相同,但是沒有花括號或方法體了牛⊙胀溃可變參數(shù)是允許的,受相同的規(guī)則為正常的方法鹰祸。但是甫窟,不能為協(xié)議定義中的方法參數(shù)指定默認值。
與類型屬性需求一樣蛙婴,當在協(xié)議中定義類型方法需求時粗井,總是在它們前面加上static關(guān)鍵字。這是真實的街图,即使類型方法需求在類實現(xiàn)時以class或static關(guān)鍵字作為前綴:
protocol SomeProtocol {
static func someTypeMethod()
}
下面的例子定義了一個協(xié)議背传,它只需要一個實例方法:
protocol RandomNumberGenerator {
func random() -> Double
}
該協(xié)議RandomNumberGenerator要求任何符合規(guī)范的類型都具有一個名為random的實例方法,該方法在調(diào)用時返回一個Double值台夺。雖然沒有將其指定為協(xié)議的一部分径玖,但是假定這個值是從0.0到1.0(不包括)之間的一個數(shù)字。
RandomNumberGenerator協(xié)議沒有對如何生成每個隨機數(shù)做任何假設(shè)——它只是要求生成器提供生成新隨機數(shù)的標準方法颤介。
下面是一個類的實現(xiàn)梳星,它采用并符合RandomNumberGenerator協(xié)議赞赖。這個類實現(xiàn)了一個偽隨機數(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())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
Mutating Method Requirements 變異方法需求
方法有時需要修改( modify or mutate )它所屬的實例冤灾。對于值類型(即結(jié)構(gòu)和枚舉)上的實例方法前域,您將 mutating 關(guān)鍵字放在方法的 func 關(guān)鍵字之前,以指示允許該方法修改其所屬的實例和該實例的任何屬性韵吨。在從實例方法中修改值類型時描述了這個過程匿垄。
如果您定義了一個協(xié)議實例方法需求,該需求旨在對采用該協(xié)議的任何類型的實例進行更改归粉,那么在該方法上使用mutating關(guān)鍵字作為協(xié)議定義的一部分椿疗。這使得結(jié)構(gòu)和枚舉能夠采用協(xié)議并滿足方法需求。
如果將協(xié)議實例方法需求標記為mutating糠悼,則在為類編寫該方法的實現(xiàn)時不需要編寫mutating關(guān)鍵字届榄。mutating關(guān)鍵字僅用于結(jié)構(gòu)和枚舉。
下面的示例定義了一個名為Togglable的協(xié)議倔喂,它定義了一個名為toggle的實例方法需求铝条。顧名思義,toggle()方法旨在切換或反轉(zhuǎn)任何符合類型的狀態(tài)席噩,通常是通過修改該類型的屬性班缰。
toggle()方法作為Togglable協(xié)議定義的一部分,使用mutating關(guān)鍵字標記為mutating()方法悼枢,表示該方法在被調(diào)用時埠忘,期望對符合約定的實例的狀態(tài)進行突變:
protocol Togglable {
mutating func toggle()
}
如果為結(jié)構(gòu)或枚舉實現(xiàn)Togglable協(xié)議,則該結(jié)構(gòu)或枚舉可以通過提供toggle()方法的實現(xiàn)來符合協(xié)議萧芙,該方法也被標記為mutating给梅。
下面的示例定義了一個名為OnOffSwitch的枚舉假丧。此枚舉切換在兩個狀態(tài)之間切換双揪,由枚舉用例on和off表示。枚舉的切換實現(xiàn)標記為mutating包帚,以匹配可切換協(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 is now equal to .on
Initializer Requirements (協(xié)議中) 初始化需求
協(xié)議可以要求特定的初始化器由遵守協(xié)議的類型實現(xiàn)渔期。你編寫這些初始化器作為協(xié)議定義的一部分,與普通初始化器的編寫方法完全相同渴邦,但是沒有花括號或初始化器主體:
protocol SomeProtocol {
init(someParameter: Int)
}
Class Implementations of Protocol Initializer Requirements 協(xié)議初始化器需求的類實現(xiàn)
您可以在遵守協(xié)議的類上實現(xiàn)協(xié)議初始化器需求疯趟,作為指定的初始化器或方便的初始化器。在這兩種情況下谋梭,都必須用所需的修飾符標記初始化器實現(xiàn):
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
}
}
使用required修飾符可以確保在符合規(guī)范的類的所有子類上提供顯式或繼承的初始化器需求實現(xiàn)信峻,以便它們也符合協(xié)議。
注意:不需要在使用final 標記的類上使用required 標記協(xié)議初始化器實現(xiàn)瓮床,因為final類不能子類化盹舞。
如果子類從超類重寫指定的初始化器产镐,并且還從協(xié)議實現(xiàn)匹配的初始化器需求,則用required和override修飾符標記初始化器實現(xiàn):
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// initializer implementation goes here
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// "required" from SomeProtocol conformance; "override" from SomeSuperClass
required override init() {
// initializer implementation goes here
}
}
Failable Initializer Requirements (協(xié)議中)可失敗初始化需求
協(xié)議可以為遵守協(xié)議的類型定義可失敗初始化器需求踢步,就像可失敗初始化器中定義的那樣癣亚。
可失敗初始化器需求可以由遵守協(xié)議的類型上的可失敗初始化器或不可失敗初始化器來滿足。不可失敗初始化器需求可以由不可失敗初始化器或隱式展開的不可失敗初始化器來滿足获印。
Protocols as Types 協(xié)議作為類型
協(xié)議本身實際上并不實現(xiàn)任何功能述雾。盡管如此,您創(chuàng)建的任何協(xié)議都將成為代碼中使用的成熟類型兼丰。
因為它是一種類型玻孟,所以您可以在許多允許使用其他類型的地方使用協(xié)議,包括:
- 作為函數(shù)地粪、方法或初始化器中的參數(shù)類型或返回類型
- 作為常量取募、變量或?qū)傩缘念愋?/li>
- 作為數(shù)組、字典或其他容器中的項的類型
因為協(xié)議是類型蟆技,所以它們的名稱以大寫字母開頭(如FullyNamed和RandomNumberGenerator)玩敏,以匹配Swift中其他類型的名稱(如Int、String和Double)质礼。
下面是一個協(xié)議作為類型使用的例子:(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的整數(shù)屬性眶蕉,它表示它們有多少個邊;還有一個名為generator的屬性砰粹,它提供了一個隨機數(shù)生成器,從中可以創(chuàng)建Dice roll值造挽。
generator屬性屬于RandomNumberGenerator類型碱璃。因此,您可以將其設(shè)置為采用RandomNumberGenerator協(xié)議的任何類型的實例饭入。除了實例必須采用RandomNumberGenerator協(xié)議外嵌器,不需要對分配給此屬性的實例執(zhí)行任何其他操作。
骰子也有一個初始化器谐丢,以設(shè)置其初始狀態(tài)爽航。這個初始化器有一個名為generator的參數(shù),它也是RandomNumberGenerator類型的參數(shù)乾忱。在初始化新的Dice實例時讥珍,可以將任何符合此參數(shù)的類型的值傳遞給該參數(shù)。
Dice提供了一個實例方法roll窄瘟,它返回骰子上1到邊數(shù)之間的整數(shù)值衷佃。該方法調(diào)用生成器的random()方法來創(chuàng)建0.0到1.0之間的新隨機數(shù),并使用該隨機數(shù)創(chuàng)建正確范圍內(nèi)的骰子擲骰值蹄葱。因為生成器已知采用RandomNumberGenerator氏义,所以它保證調(diào)用random()方法衰腌。
下面是Dice類如何使用一個線性全等生成器實例作為其隨機數(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
Delegation 代理或者說委托
委托是一種設(shè)計模式,它允許類或結(jié)構(gòu)將其部分職責移交(或委托)給另一種類型的實例觅赊。此設(shè)計模式是通過定義協(xié)議(封裝了代理的職責)來實現(xiàn)的右蕊,以便保證符合要求的類型(稱為委托)能夠提供已授權(quán)的功能∷甭荩可以使用委托響應(yīng)特定的操作饶囚,或者從外部源檢索數(shù)據(jù),而不需要知道該源的底層類型鸠补。
下面的例子定義了兩個用于基于骰子的棋盤游戲的協(xié)議:
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
DiceGame協(xié)議可以被任何涉及骰子的游戲所采用萝风。
可以使用DiceGameDelegate協(xié)議跟蹤DiceGame的進程。為了防止強引用循環(huán)紫岩,委托被聲明為弱引用规惰。有關(guān)弱引用的信息,請參見 Strong Reference Cycles Between Class Instances泉蝌。
將協(xié)議標記為僅用于類歇万,讓本章后面的SnakesAndLadders類聲明其委托必須使用弱引用。一個類專用協(xié)議由它從任何對象繼承的標記勋陪,正如在 Class-Only Protocols 中討論的那樣贪磺。
這是一個版本的蛇和梯子游戲最初引入控制流。
此版本適用于將骰子實例用于其擲骰;
采用DiceGame協(xié)議;
并將其進展通知DiceGameDelegate:
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
}
weak 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)
}
}
有關(guān)蛇和梯子游戲玩法的描述诅愚,請參見 Break寒锚。
這個版本的游戲被封裝為一個名為SnakesAndLadders的類,它采用了DiceGame協(xié)議违孝。為了符合協(xié)議刹前,它提供了一個gettable dice屬性和一個play()方法。(dice屬性被聲明為常量屬性雌桑,因為它在初始化之后不需要更改喇喉,而且協(xié)議只要求它必須是可獲取的。)
snake和Ladders游戲板設(shè)置在類的init()初始化器中進行筹燕。所有的游戲邏輯都被移動到協(xié)議的play方法中轧飞,該方法使用協(xié)議的required dice屬性來提供它的骰子擲骰值衅鹿。
請注意撒踪,delegate屬性被定義為一個可選的DiceGameDelegate,因為玩游戲不需要委托大渤。因為它是可選類型制妄,委托屬性被自動設(shè)置為nil的初始值。然后泵三,游戲?qū)嵗骺梢赃x擇將屬性設(shè)置為合適的委托耕捞。因為DiceGameDelegate協(xié)議只包含類衔掸,所以可以將委托聲明為弱委托,以防止引用循環(huán)俺抽。
DiceGameDelegate提供了三種跟蹤游戲進程的方法敞映。這三個方法已經(jīng)被合并到上面play()方法中的游戲邏輯中,并在新游戲開始磷斧、新回合開始或游戲結(jié)束時調(diào)用振愿。
因為delegate屬性是可選的DiceGameDelegate,所以play()方法每次調(diào)用委托上的方法時都使用可選鏈接弛饭。如果delegate屬性為nil冕末,這些委托調(diào)用將優(yōu)雅地失敗,并且沒有錯誤侣颂。如果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類遵守了DiceGameDelegate協(xié)議,并實現(xiàn)所需的三個方法拒担。它使用這些方法來跟蹤游戲的回合數(shù)仅炊。它在游戲開始時將numberOfTurns屬性重置為零,每次新回合開始時都將其遞增澎蛛,并在游戲結(jié)束后打印出總回合數(shù)抚垄。
上面所示的gameDidStart(:)的實現(xiàn)使用game參數(shù)來打印關(guān)于將要玩的游戲的一些介紹性信息。game參數(shù)有一種類型的DiceGame谋逻,而不是SnakesAndLadders呆馁,因此gameDidStart(:)只能訪問和使用作為DiceGame協(xié)議一部分實現(xiàn)的方法和屬性。但是毁兆,該方法仍然能夠使用類型轉(zhuǎn)換來查詢底層實例的類型浙滤。在本例中,它檢查game是否實際上是幕后蛇形梯子的實例气堕,如果是纺腊,則打印適當?shù)南ⅰ?/p>
gameDidStart(:)方法還訪問傳遞的游戲參數(shù)的dice屬性。因為game已知符合DiceGame協(xié)議茎芭,所以它保證具有dice屬性揖膜,因此gameDidStart(:)方法能夠訪問和打印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
Adding Protocol Conformance with an Extension
即使不能訪問現(xiàn)有類型的源代碼壹粟,也可以擴展現(xiàn)有類型以采用和遵循新協(xié)議。
擴展可以向現(xiàn)有類型添加新屬性宿百、方法和下標趁仙,因此能夠添加協(xié)議可能需要的任何需求洪添。有關(guān)擴展的更多信息,請參見擴展雀费。
當將一致性添加到擴展中的實例類型時干奢,類型的現(xiàn)有實例自動采用并符合協(xié)議。
例如盏袄,這個協(xié)議稱為TextRepresentable律胀,可以由任何能夠表示為文本的類型實現(xiàn)。這可能是對它自身的描述貌矿,或者是它當前狀態(tài)的文本版本:
protocol TextRepresentable {
var textualDescription: String { get }
}
上面的Dice類可以擴展為采用和符合textrepres像樣:
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
這個擴展采用新協(xié)議的方式與Dice骰子在其原始實現(xiàn)中提供的方式完全相同炭菌。協(xié)議名在類型名之后提供,以冒號分隔逛漫,并且在擴展的花括號中提供協(xié)議的所有需求的實現(xiàn)黑低。
任何Dice骰子實例現(xiàn)在可以處理為TextRepresentable:
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "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)
// Prints "A game of Snakes and Ladders with 25 squares"
Conditionally Conforming to a Protocol 條件性地遵守協(xié)議
泛型類型只有在某些條件下才能滿足協(xié)議的要求酌毡,例如當類型的泛型參數(shù)符合協(xié)議時克握。通過在擴展泛型類型時列出約束,可以使泛型類型有條件地符合協(xié)議枷踏。通過編寫一個通用where子句菩暗,將這些約束寫在要采用的協(xié)議名稱之后。有關(guān)泛型where子句的更多信息旭蠕,請參見 Generic Where Clauses停团。
下面的擴展使數(shù)組實例在存儲符合TextRepresentable類型的元素時符合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)
// Prints "[A 6-sided dice, A 12-sided dice]"
Declaring Protocol Adoption with an Extension
如果某一類型已經(jīng)符合某一協(xié)議的所有要求掏熬,但尚未聲明采用該協(xié)議佑稠,則可以使該類型采用擴展名為空的協(xié)議:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
倉鼠Hamster的實例現(xiàn)在可以在TextRepresentable是所需類型的地方使用:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
類型不會僅僅通過滿足協(xié)議的需求就自動采用協(xié)議。它們必須始終明確聲明它們采用了該議定書旗芬。
Collections of Protocol Types 協(xié)議類型集合
協(xié)議可以用作要存儲在集合(如數(shù)組或字典)中的類型舌胶。這個例子創(chuàng)建了一個可顯示文本的數(shù)組:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
現(xiàn)在可以遍歷數(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幔嫂。它不是骰子類型的,也不是DiceGame或倉鼠類型的誊薄,即使幕后的實際實例是其中一種類型的履恩。盡管如此,因為它的類型是TextRepresentable暇屋,而TextRepresentable的任何東西都有一個textualDescription屬性似袁,所以訪問它是安全的洞辣。文本描述每次通過循環(huán)咐刨。
Protocol Inheritance 協(xié)議繼承
協(xié)議可以繼承一個或多個其他協(xié)議昙衅,并且可以在其繼承的需求之上添加更多的需求。協(xié)議繼承的語法類似于類繼承的語法定鸟,但是可以列出多個繼承協(xié)議而涉,用逗號分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
下面是一個繼承自上面的TextRepresentable協(xié)議的協(xié)議示例:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
這個例子定義了一個新的協(xié)議PrettyTextRepresentable,它繼承自TextRepresentable联予。任何采用PrettyTextRepresentable的應(yīng)用程序都必須滿足TextRepresentable執(zhí)行的所有要求啼县,以及PrettyTextRepresentable執(zhí)行的附加要求。在本例中沸久,PrettyTextRepresentable添加了一個要求季眷,以提供一個名為prettyTextualDescription的可獲取屬性,該屬性返回一個字符串卷胯。
SnakesAndLadders類可以擴展到采用和遵守PrettyTextRepresentable:
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
}
}
這個擴展聲明它采用PrettyTextRepresentable協(xié)議子刮,并為SnakesAndLadders類型提供了prettyTextualDescription屬性的實現(xiàn)。PrettyTextRepresentable的任何東西也必須是TextRepresentable的窑睁,因此prettyTextualDescription的實現(xiàn)首先從TextRepresentable協(xié)議訪問textualDescription屬性挺峡,開始輸出字符串。它附加一個冒號和一個換行符担钮,并以此作為其漂亮的文本表示的開始橱赠。然后遍歷棋盤格數(shù)組,并添加一個幾何形狀來表示每個方塊的內(nèi)容:
- 如果正方形的值大于0箫津,則為梯子狭姨,用 ▲ 表示。
- 如果正方形的值小于0苏遥,那么它蛇頭送挑,用 ▼ 表示。
- 否則暖眼,平方的值為0惕耕,它是一個“自由”的平方,用 〇 表示诫肠。
prettyTextualDescription屬性現(xiàn)在可以用來打印任何蛇形和梯子實例的漂亮文本描述:
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
Class-Only Protocols 類協(xié)議
你可以限制協(xié)議只適用于 class 類型司澎,在協(xié)議的繼承列表中 添加 AnyObject協(xié)議即可,如下:
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
在上面的例子中栋豫,SomeClassOnlyProtocol 協(xié)議只能被類類型采用挤安。編寫試圖采用SomeClassOnlyProtocol的結(jié)構(gòu)或枚舉定義是編譯時錯誤。
當由協(xié)議的需求定義的行為假設(shè)或要求符合規(guī)范的類型具有引用語義而不是值語義時丧鸯,請使用僅包含類的協(xié)議蛤铜。
Protocol Composition 協(xié)議合成物
要求類型同時符合多個協(xié)議可能很有用。您可以使用協(xié)議組合將多個協(xié)議組合成單個需求。協(xié)議組合的行為就像您定義了一個臨時本地協(xié)議围肥,它具有組合中所有協(xié)議的組合需求剿干。協(xié)議組合不定義任何新的協(xié)議類型。
協(xié)議組合具有某種協(xié)議和另一種協(xié)議的形式穆刻。您可以根據(jù)需要列出任意多的協(xié)議置尔,并使用 & 分隔它們。除了協(xié)議列表之外氢伟,協(xié)議組合還可以包含一個類類型榜轿,您可以使用它來指定所需的超類。
下面是一個例子朵锣,它將兩個名為Named和Aged的協(xié)議組合成一個函數(shù)參數(shù)的協(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(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"
在本例中谬盐,Named協(xié)議只有一個可獲取的String型屬性name。Aged協(xié)議只有可獲取的Int型屬性age诚些。這兩種協(xié)議都被一個叫做Person的結(jié)構(gòu)所采用设褐。
該示例還定義了wishHappyBirthday(to:)函數(shù)。其參數(shù)的類型是Named & Aged泣刹,這意味著“符合Named和Aged協(xié)議的任何類型”助析。“傳遞給函數(shù)的具體類型并不重要椅您,只要它符合兩個必需協(xié)議即可外冀。
然后,該示例創(chuàng)建一個名為birthdayPerson的新Person實例掀泳,并將這個新實例傳遞給wishHappyBirthday(to:)函數(shù)雪隧。因為Person符合這兩個協(xié)議,所以這個調(diào)用是有效的员舵,wishHappyBirthday(to:)函數(shù)可以打印它的生日祝福脑沿。
下面是一個例子,它結(jié)合了前一個例子中的 Named 協(xié)議和一個Location類:
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)
// Prints "Hello, Seattle!"
beginConcert(in:)函數(shù)的參數(shù)類型為Location & Named马僻,意思是“Location的子類中符合Named協(xié)議的任何類型”庄拇。在這種情況下,City滿足了這兩個要求韭邓。
將birthdayPerson傳遞給beginConcert(in:)函數(shù)是無效的措近,因為Person不是Location的子類。同樣女淑,如果您創(chuàng)建了一個Location的子類瞭郑,它不符合命名協(xié)議,那么使用該類型的實例調(diào)用beginConcert(in:)也是無效的鸭你。
Checking for Protocol Conformance 檢查協(xié)議一致性
您可以使用類型轉(zhuǎn)換中描述的 is 和 as 操作符來檢查協(xié)議一致性屈张,并轉(zhuǎn)換到特定的協(xié)議擒权。對協(xié)議的檢查和轉(zhuǎn)換與對類型的檢查和轉(zhuǎn)換遵循完全相同的語法:
- is 操作符 : 如果實例遵守了協(xié)議則返回true; 否則返回false。
- as? 操作符 :返回協(xié)議類型的可選值阁谆,如果實例不符合該協(xié)議碳抄,則該值為nil。
- as! 操作符 : 強制轉(zhuǎn)換為協(xié)議類型笛厦,如果向下轉(zhuǎn)型沒有成功纳鼎,則觸發(fā)運行時錯誤俺夕。
這個例子定義了一個叫做 HasArea 的協(xié)議裳凸,它只有一個屬性需要一個叫做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類基于存儲的radius屬性將area屬性作為計算屬性實現(xiàn)姨谷。Country類直接作為存儲屬性實現(xiàn)area需求。這兩個類都正確地符合HasArea協(xié)議映九。
下面是一個名為Animal的類梦湘,它不符合HasArea協(xié)議:
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle、Country和Animal類沒有共享基類件甥。盡管如此捌议,它們都是類,所以這三種類型的實例都可以用來初始化一個數(shù)組引有,該數(shù)組存儲的值類型為AnyObject:
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
對象數(shù)組初始化為包含半徑為2個單位的圓實例的數(shù)組文字;用英國的表面積(以平方公里為單位)初始化的Country實例;還有一個有四條腿的動物瓣颅。
對象數(shù)組現(xiàn)在可以迭代譬正,并且可以檢查數(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,因此可以以一種類型安全的方式訪問和打印它的area屬性抒巢。
注意,轉(zhuǎn)換過程不會改變底層對象蛉谜。他們?nèi)匀皇且粋€圓圈,一個國家和一個動物悦陋。但是蜈彼,當它們存儲在objectWithArea常量中時俺驶,它們只知道是HasArea類型的棍辕,因此只能訪問它們的area屬性还绘。
Optional Protocol Requirements 可選協(xié)議需求
您可以為協(xié)議定義可選的需求拍顷,這些需求不必由符合協(xié)議的類型來實現(xiàn)∧蚱叮可選需求由 optional 修飾符作為協(xié)議定義的一部分作為前綴踏揣±谈澹可選要求是可用的,這樣您就可以編寫與Objective-C互操作的代碼彰亥。協(xié)議和可選要求都必須用@objc屬性標記衰齐。注意@objc協(xié)議只能被繼承自O(shè)bjective-C類或其他@objc類的類所采用娇斩。它們不能被結(jié)構(gòu)或枚舉采用。
當您在可選需求中使用方法或?qū)傩詴r锦积,其類型將自動成為可選的歉嗓。例如鉴分,類型(Int) -> String的方法變成了((Int) -> String)?哮幢。注意橙垢,整個函數(shù)類型都封裝在可選函數(shù)中伦糯,而不是方法的返回值中嗽元。
可以使用可選鏈接調(diào)用可選協(xié)議需求剂癌,以考慮需求不是由符合協(xié)議的類型實現(xiàn)的可能性翰绊。檢查可選方法的實現(xiàn)時监嗜,在方法名后面加上問號秤茅,例如someOptionalMethod?(someArgument)童叠。有關(guān)可選鏈接的信息,請參見可選鏈接五垮。
下面的示例定義了一個名為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 }
}
ounterDataSource協(xié)議定義了一個可選的方法需求increment(forCount:)和一個可選的屬性需求fixedIncrement。這些需求為數(shù)據(jù)源定義了兩種不同的方法银室,以便為計數(shù)器實例提供適當?shù)脑隽俊?/p>
嚴格地說励翼,您可以編寫一個符合CounterDataSource協(xié)議的自定義類汽抚,而不需要實現(xiàn)任何協(xié)議要求造烁。畢竟狱从,它們都是可選的季研。雖然在技術(shù)上是允許的誉察,但這并不是一個很好的數(shù)據(jù)源持偏。
下面定義的Counter類有一個可選的dataSource屬性,類型為CounterDataSource?
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的變量屬性中酌畜。Counter類還定義了一個名為increment的方法桥胞,該方法在每次調(diào)用該方法時遞增count屬性贩虾。
increment()方法首先通過在其數(shù)據(jù)源上查找increment(forCount:)方法的實現(xiàn)來檢索增量沥阱。increment()方法使用可選鏈接嘗試調(diào)用increment(forCount:)考杉,并將當前count值作為方法的單個參數(shù)傳遞。
注意咽袜,這里有兩個級別的可選鏈接酬蹋。首先抽莱,dataSource可能是nil食铐,因此dataSource的名稱后面有一個問號,表示只有當dataSource不是nil時才應(yīng)該調(diào)用increment(forCount:)象泵。其次偶惠,即使數(shù)據(jù)源確實存在,也不能保證它實現(xiàn)了increment(forCount:)绑改,因為這是一個可選的需求厘线。這里出革,increment(forCount:) 可能無法實現(xiàn)的可能性也由可選鏈接處理骂束。只有當increment(forCount:)存在時才會調(diào)用increment(forCount:),也就是說楞抡,如果它不是nil。這就是為什么increment(forCount:)在它的名字后面也要加上一個問號账胧。
因為對increment(forCount:)的調(diào)用可能因為這兩個原因之一而失敗先紫,所以調(diào)用返回一個可選的Int值遮精。即使increment(forCount:)被定義為在反數(shù)據(jù)源的定義中返回一個不可選的Int值本冲,這也是正確的。即使有兩個可選的鏈接操作狸膏,一個接一個湾戳,結(jié)果仍然封裝在一個可選操作中。
在調(diào)用increment(forCount:)之后幼驶,它返回的可選Int將使用可選綁定解包到一個名為amount的常量中县遣。如果可選的Int確實包含一個值——也就是說萧求,如果委托和方法都存在顶瞒,并且方法返回一個值——則將未包裝的數(shù)量添加到存儲的count屬性中榴徐,并且完成增量坑资。
如果不能從increment(forCount:)方法中檢索值—要么因為數(shù)據(jù)源為nil,要么因為數(shù)據(jù)源沒有實現(xiàn)increment(forCount:)—那么increment()方法嘗試從數(shù)據(jù)源的fixedIncrement屬性中檢索值仿便。fixedIncrement屬性也是一個可選的需求嗽仪,所以它的值是一個可選的Int值柒莉,即使fixedIncrement作為反數(shù)據(jù)源協(xié)議定義的一部分被定義為一個不可選的Int屬性兢孝。
下面是一個簡單的反數(shù)據(jù)源實現(xiàn)跨蟹,每次查詢數(shù)據(jù)源時,它都會返回一個常量值3相种。它實現(xiàn)了可選的fixedIncrement屬性要求:
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
你可以使用ThreeSource的一個實例作為一個新的計數(shù)器實例的數(shù)據(jù)源:
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
上面的代碼創(chuàng)建了一個新的計數(shù)器實例;將其數(shù)據(jù)源設(shè)置為一個新的ThreeSource實例;并調(diào)用計數(shù)器的increment()方法四次寝并。正如預(yù)期的那樣衬潦,每次調(diào)用increment()镀岛,計數(shù)器的count屬性都會增加3。
下面是一個更復(fù)雜的數(shù)據(jù)源驾锰,名為TowardsZeroSource椭豫,它使計數(shù)器實例從當前的count值向上或向下計數(shù)旨指,直到為零:
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ù)值確定要計算的方向搬素。如果count已經(jīng)為零蔗蹋,則該方法返回0,表示不應(yīng)該進行進一步的計數(shù)。
您可以使用TowardsZeroSource實例和現(xiàn)有計數(shù)器實例進行從-4到0的計數(shù)皂吮。一旦計數(shù)器達到零蜂筹,就不再計數(shù):
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
Protocol Extensions
可以擴展協(xié)議來為符合類型提供方法艺挪、初始化器兵扬、下標和計算屬性實現(xiàn)口蝠。這允許您在協(xié)議本身上定義行為妙蔗,而不是在每個類型的單獨一致性或全局函數(shù)中定義行為眉反。
例如穆役,RandomNumberGenerator協(xié)議可以擴展為提供randomBool()方法耿币,該方法使用所需random()方法的結(jié)果返回一個隨機Bool值:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
通過在協(xié)議上創(chuàng)建擴展掰读,所有符合標準的類型自動獲得此方法實現(xiàn)蹈集,而不需要任何額外的修改。
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"
協(xié)議擴展可以向符合規(guī)范的類型添加實現(xiàn)减响,但不能使協(xié)議擴展或從另一個協(xié)議繼承支示。協(xié)議繼承總是在協(xié)議聲明本身中指定颂鸿。
Providing Default Implementations 提供默認實現(xiàn)
您可以使用協(xié)議擴展來為該協(xié)議的任何方法或計算屬性需求提供默認實現(xiàn)嘴纺。如果符合標準的類型提供了所需方法或?qū)傩缘淖陨韺崿F(xiàn)浓冒,則將使用該實現(xiàn)而不是擴展提供的實現(xiàn)稳懒。
擴展提供的默認實現(xiàn)的協(xié)議需求與可選協(xié)議需求是不同的。盡管符合標準的類型不需要提供它們自己的實現(xiàn)纯路,但是可以調(diào)用具有默認實現(xiàn)的需求感昼,而不需要可選的鏈接罐脊。
例如萍桌,繼承了TextRepresentable協(xié)議的PrettyTextRepresentable協(xié)議可以提供其所需prettyTextualDescription屬性的默認實現(xiàn)上炎,從而簡單地返回訪問textualDescription屬性的結(jié)果:
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
Adding Constraints to Protocol Extensions 向協(xié)議擴展中添加約束
定義協(xié)議擴展時藕施,可以指定符合類型的約束裳食,這些約束必須在擴展的方法和屬性可用之前滿足。通過編寫一個通用where子句浊吏,可以在要擴展的協(xié)議名稱之后編寫這些約束找田。
例如墩衙,可以定義集合協(xié)議的擴展底桂,該擴展應(yīng)用于其元素符合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ù)組中的所有元素都是相同的,另一個數(shù)組中的所有元素都不相同:
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
因為數(shù)組符合集合簇爆,整數(shù)符合Equatable入蛆,所以equalNumbers和differentNumbers可以使用allEqual()方法:
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
如果符合標準的類型滿足多個受約束擴展的需求哨毁,這些擴展為相同的方法或?qū)傩蕴峁┝藢崿F(xiàn)挑庶,那么Swift使用與最專門化約束相對應(yīng)的實現(xiàn)迎捺。