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


108

接口中的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

注意,该方法virtual final在IL中声明。

Answers:


145

对于该接口,添加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中,您甚至可以在实现类中为接口方法名称加上别名。

[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::Method虚拟和密封(在CIL中),即使Base未实现该接口也是如此。

如果BaseDerived在不同的程序集中,则在编译该Derived程序集时,编译器不会更改其他程序集,因此它将引入一个成员,Derived该成员将是一个显式实现MyInterface::Method,只需将调用委派给即可Base::Method

因此,您将看到,每个接口方法实现都必须支持多态行为,因此即使编译器必须经过箍才能实现,也必须在CIL上将其标记为虚拟。


73

在此通过CSharp第三版引用CLR的Jeffrey Ritcher

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


25
引言没有说明为什么需要将接口方法实现标记为虚拟。这是因为它在接口类型方面是多态的,因此它需要虚拟表上放置一个插槽以允许虚拟方法分派。
Jordão酒店

1
我不允许将接口方法显式标记为虚拟方法,并收到错误“错误CS0106:修饰符'virtual'对此项目无效”。使用v2.0.50727(我的PC上最旧的版本)进行了测试。
ccppjava

3
@ccppjava在下面的Jorado注释中,您将实现虚拟接口的类成员标记为虚拟,以允许子类覆盖该类。
Christopher Stevenson

13

是的,就运行时而言,接口实现方法是虚拟的。这是一个实现细节,它使接口正常工作。虚拟方法在类的v表中获取插槽,每个插槽都有一个指向虚拟方法之一的指针。将对象强制转换为接口类型会生成一个指向实现接口方法的表部分的指针。现在,使用接口引用的客户端代码可以看到与接口指针等距偏移量0的第一个接口方法指针。

我在原始答案中未得到充分重视的是 最终属性。它可以防止派生类覆盖虚拟方法。派生类必须重新实现接口,实现方法遮盖了基类方法。这足以实现表示实现方法不是虚拟的C#语言协定。

如果在Example类中将Dispose()方法声明为虚拟方法,则将看到最终属性被删除。现在允许派生类覆盖它。


4

在大多数其他编译代码环境中,接口被实现为vtables-指向方法主体的指针列表。通常,实现多个接口的类在其内部编译器生成的元数据中的某处将具有接口vtable的列表,每个接口一个vtable(以便保留方法顺序)。这也是通常通常实现COM接口的方式。

但是,在.NET中,接口没有为每个类实现为不同的vtable。接口方法通过全局接口方法表建立索引,所有接口都包含在其中。因此,不必为使该方法实现接口方法而声明一个虚拟方法-全局接口方法表可以直接指向该类方法的代码地址。

在其他语言中,甚至在非CLR平台中,也不需要声明虚拟方法来实现接口。Win32上的Delphi语言就是一个示例。


0

它们不是虚拟的(就我们的看法而言,如果不是在底层实现方面(密封的虚拟),则可以理解-可以在此处阅读其他答案并自己学点东西:-)

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

该接口所做的只是提供类必须遵守的“合同”-一种模式(如果您愿意),即使调用者以前从未见过该特定类,也可以知道如何调用该对象。

然后由类来实现接口方法,该方法将在合同的范围内-虚拟的或“非虚拟的”(事实证明是密封的)。


该线程中的每个人都知道接口的用途。这个问题非常具体-生成的IL 对于接口方法虚拟的,而对于非接口方法则不是虚拟的。
Rex M 2010年

5
是的,在编辑问题后,批评答案真的很容易,不是吗?
杰森·威廉姆斯

0

接口是比类更抽象的概念,当您声明一个实现接口的类时,您只是在说:“该类必须具有接口中的这​​些特定方法,而无论是staticvirtualnon virtual重写,只要它具有相同的ID和相同的类型参数”。

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

但是,您可能是对的,我认为在接口中具有特定的“虚拟” /“覆盖”属性可能是一个好主意,以防您想限制实现特定接口的类方法。但是,这也意味着在两个接口上都有一个“非虚拟”,“ dontcareifvirtualornot”关键字。

我理解您的问题,因为当类方法必须使用“ @virtual”或“ @override”来确保方法是虚拟的时,我在Java中看到了类似的东西。


@override实际上并不会更改代码的行为或更改生成的字节码。它所做的是向编译器发出信号,即这样修饰的方法旨在被覆盖,这使编译器可以进行一些完整性检查。C#的工作方式不同;override是语言本身的一等关键字。
罗伯特·哈维
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.