无尘阁日记

无尘阁日记

理解一个C#调用链的问题
2026-02-27

问:

interface IA

{

    void a();

    void b();

}

interface IAA : IA

{

    // IAA继承IA,所以IAA也包含方法a(),但没有新成员

}


class B : IA

{

    public virtual void a() { }

    public  void b() { }

}


class C : B, IAA

{

    public virtual void a() { }

    public  void b() { }

}


class D : B, IAA

{

    public  void a() { }

    public virtual void b() { }

}


我是php程序员,但是这个语言的小白,请为我以通俗易懂的、平实的语言解读一下。

答:

你是做 PHP 的,我用 PHP 能理解的类比方式,把这个 C# 代码给你讲明白。我们一步一步拆。

这是 C# 面向对象里几个核心概念:

  • 接口(interface)

  • 继承(class : 父类)

  • 实现接口(class : 接口)

  • virtual(可被重写)

  • override(重写)

  • new(隐藏)

你主要会懵的地方在:

  1. IAA 明明没写方法,为什么还能用?

  2. C、D 明明都实现了 IA,为啥写法不一样?

  3. virtual 到底干嘛的?

我们从最基础开始。

一、接口是什么?(用 PHP 类比)

在 PHP 里你应该写过:

interface IA {
    public function a();
    public function b();
}

接口就是:

👉 只规定“必须有什么方法”
👉 不写具体实现
👉 谁实现它,谁就必须实现这些方法

C# 里也是一样:

interface IA
{
    void a();
    void b();
}

意思就是:

任何实现 IA 的类,都必须有 a() 和 b()。

二、IAA 是什么鬼?

interface IAA : IA
{
}

这句话的意思是:

IAA 继承 IA。

你可以理解为:

IAA = IA 的升级版。

虽然它没写任何新方法,但它自动拥有 IA 的全部方法。

就像 PHP:

interface IAA extends IA {}

所以:

实现 IAA = 必须实现 IA 的所有方法。

三、B 类在干嘛?

class B : IA
{
    public virtual void a() { }
    public void b() { }
}

B 实现了 IA。

所以它必须写:

  • a()

  • b()

现在关键来了:

public virtual void a()

virtual 的意思是:

👉 允许子类重写

而:

public void b()

没写 virtual。

意思是:

👉 子类不能“真正重写”,只能隐藏。

你可以理解为:

virtual = 给子类留接口

不写 virtual = 不打算让你改

四、C 类分析

class C : B, IAA

C 继承 B,同时声明自己实现 IAA。

但注意一个关键点:

B 已经实现了 IA,
IAA 又继承 IA。

所以 C 实际上已经通过 B 满足接口要求。

接着看:

public virtual void a() { }
public void b() { }

问题来了:

B 的 a() 是 virtual,
所以 C 可以重写。

但正确写法应该是:

public override void a()

你这里写成 virtual,是“重新定义一个新的可重写方法”。

在 C# 里这叫隐藏,不是 override。

而 b() 呢?

B 的 b() 不是 virtual。

所以 C 不能 override。

只能“隐藏”。

正确写法应该是:

public new void b()

否则编译器会警告你。

总结 C:

  • a() 可以重写(因为父类 virtual)

  • b() 不能真正重写(因为父类没 virtual)

五、D 类分析

class D : B, IAA
{
    public void a() { }
    public virtual void b() { }
}

我们看父类:

B:

virtual a()
普通 b()

D:

普通 a()
virtual b()

问题来了。

1)a()

父类是 virtual,
子类没写 override,
而是写普通方法。

这是隐藏,不是重写。

2)b()

父类不是 virtual,
子类却写 virtual。

这不叫 override。

这是定义了一个新的 virtual 方法。

父类那套 b() 机制已经断掉了。

六、核心区别总结

我们做个对比表。

父类方法是 virtual

子类写 override → 真正重写

子类写 virtual → 隐藏 + 新虚方法

子类写普通 → 隐藏

父类方法不是 virtual

子类不能 override

只能隐藏

七、给你一个直观理解(超通俗)

把 virtual 理解成:

“我允许你改我的行为。”

没有 virtual 就是:

“你别动我。”

override 是:

“我按规矩重写。”

new 是:

“我假装没看到你。”

八、最重要的一点(多态的本质)

为什么要 virtual?

因为多态。

比如:

IA obj = new C();
obj.a();

如果 a() 是 override,

那就会调用 C 的版本。

如果只是隐藏,

那可能调用的是 B 的版本。

这就是多态真正影响的地方。

九、和 PHP 的区别

PHP 没有 virtual 关键字。

PHP 所有方法默认都可重写。

C# 是显式控制。

它是强约束语言。

它会严格区分:

  • 重写

  • 隐藏

  • 新定义

