iBooks にある “The Swift Programming Language” の勉強メモ。Objective-C と C を普段書いている自分から、ちょっと馴染みがないものを特にまとめておきます。目次は こちら。
今回は、Generics に関して。
Generics
Generic コードとは、どんなタイプにでも対応できる柔軟で再利用性の高いコードのことです。この Generics は Swift の中でも強力な機能の 1 つです。すでに私たちも 配列や Dictionary を使用した際に、Generics を使ってきました。
1. Generics が解決する問題
まずは、Generics を導入するとどのような問題を解決することができるのか、みていきます。
1 2 3 4 5 6 7 8 9 10 11 | func swapTwoInts(inout a: Int, inout b: Int) { let temporaryA = a a = b b = temporaryA } var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anotherInt) println("someInt is now \(someInt), and anotherInt is now \(anotherInt)") // prints "someInt is now 107, and anotherInt is now 3" |
基本的な swap 関数です。in-out 引数にして、関数の呼び出し後も変更が持続されるようにしています。
この swapTwoInts の問題は、Int に対してしか使用できないことです。他のタイプにも対応したい場合、別途関数を用意する必要があります。その際、関数の中身は同じになります。この重複を解決してくれるのが、Generics です。
2. Generic 関数
まずは、上記の swap 関数を Generic に書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | func swapTwoValues<T>(inout a: T, inout b: T) { let temporaryA = a a = b b = temporaryA } var someInt = 3 var anotherInt = 107 swapTwoValues(&someInt, &anotherInt) // someInt is now 107, and anotherInt is now 3 var someString = "hello" var anotherString = "world" swapTwoValues(&someString, &anotherString) // someString is now "world", and anotherString is now "hello" |
この Generic な関数は、タイプを指定する際に、placeholder タイプ名を使用しています。(T
) この T には、どんなタイプでも入ることができます。a も b も T なので、この 2 つの引数は同じタイプであるということが分かります。この “T” というは伝統的に使われているもので、なんでも構いません。しかし、もっと複雑な Generic 関数を書く際には、KeyType
や ValueType
等、より説明的にした方がよいでしょう。この際に、タイプ名を大文字から始めタイプであるということを分かりやすくしたほうがよいです。また、関数名の後に、<T>
と置き、placeholder タイプ名をこの関数内で使用すると宣言しています。複数のタイプを指定したい場合は、<> 内でカンマ区切りで書くことができます。
関数の呼び出し方は、1 でみたタイプに依存した場合と全く同じです。
3. Generic タイプ
Generic な関数の他に、Swift は Generic なタイプを定義することもできます。この Generic なクラス・構造体・Enumeration は、Array 等と同じようにどんなタイプとも使用することができるようになります。
基本的なデータストラクチャーであるスタックを使用して、この Generic タイプをみていきます。
まずは、Generic でないスタックはこのようになります。
1 2 3 4 5 6 7 8 9 | struct IntStack { var items = Int[]() mutating func push(item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } } |
push と pop をサポートしています。構造体内の items を変更するので、mutating
キーワードが使用されています。このスタックは、Int でしか使用することができません。次に Generic なスタックをみていきます。
1 2 3 4 5 6 7 8 9 | struct Stack<T> { var items = T[]() mutating func push(item: T) { items.append(item) } mutating func pop() -> T { return items.removeLast() } } |
ここでは、タイプ名の後に、<T>
と書き、T を placeholder として使用することを宣言し、この T をタイプの定義内で使用しています。この T が、スタックを管理する items のデータタイプとして、push の引数のタイプとして、pop の返り値として使われています。
1 2 3 4 5 6 | var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings |
4. タイプ制限
今までみてきた swapTwoValues と Stack は文字通りどんなタイプでも受け入れることができました。しかし、受け入れることができるタイプに制限を加えることができたら、便利です。この制限は、あるクラスから継承されていること、あるプロトコルに準拠していることが指定できます。例えば、Swift の Dictionary のキーになれるのは、hashable なタイプだけです。つまり、Hashable プロトコルに準拠している必要があります。
1 2 3 | func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here } |
制限はこのように書くことができます。この例では、T は SomeClass を継承している必要があり、U は、SomeProtocol に準拠している必要があります。より具体的な例をみていきます。
1 2 3 4 5 6 7 8 | func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? { for (index, value) in enumerate(array) { if value == valueToFind { return index } } return nil } |
この findIndex 関数は、配列の中からある値を探し、その index を返すというものです。この <T: Equatable>
で、T は Equatable プロトコルに準拠している必要があるという制限を加えています。この制限がないと、この関数は、コンパイルエラーになります。それは、value == valueToFind
の部分が原因です。全てのタイプに対して、== / != を使用できるとは限らないからです。Equatable は、== / != で比較ができるようにすることを求めるので、この制限をクリアしていれば、問題がありません。
1 2 3 4 | let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3) // doubleIndex is an optional Int with no value, because 9.3 is not in the array let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea") // stringIndex is an optional Int containing a value of 2 |
5. Associated タイプ
プロトコルを定義する際に、プロトコルの定義内に associated タイプを使うと便利です。これは、プロトコルで使用できる placeholder (alias) 名です。プロトコルが実装される際に、この associated タイプに実際に何が入るのか決められます。例をみていきます。
1 2 3 4 5 6 | protocol Container { typealias ItemType mutating func append(item: ItemType) var count: Int { get } subscript(i: Int) -> ItemType { get } } |
この Container プロトコルは、append メソッド、count プロパティ、subscript を要求しています。しかし、この Container がどのようなタイプを保存しているのかは要求していません。この Generic なタイプをプロトコルの定義内で使用するために、itemType という associated タイプを宣言し、使用しています。先ほどの Int スタックと Generic なスタックを Container プロトコルに準拠させてみます。
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 31 32 33 34 35 36 37 38 39 40 41 42 | 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 ItemType = Int mutating func append(item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } } struct Stack<T>: Container { // original Stack<T> implementation var items = T[]() mutating func push(item: T) { items.append(item) } mutating func pop() -> T { return items.removeLast() } // conformance to the Container protocol mutating func append(item: T) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> T { return items[i] } } |
IntStack では、typealias ItemType = Int
を associated タイプが何であるか明示的に宣言していますが、これは必要はありません。
6. where ブロック
4 のタイプ制限以外にも、associated タイプに制限を加えることも必要な場合があります。これは、where
を使用して達成します。これにより、associated タイプが、あるプロトコルに準拠しているか、あるタイプの引数と associated タイプが同じであるか、制限を加えることができます。これは、実装されている場所の引数リストの中にて、宣言します。例をみていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | func allItemsMatch< C1: Container, C2: Container where C1.ItemType == C2.ItemType, C1.ItemType: Equatable> (someContainer: C1, anotherContainer: C2) -> Bool { // 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 are equivalent for i in 0..someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // all items match, so return true return true } |
Container プロトコルに準拠した 2 つの引数が、全く同じ値を内部に持っているかを確認する関数です。この allItemsMatch 関数は、引数に Generic な引数をとります。まず、<> 内で指定されているように、これらの引数は Container プロトコルに準拠している必要があります。さらに、where
の後で、Associated タイプ(.ItemType)に制限を加えています。1 つ目は (C1.ItemType == C2.ItemType
) は、2 つの Generics の要素が同じタイプであるということを指定しています。2 つ目は (C1.ItemType: Equatable
)、要素が、Equatable プロトコルに準拠している必要があることを宣言しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | extension Array: Container {} var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") var arrayOfStrings = ["uno", "dos", "tres"] if allItemsMatch(stackOfStrings, arrayOfStrings) { println("All items match.") } else { println("Not all items match.") } // prints "All items match. |
この allItemsMatch 関数は、引数として Container に準拠し、where 以降に指定した制限もクリアしていれば、引数として取ることができます。同じタイプである必要はありません。(この場合は配列と Stack)Array に Extension により、Container に準拠させることにより、スタックと配列の中身を全て比較しています。