ジェネリクス

ジェネリクスは、Swiftの最も強力な特徴の1つです。ジェネリクスを使うと、あらゆる型に対して柔軟で再利用可能な処理や型を記述することができます。
Swift自体でもジェネリクスは多用されており、例えば配列やディクショナリが型に依存せず同じように値を格納したり取り出したりできるのも、ジェネリクスを使って実装されているからです。

 

ジェネリクス関数

ここでは例として、Int型の値の大きい方を返す関数を考えてみます。


// Int型の大きい方を返す
func bigger(val1: Int, val2: Int) -> Int {
    return val1 > val2 ? val1 : val2
}

println(bigger(10, 20))     // 20

この関数は、Int型の引数に対して大きい方の値を返しますが、Float型やDouble型の引数を渡す様にするとコンパイルエラーになります。


var val1: Float = 10.0
var val2: Float = 20.0

println(bigger(val1, val2)) // エラー

次の様にFloat型やDouble型用にbigger関数のオーバーロードバージョンを作成する方法もありますが、


// Float型の大きい方を返す
func bigger(val1: Float, val2: Float) -> Float {
    return val1 > val2 ? val1 : val2
}

// Double型の大きい方を返す
func bigger(val1: Double, val2: Double) -> Double {
    return val1 > val2 ? val1 : val2
}

ジェネリックスを使うと1回の記述であらゆる型に対応することができます。


func bigger<T>(val1: T, val2: T) -> T {
    return val1 > val2 ? val1 : val2
}

関数名の後に、<>で囲って、型を示すTという名前を指定しています。この名前はTでなくても分かり易い名前で構いませんが、慣例としてTが使われることが多いです。また、他の名前をつける場合でも大文字で始まり単語の区切りを大文字にするCamelCase(キャメルケース)にすることが推奨されています。

上の例では型につけた名前(T)を引数の型として使用しています。また、この関数の場合は戻り値として同じ型を返すので、戻り値にもTを使っています。

この様にすると、Int型だけでなく、Float型や、Double型に対しても、同じロジックを共用することができますが、残念ながらこの場合、コンパイルエラーになります。何故なら、関数の中で値の比較をしていますが、全ての型で値の比較ができるとは限らないからです。例えば、独自に定義したクラスや構造体は、大小比較用のオペーレータ関数を定義しない限り、値の比較はできません。そのため、ジェネリクスで使用する型に、比較が可能なことを明示する必要があります。

比較が可能なことを示すためには、そのためのプロトコルやスーパークラスを定義して、型に指定してやれば良いです。Swiftには値の比較が可能なことを示すプロトコルとして、Comparableというものが予め定義されているので、これを使うことができます。


func bigger<T: Comparable>(val1: T, val2: T) -> T {
    return val1 > val2 ? val1 : val2
}

関数名の後に指定している型名に:(コロン)をつけてComparableプロトコルを指定しています。このように型名にプロトコルやスーパークラスを指定して、その型がある条件に準拠していることを示すことができます。

これでコンパイルエラーもなくなり、Comparableプロトコルに適合する全ての型で同じロジックを使うことができるようになりました。Int型、Float型、Double型は全てComparableプロトコルに適合しています。


var i1 = 10
var i2 = 20
print(bigger(i1, i2)) // 20

var f1: Float = 10.0
var f2: Float = 20.0
print(bigger(f1, f2)) // 20.0

var d1: Double = 10.0
var d2: Double = 20.0
print(bigger(f1, f2)) // 20.0

ちなみに、String型もComparableプロトコルに適合しているので、String型の引数でも動作します。


var s1: String = "Mickey"
var s2: String = "Minnie"
print(bigger(s1, s2)) // Minnie
Swiftには大きい方の値を返すジェネリクス関数としてmaxという関数が既に定義されています。このmaxは複数個の引数にも対応しています。

print(max(30, 50, 20, 10, 60, 40))    // 60

また、小さい方の値を返す関数としてminも定義されています。


print(min(30, 50, 20, 10, 60, 40))    // 10

ジェネリクス型

次はジェネリクスを使った型についてみていきます。

まず、ジェネリクスを使わずに、Setという名前の、値の重複を許可しない配列を定義してみます。ここではString型のみに対応した構造体にします。


