Swiftでプログラミング-Generics 2
Type Constraints
swapTwoValues(_:_ :)関数と Stack typeは、どの型でも機能します。ただし、ジェネリック関数やジェネリック型で使用できる型に特定の型制約を適用すると便利な場合があります。型制約は、型パラメーターが特定のクラスから継承するか、特定のプロトコルまたはprotocol compositionに準拠する必要があります。
たとえば、Swiftの辞書型では、辞書のkeyとして使用できる型に制限があります。辞書で説明されているように、辞書のkeyの型はhashableでないといけません。つまり、それ自体を一意に表現できるようにする方法を提供する必要があります。辞書には、特定のkeyの値がすでに含まれているかどうかを確認できるように、keyがhashableである必要があります。この要件がないと、辞書は特定のkeyの値を挿入するか置き換えるかを判断できず、辞書にすでに存在する特定のkeyの値を見つけることもできません。
この要件は、辞書のキータイプに対するタイプ制約によって適用されます。これは、キータイプがSwift標準ライブラリで定義されている特別なプロトコルであるHashableプロトコルに準拠する必要があることを指定します。 Swiftのすべての基本型(String、Int、Double、Boolなど)は、デフォルトでhashableです。
カスタムジェネリック型を作成するときに独自の型制約を定義できます。これらの制約は、ジェネリックプログラミングの能力の多くを提供します。 Hashableのような抽象的な概念は、概念的な特性の観点から型を特徴付けます。
Type Constraint Syntax
型制約は、型パラメータリストの一部として、コロンで区切られた型パラメータの名前の後に単一のクラスまたはプロトコル制約を配置することによって記述します。 ジェネリック関数の型制約の基本的な構文を以下に示します(ただし、構文はジェネリック型でも同じです)。
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
上記の架空の関数には、2つのタイプパラメーターがあります。 最初の型パラメーター"T"には、"T"がSomeClassのサブクラスである必要がある型制約があります。 2番目の型パラメーター"U"には、プロトコルSomeProtocolに準拠するために"U"を必要とする型制約があります。
Type Constraints in Action
これは、findIndex(ofString:in :)と呼ばれる非汎用関数です。この関数には、検索する文字列値と、検索する文字列値の配列が指定されています。 findIndex(ofString:in :)関数は、optionalのInt値を返します。これは、配列内で最初に一致する文字列が見つかった場合はそのインデックスになり、文字列が見つからなかった場合はnilになります。
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(of String:in :)関数を使用して、文字列の配列内の文字列値を検索できます。
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"
ただし、配列内の値のインデックスを見つけるという原則は、文字列だけに役立つわけではありません。 文字列の記述をT型の値に置き換えることで、ジェネリック関数と同じ機能を記述できます。
これが、findIndex(of:in :)と呼ばれるfindIndex(ofString:in :)の汎用バージョンが書き込まれることを期待する方法です。 この関数は配列からのoptionalの値ではなく、optionalのインデックス番号を返すため、この関数の戻り型はまだInt?であることに注意してください。 ただし、注意が必要です。例の後に説明されている理由により、この関数はコンパイルされません。
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
この関数は、上記のようにコンパイルされません。問題は、「if value == valueToFind」という等価性チェックにあります。 Swiftのすべての型をequalto演算子(==)と比較できるわけではありません。たとえば、複雑なデータモデルを表す独自のクラスまたは構造を作成する場合、そのクラスまたは構造の「等しい」の意味は、Swiftが推測できるものではありません。このため、このコードがすべての可能な型"T"で機能することを保証することはできず、コードをコンパイルしようとすると適切なエラーが報告されます。
ただし、すべてが失われるわけではありません。 Swift標準ライブラリは、Equatableと呼ばれるプロトコルを定義します。これには、等しい演算子(==)と等しくない演算子(!=)を実装して、その型の任意の2つの値を比較するための適合型が必要です。 Swiftのすべての標準型は、Equatableプロトコルを自動的にサポートします。
Equatableの任意の型は、equal to演算子をサポートすることが保証されているため、findIndex(of:in :)関数で安全に使用できます。この事実を表現するために、関数を定義するときに、型パラメーターの定義の一部としてEquatableの型制約を記述します。
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(of:in :)の単一型のパラメーターは、T:Equatableとして記述されます。これは、「Equatableプロトコルに準拠するすべての型T」を意味します。
findIndex(of:in :)関数は正常にコンパイルされるようになり、DoubleやString:などのEquatableの任意のタイプで使用できるようになりました。
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2
Associated Types
プロトコルを定義するときに、プロトコルの定義の一部として1つ以上の関連する型を宣言すると便利な場合があります。 Associated Typesは、プロトコルの一部として使用されるタイプにプレースホルダー名を与えます。 その Associated Typesに使用する実際の型は、プロトコルが採用されるまで指定されません。 関連するタイプは、associatedtypeキーワードで指定されます。
Associated Types in Action
次に、Itemというassociatedtypeを宣言するプロトコルContainerの例を示します。
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
プロトコルContainerは、C3つの必要な機能を定義します。
・append(_ :)メソッドを使用してContainerに新しいアイテムを追加できるます。
・countプロパティでInt値を取得できます
・Int値をとる添え字で、Container内の各要素を取得できます。
このプロトコルは、Container内のアイテムの保存方法や許可される型を指定していません。プロトコルContainerは必要な機能、わずか3つの機能を指定します。適合型は、これら3つの要件を満たすことで、追加の機能も提供できるようになります。
プロトコルContainerに準拠するすべての型は、格納する値の型を指定できます。具体的には、適切な型のアイテムのみがContainerに追加することができ、添え字によって返されるItemの型については明確にする必要があります。
これらの要件を定義するために、プロトコルContainerには、特定のコンテナの型が何であるかを知らなくても、Containerが保持する要素の型を参照する方法が必要です。Containerにはプロトコルは、append(_ :)メソッドに渡される値がContainerの要素型と同じ型である必要があり、コンテナの添え字によって返えされる値がContainerの要素型と同じ型になることを指定します。
これを実現するために、プロトコルContainerは、Associatedtype Itemとして記述されたItemと呼ばれるAssociatedtypeを宣言します。プロトコルはアイテムが何であるかを定義しません—その情報はどんな適合タイプでも提供するために残されます。それでも、Itemエイリアスは、コンテナ内のアイテムのタイプを参照し、append(_ :)メソッドと添え字で使用するタイプを定義して、コンテナの期待される動作が確実に適用されるようにする方法を提供します。
プロトコルContainerに準拠するように適合された、上記のジェネリック型の非ジェネリックIntStack型のバージョンは次のとおりです。
struct IntStack: Container {
// original IntStack implementation
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack型は、コンテナプロトコルの3つの要件すべてを実装し、いずれの場合も、これらの要件を満たすためにIntStackタイプの既存の機能の一部をラップします。
さらに、IntStackは、このContainerの実装で、使用する適切なItemがIntのタイプであることを指定しています。 typealias Item = Intの定義は、Containerプロトコルのこの実装のために、Itemの抽象型をIntの具象型に変換します。
Swiftの型推論のおかげで、IntStackの定義の一部としてIntの具体的なItemを実際に宣言する必要はありません。 IntStackはコンテナプロトコルのすべての要件に準拠しているため、Swiftは、append(_ :)メソッドのitemパラメータの型と添え字の戻りタイプを確認するだけで、使用する適切なItemを推測できます。実際、上記のコードからtypealias Item = Int行を削除しても、Itemに使用する型が明確であるため、すべてが引き続き機能します。
ジェネリックStack型をContainer プロトコルに準拠させることもできます。
struct Stack<Element>: Container {
// original Stack<Element> implementation
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
今回は、型パラメータElementが、append(_ :)メソッドのitemパラメータの型および添え字の戻り型として使用されます。 したがって、Swiftは、Elementがこの特定のContainerのitemsとして使用するのに適切な型であると推測できます。
Extending an Existing Type to Specify an Associated Type
既存のタイプを拡張してプロトコルに適合性を追加できます。 これには、 Associated Typesのプロトコルが含まれます。
Swiftの配列型には、append(_ :)メソッド、countプロパティ、および要素を取得するためのIntインデックス付きの添え字が既に用意されています。 これらの3つの機能は、Containerプロトコルの要件に一致します。 これは、Arrayがプロトコルを採用することを宣言するだけで、Arrayを拡張してContainerプロトコルに準拠できることを意味します。 これは、extensionを使用したプロトコル採用の宣言で説明されているように、空のextensionを使用して行います。
extension Array: Container {}
配列の既存のappend(_ :)メソッドと添え字により、Swiftは、上記の一般的なStack型と同様に、Itemに使用する適切な型を推測できます。 このextensionを定義すると、任意の配列をコンテナとして使用できます。
Adding Constraints to an Associated Type
プロトコル内の関連する型に型制約を追加して、適合型がそれらの制約を満たすことを要求できます。 たとえば、次のコードは、Container内のアイテムが同等である必要があるバージョンのコンテナを定義しています。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
このバージョンのContainerに準拠するには、Container内のItem型がEquatableプロトコルに準拠している必要があります。
Using a Protocol in Its Associated Type’s Constraints
プロトコルは、それ自体の要件の一部として表示される場合があります。 たとえば、これはContainerプロトコルを改良し、suffix(_ :)メソッドの要件を追加するプロトコルです。 suffix(_ :)メソッドは、Container内の末尾から指定された数の要素を返し、それらをsuffix型のインスタンスに格納します。
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
このプロトコルでは、suffixは、上記のコンテナのItem型のように、Associated Typesです 。 Suffixには2つの制約があります。SuffixableContainerプロトコル(現在定義されているプロトコル)に準拠している必要があり、そのItem型はContainer のItem型と同じである必要があります。 Itemは、ジェネリックwhere句にの制約されます。
SuffixableContainerプロトコルへの適合性を追加する、上記のGenericTypesからのStackタイプの拡張は次のとおりです。
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30
上記の例では、StackのSuffixのassociated typeもStackであるため、Stackのsuffix 操作は別のstackを返します。 または、SuffixableContainerに準拠する型は、それ自体とは異なるSuffix型を持つことができます。つまり、Suffix操作は異なる型を返すことができます。 たとえば、IntStackの代わりにStack <Int>をSuffix型として使用して、SuffixableContainer準拠を追加する非汎用IntStack型のextensionを次に示します。
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Inferred that Suffix is Stack<Int>.
}