关于Kotlin泛型不变性、协变与逆变的编译报错问题咨询
咱们先从核心概念入手,再一步步拆解你的代码问题,这样更容易理解。
首先:Kotlin泛型的默认规则——不变性
Kotlin里的泛型默认是不变的,意思是:就算A是B的子类型,Test<A>和Test<B>之间也没有任何子类型关系。不过你的代码里还涉及到上界约束(X: List<Any>),这表示X可以是List<Any>,也可以是List<Any>的协变子类型(比如List<String>,因为List本身是协变的,定义是interface List<out E>)。
为什么list1 = emptyList<Any>()会报错?
你定义了lateinit var list1: X,然后试图把emptyList<Any>()(也就是List<Any>类型)赋值给它。问题出在:X的上界是List<Any>,意味着X可以是任何继承/实现List<Any>的类型,比如List<String>。
而List是协变的,协变的规则是:子类型泛型可以赋值给父类型泛型(比如List<String> → List<Any>),但反过来不行!List<Any>并不是List<String>的子类型,所以如果X是List<String>,你把List<Any>赋值给list1就完全不合法——这就是编译器提示“Required X found List
为什么加out修饰符会报错?
你尝试把X声明为out X: List<Any>,但编译器提示“参数X在list1中为不变性”。这是因为out(协变)的核心规则是:被out修饰的泛型参数只能出现在「输出位置」——比如函数的返回值、只读属性(val),不能出现在「输入位置」(比如给可变属性赋值、函数的参数)。
你的list1是lateinit var,可变属性意味着它有一个setter,这属于「输入位置」(你需要把X类型的值“输入”到这个属性里),违反了out的使用规则,所以编译器直接报错。
怎么解决这个问题?
根据你的需求,有几种不同的解决方案:
方案1:如果list1不需要被重新赋值(只读)
把list1改成val,同时保留out修饰符,因为val是只读属性,属于「输出位置」,符合协变的规则:
class Test<out X: List<Any>>{ lateinit var list2: List<Any> val list1: X // 通过构造函数初始化list1 constructor(x: X) { list1 = x } fun putList(){ list2 = emptyList() // 这里不能再给list1赋值了,因为它是只读的 } }
方案2:如果确实需要给list1赋值
去掉out修饰符,并且确保你赋值的是X类型的实例——比如通过方法参数传入:
class Test<X: List<Any>>{ lateinit var list2: List<Any> lateinit var list1: X fun putList(x: X){ list2 = emptyList() list1 = x // 这里用传入的X类型实例赋值,类型完全匹配 } }
方案3:如果X本来就只想代表List<Any>
那完全没必要用泛型,直接把list1定义为List<Any>即可:
class Test{ lateinit var list2: List<Any> lateinit var list1: List<Any> fun putList(){ list2 = emptyList() list1 = emptyList<Any>() } }
再回顾协变/逆变的核心场景
最后再帮你梳理一下,避免以后混淆:
- 协变(out):用于「生产数据」的场景(比如只读集合),允许子类型泛型实例赋值给父类型泛型变量(
List<String>→List<Any>)。 - 逆变(in):用于「消费数据」的场景(比如
Comparator),允许父类型泛型实例赋值给子类型泛型变量(Comparator<Any>→Comparator<String>)。 - 不变:默认规则,泛型类型之间没有子类型关系,既不能协变也不能逆变。
内容的提问来源于stack exchange,提问作者Joao Neto