// 重複要素を許可しない配列
struct Set {
    var items = [String]()
    // 要素数
    var count: Int {
        return items.count
    }
    // 要素の追加
    mutating func append(item: String) {
        if !self.contains(item) {
            items.append(item)
        }
    }
    // 要素の削除
    mutating func remove(item: String) {
        if let idx = find(items, item) {
            items.removeAtIndex(idx)
        }
    }
    // 要素の存在確認
    func contains(item: String) -> Bool {
        return Swift.contains(items, item)
    }
}

var party = Set()
party.append("ルフィ")
party.append("ゾロ")
party.append("ナミ")
party.append("ゾロ")    // 2回目×
print(party.count)   // 3

このSet構造体もジェネリクスを使ってあらゆる型に対応させることができます。


// 重複要素を許可しない配列
struct Set<T: Equatable> {
    var items = [T]()
    // 要素数
    var count: Int {
        return items.count
    }
    // 要素の追加
    mutating func append(item: T) {
        if !self.contains(item) {
            items.append(item)
        }
    }
    // 要素の削除
    mutating func remove(item: T) {
        if let idx = find(items, item) {
            items.removeAtIndex(idx)
        }
    }
    // 要素の存在確認
    func contains(item: T) -> Bool {
        return Swift.contains(items, item)
    }
}

var party = Set<String>()
party.append("ルフィ")
party.append("ゾロ")
party.append("ナミ")

var numbers = Set<Int>()
numbers.append(10)
numbers.append(20)
print(numbers.contains(10))   // true

型でジェネリクスを使う場合は、上の様に型名の後に、<>で囲んで実装時に使用する仮の型名を指定します。型名の付け方や、プロトコル、スーパークラスによる条件の指定など、ジェネリクス関数と同様の書き方になります。

Set構造体のremoveメソッドで使用しているfind関数と、containsメソッドで使用しているグローバル関数のcontains関数は、共にEqutableプロトコルに適合する値を引数として要求するので、Set構造体で扱う型の条件として、Equatableプロトコルに適合することを指定しています。Equatableプロトコルは、==演算子を実装している(つまり、2つの値が同じかどうかを評価できる)ことを要求するプロトコルです。

 

関連型

上でみた様にジェネリクスを使う場合に型に対してプロトコルやスーパークラスを指定することで型に条件をつけることができます。

任意の型を扱うプロトコルを定義する場合に、その型を参照できると便利です。その様な場合は関連型(Associated Types)を使うことができます。

ここでは2つのインデックスを使って2次元の配列に任意の型を格納できるボードケーム用のプロトコルを考えてみます。


/* ゲームボードプロトコル */
protocol GameBoard {
    typealias GamePiece             // ゲームの駒
    class var rows: Int { get }     // ボードの行数
    class var columns: Int { get }  // ボードの列数
    // ボードへの駒の設定/取得用サブスクリプト
    subscript(row: Int, column: Int) -> GamePiece? { get set }
}

typealiasは、タイプエイリアスで説明した様に既存の型の別名を定義する時に使うものですが、この様にプロトコル定義の中で具体的な型を明示せずに宣言することで関連型として使うことができます。

関連型は、プロトコルで宣言したプロバティや、メソッドの引数、戻り値として参照することができます。ここでは、サブスクリプトで使用する型として参照しています。
具体的な型はプロトコルに適合させる側で指定します。次の例ではGameBoardプロトコルに適合したオセロゲーム用のボード構造体を定義しています。


/* オセロゲームのボード */
struct OthelloBoard: GameBoard {
    // オセロの駒
    enum OthelloPiece {
        case Black, White
    }
    static var rows: Int { return 8 }     // ボードの行数
    static var columns: Int { return 8 }  // ボードの列数
    // オセロの駒をゲームの駒として再定義
    typealias GamePiece = OthelloPiece
    // マス目を表す配列
    var board = [OthelloPiece?](count: 8 * 8, repeatedValue: nil)
    // ボードへの駒の設定/取得用サブスクリプト
    subscript(row: Int, column: Int) -> OthelloPiece? {
        get {
            return board[row * 8 + column]
        }
        set {
            board[row * 8 + column] = newValue
        }
    }
}

