替换规则落到方法签名
里氏替换原则要求子类对象能替换父类对象而不破坏调用方的预期。落到方法签名上就是一条类型规则:参数类型只能放宽,返回类型只能收窄。放宽参数意味着子类能接受父类能接受的一切输入,收窄返回意味着子类给出的结果至少满足父类承诺。
基本信息
| 输入参数 | 允许放宽(逆变),不允许收窄 |
|---|---|
| 返回值 | 允许收窄(协变),不允许放宽 |
| 前置条件 | 子类不能加强:不能要求比父类更多 |
| 后置条件 | 子类不能减弱:承诺的输出不能变松 |
典型使用场景
- 设计抽象类或接口时:先把方法签名定稳,避免子类被迫收窄参数
- 覆写评审:检查覆写方法是否悄悄把参数类型改窄了
- 泛型约束:集合类接口的读写方法用变体声明表达安全边界
- 适配器设计:把宽接口适配到窄接口时,在入口做校验而不是缩窄签名
- 语言选型:语言不支持参数逆变时,用重载或统一接口加运行时校验
各语言的实际支持度
各语言的实际支持度
# Java:返回类型可协变,覆写时参数类型必须完全一致,逆变只能靠重载模拟
# C#:泛型接口与委托可用 in/out 声明变体,方法覆写仍要求参数类型一致
# TypeScript:函数类型按逆变检查,但方法声明保留双变例外,能编译过不等于替换安全
# Python:动态类型,替换安全性靠测试与类型检查器(如 mypy)兜底契约要素与允许的松动方向
| 契约要素 | 子类可以 | 子类不可以 |
|---|---|---|
| 前置条件 | 减弱,接受更宽的输入 | 加强,要求更多或更严格 |
| 后置条件 | 加强,给出更有保证的结果 | 减弱,放宽承诺 |
| 不变量 | 保持成立 | 破坏父类不变式 |
| 异常 | 抛更少或更具体的异常 | 抛父类未声明的新异常 |
语言层面的差异很大:Java 与 C# 的覆写要求参数类型一致,只有泛型变体支持逆变;TypeScript 的方法参数按双变处理,因此「能编译过」不代表「替换后行为安全」,需要靠测试补位。
条件式加强是常见反例:子类在参数不满足额外要求时直接抛异常,调用方按父类契约传入合法值却崩在这里。既然父类承诺接受该输入,子类必须能处理它。