Swiftでプログラミング-Generics 3
Generic Where Clauses
型制約を使用すると、ジェネリック関数、添え字、または型に関連付けられた型パラメーターの要件を定義できます。
associated typeの要件定義をして便利に使うことが出来ます。これを使うために、ジェネリックwhere句を定義します。ジェネリックwhere句を使用するにははassociated typeを定義して特定のプロトコルに準拠、または特定の型パラメーターとassociated typeが同じという要件を満たす必要があります。一般的なwhere句は、whereキーワードで始まり、関連する型の制約、または型と関連する型の間の等価関係が続きます。型または関数の本体の開始中括弧の直前にジェネリックwhere句を記述します。
以下の例では、allItemsMatchというジェネリック関数を定義しています。この関数は、2つのContainerインスタンスに同じアイテムが同じ順序で含まれているかどうかを確認します。この関数は、すべての項目が一致する場合はtrueのブール値を返し、一致しない場合はfalseの値を返します。
チェックする2つのコンテナは、同じタイプのコンテナである必要はありませんが(同じでも良いですが)、同じタイプのアイテムを保持する必要があります。この要件は、型制約と一般的なwhere句の組み合わせによって表されます。
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// Check that both containers contain the same number of items.
if someContainer.count != anotherContainer.count {
return false
}
// Check each pair of items to see if they're equivalent.
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// All items match, so return true.
return true
この関数は、someContainerとanotherContainerという2つの引数を取ります。 someContainer引数のタイプはC1で、anotherContainer引数のタイプはC2です。 C1とC2はどちらも、関数が呼び出されたときに決定される2つのContainer型の型パラメーターです。
関数の2つの型パラメータには、次の要件があります。
・C1は、Containerプロトコル(C1:Containerとして記述)に準拠している必要があります。
・C2は、Containerプロトコル(C2:Containerと記述)にも準拠している必要があります。
・C1のアイテムは、C2のアイテムと同じである必要があります(C1.Item == C2.Itemと記述されます)。
・C1のアイテムは、Equatableプロトコル(C1.Item:Equatableと記述)に準拠している必要があります。
1番目と2番目の要件は関数の型パラメータリストで定義され、3番目と4番目の要件は関数のジェネリックwhere句で定義されます。
これらの要件は次のことを意味します。
・someContainerは、C1型のコンテナーです。
・anotherContainerは、C2型のコンテナーです。
・someContainerとanotherContainerには、同じ型のアイテムが含まれています。
・someContainerの項目は、等しくない演算子(!=)を使用してチェックし、互いに異なるかどうかを確認できます。
3番目と4番目の要件は、someContainerのアイテムとまったく同じ型であるため、anotherContainerのアイテムも!=演算子でチェックできることを意味します。
これらの要件により、allItemsMatch(_:_ :)関数は、コンテナータイプが異なる場合でも、2つのコンテナーを比較できます。
allItemsMatch(_:_ :)関数は、両方のコンテナーに同じ数のアイテムが含まれていることを確認することから始まります。含まれるアイテムの数が異なる場合、それらを一致させる方法はなく、関数はfalseを返します。
このチェックを行った後、関数は、for-inループとハーフオープン範囲演算子(.. <)を使用して、someContainer内のすべての項目を繰り返し処理します。この関数は、アイテムごとに、someContainerのアイテムがanotherContainerの対応するアイテムと等しくないかどうかを確認します。 2つの項目が等しくない場合、2つのコンテナーは一致せず、関数はfalseを返します。
不一致が見つからずにループが終了した場合、2つのコンテナーは一致し、関数はtrueを返します。
allItemsMatch(_:_ :)関数の動作は次のとおりです。
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// Prints "All items match."
上記の例では、文字列値を格納するStackインスタンスを作成し、3つの文字列をスタックにプッシュします。 この例では、スタックと同じ3つの文字列を含む配列リテラルで初期化された配列インスタンスも作成します。 スタックと配列の型は異なりますが、どちらもContainerプロトコルに準拠しており、どちらにも同じ型の値が含まれています。 したがって、これら2つのコンテナを引数としてallItemsMatch(_:_ :)関数を呼び出すことができます。 上記の例では、allItemsMatch(_:_ :)関数は、2つのコンテナー内のすべてのアイテムが一致することを正しく報告します。
Extensions with a Generic Where Clause
extensionの一部としてジェネリックwhere句を使用することもできます。 以下の例では、前の例の汎用Stack構造を拡張して、isTop(_ :)メソッドを追加しています。
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
この新しいisTop(_ :)メソッドは、最初にスタックが空でないことを確認してから、指定されたアイテムをスタックの最上位のアイテムと比較します。 ジェネリックwhere句なしでこれを実行しようとすると、問題が発生します。isTop(_ :)の実装では==演算子を使用しますが、Stackの定義ではアイテムが同等である必要がないため、 ==演算子を使用すると、コンパイル時エラーが発生します。 ジェネリックwhere句を使用すると、拡張機能に新しい要件を追加できるため、extensionは、スタック内の項目が同等である場合にのみisTop(_ :)メソッドを追加します。
isTop(_ :)メソッドの動作は次のとおりです。
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."
要素が等しくないスタックでisTop(_ :)メソッドを呼び出そうとすると、コンパイル時エラーが発生します。
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Error
プロトコルはextensionを使ってwhere句を使用できます。 以下の例では、前の例のContainerプロトコルを拡張して、startsWith(_ :)メソッドを追加しています。
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
startWith(_ :)メソッドは、最初にコンテナーに少なくとも1つのアイテムがあることを確認してから、コンテナー内の最初のアイテムが指定されたアイテムと一致するかどうかを確認します。 この新しいstartsWith(_ :)メソッドは、コンテナのアイテムが同等である限り、上記で使用したスタックや配列など、Containerプロトコルに準拠するすべての型で使用できます。
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// Prints "Starts with something else."
上記の例のジェネリックwhere句では、Itemがプロトコルに準拠している必要がありますが、Itemが特定のタイプである必要があるジェネリックwhere句を作成することもできます。 例えば:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints "648.9"
この例では、Item型がDoubleのプロトコルContainerにaverage()メソッドを追加します。 Container内のアイテムを繰り返して合計し、コンテナの数で割って平均を計算します。 カウントをIntからDoubleに明示的に変換して、浮動小数点除算を実行できるようにします。
extensionを使うことで複数の要件についてwhere句を使うことが出来ます。 リスト内の各要件はコンマで区切ります。
Contextual Where Clauses
ジェネリック型の制約を受けて既に実行している場合は、独自のジェネリック型制約を持たない宣言の一部としてジェネリックwhere句を記述できます。 たとえば、ジェネリック型の添え字またはジェネリック型のextensionのメソッドにジェネリックwhere句を記述できます。 コンテナ構造は一般的であり、以下の例のwhere句は、これらの新しいメソッドをコンテナで使用できるようにするために満たす必要のある型制約を指定します。
extension Container {
func average() -> Double where Item == Int {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
func endsWith(_ item: Item) -> Bool where Item: Equatable {
return count >= 1 && self[count-1] == item
}
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"
この例では、アイテムが整数の場合はaverage()メソッドをコンテナに追加し、アイテムが同等の場合はendsWith(_ :)メソッドを追加します。 どちらの関数にも、Containerの元の宣言からジェネリックItemタイプパラメーターにタイプ制約を追加するジェネリックwhere句が含まれています。
コンテキストwhere句を使用せずにこのコードを記述したい場合は、ジェネリックwhere句ごとに1つずつ、合計2つのextensionで記述します。 上記の例と以下の例は同じ動作をします。
extension Container where Item == Int {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
}
extension Container where Item: Equatable {
func endsWith(_ item: Item) -> Bool {
return count >= 1 && self[count-1] == item
}
}
上記の例では、コンテキストwhere句を使用するaverage()とendsWith(_ :)の実装は両方とも同じextensionになります。これは、各メソッドの汎用where句が、そのメソッドを使用可能にするために満たす必要があるからです。 これらの要件をextensionの汎用where句に移動すると、同じ状況でメソッドを使用できるようになりますが、要件ごとに1つのextensionが必要です。
Associated Types with a Generic Where Clause
associated typeにジェネリックwhere句を含めることができます。 たとえば、Sequenceプロトコルが標準ライブラリで使用するもののように、イテレータを含むバージョンのContainerを作成するとします。 これを書く方法は次のとおりです。
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
イテレータのジェネリックwhere句では、イテレータの型に関係なく、イテレータはコンテナのアイテムと同じアイテムタイプの要素をトラバースする必要があります。 makeIterator()関数は、コンテナのイテレータへのアクセスができるようになります。
別のプロトコルから継承するプロトコルの場合、プロトコル宣言にジェネリックwhere句を含めることにより、継承された関連型に制約を追加します。 たとえば、次のコードは、ItemがComparableに準拠することを要求するComparableContainerプロトコルを宣言しています。
protocol ComparableContainer: Container where Item: Comparable { }
Generic Subscripts
添え字はジェネリックにすることができ、ジェネリックwhere句を含めることができます。 下付き文字の後の山括弧内にプレースホルダータイプ名を記述し、下付き文字の本文の開始中括弧の直前にジェネリックwhere句を記述します。 例えば:
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result: [Item] = []
for index in indices {
result.append(self[index])
}
return result
}
}
Containerプロトコルのextensionは、インデックスのシーケンスを受け取り、指定された各インデックスのアイテムを含む配列を返す添え字を追加します。 この一般的な添え字は、次のように制約されます。
・山括弧内の汎用パラメータインデックスは、標準ライブラリのシーケンスプロトコルに準拠する型である必要があります。
・添え字は、そのインデックス型のインスタンスである単一のパラメータindexesを取ります。
・ジェネリックwhere句では、シーケンスのイテレータがInt型の要素を巡回させる必要があります。 これにより、シーケンス内のインデックスがコンテナに使用されるインデックスと同じ型になることが保証されます。
まとめると、これらの制約は、indexesパラメーターに渡される値が整数のシーケンスであることを意味します。