はじめに
今回はSwiftのジェネリクスという機能について紹介します。
少し難しいので果たして初級編なのか?とも思ったりするのですが、一応基本文法ではあるので初級編としています。
これでSwift初級編は最後の投稿になるかと思います。
ジェネリクス
ジェネリクスとは?
Int
やString
などの型に縛られず、柔軟に関数やクラスを定義するための機能です。
実は皆さんすでに意識することなくジェネリクスを使っています。
Swiftで提供されている型に配列があります。
この配列はジェネリクスを使って定義されています。
定義ファイルを見るとわかるのですが、配列はArray
という型です。
このArray
型の中にInt
だったりString
だったりの要素を詰め込んでいます。Int
とString
は当然異なる型ですが以下のようにそれぞれ同じように格納することができます。
let array1 = [1, 2, 3]
let array2 = ["りんご", "みかん", "スイカ"]
このように柔軟な使い方ができるようにするための機能がジェネリクスです。
なぜジェネリクスを使うのか?
例えば以下のようなInt
型の引数を表示する関数があったとします。
func printInt(value: Int) {
print(value)
}
これだけであれば特にジェネリクスを使う必要がありません。
ここでString
型も表示できる関数が欲しいとなったのでさらに定義します。
func printInt(value: Int) {
print(value)
}
func printString(value: String) {
print(value)
}
今度はDouble
型を表示したくなり、、、
このように同じ処理ですが、引数となる型が違うため型に合わせた関数を都度作成しなければなりません。
ジェネリクスを使えばこれらの関数を1つの関数でまかなうことができます。
配列でもジェネリックが使われているのはこのためです。
配列はどんな型でも入れられるように設計されています。
これをそれぞれ型を指定した定義をしていてはキリがありません。
このような無駄を省くときにジェネリクスを使用します。
ジェネリクスを実装する
ジェネリクスを実装していきます。
ジェネリック関数
関数でのジェネリクスの使い方は以下になります。
// 引数にジェネリクスを使う
func 関数名<T>(引数: T) {
}
// 戻り値にジェネリクスを使う
func 関数名<T>() -> T {
}
<T>
とあります。
これがジェネリクスの書き方で、この関数の中でT
という何かしらの型を使いますというような意味になります。
具体的な実装を見てみます。
先程の何かを表示する関数をジェネリクスを使って実装します。
func printValue<T>(value: T) {
print(value)
}
printValue(value: 1)
printValue(value: "ジェネリクス")
1
ジェネリクス
1つの関数で異なる型の引数を取ることができました。
printValue(value: 1)
この時T
はInt
型として扱われています。
printValue(value: "ジェネリクス")
このときのT
はString
型です。
これは型推論によってT
が決まっています。
つまり、
引数が1 → 1はInt
→ 関数定義の引数の型はT
→ T
はInt
という流れです。
その結果、関数は以下のように定義したのと同様の振る舞いをします。
func printValue(value: Int) {
print(value)
}
ちなみにここまで抽象的な型としてT
を使ってきましたが、実はAAA
やBBB
など何でも構いません。
慣例としてT
がよく使われていますが、配列などではElement
など色々使われています。
型を保証する
実は上記の実装はあまりジェネリクスのメリットを受けられていません。
Swiftには型を特定しない型としてAny
型というものがあります。Any
を使えばジェネリクスを使わなくても上記のコードは実装できてしまいます。
func printValue(value: Any) {
print(value)
}
ジェネリクスのメリットはT
を1つの特定の型として扱えることが最大のメリットです。
つまりT
がInt
として呼び出されると、その実行の中では常にInt
として扱われるということです。
以下のコードを見てみます。
func makeArray<T>(value1: T, value2: T) -> [T] {
return [value1, value2]
}
var array = makeArray(value1: 5, value2: 10)
ジェネリクスを使って配列を作成しています。
まずmakeArray(value1: 5, value2: 10)
という呼び出しをしています。
このときのT
はInt
です。
ではmakeArray(value1: 5, value2: "りんご")
という呼び出しをしてみます。
すると以下のようなエラーが表示されます。Cannot convert value of type 'String' to expected argument type 'Int'
「String
はInt
に変換できません」というような意味ですね。
value1
に5を入れているのでT
はInt
です。value2
に”りんご”というString
型の値を指定したためエラーが発生しています。
何が言いたいかというと、ジェネリクスを使うと2つの引数の型が同じということが保証できるということです。Any
では型の保証はできずInt
とString
が同時に引数として指定されることも有りえます。
次にvar array = makeArray(value1: 5, value2: 10)
このarray
は何型でしょうか?
答えは[Int]
型です。makeArray
の戻り値が[T]
となっているためですね。
var array = makeArray(value1: "りんご", value2: "みかん")
このような呼び出しをするとarray
は[String]
型となります。
これがジェネリクスのメリットです。
例えば以下のようにAny
を使ってみます。
func makeArray(value1: Any, value2: Any) -> [Any] {
return [value1, value2]
}
var array = makeArray(value1: 5, value2: "りんご")
こちらの場合エラーは発生しません。array
は何でも入れられる[Any]
型となります。
何でも入れられると聞くと便利そうに聞こえますが、配列から取り出した値の型が保証できないということです。
0番目は数値で、1番目は文字列で、2番目は何か他のオブジェクトで…というようなことになりかなり使いづらい配列となります。
ジェネリクスはこのような曖昧さをなくした上で汎用的に使うことができます。
型に制約を与える
ジェネリクスは型に制約を与えることができます。
値と値を比較する関数を考えてみます。
func compare<T>(value1: T, value2: T) -> Bool {
return value1 > value2
}
ジェネリクスを使っていて比較演算子を使ってその結果を返しているので良さそうに見えます。
ですが、以下のようなエラーが表示されます。Binary operator '>' cannot be applied to two 'T' operands
実は比較演算子を使うためにはComparable
というSwiftで定義されているプロトコルに準拠している必要があります。Comparable
に準拠すると各比較演算子を使った場合の処理を記述してあげる必要があります。Int
やString
はComparable
に準拠しています。Int
の場合数字の比較を行ない、String
の場合辞書順で比較するというような処理が書かれているのです。
考えてみれば当然です。
例えば自分で作ったHuman
というクラスがあったとき、human1 > human2
としたときSwiftはなにをもって大きいと判断すればいいのかわかりません。
このような処理をしたいのであればHuman
クラスをComparable
に準拠させてあげる必要があります。
話が少し脱線しました。add<T>(value1: T, value2: T)
の中でT
に対して比較演算子を使っているので>
の演算子は使えませんよ、というエラーが発生しています。
なのでT
はComparable
に準拠している値です、という制限を与えてあげます。
その場合以下のようにします。
func compare<T: Comparable>(value1: T, value2: T) -> Bool {
return value1 > value2
}
これでエラーは消えました。
呼び出すと以下のようになります。
let intResult = compare(value1: 5, value2: 10)
let stringResult = compare(value1: "りんご", value2: "みかん")
引数にComparable
に準拠していない、例えば配列を入れた場合はエラーが発生します。
let arrayResult = compare(value1: [11], value2: [22])
// Global function 'compare(value1:value2:)' requires that '[Int]' conform to 'Comparable'
このようにして与える引数に制約を与えます。
ジェネリック型
クラスや構造体の定義でもジェネリクスを使えます。
基本的には関数と同じで、以下のような定義になります。
class クラス名<T> {
}
ジェネリクスとして定義したT
はクラス内のどこでも使用することができます。
class Sample<T> {
let property: T
init(property: T) {
self.property = property
}
func getProperty() -> T {
return self.property
}
}
オブジェクトの生成は以下のようになります。
let sample = Sample<Int>(property: 10)
Sample<Int>
としていますが、これでクラス内のT
はInt
として扱うということを表します。
また、今回イニシャライザの引数にT
型を取っています。
そのためオブジェクト生成の際は何かしらT
型の値を与えることになります。
この場合、T
が何型なのかはイニシャライザの引数から推論することができるため、以下のように<Int>
を省略してオブジェクトを生成することが可能です。
let sample = Sample(property: 10)
生成した後、このオブジェクト内で使われるT
はInt
型として扱われます。
Swiftが提供しているArray
は概ねこのような形で宣言されています。
最後に
今回はSwiftのジェネリクスの使い方について紹介しました。
プロトコルでもジェネリクスを使用できますが、関数などとは若干宣言方法が異なるので今回は省いています。
興味があればassociatedtype
などで検索してみてください。
ジェネリクスは非常に便利な機能で多くの場面で使われていますが、現状ではどういったときに使えばいいのかイメージできないかと思います。
今のうちは自分で実装するのではなく、なんとなく読める程度でいいのかなと思います。
Swift標準機能やライブラリが提供しているメソッドにはジェネリクスが多く使われています。
ですが汎用的に使えるような定義をしているだけなので、ジェネリクスがでてきても戸惑わないレベルには押さえておきましょう。
今回の投稿でSwiftの基礎文法は終わりです。
もちろんかなり省いている部分も多いので調べながらという形にはなりますが、今まで投稿した内容を押さえておけば概ねSwiftプログラミングはできるのではないかと思います。
質問があればコメントやTwitterのDM等で連絡もらえればお答えしていきますのでご遠慮無くお願いします。
他にも初心者向けiOS開発の方法を公開していますので参考にしてみてください。
コメント