UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

必要最低限に理解する、ジェネリクスと共変・反変

こんにちは、アプリエンジニア見習い補佐代理のナカハシです。

最近Kotlinを勉強し始めて、読みやすくて書きやすい言語だなと思ったのですが、そこで出てきたジェネリクスの「変異」という機能で「うん?」となったので、改めてジェネリクス周りの初歩知識を整理することにしました。(そもそも型あり言語をちゃんと書くのも久しぶりだし...)

Kotlinを使って記事を書きますが、他の言語でも概念的にはだいたい同じですし超シンプルな記述でまとめていますので、Kotlin食わず嫌いの型も是非ご一読下さい。

そもそもジェネリクスがないといつ困るのか

例えば、以下のようなAnimalクラスとそれを格納できるAnimalBoxクラスがあったとします。

class Animal(var value: Int) {
  override fun toString(): String = "i am (${value}) animal instance!"
}

class AnimalBox(var value: Animal)

KotlinにはREPL環境が付属していて、Kotlinをインストールするだけで使えます。起動コマンドは kotlinc です。

Animalクラスなどの上記の定義をAnimal.ktsファイルに保存し、REPLを起動、ロードして使えるようにしてみましょう。

$ kotlinc
Welcome to Kotlin version 1.2.10 (JRE 1.8.0_102-b14)
Type :help for help, :quit for quit
>>> :load Animal.kts
>>>

これで使えるようになりました。早速Animalインスタンスを生成してみましょう。

>>> var animal1 = Animal(1)
>>> animal1
i am (1) animal instance!
>>>

生成できました。

これをAnimalBoxのインスタンスに格納してみましょう。AnimalBoxのコンストラクタに渡すだけです。

>>> var animalBox: AnimalBox = AnimalBox(animal1)
>>> animalBox.value
i'am (1) animal instance!

格納できました。

しかし、このAnimalBoxは、Animalのインスタンスしか入れられません。整数を入れたくなったとしても、

>>> var intBox: AnimalBox = AnimalBox(1)
error: the integer literal does not conform to the expected type Line_0.Animal
var intBox: AnimalBox = AnimalBox(1)
                                  ^

>>>

「型が合わんよ」とエラーになります。なので、新しくIntBoxを定義してみましょう。これなら入れられます。

class IntBox(var value: Int)

あ、StringやBooleanもBoxにいれたくなった! StringBoxとBooleanBoxも定義しましょう。

class StringBox(var value: String)
class BooleanBox(var value: Boolean)

うーん、キリがないですね。

この問題、一見全てのクラスのスーパークラスのBoxを定義すれば解決します。

Kotlinの最上位クラスはAnyなので、AnyBoxを爆誕させましょう。

class AnyBox(var value: Any)

爆誕できましたので、これにAnimalなインスタンスを格納してみましょう。

>>> var anyBox = AnyBox(animal1)
>>>

格納できました。

AnyBoxはとりあえずすべての種類のオブジェクトを格納することはできるのですが、取り出したときに困ります。取り出したときはAnyインスタンスになっているので、実際使うときにはダウンキャストが必要になるからです。

// 取り出したインスタンスをAnimal型の変数に格納しようとするとエラー
>>> var animalFromBox: Animal = anyBox.value
error: type mismatch: inferred type is Any but Line_0.Animal was expected
var animalFromBox: Animal = anyBox.value
                            ^

// 取り出したインスタンスをダウンキャストすればAnimal型の変数に格納できた
// Kotlinでは as Xxx でキャストします
>>> var animalFromBox: Animal = anyBox.value as Animal
>>> animalFromBox
i am (1) animal instance!
>>> 

ダウンキャストは型チェックを回避する行為で大変危険です、やめましょう。

というわけで、結局うまくいきませんでした。。。

ジェネリクスがあると何が嬉しいのか

ジェネリクスを使うと、クラス定義時はパラメータの型を仮置きしておいて、そのクラスの利用時にパラメータの型を指定できるようになります。(「型パラメータ」と言います)

定義では以下のように、型パラメータを<>で囲います。(慣習的にTで書く場合が多い)

class TBox<T>(val value: T)

たったこれだけで、どんな型のインスタンスでも格納できて、取得時にダウンキャストも不要な最強Boxクラスが定義できます。

利用時には、Tに当たる部分に型名を指定します。

// Animalで
>>> var animalBox = TBox<Animal>(animal1)
>>> animalBox.value
i am (1) animal instance!
>>> 

