为何这段Kotlin泛型代码必须进行类型转换?
咱们一步步拆解你遇到的问题,核心都围绕Kotlin泛型的不变性和协变/逆变规则展开。
1. 为什么直接添加FooEx1()到MutableList<Foo<Bar>>会编译失败?
Kotlin的泛型默认是不变的——也就是说,哪怕Bar1是Bar的子类,Foo<Bar1>和Foo<Bar>之间也没有任何子类型关系。编译器禁止直接添加,是为了避免潜在的类型安全问题:
假设编译器允许你这么做,那之后你可能会写出这样的代码:
val list = mutableListOf<Foo<Bar>>().apply { add(FooEx1()) } // 这行代码编译时没问题,但运行时会直接崩溃! list[0].example(Bar2())
FooEx1的example方法只接受Bar1类型的参数,你却传了Bar2,这完全违背了类型安全,所以编译器从一开始就把这种情况拦住了。
2. 为什么给Foo加out修饰符会报错?
你尝试用out T把Foo声明为协变,协变的意思是:如果Bar1是Bar的子类,那么Foo<Bar1>可以被当成Foo<Bar>来用(类似List<String>可以当成List<Any>用)。但协变有个严格的规则:泛型参数T只能出现在「输出位置」(比如函数返回值),不能出现在「输入位置」(比如函数参数)。
你的example(bar: T)方法里,T是作为方法的输入参数,属于「in位置」,这直接违反了协变的规则,所以编译器会报错:Type parameter T is declared as 'out' but occurs in 'in' position in type T。
3. 怎么安全地解决这个问题?
如果不想用不安全的强制类型转换,这里有几个更稳妥的方案:
方案1:使用通配符Foo<out Bar>
把列表的类型改成MutableList<Foo<out Bar>>,这样就能直接添加FooEx1()和FooEx2(),不需要任何转换:
class ExampleWorksSafely { val collection = mutableListOf<Foo<out Bar>>().apply { add(FooEx1()) add(FooEx2()) } }
不过要注意:这样的列表里,你不能调用example方法——因为编译器不知道每个Foo实例具体的T类型是什么,无法确定你能传入什么参数,这是保证类型安全的代价。如果你的场景只是需要持有这些Foo实例,不需要调用example,这就是最优解。
方案2:重新设计接口(如果业务允许)
如果你的example方法确实需要接收T类型的参数,那协变这条路走不通。你可以考虑把接口拆分成两个:一个协变的只读接口(只包含返回T的方法),一个不变的可写接口(包含接收T的方法),或者调整泛型的使用逻辑,让T只出现在输出位置。
方案3:谨慎使用强制转换(不推荐)
你原来的强制转换方案能编译,但本质是绕过了编译器的类型检查,属于不安全操作。如果之后不小心调用了example方法并传入错误类型的参数,运行时会抛出ClassCastException。只有在你能绝对保证不会调用example,或者能确保传入参数类型完全匹配的情况下,才建议这么用。
内容的提问来源于stack exchange,提问作者Thomas Cook

