読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(17) - Generics

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Genericsジェネリクス

Swiftではジェネリクスがサポートされています。まぁコレクションの話をしたときに既に触れているので今更って感じですね。

ジェネリクスのメリットなんて話は案の定しません。文法だけ見ていきます。

The Problem That Generics Solve

と思っていたらいきなりジェネリクスのメリットの話ですね。一応後の話に続いてくるのでさっと説明しましょう。

このような関数を作成します。aとbの値を入れ替える関数です。

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"

この関数の引数はIntで宣言されてしまっているので、他の型で同じことをしたかったらそれぞれ別の関数を作成する必要があります。

func swapTwoStrings(inout a: String, inout b: String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(inout a: Double, inout b: Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Generic Functions

いちいちこんなことをするのは面倒なので、受け取る引数の型をパラメータ化してしまいましょう。

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"

Type Parameters

とまぁ関数名の後に<Type>とすることで型パラメータを設定できます。複数の型パラメータを渡したい時は<T1, T2>のようにすればOKです。

Naming Type Parameters

型パラメータの名前はわかりやすくUpperCamelCaseで書け、と仰られています。

例としてはDictionaryの<KeyType, ValueType>を参考にしろとのことです。

Generic Types

試しにStackと言う構造体を作ってみましょう。

struct Stack<T> {
    var items = T[]()

    mutating func push(item: T) {
        items.append(item)
    }

    mutating func pop() -> T {
        return items.removeLast()
    }
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

何も説明することがないですね。

Type Constraints

ジェネリクスの制約のお話です。

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

上記の例であればTはSomeClass or SomeClassの子クラスのみを受け取ります。UはSomeProtocol or SomeProtocolに適合している型を受け取ります。

例えばこんな関数があったとします。

引数で受け取ったarrayをループさせ、valueToFindと一致するものがあったらその位置と値のタプルを返します。

func findIndex<T>(array: T[], valueToFind: T) -> Int? {
    for (index, value) in enumerate(array) {
        if value == valueToFind {
            return index
        }
    }

    return nil
}

実はこのコード、コンパイルが通りません。「if value == valueToFind」と何気なくやっていますが、Tが「==」(及び「!=」)と言う演算子を実装しているかどうかわからないからです。

Swiftでは確実に「==」(及び「!=」)を実装していることを示すプロトコルとしてEquatableと言うものを標準ライブラリに用意しています。

と言うわけで、Tが少なくともEquatableに適合していれば上記のコードから危険がなくなるわけです。直してみましょう。

func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? {
    for (index, value) in enumerate(array) {
        if value == valueToFind {
            return index
        }
    }

    return nil
}

このように、型パラメータであっても制約を与えれば最低限そこで保障されている機能を使うことができます。まぁ、今更って感じですが。

Associated Types

typealiasキーワードを使うことでも型パラメータを設定することができます。

例えば以下のようなプロトコルがあるとします。

protocol Container {
    typealias ItemType
    mutating func append(item: ItemType)
    var count: Int { get }
    subscript(i: Int) -> ItemType { get }
}

「typealias ItemType」により型パラメータが設定されました。Containerに適合させるには以下の機能を実装する必要があります。

  • ItemTypeを受け取る「mutating func append(item: ItemType)」
  • 素数を返す「count: Int」
  • 指定された場所(i: Int)の値を返す「subscript(i: Int) -> ItemType」

これを実際に適合させてみるとこんな感じです。

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]
    }
}

「typealias ItemType = Int」を宣言することでItemTypeはIntになりました。後はそれに合わせて各機能を実装してやればOKです。

また、typealiasで宣言されていたとしても下記のように記述することが可能です。

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]
    }
}

このようにtypealiasを用いた型パラメータのことをAssociated Typesと呼んでいるらしいです。

いまいち有用な使い方がわからないんですが、sturct Stack<T>: Container<T>と書くのが冗長すぎるとか、そう言う時に使うんですかね?

また、制約を使用できるのかどうか書いてないのでその辺はわかりません。

Where Clauses

whereを使うことで更に細かな制約を与えることができます。

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
    
}

順を追って説明しましょう。型パラメータはC1とC2の二つです。二つともContainerである、と言う制約が設定されています。(C1: Container, C2: Container)

whereの中身を見ていくと、Containerで使用されているItemTypeと言う型パラメータに制約を設けています。C1とC2のItemTypeが同じ型であること(C1.ItemType == C2.ItemType)、またItemTypeはEquatableに適合していることが、このallItemsMatchと言う関数を使用する条件になります。

C2.ItemType: Equatableをwhereに入れる必要はありません。C1.ItemType == C2.ItemTypeでなければならないのだから、C1.ItemTypeがEquatableならC2.ItemTypeもEquatableです。(コンパイラがそこまで考慮してくれるかは謎。)

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."