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

Codable Tech Blog

CodableがiOSプログラミング、クライアントサイド(JavaScript)・サーバーサイド(Java,C#,PHP)プログラミング、その他技術(MySQL、Linuxなど)について発信しています

MENU

Swift ジェネリックス(Generics)

ジェネリックスとは型に束縛されず、型自体をパラメータ化して扱うことです。ジェネリックスを使用しない場合の問題を確認することでジェネリックスを使用する有用性を確認してみましょう。

既存コードの問題点

Swiftのように型付けを行う言語では、同じ問題を解決するために異なる型の関数やメソッドを記述する必要があります。例えば、受け取ったパラメータをそのまま返却する単純な関数を定義する場合を考えてみましょう。受け取るパラメータの型として整数型と実数型、いずれの型でも受け取れるようにしたい場合、次のようになります。

上記の2つの関数で実行されている処理は同じであるにもかかわらず、型が異なるために別々の関数を用意しなければなりません。このように関数で実行する処理は同じなのに型が異なることによって重複した処理がいくつも書かれてしまう問題を解消するのがジェネリックスです。

ジェネリックス

ジェネリックス関数

ジェネリックスは関数やメソッドに適用することができます。関数およびメソッドに対してジェネリックスを適用したい場合、次のような構文で利用できます。

func 関数名<プレースホルダ名1,プレースホルダ名2,..>(パラメータ:プレースホルダ名,...)

ジェネリックスは型自体をパラメータとして扱いますので、実際の型の代わりにプレースホルダーを配置します。関数名のあとに配置された「<プレースホルダー名>」によって、この関数で利用するプレースホルダ名が決定します。この関数名の後に配置された「<プレースホルダー名>」はタイプパラメータと呼びます。タイプパラメータは関数を呼び出す時に渡す値により、自動で決定されます。また、タイプパラメータは関数のパラメータだけではく、戻り値の型や関数内のローカル変数の型として利用することも可能です。さらに、カンマで区切ることで複数のタイプパラメータを定義することもできます。
次の例は先ほどの受け取ったパラメータをそのまま返却する関数をジェネリックスを使用して定義した例です。

関数名returNumの後に記述されているがタイプパラメータになります。タイプパラメータの型は呼び出し側がどんな型の値を渡すかによって決定します。例えば、変数num1はInt型の値です。この変数をパラメータとしてreturnNum関数を呼び出した場合、タイプパラメータTはInt型になります。また、変数num2はDouble型の値です。この変数をパラメータとしてreturnNum関数を呼び出した場合、タイプパラメータTはDouble型になります。このようにジェネリックスを利用することで2つの関数を定義しなければいけなかったところが一つの関数を定義するだけで済むようになります。

補足)タイプパラメータの名前付けについて

タイプパラメータには任意の名前をつけて構いません。しかし、慣例としてタイプパラメータが一つしかない場合は「T」がよく利用されます。

ジェネリックス型

クラスや構造体・列挙体を定義する時にジェネリックスを利用することができます。ジェネリックスを利用して定義された型はジェネリックス型と呼ばれます。次の構文はクラスをジェネリックス型として定義したい場合に用います。

class クラス名<タイプパラメータ> {
    //クラス定義
}

次の例はSampleGenericsクラス定義時にジェネリックスを採用しています。

SampleGenericsクラスをインスタンス化する文に注目してください。通常のインスタンス化とは異なり、次のような形でインスタンス化を行っています。

・・・ = クラス名<型>()

インスタンス化を行う時に指定する型によって、タイプパラメータの型が決定されます。今回の例でいれば、「<型>」の部分にInt型を指定します。これはSampleGenericsクラスのタイプパラメータTをInt型にするという意味になります。このため、SampleGenericsクラスで定義されているreturnNumメソッドを呼び出す時にはInt型のパラメータを渡す必要があります。また、returnNumメソッドを呼び出した結果、戻り値としてInt型の値が返却されます。

型制約

ジェネリックスはあらゆる型に適合することが可能です。しかし、ジェネリックスがどの型をとれるかを制約したほうがよい場面があります。例えば、メソッド呼び出しを行う場合です。タイプパラメータは実際に呼び出されるまで型が不定であるため、ある型のメソッドを呼び出そうと思った場合、キャスト指定してメソッドの呼び出し処理を記述する必要があります。

キャストを利用した場合、2つの問題が発生します。1つはキャスト処理を行う手間が発生してしまうこと。2つめは渡された値がキャストで指定した型でなければ実行時エラーが発生することです。そもそも、ジェネリックスの目的は型をパラメータ化することで重複した記述をなくすでした。キャストを利用した途端、ジェネリックスのメリットが失わされてしまいます。この問題は型制約を行うことで解消できます。型制約を行うことでキャストなしでメソッドの呼び出しを行うことができるようになります。型制約ではあるクラスを継承しているとか、あるプロトコルに適合しているかとかを制約することができます。

型制約の構文と使い方

型制約は<プレースホルダー名 : 制約>の形で記述できます。

