今回は、Swift における Generic Programming について理解を含めようということで、Swift の公式ドキュメントを眺めがらメモした内容をまとめていきます。
目次
- Why generics?
- Generic Functions
- Type Parameters
- Generic Type
- Type Constraints
- Associated Types
- Generic Where
- Generic Subscripts
Why Generics?
Generic Code を使用すると、定義した用件に応じて、任意の型で機能する柔軟で再利用可能な関数と型を記述できます。そのため、重複を避け、その意図を明確で抽象化された方法で表現するコードを書くことができます。
- タイプセーフ
- コードの重複
- ライブラリの柔軟性
Generics は Swift の最も強力な機能の1つであり、Swift standard library の多くは generic code で構築されています。たとえば、Swift の Array と Dictionary タイプはどちらも Generic collections です。Int 値を保持する配列、String 値を保持する配列、または Swift で作成できる他のタイプの配列を作成できます。同様に、指定したタイプの値を格納する Dictionary を作成できます。そのタイプに制限はありません。
Generic Functions
これは、swapTwoInts(_:_:)
と呼ばれる標準の nongeneric 関数で、2つの Int 値を交換します。
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA }
これを Generic function にして汎用的にメソッドが使えるようにしてみましょう。
※ これらの引数の値の型は両方同じである必要があります。Swift は type-safe 言語なので、相互に違う方の変数が変換されることを許容しません。
改善がこちら。Generic バージョンの関数は、実際の型名(Int, Stringなど)の代わりに、Placeholder type name を使用します(この場合は T)。T
の代わりに使用する実際のタイプは、swapTwoValue(_:_:)
関数が呼び出されるたびに決定されます。
func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
Type Parameters
swapTwoValues(_:_:)
の例では、Placeholder type の T が Type Parameter の例になります。
Type Prameter を使用すると、それを使用して関数のパラメータの型を定義できたり、関数の戻り値、関数の本体内の型の注釈として使用できます。いずれの場合も、関数が呼び出されるたびに、Type Prameter は、実際の型に置き換えられます。
<>
内をコンマで区切って、複数の Type Prameter を指定することもできます。(例: <T: Hoge1, U: Hoge2>
)
Type Parameters のネーミング
ほとんどの場合、Type Parameters には、Dictionary<Key, Value>
の Key
と Value
、Array<Element>
の Element
など、分かりやすい名前がついています。これは、Type Parameter と、それが使用される Generic Type または Generic functions との関係を分かりやすくします。ただし、それらの間に意味のある関係が存在しない場合などは、慣例的に T
、U
、V
などの1文字を使用して名前をつけるのが一般的です。
※ Type Parameters には T
や MyTypeParameter
のように Upper camel case を付けて、値ではなく型のプレースホルダーであることを示す必要があります。
Generic Type
Generic functions に加えて、Swift では独自の Generic Types を定義することができます。これらは、Array や Dictionary のように、任意のタイプで機能できる custom class
、struct
、enum
を定義することができます。
下記は、スタックとしての機能を持った nongeneric の Struct です。push
や pop
といったメソッドは、Structure の items 配列を変更(mutate)する必要があるため、mutating
としてマークされています。
struct IntStack { var items = [Int]() mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
上記の IntStack は Int
値でのみ使用することができるので、これを汎用的に使えるようするため、下記のように Generic Type として定義します。
struct Stack<Element> { var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }
この Generic Type としての定義により、Array や Dictionary と同様に、Swift で任意の有効な型のスタックを作成できます。
また、Generic Type として定義した上記の Structure は下記のようにインスタンス化することができます。
var stackOfStrings = Stack<String>()
Generic Type の Extension
Generic Type を Extension する場合は、元のオブジェクトで定義した Type Parameter を使用することが可能です。
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }
Generic Type の extension には、WHERE
句を使用して、関数やプロパティを取得するために拡張型のインスタンスが満たさなければならない要件を含めることもできます。(こちらについては記事の後半で触れます。)
Type Constrains
Generic function および Generic type で使用できる型に特定の Type constrains(型制約) を適用すると便利な場合があります。Type Constraints は、Type Prameter が特定のクラスから継承するか、特定のプロトコルまたはプロトコル構成に準拠する必要があることなどを指定することができます。
例: Swift's Dictionary の Key は hashable である必要がある。つまり、Key を一意に表現できるようにするために、Dictionary には、特定の Key の値がすでに含まれているかどうかを確認できるように、Keyが hashable である必要があります。
この要件は、Dictionary の Key Type に対する Type Constraints によって適用されています。これは、Key Type が Swift Standard Library で定義されている、Hashable プロトコルに準拠する必要があることを指定します。Swift の全ての basic types (String, Int, Double, Boolなど) は、デフォルトで hashable です。
Type Constrains の構文
Type Parameter の一部として、コロンで区切られた Type Parameter の名前の後に単一のクラスまたはプロトコル制約を配置することにより、型の制約を記述することができます。
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
Associated Types
プロトコルを定義する時に、プロトコルの定義の一部として Associate Types を宣言すると便利な場合があります。これらの Associated Types に使用される型は、プロトコルが採用されるまで指定されません。
Associated Types in Action
下記が、Protocol に Associated Type を設けた場合の例になります。
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Container
プロトコルに準拠するように適合された、Generic Type の例が下記になります。
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] } }
typealias Item = Int
により、Item の抽象型を Int の具象型に変換します。また、Swift には型推論があるので、Item の具体的な値を宣言する必要はなく、それぞれのメソッドにプロパティに独自の型を定義することでそれらが解決します。
Generic Type を 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] } }
また、今回定義した Container
での複数の機能は、Swift の Array ではすでに存在するプロパティや関数が含まれているので、下記のように Array を拡張して Container
プロトコルに準拠させるだけで、Swift が Array の Element を Associated Type として推測するので、空の状態で、Container
Type として Array を扱えるようになります。
extension Array: Container {}
Associated Type への制約の追加
下記のように、プロトコル内の Associated Type に型制約を追加して、適合型がそれらの制約を満たすことを要求することができます。
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Associated Type の制約でプロトコルを使用する
プロトコルは、それ自体の要件の一部として表示することができます。下記の例は、Container
プロトコルに suffix(_:)
メソッドの要件を追加するプロトコルです。
protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
このプロトコルでは、Suffix
は、Associated Type です。Suffix
に2つの制約があります。
SuffixableContainer
プロトコルに準拠している- Item Type は、
Container
の Item Type と同じである必要があります。
Stack
に SuffixableContainer
プロトコルへの適合性を追加した例は下記のようになります。
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
Generic Where
下記のように、where句を定義して、Associated Type が特定のプロトコルに準拠する必要があることや、または、特定の Type Prameter に関連づけられた型が同じである必要があることなどを要求することができます。
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 }
Generic Where句を使用した Extension
extension の一部の機能として、Generic where句を使用することもできます。下記の例では、以前に定義した Stack の Type Prameter である Element が Equatable に準拠している場合にのみ使用できる isTop(_:)
関数を定義しています。これにより、Elemnt が Equatable であることが担保されるので、==
operator を問題なく使用することができるようになります。
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }
これらの Equtable に準拠していないオブジェクトを Element として使用した場合、isTop(_:)
にアクセスした場合にコンパイルエラーが発生します。
struct NotEquatable { } var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) notEquatableStack.isTop(notEquatableValue) // Error
また、Protocol の Extension としても Generic の Where句を使用できます。
extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } }
上記の例では、Item が特定のプロトコルに準拠している必要があることを表しましたが、Item が特定のタイプである必要がある場合には Generic 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"
コンテキスト Where句
下記のように Generic Type の Extension のメソッドに Generic 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"
Generic Where句 を使用した Associated Types
下記のように、Associated Type に generic where句を含めることができます。
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 }
別のプロトコルから継承するプロトコルの場合、プロトコル宣言に generic where句 を含めることにより、継承された Associated Type に制約を追加することができます。
protocol ComparableContainer: Container where Item: Comparable { }
Generic Subscripts
subscript は generic にすることができ、generic 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 } }
これにより、indices パラメータ渡される値が整数のシーケンスであることの制約を要求することができます。
参考
- https://docs.swift.org/swift-book/LanguageGuide/Generics.html
- http://austinzheng.com/2015/09/29/swift-generics-pt-2/