iBooks にある “The Swift Programming Language” の勉強メモ。Objective-C と C を普段書いている自分から、ちょっと馴染みがないものを特にまとめておきます。目次は こちら。
今回は、プロトコルをより実践的に使用していく方法。
プロトコルの基本編の記事は、こちら。
Protocols
5. タイプとしてのプロトコル
例をみてみます。
1 2 3 4 5 6 7 8 9 10 11 | 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 プロトコルです。この generator プロパティには、RandomNumberGenerator プロトコルに準拠してる限りどんなタイプでも設定することができます。また、Initializer の引数のタイプとしてもこのプロトコルは使われています。generator が RandomNumberGenerator プロトコルに準拠していることが保証されているので、安心して random メソッドを使用することができます。
1 2 3 4 5 6 7 8 9 | var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator()) for _ in 1...5 { println("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 |
このように実際に使用されます。
6. Delegation
Delegation とはデザインパターンの 1 つで、クラス・構造体のある責任(機能)を他のタイプのインスタンスに転嫁することです。この Delegation は、転嫁される責任をまとめたプロトコルによって実装されます。例をみていきます。
1 2 3 4 5 6 7 8 9 | 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 プロトコルは、そのゲームの状況を観察するどんなタイプにでも採用されることができます。
このプロトコルを使い Snakes and Ladders というゲームの実装をしてみます。ゲームの内容は、iBooks の Control Flow にあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class SnakesAndLadders: DiceGame { let finalSquare = 25 let dice = Dice(sides: 6, generator: LinearCongruentialGenerator()) var square = 0 var board: Int[] init() { board = Int[](count: finalSquare + 1, repeatedValue: 0) 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) } } |
ここで定義したクラス SnakesAndLadders が DiceGame プロトコルに準拠しています。play メソッドと dice プロパティを持っています。このクラスがゲームの本体です。
そして、Delegation というデザインパターンとして重要なのが、プロパティとして delegate を持っていることです。11 行目ですね。これは Optional として宣言されているため、初期値は nil です。ゲーム状況を観察する処理は必ずしもゲームを実行するのに必要ではないので、処理が Optional で、delegate されています。このデザインパターンにより、ゲーム本体とそれを補助する処理を分離させ、責任を綺麗に分離させることができます。
ここでは、delegate が Optional のため、これを使用する際には、Optional Chaining (?
) を使っています。
肝心の転嫁された処理は、例えばこのように実装することができるでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class DiceGameTracker: DiceGameDelegate { var numberOfTurns = 0 func gameDidStart(game: DiceGame) { numberOfTurns = 0 if game is SnakesAndLadders { println("Started a new game of Snakes and Ladders") } println("The game is using a \(game.dice.sides)-sided dice") } func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) { ++numberOfTurns println("Rolled a \(diceRoll)") } func gameDidEnd(game: DiceGame) { println("The game lasted for \(numberOfTurns) turns") } } |
状況を print しているだけのシンプルなクラスです。DiceGameDelegate プロトコルに準拠し、そのためのメソッドを実装しています。
gameDidStart の引数の game は、SnakesAndLadders タイプではなく、DiceGame タイプです。そのため、DiceGame プロトコルで実装されているメソッド・プロパティにしかアクセスすることができません。しかし、適切なクラスにキャストすることはできます。この例では、game は SnakesAndLadders (DiceGame に準拠している) のインスタンスであることを確認しています。(game is SnakesAndLadders
の部分)
1 2 3 4 5 6 7 8 9 10 11 | 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 |
ゲームのルールが分からなくとも Delegate というデザインパターンがプロトコルを通して実装されたことが分かると思います。
7. Extension による プロトコル準拠
たとえ、もとのソースコードに対するアクセス権がなくとも、Extension により既存タイプをあるプロトコルに準拠させることができます。例をみていきます。
1 2 3 | protocol TextRepresentable { func asText() -> String } |
これはどんなタイプに対しても文字列として表現させるためメソッドを実装するプオロトコルです。これを先ほどの Dice クラス と SnakesAndLadders クラスに対し、Extension によって、この TextRepresentable プロトコルに準拠させるにはこのようにします。
1 2 3 4 5 6 7 8 9 10 11 | extension Dice: TextRepresentable { func asText() -> String { return "A \(sides)-sided dice" } } extension SnakesAndLadders: TextRepresentable { func asText() -> String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } |
プロトコルの名前は、タイプの名前の後にコロンを書き、宣言されます。そして、下記のように使用できます。
1 2 3 4 5 6 | let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator()) println(d12.asText()) // prints "A 12-sided dice" println(game.asText()) // prints "A game of Snakes and Ladders with 25 squares" |
すでにあるタイプがプロトコルの全ての要件を満たしても、プロトコルに準拠されていることが明示されていない場合には、空の Extension にて正式に準拠させることができます。あるタイプは、全ての要件を満たしているとしても、自動的にはプロトコルには準拠されないので、明示的に宣言する必要があります。
1 2 3 4 5 6 7 | struct Hamster { var name: String func asText() -> String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {} |
これにより、Hamster のインスタンスは、どこでも TextRepresentable タイプとして使用することができます。
1 2 3 4 | let simonTheHamster = Hamster(name: "Simon") let somethingTextRepresentable: TextRepresentable = simonTheHamster println(somethingTextRepresentable.asText()) // prints "A hamster named Simon" |