为什么C#接口方法不声明为抽象或虚拟的?

119

在C#接口中声明的方法不使用 virtual 关键字,在派生类中覆盖该方法时也不使用 override 关键字。

这样做有什么原因吗?我认为这只是一种语言上的便利,显然CLR知道如何在内部处理此操作(默认情况下方法并不是虚拟的),但还有其他技术原因吗?

下面是派生类生成的IL:

class Example : IDisposable {
    public void Dispose() { }
}

.method public hidebysig newslot virtual final 
        instance void  Dispose() cil managed
{
  // Code size       2 (0x2)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ret
} // end of method Example::Dispose

请注意该方法在 IL 中声明为 virtual final

6个回答

156

对于接口(interface)而言,添加abstract甚至是public关键字都是多余的,因此你可以省略它们:

interface MyInterface {
  void Method();
}
在CIL中,该方法被标记为virtualabstract。(请注意,Java允许声明接口成员为public abstract。)
对于实现类,有一些选择: 不可重写:在C#中,类没有将该方法声明为virtual。这意味着它不能在派生类中被重写(只能被隐藏)。在CIL中,该方法仍然是虚拟的(但是被封闭),因为它必须支持与接口类型相关的多态性。
class MyClass : MyInterface {
  public void Method() {}
}

可重写的:在C#和CIL中,该方法是virtual。它参与多态分派并且可以被覆盖。

class MyClass : MyInterface {
  public virtual void Method() {}
}

显式接口实现:这是一种类实现接口但不在类的公共接口中提供接口方法的方式。在CIL中,该方法将是private类型的,但仍然可以通过对应接口类型的引用从类外部调用它。显式实现也是不可重写的。这是因为存在一个CIL指令(.override),它将把私有方法链接到正在实现的相应接口方法。

[C#]

class MyClass : MyInterface {
  void MyInterface.Method() {}
}
[CIL]
.method private hidebysig newslot virtual final instance void MyInterface.Method() cil managed
{
  .override MyInterface::Method
}
在 VB.NET 中,你甚至可以在实现类中为接口方法命名别名。
Public Class MyClass
  Implements MyInterface
  Public Sub AliasedMethod() Implements MyInterface.Method
  End Sub
End Class
[CIL]
.method public newslot virtual final instance void AliasedMethod() cil managed
{
  .override MyInterface::Method
}

现在,考虑这个奇怪的情况:

interface MyInterface {
  void Method();
}
class Base {
  public void Method();
}
class Derived : Base, MyInterface { }
如果在同一个程序集中声明了BaseDerived,即使Base没有实现接口,编译器也会使Base::Method在CIL中变成虚拟的并且被标记为sealed。
如果在不同的程序集中声明了BaseDerived,编译Derived程序集时,编译器不会更改其他程序集,因此它会引入Derived中的一个成员,该成员将是一个显式实现MyInterface::Method,仅将调用委托给Base::Method
因此,你会发现每个接口方法的实现都必须支持多态行为,并且必须在CIL上标记为virtual,即使编译器必须通过各种方式来实现。

78

以下是Jeffrey Ritcher在CLR via CSharp第3版中的引用:

CLR要求接口方法被标记为虚方法。如果您在源代码中没有明确将方法标记为虚方法,则编译器会将该方法标记为虚方法并密封它;这会防止派生类覆盖接口方法。如果您明确将方法标记为虚方法,则编译器将该方法标记为虚方法(并使其未密封);这允许派生类覆盖接口方法。如果接口方法被密封,派生类无法覆盖该方法。但是,派生类可以重新继承相同的接口,并为接口的方法提供自己的实现。


27
这个引用并没有说明为什么接口方法的实现需要标记为虚拟的。原因是它在接口类型方面是多态的,因此它需要在虚拟表上有一个插槽来允许虚拟方法分派。 - Jordão
1
我不允許明確地將接口方法標記為虛擬的,並得到錯誤“error CS0106: The modifier 'virtual' is not valid for this item”。在我的PC上測試使用v2.0.50727(最老版本)。 - ccppjava
3
根据Jorado的评论,你需要将实现接口的类成员标记为虚拟的,以允许子类重写该类。 - Christopher Stevenson

14

是的,就运行时而言,接口实现方法是虚拟的。这是一种实现细节,它使接口起作用。虚拟方法在类的虚函数表中获得插槽,每个插槽都有一个指向其中一个虚拟方法的指针。将对象强制转换为接口类型会生成一个指向实现接口方法的表段的指针。使用接口引用的客户端代码现在可以从接口指针偏移0看到第一个接口方法指针,以此类推。

我在原回答中低估了final属性的重要性。它防止派生类覆盖虚拟方法。派生类必须重新实现接口,实现方法遮蔽基类方法。这已足以实现C#语言契约,即实现方法不是虚拟的。

如果您将示例类中的Dispose()方法声明为虚拟的,则会看到final属性被删除。现在允许派生类对其进行覆盖。


4
在大多数其他编译代码环境中,接口被实现为vtable - 一个指向方法体的指针列表。通常,实现多个接口的类在其内部编译器生成的元数据中将具有一个接口vtable列表,每个接口一个vtable(以保留方法顺序)。这也是COM接口通常实现的方式。
然而,在.NET中,接口不作为每个类的独立vtable来实现。接口方法通过全局接口方法表进行索引,所有接口都是其中的一部分。因此,为了使方法实现接口方法,不需要声明方法为虚拟方法 - 全局接口方法表可以直接指向类方法的代码地址。
在其他语言中也不需要声明方法为虚拟方法才能实现接口,即使在非CLR平台上也是如此。Win32上的Delphi语言就是一个例子。

1

接口是比类更抽象的概念,当你声明一个实现接口的类时,你只是在说“这个类必须有来自接口的这些特定方法,而不管它们是静态的、虚拟的、非虚拟的、重写的,只要它们具有相同的I.D.和相同的类型参数”。

其他支持接口的语言,如Object Pascal(“Delphi”)和Objective-C(Mac),也不需要将接口方法标记为虚拟或非虚拟。

但是,你可能是对的,我认为在接口中有一个特定的“virtual”/“override”属性可能是一个好主意,以便你想要限制实现特定接口的类的方法。但是,这也意味着需要有“nonvirtual”、“dontcareifvirtualornot”关键字,用于接口。

我理解你的问题,因为我在Java中看到了类似的情况,当一个类方法必须使用“@virtual”或“@override”来确保该方法是虚拟的。


1
@override 实际上并不改变代码的行为或改变生成的字节码。它所做的是向编译器发出信号,指示装饰的方法旨在成为一个覆盖,这允许编译器进行一些合理性检查。C# 的工作方式不同;override 是语言本身的一流关键字。 - Robert Harvey

0

它们并不是虚拟的(从我们思考的角度来看,如果不考虑底层实现方式(sealed virtual)- 非常好阅读这里其他的回答,让我自己学习一些东西 :-)

它们没有覆盖任何内容 - 接口中没有实现。

接口的作用仅仅是提供一个“合同”,告诉类必须遵循的约定 - 一种模式,让调用者知道如何调用对象,即使他们以前从未见过这个特定的类。

然后由类根据合同的限制来实现接口方法,可以是虚拟的或者“非虚拟”的(事实证明是sealed virtual)。


这个讨论串中的每个人都知道接口的作用。问题非常具体——对于接口方法生成的IL是虚拟的,而对于非接口方法则不是。 - Rex M
5
是的,问题被编辑后批评答案确实很容易,不是吗? - Jason Williams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接