【プログラミング初心者】Swift基礎~ジェネリクス~

はじめに

今回はSwiftのジェネリクスという機能について紹介します。

少し難しいので果たして初級編なのか?とも思ったりするのですが、一応基本文法ではあるので初級編としています。

これでSwift初級編は最後の投稿になるかと思います。

ジェネリクス

ジェネリクスとは?

IntStringなどの型に縛られず、柔軟に関数やクラスを定義するための機能です。

実は皆さんすでに意識することなくジェネリクスを使っています。

Swiftで提供されている型に配列があります。
この配列はジェネリクスを使って定義されています。

定義ファイルを見るとわかるのですが、配列はArrayという型です。
このArray型の中にIntだったりStringだったりの要素を詰め込んでいます。
IntStringは当然異なる型ですが以下のようにそれぞれ同じように格納することができます。

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)
この時TInt型として扱われています。

printValue(value: "ジェネリクス")
このときのTString型です。

これは型推論によってTが決まっています。
つまり、
引数が1 → 1はInt → 関数定義の引数の型はTTInt
という流れです。
その結果、関数は以下のように定義したのと同様の振る舞いをします。

func printValue(value: Int) {
    print(value)
}

ちなみにここまで抽象的な型としてTを使ってきましたが、実はAAABBBなど何でも構いません。
慣例としてTがよく使われていますが、配列などではElementなど色々使われています。

型を保証する

実は上記の実装はあまりジェネリクスのメリットを受けられていません。
Swiftには型を特定しない型としてAny型というものがあります。
Anyを使えばジェネリクスを使わなくても上記のコードは実装できてしまいます。

func printValue(value: Any) {
    print(value)
}

ジェネリクスのメリットはTを1つの特定の型として扱えることが最大のメリットです。
つまりTIntとして呼び出されると、その実行の中では常にIntとして扱われるということです。

以下のコードを見てみます。

func makeArray<T>(value1: T, value2: T) -> [T] {
    return [value1, value2]
}

var array = makeArray(value1: 5, value2: 10)

ジェネリクスを使って配列を作成しています。

まずmakeArray(value1: 5, value2: 10)という呼び出しをしています。
このときのTIntです。

ではmakeArray(value1: 5, value2: "りんご")という呼び出しをしてみます。

すると以下のようなエラーが表示されます。
Cannot convert value of type 'String' to expected argument type 'Int'
StringIntに変換できません」というような意味ですね。

value1に5を入れているのでTIntです。
value2に”りんご”というString型の値を指定したためエラーが発生しています。

何が言いたいかというと、ジェネリクスを使うと2つの引数の型が同じということが保証できるということです。
Anyでは型の保証はできずIntStringが同時に引数として指定されることも有りえます。

次に
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に準拠すると各比較演算子を使った場合の処理を記述してあげる必要があります。
IntStringComparableに準拠しています。
Intの場合数字の比較を行ない、Stringの場合辞書順で比較するというような処理が書かれているのです。

考えてみれば当然です。
例えば自分で作ったHumanというクラスがあったとき、human1 > human2としたときSwiftはなにをもって大きいと判断すればいいのかわかりません。
このような処理をしたいのであればHumanクラスをComparableに準拠させてあげる必要があります。

話が少し脱線しました。
add<T>(value1: T, value2: T)の中でTに対して比較演算子を使っているので
>の演算子は使えませんよ、というエラーが発生しています。

なのでTComparableに準拠している値です、という制限を与えてあげます。
その場合以下のようにします。

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>としていますが、これでクラス内のTIntとして扱うということを表します。

また、今回イニシャライザの引数にT型を取っています。
そのためオブジェクト生成の際は何かしらT型の値を与えることになります。
この場合、Tが何型なのかはイニシャライザの引数から推論することができるため、以下のように<Int>を省略してオブジェクトを生成することが可能です。

let sample = Sample(property: 10)

生成した後、このオブジェクト内で使われるTInt型として扱われます。

Swiftが提供しているArrayは概ねこのような形で宣言されています。

最後に

今回はSwiftのジェネリクスの使い方について紹介しました。
プロトコルでもジェネリクスを使用できますが、関数などとは若干宣言方法が異なるので今回は省いています。
興味があればassociatedtypeなどで検索してみてください。

ジェネリクスは非常に便利な機能で多くの場面で使われていますが、現状ではどういったときに使えばいいのかイメージできないかと思います。
今のうちは自分で実装するのではなく、なんとなく読める程度でいいのかなと思います。

Swift標準機能やライブラリが提供しているメソッドにはジェネリクスが多く使われています。
ですが汎用的に使えるような定義をしているだけなので、ジェネリクスがでてきても戸惑わないレベルには押さえておきましょう。

今回の投稿でSwiftの基礎文法は終わりです。
もちろんかなり省いている部分も多いので調べながらという形にはなりますが、今まで投稿した内容を押さえておけば概ねSwiftプログラミングはできるのではないかと思います。

質問があればコメントやTwitterのDM等で連絡もらえればお答えしていきますのでご遠慮無くお願いします。

他にも初心者向けiOS開発の方法を公開していますので参考にしてみてください。

コメント

タイトルとURLをコピーしました