首页/开发/代码质量与评审速查/LSP 与协变逆变:类型层面的替换规则
设计原则与模式62 条

LSP 与协变逆变:类型层面的替换规则

参数类型可以放宽(逆变:子类接受更宽输入),返回类型可以收窄(协变:子类返回更具体类型);语言若不允许表达更宽参数,常用重载或 Object 参数配合运行时校验。

替换规则落到方法签名

里氏替换原则要求子类对象能替换父类对象而不破坏调用方的预期。落到方法签名上就是一条类型规则:参数类型只能放宽,返回类型只能收窄。放宽参数意味着子类能接受父类能接受的一切输入,收窄返回意味着子类给出的结果至少满足父类承诺。

基本信息

输入参数允许放宽(逆变),不允许收窄
返回值允许收窄(协变),不允许放宽
前置条件子类不能加强:不能要求比父类更多
后置条件子类不能减弱:承诺的输出不能变松

典型使用场景

  • 设计抽象类或接口时:先把方法签名定稳,避免子类被迫收窄参数
  • 覆写评审:检查覆写方法是否悄悄把参数类型改窄了
  • 泛型约束:集合类接口的读写方法用变体声明表达安全边界
  • 适配器设计:把宽接口适配到窄接口时,在入口做校验而不是缩窄签名
  • 语言选型:语言不支持参数逆变时,用重载或统一接口加运行时校验

各语言的实际支持度

各语言的实际支持度
# Java:返回类型可协变,覆写时参数类型必须完全一致,逆变只能靠重载模拟
# C#:泛型接口与委托可用 in/out 声明变体,方法覆写仍要求参数类型一致
# TypeScript:函数类型按逆变检查,但方法声明保留双变例外,能编译过不等于替换安全
# Python:动态类型,替换安全性靠测试与类型检查器(如 mypy)兜底

契约要素与允许的松动方向

契约要素子类可以子类不可以
前置条件减弱,接受更宽的输入加强,要求更多或更严格
后置条件加强,给出更有保证的结果减弱,放宽承诺
不变量保持成立破坏父类不变式
异常抛更少或更具体的异常抛父类未声明的新异常
语言层面的差异很大:Java 与 C# 的覆写要求参数类型一致,只有泛型变体支持逆变;TypeScript 的方法参数按双变处理,因此「能编译过」不代表「替换后行为安全」,需要靠测试补位。
条件式加强是常见反例:子类在参数不满足额外要求时直接抛异常,调用方按父类契约传入合法值却崩在这里。既然父类承诺接受该输入,子类必须能处理它。

易错点

  • 误以为所有语言都支持参数逆变:写出编译不过的覆写,或用一堆重载堆出相近代码
  • 依赖双变语言的宽松检查:把类型错误留到运行时,测试没覆盖就直接上线
  • 用加强前置条件代替内部校验:子类要求比父类更严的输入,替换即崩
  • 覆写时抛出新异常类型:按父类契约写的错误处理接不住,故障向上扩散

数据来源与参考

  • Liskov & Wing(1994);语言类型系统文档(Java/C#/TypeScript)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