委托
委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来
封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部
数据源提供的数据,⽽无需关心外部数据源的类型。
下⾯的例子定义了两个基于骰子游戏的协议:
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 类在声
明协议时强制要使用弱引用。若要声明类专属的协议就必须继承于 AnyObject ,更多请看《类专属的协议》。
如下所示, SnakesAndLadders 是《控制流》章节引入的蛇梯棋游戏的新版本。新版本使用 Dice 实例作为骰子,并且实现了
DiceGame 和 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
}
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 协议,并且提供了相应的可读的 dice 属性和 play()
方法。( dice 属性在构造之后就不再改变,且协议只要求 dice 为可读的,因此将 dice 声明为常量属性。)
游戏使用 SnakesAndLadders 类的 init() 构造器来初始化游戏。所有的游戏逻辑被转移到了协议中的 play() ⽅法, play() 方法使
用协议要求的 dice 属性提供骰子摇出的值。
注意, delegate 并不是游戏的必备条件,因此 delegate 被定义为 DiceGameDelegate 类型的可选属性。因为 delegate 是可选
值,因此会被自动赋予初始值 nil 。随后,可以在游戏中为 delegate 设置适当的值。
DicegameDelegate 协议提供了三个方法用来追踪游戏过程。这三个方法被放置于游戏的逻辑中,即 play() 方法内。分别在游戏
开始时,新一轮开始时,以及游戏结束时被调用。
因为 delegate 是一个 DiceGameDelegate 类型的可选属性,因此在 play() ⽅法中通过可选链式调用来调用它的方法。若
delegate 属性为 nil ,则调用方法会优雅地失败,并不会产生错误。若 delegate 不为 nil ,则⽅法能够被调用,并传递
SnakesAndLadders 实例作为参数。
如下示例定义了 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 += 1 print("Rolled a \(diceRoll)")
}
func gameDidEnd(_ game: DiceGame) {
print("The game lasted for \(numberOfTurns) turns")
}
}
DiceGameTracker 实现了 DiceGameDelegate 协议要求的三个方法,用来记录游戏已经进⾏的轮数。当游戏开始时,
numberOfTurns 属性被赋值为 0 ,然后在每新一轮中递增,游戏结束后,打印游戏的总轮数。
gameDidStart(_:) ⽅法从 game 参数获取游戏信息并打印。 game 参数是 DiceGame 类型而不是 SnakeAndLadders 类型,所以
在 gameDidStart(_:) 方法中只能访问 DiceGame 协议中的内容。当然了, SnakeAndLadders 的方法也可以在类型转换之后调
用。在上例代码中,通过 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
在扩展⾥添加协议遵循
即便无法修改源代码,依然可以通过扩展令已有类型遵循并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,
因此可以符合协议中的相应要求。详请在《扩展》章节中查看。
注意
通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。
例如下面这个 TextRepresentable 协议,任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是
实例本身的描述,也可以是实例当前状态的⽂本描述:
protocol TextRepresentable {
var textualDescription: String { get }
}
可以通过扩展,令先前提到的 Dice 类可以扩展来采纳和遵循 TextRepresentable 协议:
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
通过扩展遵循并采纳协议,和在原始定义中遵循并符合协议的效果完全相同。协议名称写在类型名之后,以冒号隔开,然 后在扩
展的大括号内实现协议要求的内容。
现在所有 Dice 的实例都可以看做 TextRepresentable 类型:
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”
同样, SnakesAndLadders 类也可以通过扩展来采纳和遵循 TextRepresentable 协议:
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”
有条件地遵循协议
泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出
限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。更多关于泛型 where 分句,⻅《泛型
Where 分句》。
下⾯的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。
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]"
在扩展⾥声明采纳协议
当一个类型已经符合了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
从现在起, Hamster 的实例可以作为 TextRepresentable 类型使用:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”
注意
即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。
协议类型的集合
协议类型可以在数组或者字典这样的集合中使用,在《协议类型》提到了这样的用法。下面的例子创建了一个元素类型为
TextRepresentable 的数组:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
如下所示,可以遍历 things 数组,并打印每个元素的文本表示:
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 属性,所以在每次循
环中可以安全地访问 thing.textualDescription 。
协议的继承
协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的
协议间用逗号分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 这⾥是协议的定义部分
}
如下所示, PrettyTextRepresentable 协议继承了 TextRepresentable 协议:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
例子中定义了一个新的协议 PrettyTextRepresentable ,它继承自 TextRepresentable 协议。任何遵循 PrettyTextRepresentable
协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。在这个例子中, PrettyTextRepresentable 协议
额外要求遵循协议的类型提供一个返回值为 String 类型的 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
}
}
上述扩展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 协议,并提供了协议要求的 prettyTextualDescription 属性。每
个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问
textualDescription 属性。然后,拼接上了冒号和换⾏符。接着,遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内
容:
当从数组中取出的元素的值大于 0 时,⽤ ▲ 表示。
当从数组中取出的元素的值小于 0 时,⽤ ▼ 表示。
当从数组中取出的元素的值等于 0 时,⽤ ○ 表示。
任意 SankesAndLadders 的实例都可以使用 prettyTextualDescription 属性来打印一个漂亮的文本描述:
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
类专属的协议
你通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// 这⾥是类专属协议的定义部分
}
在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型采纳。如果尝试让结构体或枚举类型采纳
SomeClassOnlyProtocol ,则会导致编译时错误。
注意
当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。关于引⽤语义和值语义的更多
内容,请查看《结构体和枚举是值类型》和《类是引用类型》。
协议合成
要求一个类型同时遵循多个协议是很有用的。你可以使用协议组合来复合多个协议到一个要求里。协议组合⾏为就和你定义的临
时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。
协议组合使用 SomeProtocol & AnotherProtocol 的形式。你可以列举任意数量的协议,用和符号( & )分开。除了协议列表,协
议组合也能包含类型,这允许你标明一个需要的父类。
下⾯的例子中,将 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(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 协议包含 String 类型的 name 属性。 Aged 协议包含 Int 类型的 age 属性。 Person 结构体采纳了这两个协议。
wishHappyBirthday(to:) 函数的参数 celebrator 的类型为 Named & Aged , 这意味着“任何同时遵循 Named 和 Aged 的协议”。
它不关心参数的具体类型,只要参数符合这两个协议即可。
上⾯的例子创建了一个名为 birthdayPerson 的 Person 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。因为 Person
同时符合这两个协议,所以这个参数合法,函数将打印生日问候语。
这里有一个例子:将 Location 类和前面的 Named 协议进⾏组合:
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:) 函数接受一个类型为 Location & Named 的参数,这意味着“任何 Location 的子类,并且遵循 Named 协议”。
例如,City 就满足这样的条件。
将 birthdayPerson 传入 beginConcert(in:) 函数是不合法的,因为 Person 不是 Location 的子类。同理,如果你新建一个类继承
于 Location,但是没有遵循 Named 协议,⽽用这个类的实例去调用 beginConcert(in:) 函数也是非法的。