📔

Kotlinのout/inを整理してみた

2024/05/14に公開

モチベーション

そもそも整理してみようと思ったきっかけは、下記のようなケースでコンパイルエラーが出るな・・、あまり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の場合は、問題なさそうだ。じゃあなにが違うんだ・・・・

というのが調べ始めたモチベーションになります。
その中でoutinが重要となっており、せっかくなので整理してみようと考えました。
この記事を見てもらえれば、上記のコンパイルエラーが出る出ないの仕組みが理解できるはずです!(頑張ります)

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にあります。
ここでは詳細を省きますが、outinを活用すれば便利なこともあるわけです。

out使ったの例
public interface List<out E> : Collection<E>
inを使ったの例
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

out/in/デフォルトの比較表

それぞれ何ができるのか、比較表にしてみました。デフォルトとは下記のようなGenericsにoutinも無いような状態のことになります。

デフォルトの例
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の定義を確認するとoutinも無く、デフォルトなので厳密な型一致が必要となり、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) // コンパイルエラーにならない

まとめ

調べ始めのときはoutinはこれまで意識して見かけたことはなく、関わりがないと思っていましが、Collectionsでがっつり活用されており、今まで無意識に使っていた(使わされていた)んだなぁと感心しました。

参考

https://kotlinlang.org/docs/generics.html

GitHubで編集を提案

Discussion