variance in kotlin
varianceとは
パラメータ化されたクラス間の関係を表す
Variance describes the relationship between parameterized classes
Idiomatic Kotlin: Variance
パラメータ化されたクラスとは、List<MyClass>におけるMyClass。
covariant, contravariant and invariant
共変 (covariant): 広い型(例:double)から狭い型(例:float)へ変換すること。
反変 (contravariant) : 狭い型(例:float)から広い型(例:double)へ変換すること。
不変 (invariant): 型を変換できないこと。
共変性と反変性 (計算機科学)
kotlinでは、デフォルトだと、
パラメータ化されたクラス間の関係は不変(型を変換できない)となる。
例えば、
List<String>を例にとると、
kotlinでは、StringはAnyの派生型であるが
List<String>は、List<Any>へ変換できない。
kotlinにおける共変と反変
kotlinでは、outとinキーワードを使って、共変と反変を表す。
outとinを使わない場合は、不変となる。
List<String>を、List<Any>へ代入しようとすると、
以下の通り、type mismatchによりcomplie errorが発生する。
val ls = mutableListOf("String")
val la: MutableList<Any> = ls // Type mismatch
// 上のコードのコンパイルが通ると、
// 下記の通り、int型の変数を、MutableList<String>に代入できることになる
// ∵ MutableList<String>はMutableList<Any>に代入できるため
ls.add(42)
MutableList<Any>へStringを追加
fun <T> addAll(list1: MutableList<T>,
list2: MutableList<T>) {
for (elem in list2) list1.add(elem)
}
// MutableList<String>
val ls = mutableListOf("A String")
// MutableList<Any>
val la: MutableList<Any> = mutableListOf()
/**
* Type inference failed:
* Cannot infer type parameter T in
* fun <T> addAll(list1: MutableList<T>,list2: MutableList<T>)
*/
addAll(la, ls) // compile error
型Tが推量できないためエラーとなっている。
None of the following substitutions
(MutableList<Any>,MutableList<Any>)
(MutableList<String>,MutableList<String>)
can be applied to
(MutableList<Any>,MutableList<String>)
上のエラーメッセージのように、
引数で渡しているAnyとStringに型Tが合致していない。
これは、型Tが不変であるからに他ならない。
outを用いて型を共変化
fun <T> addAll(list1: MutableList<T>,
list2: MutableList<out T>) {
for (e in list2) list1.add(e)
}
// MutableList<String>
val ls = mutableListOf("String")
// MutableList<Any>
val la: MutableList<Any> = mutableListOf()
val f = addAll(la, ls)
上記のコードはcompile errorが発生しない。
list2のMutableListの型Tにoutを付与したことで、
list2が、型T(ここではAny)において共変(AnyからStringへ変換)となる。
outが使用できる条件
なぜoutを付与することで、共変となるのか。
Covariance is read-only and useful as a return type (out)
共変は、読み取り専用で、戻り値の型として有効
Idiomatic Kotlin: Variance
MutableList<E>の定義を見てみる。
/**
* A generic ordered collection of elements that supports adding and removing elements.
* @param E the type of elements contained in the list. The mutable list is invariant on its element type.
*/
public interface MutableList<E> : List<E>, MutableCollection<E> {
override fun add(element: E): Boolean
override fun remove(element: E): Boolean
override fun addAll(elements: Collection<E>): Boolean
public fun addAll(index: Int, elements: Collection<E>): Boolean
override fun removeAll(elements: Collection<E>): Boolean
override fun retainAll(elements: Collection<E>): Boolean
override fun clear(): Unit
public operator fun set(index: Int, element: E): E
public fun add(index: Int, element: E): Unit
public fun removeAt(index: Int): E
override fun listIterator(): MutableListIterator<E>
override fun listIterator(index: Int): MutableListIterator<E>
override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}
上記のコメントにあるように、型Eは、リストに含まれる要素の型であり、
mutable listはその型に関して不変(型を変換できない)である。
先ほどのaddAllで使用しているlist2の関数inは、
override fun iterator(): MutableIterator<E>で、
戻り値に型Eが指定されている。
これにより、outを付与することで、list2の型Eに対して共変が成立する。
つまり、addAll内で使用しているlist2の関数は、戻り値にのみEを指定しているため、
AnyからStringへ変換し、それを戻り値として返すことに矛盾が生じない。
一方で、list1の関数addは、
override fun add(element: E): Booleanで、
引数に型Eが指定されている。
そのため、list1にoutを付与することはできない。
fun <T> addAll(list1: MutableList<out T>, /* MutableList<String> */
list2: MutableList<T> /* MutableList<Any> */) {
for (e in list2) list1.add(e)
}
val ls = mutableListOf("String")
val la: MutableList<Any> = mutableListOf()
val f = addAll(ls, la)
ここで共変を許容すると、
AnyからStringへ変換した要素EをMutableList<Any>に追加できることになる。
inはoutの逆
Contravariance is write-only and useful as an input type (in)
反変は、書き込み専用で、引数の型として有効
Idiomatic Kotlin: Variance
先ほどの例でいうと、list1にinを指定することでコンパイルが通る。
fun <T> addAll(list1: MutableList<in T>, /* MutableList<String> */
list2: MutableList<T> /* MutableList<Any> */) {
for (e in list2) list1.add(e)
}
val ls = mutableListOf("String")
val la: MutableList<Any> = mutableListOf()
val f = addAll(ls, la)
list1の関数addは、引数にEが指定されているため、
StringからAnyへ変換し、MutableListに追加することに矛盾は生じない。
これにより、反変が成り立つ