var board = OthelloBoard()
board[3, 3] = .Black
board[3, 4] = .White

この構造体では、オセロの駒としてネストした列挙型で定義し、GamePieceとして再定義しています。サプスクリプトも定義しプロトコルの要件を満たしています。但し、上のtypealiasによる再定義は、無くても実装されているメソッドからSwiftが推論してくれます。

上の場合は、オセロの駒を列挙型を使って定義していますが、次の様に簡便にBool型を使っても問題有りません。


/* オセロゲームのボード
   オセロの駒としてBool型を使用
*/
struct OthelloBoard: GameBoard {
    static var rows: Int { return 8 }     // ボードの行数
    static var columns: Int { return 8 }  // ボードの列数
    // マス目を表す配列
    var board = [Bool?](count: 8 * 8, repeatedValue: nil)
    // ボードへの駒の設定/取得用サブスクリプト
    subscript(row: Int, column: Int) -> Bool? {
        get {
            return board[row * 8 + column]
        }
        set {
            board[row * 8 + column] = newValue
        }
    }
}

var board = OthelloBoard()
board[3, 3] = true
board[3, 4] = false

この場合も、GamePieceはBool型であるということをSwiftが推論してくれるので、typealiasによる再定義は省略しています。

そしてジェネリクスを使うと、駒の型を自由に決められるようになります。


/* オセロゲームのボード
   オセロの駒の型をジェネリクスにより汎用的にしたバージョン
*/
struct OthelloBoard<T>: GameBoard {
    static var rows: Int { return 8 }     // ボードの行数
    static var columns: Int { return 8 }  // ボードの列数
    // マス目を表す配列
    var board = [T?](count: 8 * 8, repeatedValue: nil)
    // ボードへの駒の設定/取得用サブスクリプト
    subscript(row: Int, column: Int) -> T? {
        get {
            return board[row * 8 + column]
        }
        set {
            board[row * 8 + column] = newValue
        }
    }
}

var board = OthelloBoard<String>()
board[3, 3] = "黒"
board[3, 4] = "白"

 

where節

ジェネリクスの型にプロトコルやスーパークラスを指定することに加えて、whereを使ったより細かい条件の指定ができます。

2つのゲームボードの駒の配置が同じかどうかを検証する場合、次の様に記述できます。


// 2つのゲームボードが同じ駒の配置か調べる関数
func isSameBoard<B1: GameBoard, B2: GameBoard
  where B1.GamePiece == B2.GamePiece, B1.GamePiece: Equatable>
    (someBoard: B1, anotherBoard: B2) -> Bool {
        if (someBoard.dynamicType.rows != anotherBoard.dynamicType.rows) ||
            (someBoard.dynamicType.columns != anotherBoard.dynamicType.columns) {
                return false
        }
        for var row = 0; row < someBoard.dynamicType.rows; row++ {
            for var col = 0; col < someBoard.dynamicType.columns; col++ {
                if someBoard[row, col] != anotherBoard[row, col] {
                    return false
                }
            }
        }
        return true
}

var boardA = OthelloBoard<Bool>()
boardA[3, 3] = true; boardA[3, 4] = false; boardA[4, 3] = false; boardA[4, 4] = true;

var boardB = OthelloBoard<Bool>()
boardB[3, 3] = true; boardB[3, 4] = false; boardB[4, 3] = false; boardB[4, 4] = true;

print(isSameBoard(boardA, boardB))    // true

この関数では、ジェネリクスによる型の条件をwhereを使って次の様に指定しています。

  1. 引数の2つの型に対して、GameBoardプロトコルの関連型であるGamePieceが同じ型であること。
  2. 最初の引数のGameBoardのGamePieceがEquatableプロトコルに適合すること。(1.の条件により、必然的に2番目の引数のGameBoardのGamePieceもEquatableプロトコルに適合することになります。)

この指定により、2つのGmaeBoardのGamePieceがどんな型であれ、関数の実装の中で=演算子や!=演算子を使って比較できることが保証されます。

上の例では、まず2つのボードの行と列が同じ数かどうか調べて、同じだった場合、全てのマス目の駒の値が等しいか調べています。行と列が同じで駒も全て同じだった場合にtrueを返し、それ以外はfalseを返しています。