Kotlin:能否将类/接口层级泛型用作函数参数类型?
你的代码报错的核心原因是Kotlin泛型的不变性:BaseMapper<ChildB1>并不是BaseMapper<BaseB>的子类型。因为BaseMapper包含一个接收V类型参数的test方法——如果允许把TestMapper1(本质是BaseMapper<ChildB1>)存入List<BaseMapper<BaseB>>,理论上你可以调用mappers[0].test(ChildB2()),但TestMapper1的test方法只能接受ChildB1类型,这会触发运行时类型转换错误,所以编译器直接禁止这种不安全的赋值。
你提到的out/in关键字确实能解决泛型子类型兼容问题,但需要根据泛型的使用场景正确选择:
out用于生产者场景:泛型类型仅作为函数返回值(输出),此时BaseMapper<out V>允许BaseMapper<ChildB1>赋值给BaseMapper<BaseB>,但代价是不能定义任何接收V作为参数的方法(out修饰的泛型无法被消费)。in用于消费者场景:泛型类型仅作为函数参数(输入),此时BaseMapper<in V>允许BaseMapper<BaseB>赋值给BaseMapper<ChildB1>,但不能定义返回V的方法。
你的BaseMapper既消费V(test方法接收V),属于不变泛型类型,因此无法直接将BaseMapper<ChildB>转为BaseMapper<BaseB>。以下是几种适配你场景的解决方案:
方案1:星号投影+智能转换
如果能确保调用test时传入的参数类型正确,可以用星号投影声明集合,再通过智能转换指定具体类型:
@Test fun t() { val mappers: List<BaseMapper<*>> = listOf(TestMapper1(), TestMapper2()) // 智能转换后调用test,需自行保证类型匹配 (mappers[0] as TestMapper1).test(ChildB1()) }
这种方式绕过了编译器的泛型检查,需要你自己维护类型安全,避免ClassCastException。
方案2:配对Mapper与对应数据
如果你的场景是每个Mapper只处理特定类型的数据,可以将Mapper和对应数据配对存储,让编译器自动推断类型:
@Test fun t() { val mapperDataPairs = listOf( TestMapper1() to ChildB1(), TestMapper2() to ChildB2() ) mapperDataPairs.forEach { (mapper, data) -> mapper.test(data) } }
这种方式完全利用编译器的类型推断,不需要任何转换,是最安全的方案。
方案3:调整泛型设计(针对特定需求)
如果你的BaseMapper不需要返回V类型的值,可以将泛型声明为逆变,同时限制调用test时传入的参数类型为BaseB的子类:
abstract class BaseMapper<in V : BaseB> { open fun test(v: V) {} } // 此时可以声明为接收所有BaseMapper子类型的集合,但调用test时仍需智能转换匹配类型 @Test fun t() { val mappers: List<BaseMapper<in BaseB>> = listOf(TestMapper1(), TestMapper2()) (mappers[0] as TestMapper1).test(ChildB1()) }
这种方案仅适用于你不需要从BaseMapper获取V类型返回值的场景。
内容的提问来源于stack exchange,提问作者Valentina Chumak

