はじめに
ジェネリクスを使うとき、型パラメータの「代入可能性(互換性)」をどう扱うかが問題になります。
Kotlin ではこれを 変性(variance) と呼び、out と in キーワードで制御します。
1. なぜ変性が必要?
次の例を考えてみます:
val strings: List<String> = listOf("a", "b")
// val anys: List<Any> = strings // エラー!
一見「String は Any のサブタイプだから代入できるのでは?」と思えますが、
もし List<Any> に 42 を追加できてしまったら、元は List<String> なので破綻してしまいます。
このような 不整合を防ぐために、デフォルトでは代入できない のです。
そこで Kotlin では out と in を使って安全な代入関係を表現します。
2. 共変 (out)
定義
-
outは 型パラメータが「出力専用(読み取り専用)」 であることを示す。 -
Producer<out T>のように宣言すると、T を生成する側(返すだけ) に使える。
例
class Producer<out T>(private val value: T) {
fun produce(): T = value
// fun consume(item: T) {} // エラー:in 引数には使えない
}
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // 代入可能(共変)
println(anyProducer.produce()) // Hello
Producer<String> は Producer<Any> として扱える。
なぜなら「出すだけ」なので Any として読んでも安全だからです。
3. 反変 (in)
定義
-
inは 型パラメータが「入力専用(書き込み専用)」 であることを示す。 -
Consumer<in T>のように宣言すると、T を受け取る側(引数として消費) に使える。
例
class Consumer<in T> {
fun consume(item: T) {
println("Consumed: $item")
}
// fun produce(): T {} // エラー:戻り値には使えない
}
val numberConsumer: Consumer<Number> = Consumer()
val intConsumer: Consumer<Int> = numberConsumer // 代入可能(反変)
intConsumer.consume(10) // Consumed: 10
Consumer<Number> は Consumer<Int> として扱える。
なぜなら「消費するだけ」なので、Int を渡しても安全だからです。
4. イメージ図
共変(out) : サブタイプ関係をそのまま維持
Producer<String> → Producer<Any>
反変(in) : サブタイプ関係が逆になる
Consumer<Number> → Consumer<Int>
5. 実用例:関数型の変性
関数型 (A) -> B は次のルールで変性が自動的に付与されています:
- 引数(入力) →
in - 戻り値(出力) →
out
val f: (Number) -> String = { "Number: $it" }
val g: (Int) -> Any = f // OK
Kotlin の関数型は 入力は反変・出力は共変 という仕組みになっています。
6. まとめ
-
共変 (
out)- 出力専用(返すだけ)
Producer<out T>-
Producer<String>をProducer<Any>に代入できる
-
反変 (
in)- 入力専用(受け取るだけ)
Consumer<in T>-
Consumer<Number>をConsumer<Int>に代入できる
-
関数型は自動的に
- 引数 →
in(反変) - 戻り値 →
out(共変)
- 引数 →