这篇讲清一件事:C# 多态里,默认非虚 、override / new、接口与抽象类、构造里调虚,以及 sealed 与虚调用代价,各自管什么、怎么说才准。

下面按主题给口径,不铺过程流水账。

默认非虚 :C# 实例方法默认不是虚方法;Java 默认是虚的。要开放扩展点,必须显式标 virtual(或 abstract)。

这样设计的常见理由:

  • 未标虚的方法更容易被 JIT 内联 ,热路径更稳
  • 减少 脆弱基类问题 :基类随便加可覆盖行为,派生类悄悄被改语义
  • 版本控制 :作者主动 opt-in,才把某成员变成可被 override 的契约

设计含义:基类公开 API 只在「故意允许派生改写」的地方标 virtual,不要为了「以后也许要扩展」整片打开。

override :改写虚表槽位,按运行时类型调度。

new :隐藏基类同名成员,按编译时类型看;不加入虚调用链,也不替换已有虚槽。

最小对照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public virtual void V() => Console.WriteLine("Base.V");
public void H() => Console.WriteLine("Base.H");
}
class Derived : Base
{
public override void V() => Console.WriteLine("Derived.V");
public new void H() => Console.WriteLine("Derived.H");
}

Base r = new Derived();
r.V(); // Derived.V —— 虚调度
r.H(); // Base.H —— 编译时类型是 Base,new 不参与虚链

面试口径一句话:要多态走 virtual + override;new 只是同名隐藏,别当成「另一种 override」。

1
2
3
4
5
flowchart LR
R["Base r = new Derived()"] --> V["r.V()"]
R --> H["r.H()"]
V -->|"override · 运行时"| DV["Derived.V"]
H -->|"new · 编译时类型 Base"| BH["Base.H"]

接口 :不能有实例字段;可以有 default 实现。default 方法通常只有通过接口类型的引用才能调到(类未提供自己的实现时)。类若再写了同名实例方法,经类类型调用优先走类自己的方法;经接口引用时,一般也绑定到类的那份实现(待核实:若仅显式实现或仅 default、无公开同名方法时的精确绑定,以当前语言版本规范为准)。

抽象类 :可以有字段与具体实现;单继承 。适合「一族类型共享状态与部分实现」的模板。

显式接口实现 :成员不能通过类类型直接调用,只经接口引用可见——用来藏公开面,或消解多接口同名冲突。

何时选哪个(短口径):

  • 要多实现、定义能力契约、尽量不绑状态 → 接口
  • 要共享字段/受保护实现、强制同一继承树 → 抽象类
  • 两者可组合:抽象类实现若干接口,对外仍以接口边界协作

构造顺序里与本题直接相关的一点:派生字段初始化器 会在基类构造函数体执行之前跑完;派生构造函数体则更晚。

因此:基类构造里调用虚方法,在 C# 里合法,但危险——可能打到派生类的 override,而那时派生构造体尚未执行,依赖构造体才赋值的字段仍是默认值。

改法:构造阶段不要调虚/可覆盖成员。需要可定制启动逻辑时,改成两阶段初始化、工厂方法,或单独的 Initialize(由外部在对象完整构造后再调)。

1
2
3
4
5
flowchart TD
A["1 派生类字段初始化器"] --> B["2 基类字段初始化器"]
B --> C["3 基类构造函数体"]
C --> D["4 派生类构造函数体"]
C -.->|"若此处调虚方法"| X["可能打到派生 override\n此时步骤 4 未执行"]

sealed :封住这条虚链,派生类不能再 override 同一成员(仍可用 new 藏同名,但不接虚链)。密封后 JIT 更有机会 去虚化 。

虚调用代价 :经虚表间接跳转,且难以内联。把虚调用包进一个非虚方法里,不会去掉底下的虚本质——外层只是多了一层普通调用。

顺带一句(接 C1,不当主线):把 struct 赋给接口变量会 装箱 ,之后经接口的调用是接口分发;细节留给后续专题。

结论与未尽

上面把 默认非虚 、override 与new 、接口与抽象类、构造调虚、sealed 与虚调用代价收成一套可复述的口径。

刻意往后挂的几块,不在这篇展开:

  • struct → 接口装箱与接口分发的更深路径 → C3 / 泛型专题
  • Unity MonoBehaviour 的 Update / OnEnable 等消息机制,与 C# virtual/override 不是同一套调度 → 后续一行对照即可,本文不展开

这一层不靠背关键字,而靠追问「为什么默认非虚 / 为什么构造里不能调虚」时判断还能站住。