Kotlin父构造器无法识别泛型父类型的编译问题求解
报错核心是Kotlin泛型默认是**不变(invariant)**的,你的类型推导没有满足泛型的型变校验规则:
- 你在继承
Frag时传入的泛型是Frag<PRepo, V>,父类对V的约束是V : ViewModel<R>,这里父类的R被你写死为PRepo,相当于要求V必须是ViewModel<PRepo>的子类型。 - 你给PFrag声明的V约束是
V : PViewModel<M, R>,而PViewModel<M, R>继承的是ViewModel<R>——这里的R是PFrag自己声明的类型参数,约束仅为R : PRepo,也就是R是PRepo的子类型,而非PRepo本身。 - 由于泛型不变特性,即使
R是PRepo的子类,ViewModel<R>也不会被识别为ViewModel<PRepo>的子类型,自然V无法满足父类的泛型约束,编译失败。
不需要大改代码结构,两种小幅调整都可以解决问题:
方案1:调整父类泛型传递,零注解无安全隐患
这是最符合类型安全逻辑的改法,不需要修改原有父类的泛型声明,只需要把PFrag继承时传给父类的R参数,从写死的PRepo换成PFrag自身声明的泛型R即可:
interface Model abstract class Repo abstract class ViewModel<R: Repo> abstract class Frag<R: Repo, V: ViewModel<R>> interface PModel: Model abstract class PRepo: Repo() abstract class PViewModel<M: PModel, R: PRepo>: ViewModel<R>() // 仅修改这里,把传入Frag的第一个泛型从PRepo改成R abstract class PFrag<M: PModel, R: PRepo, V: PViewModel<M, R>>: Frag<R, V>()
改完后父类约束的V: ViewModel<R>和你声明的V类型完全匹配,不需要额外加任何关键字或注解,直接通过编译。
方案2:给Frag的泛型R加out协变声明
如果你确实需要PFrag对应的父类泛型R固定为PRepo,可以给Frag的R参数加out关键字声明协变,同时用@UnsafeVariance注解抑制型变检查警告即可:
// 仅修改Frag的泛型声明 abstract class Frag<out R: Repo, V: ViewModel<@UnsafeVariance R>>
这种改法下你原来写的Frag<PRepo, V>就可以正常编译,因为协变声明允许ViewModel<R : PRepo>被识别为ViewModel<PRepo>的子类型。@UnsafeVariance在这里是安全的,因为R仅作为ViewModel的泛型约束存在,不会出现在会引发类型漏洞的入参位置。
你之前的写法本质是想把ViewModel<out R : PRepo>当成ViewModel<PRepo>使用,在默认不变的泛型规则下这种写法会被拦截,是为了避免类型安全漏洞:假设存在SpecificRepo : PRepo()、SpecificViewModel : ViewModel<SpecificRepo>(),如果不加协变声明就允许把SpecificViewModel当成ViewModel<PRepo>,就可能出现往这个ViewModel里塞入其他PRepo子类实例的运行时类型错误,因此编译器默认不会放行这类隐式类型转换。
内容的提问来源于stack exchange,提问作者Louis Saglio

