本节摘要:多态的地基是每个对象携带的虚方法表指针。本节对比普通调用与虚调用的机器码差异,讲清 virtual、override、new 的槽位语义,抽象类与密封类在分派与优化上的意义,并解析构造函数里调虚方法的经典陷阱。
public class Shape { public double Area() => 0; // 非虚方法 public virtual double Perimeter() => 0; // 虚方法 } public class Circle : Shape { public override double Perimeter() => 2 * Math.PI * R; public double R; }
调用 shape.Area() 时,编译期已知目标,JIT 产出一条直接 call 指令,地址写死。调用 shape.Perimeter() 时,编译器必须生成间接调用:从对象头拿方法表指针,定位到 Perimeter 槽位,取出函数地址再 call。多花两三次内存访问——这就是动态分派的全部价格,也是它带来的能力:实际执行哪个版本,取决于运行时对象是谁,而不是变量声明成谁。
Shape s = new Circle { R = 1 }; Console.WriteLine(s.Perimeter()); // Circle 的版本:约 6.28
变量是 Shape 类型,执行的是 Circle 的实现。这一行为是所有框架回调、依赖注入、模板方法的底层支撑:框架代码持有基类引用,业务代码提供子类实现,两者编译互不认识,运行时精准对接。

父类虚方法,子类用 new 而不是 override 声明同名方法时,行为完全不同:
public class Rect : Shape { public new double Perimeter() => 42; // 隐藏,不是重写 } Shape a = new Rect(); Console.WriteLine(a.Perimeter()); // 0:走虚表,Rect 没重写,落到 Shape 版 Rect b = new Rect(); Console.WriteLine(b.Perimeter()); // 42:静态类型是 Rect,绑定到新方法
new 是"另立门户":虚表槽位不变,只是编译器在静态类型恰好是 Rect 时绑定到新方法。这种"声明类型看什么、实际类型看什么"的分裂极易埋雷,规范的态度是把 new 当成显式的警告标记——绝大多数场景你要的是 override,写出 new 通常是在掩盖设计问题。
抽象方法声明槽位但基类不提供实现,强制子类填坑:
public abstract class Report { public abstract string Render(); // 无实现,槽位待填 public void Export() => File.WriteAllText(... Render() ...); // 模板方法 } public class HtmlReport : Report { public override string Render() => "<html>...</html>"; }
Export 是模板方法模式的精髓:骨架在基类,细节在子类,虚表保证 Export 内部那次 Render 调用落到子类实现。抽象类与接口的选型第三章接口节细比,这里先记一条:抽象类适合"有共同状态与部分实现"的家族,接口适合"跨家族的能力契约"。
密封类 sealed 断掉继承链。它不只是设计约束,还是性能开关:某个类型确定无子类后,JIT 知道它的虚方法不可能被重写,间接调用可以退化为直接调用、方法体可以放心内联。把不打算被继承的类密封,是零成本的优化习惯——基类库大量类型(string、DateTime)都是 sealed,一半原因正在于此。
⚠️ 经典陷阱:基类构造函数调用虚方法。构造 Circle 实例时,基类构造先执行,此时对象的方法表指针已经是 Circle 的(CLR 在分配时就设定了实际类型),所以基类里那次调用会分派到 Circle 的 override——而 Circle 的构造函数还没跑,字段全是默认值。轻则读到 0 与 null,重则崩溃。规则:构造函数里只调私有或密封方法。
机制给你多态的能力,设计纪律决定多态不翻车。里氏替换原则是所有 override 的隐性合同:子类必须能无差别替换父类,不加强前置条件、不削弱后置条件。经典反例是"正方形继承长方形"——setWidth 对正方形必须同时改高,违反了长方形"宽高独立"的隐含承诺,所有依赖该承诺的代码在替换后悄悄出错。虚方法表保证了分派正确,但语义合同只有设计者能保证。
关于分派还有一条值得知道的优化边界:虚调用阻止方法内联,而内联是 JIT 最有力的微观优化之一(把小方法体直接嵌入调用点,省掉调用开销并打开后续优化空间)。所以基类库里那些被设计成虚方法的高频小方法,框架作者都在权衡扩展性与性能。你在自己的热点路径上同样可以权衡:一个被千万次调用的小方法,如果确定不需要被子类改写,就不要随手标 virtual;能用 sealed 封死的类就封死。这类决策单看一行代码毫无差别,累积起来就是热路径上可测的差距,机制知识在此直接兑换成性能。
下一节从纵向继承转向横向契约:接口在元数据里如何表示,又为什么需要默认方法。