// Intで
>>> var intBox = TBox<Int>(1)
>>> intBox.value
1

AnimalもIntも、TBoxのインスタンスに格納できました。素晴らしい!

ジェネリクスなインスタンスの融通の効かなさ

上記で基本的にはめでたしめでたしなのですが、ジェネリクスなインスタンスは融通の効かなさで困ることがあります。

Animalクラスを継承したDogクラスがあったとします。

// Kotlinでは、クラスを継承可能にするには open を指定する必要があります
open class Animal(var value: Int) {
  override fun toString(): String = "i am (${value}) animal instance!"
}

class Dog(value: Int) : Animal(value) {
  override fun toString(): String = "i am (${value}) dog instance!"
}

この場合、Animal型の変数にDogのインスタンスは代入できます。これはOOPプログラミングの基本かつ醍醐味な部分ですよね。

>>> var animal: Animal = Dog(2)
>>> animal
i am (2) dog instance!
>>> 

であるならば、TBox<Animal>型の変数に、TBox<Dog>のインスタンスを代入できるのではなかと、直感的には思ってしまうのですが、、

>>> var animalBox: TBox<Animal> = TBox<Dog>(Dog(2))
error: type mismatch: inferred type is Line_0.TBox<Line_0.Dog> but Line_0.TBox<Line_0.Animal> was expected
var animalBox: TBox<Animal> = TBox<Dog>(Dog(2))
                              ^

>>> 

ダメでした。悲しい。

共変

さて、Kotlinを含む一部の言語では、T の前に out をつけることでこれが実現できます。

ですがこれには制限があって、out付きの変数は、メソッドの戻り値としてしか利用できなくなります。

// val は取得のみ
// var は取得/更新両方定義
class OutBox<out T>(val value: T)

Kotlinではコンストラクタに引数に var を付けるとプロパティが生成されますが、 val を付けるとgetterは定義されるもののsetterは定義されません。これにより、value: T は戻り値にしか使われなくなり、out指定の条件を満たせたことになります。

さて、このOutBoxクラスを使って、先ほど失敗したような代入を試みてみましょう。

// 成功!
>>> var animalBox: OutBox<Animal> = OutBox<Dog>(Dog(2))

// 取り出しも成功!
>>> animalBox.value
i am (2) dog instance!
>>> 

大丈夫でした。

このout指定のことを「共変指定」と言います。

なぜこんなことができるのでしょう?

Tがメソッドの戻り値にしか登場しないということは、このインスタンスの利用側とすれば、メソッドの戻り値としてAnimal型が返ってくると思って呼び出すとDog型が返ってくるということになります。

安全ですね...! 返ってきたDogはAnimalとしてしか使わないわけですから。

反変

out があればinもあります。outの場合とは逆で、in付きの変数は、メソッドのパラメータとしてしか利用できなくなります。

そんなクラスを作ってみましょう。

class AnimalChecker<in T>(val value: Any) {
    fun check(_value: T): Boolean = (_value == value)
}

AnimalCheker<Dog>の変数にAnimalCheker<Animal>のインスタンスは代入できませんよね、直感的には。 でもin指定がある場合はこれができてしまうようになります。

>>> var dog3 = Dog(3)

// 代入できた!!
>>> var dogCheker: AnimalChecker<Dog> = AnimalChecker<Animal>(dog3)

// checkメソッドも呼び出せた!!
>>> dogCheker.check(Dog(4))
false
>>> dogCheker.check(dog3)
true
>>> 

不思議!

このin指定のことを「反変指定」と言います。

なぜこんなことができるのでしょう?

Tがメソッドのパラメータにしか登場しないということは、このインスタンスの利用側からすれば、Dog型インスタンスをパラメータとして渡す必要があると思ってメソッドを呼ぶのですが、実際動作するのはパラメータをAnimal型だと思って処理するメソッドなわけです。

うーん、安全ですね...!!

まとめ

わたしが初めてジェネリクスの変異についての記事を読んだのは、C#の解説でお馴染みの ++C++; // 未確認飛行 C で8年近く前だったのですが、そのときの理解も中途半端でそれ以来C#をプロダクションで書くこともなく、ここまで放置していました。

やっとKotlin/Androidエンジニア見習い補佐代理になって、ジェネリクスを使ったコードをバリバリ使えると思うと嬉しい限りです。

参考