【Swift】ジェネリクスの使い方。メソッドで使うデータの型に幅を持たせる。(Swift 2.1、XCode 7.2)
ジェネリクスとは
ジェネリクスとは、プロパティやメソッドで使用される型が実際に使う段階で決定される機能である。これにより、型の制限の幅を広くできる。
ジェネリクスを利用してコードを書くことをジェネリックプログラミングという。ジェネリクス(generics)とジェネリック(generic)、読み方が微妙に違うが同じことをいっている。
以下のコードはInt型の引数を1つ受け取って、それをキーと値にして辞書に格納して返す普通の関数の例である。この関数をStringの引数にも対応させたいときどうするか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* ** Int型の引数をキーと値にして格納した辞書を返す関数 */ func makeZisho(data:Int) -> [Int:Int] { //辞書を作成 let zisho = [data:data] return zisho } print(makeZisho(55)) //実行結果 //[55: 55] |
普通に考えると、以下のコードのようにIntをStringに置換したもう1つの関数を作る。そして、Double型やChar型のものも必要になると似たようなコードが沢山出来上がってしまう。そんなときに利用できるのがジェネリクスである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* ** String型の引数をキーと値にして格納した辞書を返す関数 */ func makeZishoString(data:String) -> [String:String] { //辞書を作成 let zisho = [data:data] return zisho } print(makeZishoString("こんばんは")) //実行結果 //["こんばんは": "こんばんは"] |
ジェネリックメソッドの定義方法
ジェネリックなメソッドは以下のように定義する。メソッド名の横に記述されている<T>が型パラメータと言われるもので、「このメソッドでTという型宣言を使います。」ということを意思表示したことになる。
そして、メソッド内にTという型宣言があると、メソッドが呼び出されるときにシステムが自動で型を判断してTを適切な型に置き換えて処理してくれる。なお、Tはすべて同一の型を意味しているので、型を混在させることは出来ない。
なお、Tが広く使われているためにTと記述しているだけで、規約に沿って好きな文字列に変更してもいい。
1 2 3 |
func メソッド名<T>(引数名:型, ..) -> 戻り値の型 { 処理 } |
先ほどの辞書を作る関数をジェネリックな関数にすると以下のコードになる。「T型の変数を受け取り、T型のキーと値を持つ辞書を返すメソッド」であることを表している。例えば、このメソッドにInt型の変数を与えると、「Int型の変数を受け取り、Int型のキーと値を持つ辞書を返すメソッド」に様変わりするというイメージだ。
このメソッドにInt、String、Double、Bool、どんな型を引数に与えても動作する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* ** 引数をキーと値にして格納した辞書を返すジェネリックな関数 */ func makeZisho<T>(data:T) -> [T:T] { //辞書を作成 let zisho:Dictionary<T,T> = [data:data] return zisho } print(makeZisho(777)) print(makeZisho("こんばんは")) print(makeZisho(1.24)) print(makeZisho(true)) //実行結果 //[777: 777] //["こんばんは": "こんばんは"] //[1.24: 1.24] //[true: true] |
型パラメーターにプロトコルを指定する
上記のコードはどのような型でも引数に与えることができたが、そうできないコードがある。
例えば、以下のコードは2つの引数を比較し、「小さい方をキー、大きい方を値」で辞書に格納して返す関数であるがコンパイルエラーが発生する。
なぜなら、Int型やDouble型など大きさを比較できる型が引数として渡ってきた場合は動くが、もし、比較できない型が渡ってきた場合は動きようがないためである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* ** 2つの引数の小さい方をキー、大きい方を値として辞書に格納して返す関数 */ func makeZisho<T>(data1:T, data2:T) -> [T:T] { //辞書を作成 var zisho:Dictionary<T,T> = [:] if(data1 < data2) { zisho = [data1:data2] } else { zisho = [data2:data1] } return zisho } //コンパイルエラー //error: binary operator '<' cannot be applied to two 'T' operands |
そんなときは<T:プロトコル名>のように定義すれば、型に適用されているプロトコルを指定することができる。
比較可能を表すプロトコルといえばComparable。以下のコードは型パラメータに<T:Comparable>を指定したものである。「T型はComparableプロトコルを適用したものでなければならない。」ことを表している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* ** 2つの引数の小さい方をキー、大きい方を値として辞書に格納して返す関数 */ func makeZisho<T:Comparable>(data1:T, data2:T) -> [T:T] { //辞書を作成 var zisho:Dictionary<T,T> = [:] if(data1 < data2) { zisho = [data1:data2] } else { zisho = [data2:data1] } return zisho } print(makeZisho(777, data2:555)) print(makeZisho("こんばんは", data2:"おはよう")) //実行結果 //[555: 777] //["おはよう": "こんばんは"] |
型パラメーターの複数定義
型パラメータは何種類でも定義できる。以下のコードはTとUの2種類の型パラメータを定義した例。第1引数と第2引数は大きさを比較できる値(Comparable)、第3引数はDoubleやFloatなどの浮動小数の値(FloatingPointType)を渡す関数である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* ** 2種類の型パラメータを渡すジェネリック関数 */ func makeZisho<T:Comparable, U:FloatingPointType>(data1:T, data2:T, data3:U) -> [T:U] { //辞書を作成 var zisho:Dictionary<T,U> = [:] if( data1 > data2) { zisho = [data1:data3] } else { zisho = [data2:data3] } return zisho } print(makeZisho(777, data2:555, data3:3.16)) print(makeZisho("こんばんは", data2:"おはよう", data3:12.77)) //実行結果 //[777: 3.16] //["こんばんは": 12.77] |
型パラメーターにクラスを指定する
<T:クラス名>のように定義すれば、指定したクラス、または、そのサブクラスに限定できる。以下のコードは商品クラス、または、商品クラスのサブクラスのインスタンスを受け取るジェネリックメソッドの例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* ** 商品クラス、または、商品クラスのサブクラスのインスタンスを受け取るメソッド。 */ //商品クラス class Shouhin { var price = 900 func addPrice(price:Int){ self.price += price } } //本クラス(商品クラスのサブクラス) class Book:Shouhin { var page = 350 } //値上げメソッド(ジェネリック) func addPrice500<T:Shouhin>(data:T) -> T { data.addPrice(500) return data } //本クラスのインスタンスを生成し、値上げメソッドを呼び出す。 var test = Book() addPrice500(test) print(test.price) //実行結果 //1400 |
クラス、プロトコルを作るときにジェネリクスを利用する
クラスやプロトコルを作るときにもジェネリクスを利用できる。
以下のコードは商品クラスの内部変数codeを型パラメータで扱うようにした例。第1引数にどんな型を指定しても商品クラスのインスタンスを生成することができる。便利だ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/* ** ジェネリクスを利用して商品クラスを定義する。 */ //商品クラス class Shouhin<T> { var code:T var name:String //イニシャライザ init(code:T, name:String){ self.code = code self.name = name } } //第1引数にはどんな型でも指定できる。 var test = Shouhin(code:1459, name:"楽しい料理") print(test.code) var test2 = Shouhin(code:"NO_13590", name:"楽しい料理") print(test2.code) //実行結果 //1459 //NO_13590 |
以下のコードはジェネリクスを利用したプロトコル定義の例。プロトコル名の横に<T>を記述するのでは無く、定義内部で「typealias 型パラメータ名」のように任意の型名を宣言するのが特徴だ。
例では、商品プロトコルはT型の商品コードと、T型を引数に商品コードを変更するメソッドを宣言している。そして、Int型、または、String型を採用した2つの商品クラスを実装している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* ** ジェネリクスを利用したプロトコル */ //商品プロトコル protocol ShouhinProtocol { typealias T var code:T { get } //商品コード func changeCode(code:T) //商品コード変更メソッド } //商品クラス(Int型のバージョン) class Shouhin:ShouhinProtocol { var code:Int = 980 func changeCode(code: Int) { self.code += code //引数の値をコード値に加算する。 } } //商品クラス(String型のバージョン) class Shouhin2:ShouhinProtocol { var code:String = "980" func changeCode(code: String) { self.code = "NO_" + code //引数にNO_を付与して設定する。 } } |
型パラメータの宣言箇所でwhere句を用いると型に関する複雑な条件を指定することができる。
例えば、以下のコードは商品プロトコルが適用されている2つのインスタンスの商品コードを比較して、大きい方を返すメソッドである。where句の条件式で、「2つのインスタンスの商品コードの型が同じ(S1.T == S2.T)」、「商品コードは比較可能(S1.T:Comparable)」が指定されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//商品プロトコルを適用したクラスを比較するメソッド func hikaku<S1:ShouhinProtocol, S2:ShouhinProtocol where S1.T == S2.T, S1.T:Comparable>(data1:S1, data2:S2) -> S1.T { if(data1.code > data2.code) { return data1.code } else { return data2.code } } //商品クラスのインスタンスを生成0 var test1 = Shouhin1() var test2 = Shouhin1() //比較メソッドを呼び出す。 print(hikaku(test1,data2:test2)) //実行結果 //980 |
上記のwhere句によって、先ほど作成したInt型バージョンとString型バージョンのインスタンスを引数に比較メソッドを呼び出すことはできない。
1 2 3 4 5 6 7 8 9 10 |
//商品クラスのインスタンスを生成0 var test1 = Shouhin1() //Int型バージョン var test2 = Shouhin2() //String型バージョン //比較メソッドを呼び出す。 print(hikaku(test1,data2:test2)) //コンパイルエラー //error: value of type '(_, data2: _) -> _' has no member 'T' |