ジェネリクスとは
さまざまなデータ型に共通のアルゴリズムや処理を型引数を用いてクラスや関数などの部品として記述し、あとでその部品を用いるときに型パラメータに対する具体的な型を決定するようなプログラミングスタイルをジェネリックプログラミングといいます。
型引数とは関数やメソッドの引数とは異なった概念で、関数の引数や返り値に対するデータ型をいったんTなどの間に合わせの文字として仮決めしておいたものです。
JavaのジェネリクスやC++のテンプレートなどではこの機能が実装されていますが、Objective-Cには言語仕様として存在しませんでした。
Swiftではジェネリックプログラミングのための仕組みとしてジェネリクスが提供されています。
ジェネリック関数
まずは、関数と型引数を一緒に扱うことで、型引数の型が使用時どのように決定されるかについて見ていきましょう。
次のように、型引数を用いて宣言された関数をジェネリック関数といいます。
func 関数名<型引数のリストと制約>(引数のリスト) -> 返り値 { (処理) }
関数名の直後に型引数のリストと制約を<>で囲って記述し、その直後に引数のリストと返り値を宣言します。型引数のリストと制約についてはもう少し込み入ったルールがありますが、後ほど確認していきます。
関数の引数から型引数を推論するジェネリック関数
ジェネリック関数の定義と挙動を確かめてみましょう。
func duplicate<T>(value: T) -> (T, T) { return (value, value) } duplicate("ninja") // ("ninja", "ninja") --(1) duplicate(2015) // (2015, 2015) --(2)
このduplicate関数は、型引数として宣言された型Tの引数をとり、(T, T)型のタプルを返却します。型Tが実際にはどんな型であるかは、duplicate関数の呼び出し時に渡された引数から推論されます。
例えば、ここでは--(1)のduplicate("ninja")の呼び出し時点で、関数の引数から型引数がString型であると推論され、それに応じた返り値の型も(String, String)型であると決定されます。
また--(2)でも、duplicate(2015)の呼び出し時点で型引数がInt型であると推論され、それに応じた返り値の型も(Int, Int)型であると決定されます。
関数の返り値から型引数を推論するジェネリック関数
先の例では、関数の引数から型引数を推論していました。今度は、関数の返り値から型引数を推論する例を見てみましょう。
protocol EmptyCreatable { init() } class Object: EmptyCreatable { required init() {} } extension String: EmptyCreatable {} func createEmpty<T: EmptyCreatable>() -> T { return T() } var str: String = createEmpty() // "" --(1) var obj: Object = createEmpty() // Object --(2)
この例では、引数なしのイニシャライザを持つ型一般をEmptyCreatableというプロトコルでくくり、そのプロトコルに既存のString型と、自分で定義したObject型を準拠させています。String型は引数なしのイニシャライザを元から持っているので、具体的な実装は必要ありません。
その上で、EmptyCreatableプロトコルに準拠した型引数を持つcreateEmpty関数を宣言し、型引数で指定された型のインスタンスを生成しています。
型引数に対して特定のプロトコルに準拠した型を使う場合には、型引数の後に「:」(コロン)を付けた後、型引数が準拠するプロトコルを記述します。これを型注釈といいます(前回解説しました)。
実際に使用している箇所を確認してみます。
--(1)の変数strについては、String型の型注釈がcreateEmpty関数の型引数の推論についての助けとなり、関数の返り値の型がString型となっています。
--(2)の変数objについても同様に、Object型の型注釈が推論の手助けとなり、関数の返り値の型をObject型としています。
これらの型注釈がない場合や、EmptyCreatable型に準拠していない型を型注釈に用いた場合には、コンパイルエラーとなります。
以上をまとめると、型引数を用いた関数では、引数や関数の返り値から、型引数の実際の型が推論(型推論)されます。型引数の実際の型を直接指定することはできず、引数や返り値を使った型推論に任せることになります。
プロトコルの付属型
プロトコルについては、typealiasキーワードを用いることで付属型も宣言できます。付属型を定義することで、プロトコルに型の情報を付加できます。
プロトコルの付属型は、ジェネリックプログラミング機能の1つとみなせます。プロトコル内では、付属型をメソッドの引数や返り値の型として使用できます。また、クラスをプロトコルに準拠させるときに具体的な付属型を定めることで、プロトコルに紐づいたメソッドの型も決定できます。
protocol プロトコル名(: プロトコル1, プロトコル2, ...) { typealias 付属型名(: クラス名 or プロトコル名, ... ) (その他プロパティ、メソッド、イニシャライザなど実装を要求するもの) …… }
typealias 付属型名の後ろには、継承するクラス名や準拠すべきプロトコルを「:」を付けて列記します。付属型が定義されたプロトコルを準拠したクラスでは、typealias宣言によって、付属型に実際の型を定める必要があります。
例を見てみましょう。
protocol EntityType { typealias IdentifierType: Hashable var identifier: Identifier { get } } struct User: EntityType { typealias IdentifierType = UInt var identifier: IdentifierType } let user = User(identifier: 1)
このEntityTypeプロトコルは、一意な識別子を表すidentifierプロパティと、識別子の型を表すIdentifierType付属型の実際の型の定義を、準拠するクラスに対して要求します。
ストラクチャUserはEntityTypeプロトコルに準拠しており、typealias宣言によって、Hashableプロトコルに準拠したUInt型を、IdentifierType付属型の実際の型として定めています。
Hashableプロトコルは、ハッシュ値をhashValueプロパティによって取得できるプロトコルです。
また、UserのIdentifierType付属型の実際の型は、Userのidentifierプロパティの型宣言から推論できます。そのため、Userの宣言は次のように書くこともできます。
struct User: EntityType { var identifier: UInt }
なお、プロトコルの付属型には、プロトコル名.付属型名という記述でアクセスできます。
EntityType.Identifier