Dart重写方法参数为何需宽于父类?逆变相关问题(二)
子类重写方法禁止使用更窄参数类型的原因
这个规则的核心依据是里氏替换原则:所有父类实例可以出现的位置,子类实例必须能直接替换使用,且不会引发运行时故障。方法参数要求使用与父类一致或更宽泛的父类型(类型系统里叫逆变)、返回值允许使用更窄的子类型(类型系统里叫协变),都是为了满足这个替换约束。
如果强行把重写方法的参数改成更窄的子类型,会直接破坏类型安全,我们可以用可运行的Dart代码复现这个故障:
注:Dart默认会对这类错误写法做静态检查拦截,下面的示例用
covariant关键字临时绕过检查,还原放开限制后的真实故障场景。
class Animal {} class Cat extends Animal { void meow() => print("喵"); } class Dog extends Animal { void bark() => print("汪"); } class AnimalShelter { void putAnimal(Animal animal) { print("将动物收入普通收容所"); } } // 不符合规范的错误写法 class BadCatShelter extends AnimalShelter { @override // 加covariant绕过静态类型检查,把参数收窄为Cat void putAnimal(covariant Cat animal) { // 既然认定参数是Cat,直接调用Cat专属的meow方法 animal.meow(); print("将猫咪收入猫咪专属收容所"); } } void main() { // 按照里氏替换原则,父类类型的变量可以指向子类实例 AnimalShelter shelter = BadCatShelter(); // 父类方法签名明确承诺可以接收任意Animal类型参数,传Dog完全符合静态类型规则 Dog strayDog = Dog(); // 运行到这里直接抛出类型错误:Dog不是Cat的子类型,调用meow方法失败 shelter.putAnimal(strayDog); }
故障的本质很简单:父类的方法签名相当于对外做了一个承诺——「我这个方法可以接收所有Animal类型的参数」,子类作为父类的替换实现,必须能兑现这个承诺。如果子类把参数收窄成Cat,就相当于偷偷把承诺改成了「我只收Cat」,此时调用方按照父类的承诺传进来Dog、Rabbit等其他Animal,子类根本处理不了,直接崩溃。
符合规范的写法应该是把参数类型定义为相同或更宽泛的类型,对传入的参数做兼容处理:
// 符合规范的正确写法 class GoodCatShelter extends AnimalShelter { @override // 参数用比Animal更宽泛的Object类型,完全符合Dart重写规则 void putAnimal(Object animal) { if (animal is Cat) { animal.meow(); print("将猫咪收入猫咪专属收容所"); return; } print("当前对象不是猫,转入普通收容所流程"); } } void main() { AnimalShelter shelter = GoodCatShelter(); shelter.putAnimal(Cat()); // 输出:喵 → 将猫咪收入猫咪专属收容所 shelter.putAnimal(Dog()); // 输出:当前对象不是猫,转入普通收容所流程 }
对应Dart官方文档举的setter例子逻辑完全一致:父类setter参数是int,子类重写时改成父类型num,意味着子类可以接收所有num类型的值(包括int、double),自然能兼容父类承诺接收的int参数,不会出现类型问题。
几个容易混淆的规则点:
- 返回值可以用更窄的子类型:比如父类方法返回
Animal,子类重写时返回Cat是合法的。因为调用方按照父类签名拿到的是Animal类型,实际返回Cat完全可以当Animal正常使用,不会破坏类型安全。 - 参数必须用相同或更宽的父类型:本质是强制子类兑现父类对外的参数接收承诺,避免出现调用方传了合法参数、子类却无法处理的运行时故障。
- 如果业务确实需要子类只处理特定子类型的参数,必须在方法内部做类型判断和兼容,不能直接修改方法签名的参数类型收窄范围。
内容的提问来源于stack exchange,提问作者森口万太郎
相关产品推荐
相关产品推荐

