默认非虚、override 与契约:一次 C2 复习复盘
这篇讲清一件事:C# 多态里,默认非虚 、override / new、接口与抽象类、构造里调虚,以及 sealed 与虚调用代价,各自管什么、怎么说才准。
下面按主题给口径,不铺过程流水账。
默认非虚 :C# 实例方法默认不是虚方法;Java 默认是虚的。要开放扩展点,必须显式标 virtual(或 abstract)。
这样设计的常见理由:
- 未标虚的方法更容易被 JIT 内联 ,热路径更稳
- 减少 脆弱基类问题 :基类随便加可覆盖行为,派生类悄悄被改语义
- 版本控制 :作者主动 opt-in,才把某成员变成可被 override 的契约
设计含义:基类公开 API 只在「故意允许派生改写」的地方标 virtual,不要为了「以后也许要扩展」整片打开。
override :改写虚表槽位,按运行时类型调度。
new :隐藏基类同名成员,按编译时类型看;不加入虚调用链,也不替换已有虚槽。
最小对照:
1 | class Base |
面试口径一句话:要多态走 virtual + override;new 只是同名隐藏,别当成「另一种 override」。
1 | flowchart LR |
接口 :不能有实例字段;可以有 default 实现。default 方法通常只有通过接口类型的引用才能调到(类未提供自己的实现时)。类若再写了同名实例方法,经类类型调用优先走类自己的方法;经接口引用时,一般也绑定到类的那份实现(待核实:若仅显式实现或仅 default、无公开同名方法时的精确绑定,以当前语言版本规范为准)。
抽象类 :可以有字段与具体实现;单继承 。适合「一族类型共享状态与部分实现」的模板。
显式接口实现 :成员不能通过类类型直接调用,只经接口引用可见——用来藏公开面,或消解多接口同名冲突。
何时选哪个(短口径):
- 要多实现、定义能力契约、尽量不绑状态 → 接口
- 要共享字段/受保护实现、强制同一继承树 → 抽象类
- 两者可组合:抽象类实现若干接口,对外仍以接口边界协作
构造顺序里与本题直接相关的一点:派生字段初始化器 会在基类构造函数体执行之前跑完;派生构造函数体则更晚。
因此:基类构造里调用虚方法,在 C# 里合法,但危险——可能打到派生类的 override,而那时派生构造体尚未执行,依赖构造体才赋值的字段仍是默认值。
改法:构造阶段不要调虚/可覆盖成员。需要可定制启动逻辑时,改成两阶段初始化、工厂方法,或单独的 Initialize(由外部在对象完整构造后再调)。
1 | flowchart TD |
sealed :封住这条虚链,派生类不能再 override 同一成员(仍可用 new 藏同名,但不接虚链)。密封后 JIT 更有机会 去虚化 。
虚调用代价 :经虚表间接跳转,且难以内联。把虚调用包进一个非虚方法里,不会去掉底下的虚本质——外层只是多了一层普通调用。
顺带一句(接 C1,不当主线):把 struct 赋给接口变量会 装箱 ,之后经接口的调用是接口分发;细节留给后续专题。
结论与未尽
上面把 默认非虚 、override 与new 、接口与抽象类、构造调虚、sealed 与虚调用代价收成一套可复述的口径。
刻意往后挂的几块,不在这篇展开:
- struct → 接口装箱与接口分发的更深路径 → C3 / 泛型专题
- Unity
MonoBehaviour的Update/OnEnable等消息机制,与 C#virtual/override不是同一套调度 → 后续一行对照即可,本文不展开
这一层不靠背关键字,而靠追问「为什么默认非虚 / 为什么构造里不能调虚」时判断还能站住。


