Delegation委托是Apple iOS开发中很常见的一种模式,不过在之前开发Hipo中更多算是照猫画虎,这次来一篇Apple iBooks中《The Swift Programming Language ( Swift 4.2 beta)》中Protocol -- Delegation的译文。
委托(Delegation)是一种设计模式,能够让类或者结构体将自己一部分责任移交(或者称之为委托delegate)给另一个类型实例。该设计模式首先定义一个协议(protocol)描述被委托的责任,然后由受托者保证委托功能的实现。委托设计模式能够用来响应特定的动作或者从其他来源接受数据而不需要知道对方的类型。
下面例子为骰子积分榜类游戏定义了两个协议
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
DiceGame
协议可以用在任何骰子类游戏。
DiceGameDelegate
协议则可以用来追踪DiceGame
的进度。为避免强引用的环形依赖,委托被声明为弱引用。更多弱引用的介绍可以查看Strong Reference Cycles Between Class Instances。作为一个只能用在类上的协议,下面例子中SnakesAndLadders
类只能用弱引用来声明它的委托。 一个协议继承AnyObject
之后就只能用在类上面了,更多信息可以查看Class-Only Protocols。
下面版本的Snakes and Ladders游戏代码最初在Control Flow
中介绍过。这个版本采用了DiceGame
协议,使用Dic
实例做它的骰子机,并且将它的进度通知给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)
}
}
对于Snakes and Ladders游戏介绍,见Break。
这个版本的游戏使用采用了DiceGame
协议的SnakesAndLadders
类来实现。它实现了一个可取值属性dice
和一个方法play()
来遵循协议(属性dice
声明为常量,是因为它在初始化之后就不再更改了,并且协议中只规定它必须可取值)。
游戏的计分卡在类的init()
中设置了。所有的游戏逻辑移到了协议的play
方法中,并且使用了协议所规定的dice
属性来提供骰子的值。
注意,delegate
属性被定义为optional的DiceGameDelegate
。是因为这个协议不是玩游戏所必须的。它是optional类型的,delegate
属性自动设置初始值位nil
。因此游戏的实例化使用者可以选择给它设置一个合适的委托。DiceGameDelegate
协议是class-only的,你可以声明这个委托为weak
引用类避免环形依赖。
DiceGameDelegate
协议提供三个方法来跟踪游戏的进度。并且这三个方法已经在上述的play
方法中集成了。它们会在新游戏开始,新一轮开始,或者游戏结束时候调用。
因为delegate
属性可能是(optional)DiceGameDelegate
。play()
方法每次使用optional chaining来调用委托的方法。如果delegate
属性是nil值,那么调用这些委托方法会优雅的失败而不会报错。如果delegate
属性不是nil的值,当委托方法被调用的时候SnakesAndLadders
的实例会被当作参数传进去。
下面例子DiceCgameTracker
类实现了DiceGameDelegte
协议。
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 lated for \(numberOfTurns) turns")
}
}
DiceGameTracker
实现了DiceGameDelegate
所要求的三个方法。并且使用这三个方法来记录游戏进行了多少轮。它在游戏开始后重置numberOfTurns
为0,每次新一轮开始后递增,并且一旦游戏结束就打印出游戏总共进行了多少轮。
上述gameDidStart(_:)
的实现中,使用game
参数来打印所玩游戏的介绍性信息。game
的类型是DiceGame
而不是SnakesandLadders
,因此gameDidStart(_:)
只能访问和使用DiceGame
协议中的属性和方法。不过仍然能够使用类型推断(type casting)来判断实例的类。在这个例子中。它检查了如果game
是SnakesandLadders
的实例,就打印一些信息。
gameDidStart(_:)
方法中还访问了game
参数的dice
属性。因为game
实现了DiceGame
协议,因此就保证了一定有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”
下面代码的高亮截图,