而 PHP 不太管这些。

十、用一句话总结

这个例子核心是:

C# 用 virtual + override 来严格控制“是否允许子类改行为”。

而你的代码里:

C 和 D 都在“隐藏父类方法”,不是真正重写。

如果你想写标准写法,应该是:

class C : B
{
    public override void a() { }
    public new void b() { }
}
class D : B
{
    public override void a() { }
    public new virtual void b() { }
}

十一、给你一个学习建议

你是 PHP 出身。

建议你重点理解三件事:

  1. override 和 new 的区别

  2. virtual 才能 override

  3. 多态是通过基类引用触发的

只要理解“基类引用调用子类实现”这件事,

你就真正理解 C# 继承体系了。

如果你愿意,我可以给你画一个“调用链图”,

用执行流程的方式让你彻底搞明白。


好,我们直接用“执行路径图”的方式,让你一眼看懂。

我们用一个统一测试代码:

IA obj1 = new C();
B  obj2 = new C();
C  obj3 = new C();

obj1.a();
obj2.a();
obj3.a();

obj1.b();
obj2.b();
obj3.b();

下面分别画出 C 和 D 的调用链。

一、类 C 的调用链

类结构回顾:

class B : IA
{
    public virtual void a() { Console.WriteLine("B.a"); }
    public void b() { Console.WriteLine("B.b"); }
}

class C : B, IAA
{
    public override void a() { Console.WriteLine("C.a"); }
    public new void b() { Console.WriteLine("C.b"); }
}

注意:

  • a() 是 override(真正重写)

  • b() 是 new(隐藏)

① 调用 a()

a 是 virtual + override
=> 走“虚函数表”(多态)

调用链图:

IA obj1 = new C();
        │
        ▼
接口指向 B 的虚函数槽
        │
        ▼
发现 C override
        │
        ▼
执行 C.a()
B obj2 = new C();
        │
        ▼
走虚函数表
        │
        ▼
执行 C.a()
C obj3 = new C();
        │
        ▼
直接调用 C.a()

结论:

无论 IA / B / C 类型
全部执行 C.a()

✔ 这才叫真正的多态

② 调用 b()

b 在 B 里不是 virtual
C 用 new 隐藏

这时就不走虚函数表了。

调用链图:

IA obj1 = new C();
        │
        ▼
接口找实现类
        │
        ▼
执行 B.b()
B obj2 = new C();
        │
        ▼
类型是 B
        │
        ▼
执行 B.b()
C obj3 = new C();
        │
        ▼
类型是 C
        │
        ▼
执行 C.b()

结论:

IA → B.b
B → B.b
C → C.b

❗ 这里没有多态
这是“静态绑定”

你可以理解成:

编译器在编译时就决定调用谁。

二、类 D 的调用链

类结构:

class D : B, IAA
{
    public override void a() { Console.WriteLine("D.a"); }
    public new virtual void b() { Console.WriteLine("D.b"); }
}

注意:

  • a 是 override

  • b 是 new virtual(重新开一个新的虚函数)

① 调用 a()

和 C 一样:

IA obj1 = new D();
        ▼
执行 D.a()

B obj2 = new D();
        ▼
执行 D.a()

D obj3 = new D();
        ▼
执行 D.a()

✔ 全部走多态

② 调用 b()

关键来了。

B 里的 b 不是 virtual
D 重新定义了一个 virtual b

这意味着:

B 的 b 和 D 的 b 是两条不同链。

调用图:

IA obj1 = new D();
        │
        ▼
接口映射到 B.b
        │
        ▼
执行 B.b()
B obj2 = new D();
        │
        ▼
类型是 B
        │
        ▼
执行 B.b()
D obj3 = new D();
        │
        ▼
调用 D 自己的虚函数 b()
        │
        ▼
执行 D.b()

结论和 C 完全一样:

IA → B.b
B → B.b
D → D.b

但是 D 的 b 可以被“它的子类” override。

而 C 的 b 不能。

三、最核心总结图

把它浓缩成一句:

如果父类方法是 virtual:

子类 override → 真多态
子类 new      → 假重写

如果父类方法不是 virtual:

子类永远不能真正重写

四、用 PHP 思维帮你理解

在 PHP 里:

class B {
    public function a() {}
}

class C extends B {
    public function a() {}
}

默认就是 override。

而 C# 是:

必须父类写 virtual,
子类写 override,
才算 override。

否则就是隐藏。

C# 比 PHP 严格得多。

五、给你一个最直观记忆法

看调用时用的“变量类型”。

变量类型决定能看到哪个方法。

对象类型决定 virtual 走向。

所以记住一句话:

普通方法 → 看变量类型
virtual方法 → 看对象类型

这个一旦理解,你就彻底通了。