func 関数名<T: クラス名, U: プロトコル名>(パラメータ1: T, パラメータ2: U) {
    //関数定義
}

上記の記述でタイプパラメータTは:の後ろに配置されたクラスのサブクラスであること、タイプパラメータUは:の後ろに配置されたプロトコルに適合していることを制約されます。
次の例ではSampleGenericsクラスにaddNumメソッドを追加しています。

この場合、タイプパラメータTの型が+メソッドを実装している保証がないため、コンパイルエラーが発生します。このコンパイルエラーを解消するためには次の手順を踏む必要があります。

  1. +メソッドの実装を要求するプロトコルの定義
  2. 拡張を利用して手順1で定義したプロトコルに既存の型を適合させる
  3. 型制約で手順1のプロトコルに適合した型だけを指定できるようにする

まず、手順1のプロトコルの定義を行います。ここではAddプロトコルを新規に定義します。Addプロトコルは+メソッドの実装を要求するプロトコルです。パラメータと戻り値の型として定義しているSelfはselfプロパティの型を表現する特別な型です。例えば、Int型がAddプロパティに適合した場合、SelfはIntになります。

次にAddプロトコルに適合させたい型を拡張します。今回はInt型、Float型、Double型を拡張します。Int型、Float型、Double型は既に+メソッドが実装されているため、単純にAddプロトコルに適合することだけを宣言するだけです。

最後に型制約を使用してタイプパラメータTはAddプロトコルに適合した型でなければいけないという制約を設けます。こうすることによって、ジェネリックスを利用した汎用的なメソッド定義が実現できます。

補足 拡張(イクステンション)とタイプパラメータ

オリジナルのクラスで定義されているタイプパラメータは拡張(イクステンション)の中でも利用することが可能です。しかし、拡張(イクステンション)を定義する時にに新しいタイプパラメータを定義することはできませんので注意してください。

連想型とジェネリックス

Swiftでは、プロトコル定義のタイミングではプロパティやメソッドのパラメータの型を不定にしておいて、プロトコルを適合する時に型を決定することが可能です。プロトコル定義時に用いるここの不確定な型のことを連想型と呼びます。連想型はtypealiasキーワードで指定できます。

protocol プロトコル名 {
    typealias 連想型名
    //以降、プロトコルの定義を記述
}

連想型を使用しているプロトコルに適合する構造体やクラスは、連想型の型を決定する必要があります。連想型は構造体やクラス定義の中で次のように記述することでを型を決定できます。

typealias 連想型名 = 型名

たとえば、連想型をInt型にしたい場合は「typealias 連想型名 = Int」となります。
次の例では連想型を使用したCountプロトコルを定義しています。ついで、Countプロトコルに適合したStudentクラスを定義しています。

Countプロトコルではtypealiseキーワードを利用して連想型であるCountTypeを定義しています。このため、CountTypeはCountプロトコルに適合する構造体やクラスを定義する時に実際の型が決定します。Student型はCountプロトコルに適合したクラスです。このクラスではtypealiasキーワードを利用して、CountTypeの型をInt型に決定しています。このため、コンピューティッドプロパティcountのゲッターはInt型の値を返却するように定義してあげる必要があります。ただ、Swiftには型推論があるおかげで「typealias 連想型名 = 型名」を指定しなくても問題なく動作します。仮に、typealiasの行を削除しても問題なく動作します。

連想型をもったプロトコルをジェネリックス型のクラスに適合させることも可能です。しかも、ジェネリックス型のクラスを連想型をもったプロトコルに適合させるとき、タイプパラメータを連想型の型として指定することが可能です。このようにした場合、タイプパラメータと同じ型が連想型に適用されます。次の例では連想型の部分にタイプパラメータTを指定しています。今回の例ではあればタイプパラメータにIntを指定していますので、連想型もIntになります。

Where句

ジェネリックス型を定義する時、型制約を使うことでジェネリックス型がとりうる型に対して制約を設けることが出来ました。この型制約では連想型に対して制約を課すことも可能です。連想型に対して制約を課すには型制約の中でwhere句を使います。

<パラメータ:型名 where 連想型に対する条件>

where句は連想型がどんなプロトコルに適合しているべきか、どの型と同じであるべきかなどを定義することできます。次の例ではジェネリックスをりようしてisEqual関数を定義しています。isEqual関数は2つのパラメータの値を比較する関数です。isEqual関数のタイプパラメータT1とT2はwhere句を伴った型制約で宣言されています。

この例ではタイプパラメータT1とT2はCountプロトコルに適合している必要があります。さらにwhere句で「T1.CountType == T2.CountType」と記述されています。これはT1とT2の連想型CountTypeが同じ型でなければいけないということを意味しています。また、Equatableプロトコルに適合していないとパラメータnum1とnum2を「==」で比較できないため、タイプパラメータT1の連想型CountTypeはEquatableプロトコルに適合していなければいけないという指定を「T1.CountType:Equatable」で行っています。
このように連想型に対する指定をwhere句で行うことが可能になっています。