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
に追加することに矛盾は生じない。
これにより、反変が成り立つ