Kotlin泛型函数中协变逆变的表现疑惑求解
先看示例主函数:
fun main() { val sr = mutableListOf<Int>(1, 2, 3) val des = mutableListOf<Number>() copyData(sr, des) des.forEach { println(it) } }
版本A的函数实现:
// A: fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) { for (item in source) { destination.add(item) } }
我理解MutableList<out T>让MutableList在T上协变,这部分没问题。
但改成版本B后:
// B: fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) { for (item in source) { destination.add(item) } }
我也明白destination: MutableList<in T>让MutableList在T上逆变,但搞不懂为什么此时source没标记out(生产型)却能从中取出item,代码还能正常运行。我有两个推测:
- 仅在最后一个参数添加
in修饰符时,第一个参数的T不受影响; - 在第一个参数添加方差修饰符时,后续所有参数的T都会受影响。
解答
首先得明确:函数的泛型参数T是编译器自动推导出来的,不是固定死的类型。
看你调用的场景:传入的sr是MutableList<Int>,des是MutableList<Number>。编译器会自动找最合适的T——这里T会被推导成Int,因为Int是两个类型里最具体的那个。
此时source的类型就是MutableList<Int>,从里面读取Int类型的元素本来就是完全合法的操作,根本不需要协变标记out。协变标记的作用不是“允许读取”,而是允许你传入T的子类型的列表作为source。比如在版本A里,T会被推导成Number,这时MutableList<Int>(Int是Number的子类型)能作为MutableList<out Number>传入,这才是out的作用。
而版本B里,destination: MutableList<in T>的作用是允许destination接受T的父类型的列表,比如这里T是Int,MutableList<Number>(Number是Int的父类型)符合MutableList<in Int>的约束,所以能正常添加Int元素。
再说说你的两个推测:
- 第一个不对:每个参数的方差修饰符是独立的,只约束自身类型和T的关系,和参数顺序、其他参数没有关联。给destination加
in,只是限制这个参数的类型范围,source的约束还是MutableList<T>,只要实际传入的类型符合推导后的T就行。 - 第二个也不对:参数的方差修饰符只作用于自身,不会影响其他参数的T。比如版本A里source用
out T,只是对source的类型做了协变约束,destination的MutableList<T>是独立的约束,两者互不影响。
核心逻辑是:从MutableList<T>读取元素本来就是该容器的基本操作,协变标记out的意义是扩展source的可接受类型范围,而非“开启读取权限”。比如你直接写val list = mutableListOf<Int>(); val num = list[0],不需要任何out就能读取,道理是一样的。
内容的提问来源于stack exchange,提问作者ntos

