型引数宣言のルールと制約
ジェネリック関数の宣言方法をもう一度見てください。
func 関数名<型引数のリストと制約>(引数のリスト) -> 返り値 { (処理) }
<型引数のリストと制約>部分で宣言されている型引数には、次のような記述も行えます。
- 継承あるいは準拠しなければならないクラス・プロトコル
- 型引数同士の関係(制約)
継承あるいは準拠しなければならないクラス・プロトコル
<型引数のリストと制約>部分で次のように宣言することで、各型引数が、各型引数が特定のクラスそのものまたはサブクラスであるか、特定のプロトコルに準拠していることを要求できます。
<型引数1: (クラス名 or プロトコル名), ……, 型引数n: (クラス名 or プロトコル名)>
前節で例に出したcreateEmpty関数でも、型引数に準拠すべきプロトコルを指定しました。ここでは、別の例で確認してみましょう。
func notEqual<T: Equatable>(first: T, second: T) -> Bool { return first != second } notEqual(1, 2) // true
このnotEqual関数は、型引数としてEquatableプロトコルに準拠したものだけを扱うように要求しています。ここでEquatableに準拠しないクラスを別に定義しても、そのクラスのインスタンスを次の例のようにnotEqual関数の引数に渡すことはできません。
class Food {} notEqual(Food(), Food()) // コンパイルエラー
Equatableプロトコルは等値演算子(==)の実装を要求するプロトコルです。プロトコルに準拠した型は次の2項等値演算子を実装する必要があります。
func == (lhs: Self, rhs: Self) -> Bool演算子とプロトコルをともに扱う方法については演算子の章で説明します。また、プロトコルやクラスで用いるメタな型を表すSelfという記号については次回のメタタイプの章で説明します。
これは型引数の後に続く型についての要求がプロトコルではなく、クラスだった場合も同様です。特定のクラスかまたはサブクラスでない型が型引数として推論される場合には、次のようにコンパイルエラーになります。
class Apple: Food {} class Metal {} func eat<T: Food>(food: T) { } eat(Food()) eat(Apple()) eat(Metal()) // コンパイルエラー
クラスと関数を用いるケースはメソッドという代替手段があるため、実用的にこのようなコードを書くことはまれですが、プロトコルと関数については、公式のAPIでもジェネリクスを用いて引数の型に対する要求を行う関数が多く存在します。
where節での制約
型引数のリスト宣言の後ろに記述するwhere節では、次のように前に宣言された引数同士の制約を記述できます。
<型引数のリスト where 型引数に関連した型: (クラス名 or プロトコル名), 型引数に関連した型 == 型名, ...>
ここでの「型引数に関連した型」とは、具体的には以下の2つを指します。
- リスト部で宣言された型引数そのもの
- リスト部で宣言されたプロトコル準拠型引数の付属型
protocol Readable { typealias IndexType typealias ContentType var index: IndexType { get } var content: ContentType { get } } struct Page: Readable { var index: Int var content: String } func readContentsFromTop<R: Readable where R.IndexType: Comparable>(var readables: [R]) { sort(&readables) { lReadable, rReadable in lReadable.index < rReadable.index } for readable in readables { println("read \(readable.content)") } } let pages = [Page(index: 6, content: "How to cook"), Page(index: 0, content: "Title")] readContentsFromTop(pages) //read Title //read How to cook
readContentsFromTop関数は、引数で渡された配列の中身をindexの昇順に出力していきます。Readableプロトコルに準拠した型Rの配列の変数を、引数としています(変数を引数とする関数の宣言方法は本連載の第2回を参照してください)。
Readableプロトコルに準拠した型引数Rの付属型には、where節でさらに次の制約が加えられています。
R: Readable where R.IndexType: Comparable
これはRがReadableプロトコルに準拠しており、さらにその付属型IndexTypeはComparableプロトコルに準拠することを示しています。
Comparableプロトコルは比較演算子(<)の実装を要求するプロトコルです。プロトコルに準拠した型は次の2項比較演算子を実装する必要があります。
func < (lhs: Self, rhs: Self) -> Bool
readContentsFromTop関数の内部では、次のsort関数が使われています
func sort<T>(inout array: [T], isOrderedBefore: (T, T) -> Bool)
この関数は第1引数にT型の配列をとり、第2引数に昇順降順を定めるクロージャをとります。実行されると、第1引数に渡された配列の中身が、第2引数に定められた昇順/降順に応じてソートされます(inoutキーワードは本連載の第2回を参照してください)。
ここでは第2引数として末尾クロージャを用いていますが、R.IndexType型がComparableプロトコルに準拠しているという制約があるため、その中で次のように比較演算子を使用できています。
{ lReadable, rReadable in lReadable.index < rReadable.index }
さらに、先ほどの例に加える形で次のように実装してみましょう。
func equal<R1: Readable, R2: Readable where R1.IndexType == R2.IndexType, R1.ContentType == R2.ContentType, R1.IndexType: Equatable, R1.ContentType: Equatable>(lhs: R1, rhs: R2) -> Bool { return lhs.index == rhs.index && lhs.content == rhs.content } struct Paper: Readable { var title: String var index: Int var content: String } let page = Page(index: 1, content: "Thanks to wife") let paper = Paper(title: "On mathemetics", index: 1, content: "Thanks to wife") equal(page, paper) // true
このequal関数は、Readableプロトコルに準拠した型を持つ2つの引数の等値判定を行います。型引数の部分はご覧のとおり、ずいぶん複雑です。
<R1: Readable, R2: Readable where R1.IndexType == R2.IndexType, R1.ContentType == R2.ContentType, R1.IndexType: Equatable, R1.ContentType: Equatable>
この宣言では、型引数へ次のような要請がなされています。
まず、型引数のリストに宣言されたR1、R2の型引数はともにReadableプロトコルに準拠したものでなければなりません。さらに、where節でReadableプロトコルに準拠した型引数の付属型については、次の3つが要請されています。
- R1.IndexTypeとR2.IndexTypeが同じ型であること
- R1.ContentTypeとR2.ContentTypeが同じ型であること
- R1.IndexTypeと R1.ContentTypeがEquatableプロトコルに準拠していること
結果的に、R2.IndexTypeとR2.ContentTypeがEquatableプロトコルに準拠していることも型推論によって導かれるため、関数の宣言内部での双方の引数のindexプロパティ、contentプロパティの型については等値演算子==を使ってReadableプロトコルとしての等値性を判定できることが分かります。