Kotlinのout/inを整理してみた
モチベーション
そもそも整理してみようと思ったきっかけは、下記のようなケースでコンパイルエラーが出るな・・、あまりGenericsを理解できていなさそうだったので色々調べてみようと思いました。
コンパイルエラーのケース
fun xEvent (flow: MutableSharedFlow<Any>) {
// emitしたい
}
val stringFlow = MutableSharedFlow<String>()
xEvent(flow = stringFlow) // コンパイルエラーになる。
ん、、コンパイルエラーになるのか・・なんか問題ないケースもあったような・・?🤔
コンパイルエラーにならないケース
fun feedAny(list: List<Any>) {
// listの処理をする
}
val strings = listOf<String>("1", "2")
feedAny(list = strings) // コンパイルエラーにならない
そうそうList
の場合は、問題なさそうだ。じゃあなにが違うんだ・・・・
というのが調べ始めたモチベーションになります。
その中でout
やin
が重要となっており、せっかくなので整理してみようと考えました。
この記事を見てもらえれば、上記のコンパイルエラーが出る出ないの仕組みが理解できるはずです!(頑張ります)
Genericsについて
まずは、Generics(ジェネリクス)についての説明です。
Genericsとは、変数の型やクラス名を「<>」で囲む記述方法を使うことを指します。
例えばlistOf<String>
みたいなやつです。
一般的にはT
などが用いられます。
T ・・・ Type(タイプ。型をパラメータとするときに使われる)
E ・・・ Element(要素。Listなどで使われる)
K ・・・ Key(キー。Mapなどで使われる)
V ・・・ Value(値。Mapなどで使われる)
R ・・・ Result(戻り値)
out/inってそもそも何?
ここで言っているout
,in
とはGenericsの型の手前に記載するものになります。
実装すると下記の通りです。List
は標準パッケージのCollectionsにあります。
ここでは詳細を省きますが、out
やin
を活用すれば便利なこともあるわけです。
public interface List<out E> : Collection<E>
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
out/in/デフォルトの比較表
それぞれ何ができるのか、比較表にしてみました。デフォルトとは下記のようなGenericsにout
もin
も無いような状態のことになります。
public interface MutableSharedFlow<T> : SharedFlow<T>
比較表
特性 | out |
in |
デフォルト (非変) |
---|---|---|---|
読み書き性 | 読み取り専用 (プロデューサー) | 書き込み専用 (コンシューマー) | 読み書き両用 |
方向性 | 共変 (スーパークラスに代入可) | 反変 (サブクラスに代入可) | 非変 (厳密な型一致が必要) |
使用例 | メソッドの戻り値 | メソッドの引数 | メソッドの引数と戻り値 |
サブタイプ代入 |
Box<String> → Box<Any>
|
Sink<Any> → Sink<String>
|
厳密な型一致が必要 |
安全性 | サブタイプからスーパータイプへ安全 | スーパータイプからサブタイプへ安全 | 型安全性を最大限に保持 |
この表はさっと流し見していただいて、次に具体的な使い方を解説していきます。
具体的な使い方
outの場合
冒頭の事例によく似ていますが、List
を使って少し意味ありげな実装にしていきます。
public interface List<out E> : Collection<E>
List
にはout
がついています。ということは、読み取り専用でサブクラスからスーパークラスに代入可です。
// `Animal`というスーパークラスがあり、サブクラスに`Cat`があります。
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
// List<Animal>を引数にしています。List<スーパークラス>となっている。
fun feedAnimals(animals: List<Animal>) {
animals.forEach { println("Feeding ${it.name}") }
}
// List<Cat>を作成します。List<サブクラス>となっている
val cats: List<Cat> = listOf(Cat("Whiskers"), Cat("Tom"))
// outを使っているので、List<Animal> = List<Cat>が成り立ち、引数として渡すことが可能です。
feedAnimals(cats)
このケースは使い所がありそうですね。Dog
などのサブクラスが増えたとしても、Animal
共通の処理をしたいときなどに活用できそうです。
inの場合
in
の場合は、正直使い所がしっくりきておりません。
公式サンプルでは以下のように示されています。
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0はDouble型を持ち、これはNumberのサブタイプである。
// したがって、xをComparable<Double>型の変数に代入することができる。
val y: Comparable<Double> = x // OK! もしinが無ければコンパイルエラー
}
in
は書き込み専用で、スーパークラスからサブクラスに代入可となります。
Number(スーパークラス)、Double(サブクラス)という関係なので、in
を使うことで代入が可能になります。
ただ、どのような場面で使うかあまり分かっていないです・・・
冒頭の問題について
ここまで来れば、冒頭の違いが分かるはずです。
コンパイルエラーのケース
MutableSharedFlow
の定義を確認するとout
もin
も無く、デフォルトなので厳密な型一致が必要となり、MutableSharedFlow<Any>
とMutableSharedFlow<String>
は別物とみなされます。したがって、コンパイルエラーになります。
public interface MutableSharedFlow<T> : SharedFlow<T>
fun xEvent (flow: MutableSharedFlow<Any>) {
// emitしたい
}
val stringFlow = MutableSharedFlow<String>()
xEvent(flow = stringFlow) // コンパイルエラーになる。
コンパイルエラーにならないケース
なんども出てきていますが、List
にはout
がついています。ということは読み取り専用で、サブクラスからスーパークラスに代入可です。
public interface List<out E> : Collection<E>
fun feedAny(list: List<Any>) {
// listの処理をする
}
val strings = listOf<String>("1", "2")
feedAny(list = strings) // コンパイルエラーにならない
まとめ
調べ始めのときはout
やin
はこれまで意識して見かけたことはなく、関わりがないと思っていましが、Collectionsでがっつり活用されており、今まで無意識に使っていた(使わされていた)んだなぁと感心しました。
参考
Discussion