第二十一章 协议
6. Protocols as Types (协议当作类型)
协议实际上并不能自行实现任何功能,尽管如此我们可以把协议当作普通类型那样在我们的代码中使用。把协议当作类型使用有些时候我们称之为 “存在类型” (existential type),来自于 “ 如果存在某个类型T,那么这个类型T就会遵循某个协议 ”。Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”.
我们可以在很多地方使用到某个类型允许的协议,包括了:
- 作为函数,方法或者构造器的参数类型或返回类型。
As a parameter type or return type in a function, method, or initializer - 作为常量,变量和属性的一个类型
As the type of a constant, variable, or property - 作为数组,字典或其他容器的一个类型
As the type of items in an array, dictionary, or other container
下面是一个协议当作类型使用的例子,该例定义了一个新的类Dice
,表示在游戏中使用的一个n
面的骰子。该类的实例有一个属性generator
,用来生成一个用来摇骰子的随机的点数,属性generator是一个RandomNumberGenerato
r(协议)的类型,所以可以给属性可以设置任何类型中的任何一个实例,实例也会采用和遵循个此协议,所以我们分配给这个属性的也没有其他对该实例的要求了,除了该实例必须遵循这个协议,因为它是一个类型是RandomNumberGenerator这个协议。Dice类中的代码只能以适用于所有符合此协议的生成器的方式与生成器进行交互。这意味着它无法使用由生成器的基础类型定义的任何方法或属性,但是,我们可以把它像父类到子类的方式向下转换成一个基础类型。
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
同样提供了一个构造器,用来设置骰子的初始状态,该构造器有一个generator
的参数, 是一个RandomNumberGenerator
的类型,所以我们可以传入任何确定类型的值给这个参数,当Dice实例初始化的时候。
Dice提供了一个实例方法roll
,返回的整数类型值正好是骰子所在面的点数,实例方法roll会调用生成器的random()
方法,来创建一个0.0
到1.0
之间的随机数字,用生成的随机数字来确定摇骰子的数字在一个合理的范围内。因为generator
它是遵循这个协议RandomNumberGenerator
的 所以说就能一定能调用random()方法。
下面是这个类Dice是如何用实例LinearCongruentialGenerator来作为一个随机数字生成器创建一个6面的骰子的。
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
7. Delegation (代理)
代理是类和结构体交出(代理)某些职责给其他类型的实例的一个设计模式,定义封装这种职责的协议可以实现这种设计模式。代理可以用来响应一个特定的行为或从内部代码中检索接受数据,并不需要知道与之代码所相关的基础类型,Delegation can be used to respond to particular action, or to retrieve data from an external source without needing to know the underlying type of that source.
下面这个例子定义了两个协议用以骰子游戏中
protocol DiceGame {
var dice: Dice { get }
func play()
}
// class-only protocol with AnyObject 参考相关章节
protocol DiceGameDelegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
这个协议DiceGameDelegate
协议被用来采用和追踪这个DiceGame
,从来预防强引用循环,所以这个代理被定义为弱引用,更多有关弱引用的内容详见Strong Reference Cycles Between Class Instances
,所以将这个协议定义为类类型协议(class-only protocol),可以使代理的类SnalesAndLadders
同样采用这个弱应用,类类型的协议将会在后面章节中有详细描述。
下面这个版本的蛇和梯子的游戏的基础描述和玩法在控制流中有介绍过,实例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
}
// 参考强弱引用相关篇章
weak var delegate: DiceGameDelegate?
// 方法play() 实现了游戏的逻辑
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类的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
// 1. start 游戏开始的时候轮数为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")
}
// 2. re-start 再次开始新一轮游戏,轮数加1
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
numberOfTurns += 1
print("Rolled a \(diceRoll)")
}
// 3. end 游戏结束 并输出最后游戏进行的轮数次数。
func gameDidEnd(_ game: DiceGame) {
print("The game lasted for \(numberOfTurns) turns")
}
}
DiceGameDelegate要求DiceGameTracker实现三个方法,用来追踪游戏进行的轮数,所以这个类里面就有一个初始值为0
的变量属性numberOfTurns
,当游戏开始,再次开始新一轮游戏,和游戏结束并输出最后的轮数。
gameDidStart()
方法是利用游戏的参数来输出一些游戏即将开始是的介绍性的信息,这个游戏参数game
其实是一个DiceGame的类型。所以说这个方法gameDidStart()只能读取,使用实现在DiceGame
协议中方法和属性。其实在幕后方法依然可以使用类型转换来查询蛇和梯子游戏中潜在实例的类型,在上面例子中用来查询game是否是SnakesAndLadders的一个实例
并且在游戏中尽可能地输出相关的信息。gameDidStart()方法也可以用来读取dice属性中被传入的game参数,因为game是遵循这个DiceGame协议,所以game确定会有dice这个属性,所以该方法可以用来读取和输出dice中的sides
属性,
实践中的DiceGameTrackers
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