Swiftでプログラミング-Protocols 2
Protocols as Types
プロトコル自体は実際には機能を実装していません。 それでも、コードではプロトコルを本格的な型として使用できます。 プロトコルを型として使用することは、"existential type"と呼ばれることもあります。これは、「"T"がプロトコルに準拠するような型"T"が存在する」というフレーズに由来します。
プロトコルは、次のような他のタイプが許可されている多くの場所で使用できます。
・関数、メソッド、または初期化子のパラメーター型または戻り型として
・定数、変数、またはプロパティの型として
・配列、辞書、またはその他のコンテナ内のアイテムの型として
プロトコルは型であるため、Swiftの他の型の名前(Int、String、Doubleなど)と一致するように、名前を大文字(FullyNamedやRandomNumberGeneratorなど)で始めます。
型として使用されるプロトコルの例を次に示します。
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
}
}
この例では、ボードゲームで使用するn面のサイコロを表すDiceという新しいクラスを定義します。ダイスインスタンスには、サイドの数を表すsidesと呼ばれる整数プロパティと、ダイスロール値を作成するための乱数ジェネレーターを提供するgeneratorと呼ばれるプロパティがあります。
ジェネレータプロパティの型はRandomNumberGeneratorです。したがって、RandomNumberGeneratorプロトコルを採用する任意の型のインスタンスに設定できます。インスタンスがRandomNumberGeneratorプロトコルを採用する必要があることを除いて、このプロパティに割り当てるインスタンスには他に何も必要ありません。その型はRandomNumberGeneratorであるため、Diceクラス内のコードは、このプロトコルに準拠するすべてのジェネレーターに適用される方法でのみジェネレーターと対話できます。つまり、ジェネレータの基になるタイプによって定義されたメソッドやプロパティを使用することはできません。ただし、ダウンキャストで説明したように、スーパークラスからサブクラスにダウンキャストするのと同じ方法で、プロトコルタイプから基になるタイプにダウンキャストできます。
Diceには、初期状態を設定するための初期化子もあります。この初期化子には、generatorと呼ばれるパラメーターがあります。これもRandomNumberGeneratorタイプです。新しいDiceインスタンスを初期化するときに、任意の適合タイプの値をこのパラメーターに渡すことができます。
Diceは、1からダイスの辺の数までの整数値を返す1つのインスタンスメソッドrollを提供します。このメソッドは、ジェネレーターのrandom()メソッドを呼び出して、0.0〜1.0の新しい乱数を作成し、この乱数を使用して、正しい範囲内のダイスロール値を作成します。ジェネレーターはRandomNumberGeneratorを採用することが知られているため、ランダム()メソッドを呼び出すことが保証されています。
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
Delegation
委任は、クラスまたは構造がその責任の一部を別のタイプのインスタンスに引き渡す(または委任する)ことを可能にするデザインパターンです。 このデザインパターンは、委任された責任をカプセル化するプロトコルを定義することによって実装されます。これにより、準拠タイプ(委任と呼ばれる)が委任された機能を提供することが保証されます。 委任を使用して、特定のアクションに応答したり、外部ソースの基になるタイプを知らなくても外部ソースからデータを取得したりできます。
以下の例では、サイコロを使ったボードゲームで使用する2つのプロトコルを定義しています。
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プロトコルは、サイコロを使用するすべてのゲームで採用できるプロトコルです。
DiceGameDelegateプロトコルを採用して、DiceGameの進行状況を追跡できます。 強参照サイクルを防ぐために、デリゲートは弱参照として宣言されます。 弱参照については、クラスインスタンス間の強参照サイクルを参照してください。 プロトコルをクラスのみとしてマークすると、この章の後半のSnakesAndLaddersクラスは、デリゲートが弱参照を使用する必要があることを宣言できます。 クラスのみのプロトコルは、クラスのみのプロトコルで説明されているように、AnyObjectからの継承によってマークされます。
これは、制御フローで紹介したSnakes and Ladders ゲームです。 このゲームは、DiceGameプロトコルに準拠して、サイコロを降るために"Dice"のインスタンスを作っり、進行については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)
}
}
このバージョンのゲームは、DiceGameプロトコルを採用し、クラスSnakesAndLaddersでまとめられています。プロトコルに準拠するために、gettable diceプロパティとplay()メソッドをが必要となっています。 (diceプロパティは、初期化後に変更する必要がなく、プロトコルではgettableである必要があるだけなので、定数プロパティとして宣言されています。)
Snakes and Laddersゲームボードのセットアップは、クラスのinit()イニシャライザー内で行われます。すべてのゲームロジックは、プロトコルのplayメソッドに移動されます。このメソッドは、プロトコルに必要なサイコロのプロパティを使用して、サイコロの目が振られる値を提供します。
ゲームをプレイするためにデリゲートは必要ないため、デリゲートプロパティはoptionalのDiceGameDelegateとして定義されていることに注意してください。optional型であるため、デリゲートプロパティは自動的に初期値nilに設定されます。その後、ゲームのインスタンス化機能には、プロパティを適切なデリゲートに設定するオプションがあります。 DiceGameDelegateプロトコルはクラスのみであるため、参照サイクルを防ぐためにデリゲートを弱、"weak"と宣言できます。
DiceGameDelegateは、ゲームの進行状況を追跡するための3つのメソッドを提供します。これらの3つのメソッドは、上記のplay()メソッド内のゲームロジックに組み込まれており、新しいゲームの開始時、新しいターンの開始時、またはゲームの終了時に呼び出されます。
デリゲートプロパティはoptionalのDiceGameDelegateであるため、play()メソッドは、デリゲートのメソッドを呼び出すたびにoptionalのチェーンを使用します。デリゲートプロパティがnilの場合、これらのデリゲート呼び出しはエラーなしで正常に失敗します。デリゲートプロパティがnil以外の場合、デリゲートメソッドが呼び出され、SnakesAndLaddersインスタンスがパラメータとして渡されます。
次の例は、DiceGameDelegateプロトコルを採用するDiceGameTrackerというクラスを示しています。
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に必要な3つのメソッドすべてを実装します。これらの方法を使用して、ゲームのターン数を追跡します。ゲームの開始時にnumberOfTurnsプロパティをゼロにリセットし、新しいターンが開始するたびにインクリメントし、ゲームが終了すると合計ターン数を出力します。
上記のgameDidStart(_ :)の実装では、gameパラメータを使用して、プレイしようとしているゲームに関するいくつかの紹介情報を出力します。ゲームパラメータのタイプはSnakesAndLaddersではなくDiceGameであるため、gameDidStart(_ :)は、DiceGameプロトコルの一部として実装されているメソッドとプロパティにのみアクセスして使用できます。ただし、このメソッドでは、型キャストを使用して、基になるインスタンスの型をクエリできます。この例では、ゲームが実際に舞台裏でSnakesAndLaddersのインスタンスであるかどうかを確認し、そうである場合は適切なメッセージを出力します。
gameDidStart(_ :)メソッドは、渡されたゲームパラメータのdiceプロパティにもアクセスします。ゲームはDiceGameプロトコルに準拠していることがわかっているため、サイコロプロパティが保証されます。したがって、gameDidStart(_ :)メソッドは、プレイされているゲームの種類に関係なく、サイコロの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