有关Virtual / new …以及接口的更多信息!

昨天我发布了一个关于new / virtual / override关键字的问题,我从你的答案中学到了很多东西。 但我仍有一些疑虑。

在所有“盒子”之间,我与类型的方法表中真正发生的事情失去联系。 例如:

interface I1 { void Draw(); } interface I2 { void Draw(); } class A : I1, I2 { public void Minstance() { Console.WriteLine("A::MInstance"); } public virtual void Draw() { Console.WriteLine("A::Draw"); } void I2.Draw() { Console.WriteLine("A::I2.Draw"); } } class B : A, I1, I2 { public new virtual void Draw() { Console.WriteLine("B::Draw"); } void I1.Draw() { Console.WriteLine("B::I1.Draw"); } } class Test { public static void Main() { A a = new B(); a.Draw(); I1 i1 = new A(); i1.Draw(); I2 i2 = new B(); i2.Draw(); B b = (B)a; b.Draw(); } } } 

在本练习中提出的问题是:根据代码填写类型的方法表,并解释运行Main()生成的输出。

我的答案是:在类型A中我们有3个方法:MInstance(),Draw() – A :: Draw版本 – 和I2 :: Draw在类型B中我们有4个方法:来自A的BInstance,B :: Draw,I1 :: Draw和I2 :: Draw

我对我的答案不是很有信心,这就是为什么我发布这个问题。 当我们实现接口时,它在方法表上为所述接口的方法创建了一个新槽? 我们不应该在A类中实现I2 :: Draw吗?

同样,当我们使用接口变量(如i1.Draw())调用方法时,我理解我们正在进行动态调度,因此我们应该查看变量所持有的对象的类型(在这种情况下为类型A) )并在A的方法表中搜索专门调用I1.Draw的方法。 但是,如果我们找不到它呢? 我应该如何处理这些案件? 为了成功解决这些问题,我应该知道任何经验法则吗?

很抱歉这个问题很无聊,但我真的需要解开这个问题;)

干杯!

好问题。

考虑这一点的方法是:接口获得自己的一组插槽。 需要一个实现接口的类来填充这些插槽。

  • 接口I1有一个我们称之为I1SLOT的插槽。
  • 接口I1有一个我们称之为I2SLOT的插槽。
  • A类有两个自己的插槽,AMinSLOT和ADrawSLOT。
  • A类有三种方法,我们称之为AMinMethod,ADrawMethod和AI2DrawMethod。
  • 当您说“new A”时,运行时有四个要填写的插槽。
  • I1SLOT用ADrawMethod填充。
  • I2SLOT填写了AI2DrawMethod。
  • AMinSLOT填充了AMinMethod。
  • 使用ADrawMethod填充ADrawSLOT。
  • B类有三个插槽。 它inheritance了AMinSLOT和ADrawSLOT,并定义了一个新槽,BDrawSLOT。
  • B类有两种方法,BDrawMethod和BI1DrawMethod。
  • 当你说“new B”时,运行时有五个插槽可以填写。
  • 使用BI1DrawMethod填充I1SLOT。
  • 用BDrawMethod填写I2SLOT。
  • AMinSLOT填充了AMinMethod。
  • 使用ADrawMethod填充ADrawSLOT。
  • 用BDrawMethod填充BDrawSLOT。

现在请记住,重载解析的工作是根据类型和参数选择插槽。 没有参数,因此编译器只有类型可以使用。

  • 当您在编译时类型A的对象上调用Draw时,最佳匹配是ADrawSLOT。
  • 当您在编译时类型B的对象上调用Draw时,最佳匹配是BDrawSLOT。
  • 当您在编译时类型I1的对象上调用Draw时,最佳匹配是I1SLOT。
  • 当您在编译时类型I2的对象上调用Draw时,最佳匹配是I2SLOT。

编译器生成的代码“在运行时调用所选插槽中的任何方法”。

加起来:

 A a1 = new A(); A a2 = new B(); B b = new B(); (a1 as A).Draw(); // ADrawSLOT contains A::Draw (a1 as I1).Draw(); // I1SLOT contains A::Draw (a1 as I2).Draw(); // I2SLOT contains A::I2.Draw (a2 as A).Draw(); // ADrawSLOT contains A::Draw (a2 as B).Draw(); // BDrawSLOT contains B::Draw (a2 as I1).Draw(); // I1SLOT contains B::I1.Draw (a2 as I2).Draw(); // I2SLOT contains B::Draw (b as A).Draw(); // ADrawSLOT contains A::Draw (b as B).Draw(); // BDrawSLOT contains B::Draw (b as I1).Draw(); // I1SLOT contains B::I1Draw (b as I2).Draw(); // I2SLOT contains B::Draw 

如果您对如何实现它感兴趣,请使用ILDASM反汇编程序,然后查看元数据表25(0x19),MethodImpl表。 这个程序的MethodImplTable是:

 1 == 0:TypeDef[2000004], 1:MethodDefOrRef[06000005], 2:MethodDefOrRef[06000002] 2 == 0:TypeDef[2000005], 1:MethodDefOrRef[06000008], 2:MethodDefOrRef[06000001] 

然后你可以查看typedef和methoddef表,你会看到这个解码为:

 in type A the method A::I2.Draw implements the method I2::Draw in type B the method B::I1.Draw implements the method I1::Draw 

MethodImpl表是CLI如何表示“我需要在此插槽中粘贴某些内容与常规名称匹配规则选择的内容不同”的概念。 通常,名称匹配规则会选择一个名为“Draw”的方法进入该槽,而不是“I1.Draw”或“I2.Draw”。

您可能还想阅读CLI规范的第II部分第22.27节。

根据我的理解,你会问,给定一个带有一些重写方法的子类,如何知道将调用哪个方法。

基本上,我可以想到3种选择:

  1. 如果SubClass没有在BaseClass上定义方法,那么将调用BaseClass的方法
  2. 如果SubClass在BaseClass上覆盖了一个方法,那么将调用SubClass的方法
  3. 如果SubClass定义了一个也存在于BaseClass上的NEW方法,那么被调用的方法将取决于对象的REFERENCE(无论你的变量是否被键入为BaseClass或SubClass)

我希望我理解你的问题

编辑:关于接口的快速说明:如果BaseClass实现了IInterface,那么派生自BaseClass的SubClass已经具有IInterface的实现,并且不需要重新实现它。 例如,如果有一个带有MPH属性的IVehicle,并且有一个实现该属性的BaseVehicle,因为Car派生自BaseVehicle,Car 已经有一个MPH属性

“方法表”? 不,这只是一份必须满足的合同。 I1I2的契约可以用相同的Draw方法来满足,并且将是,除非你用一个隐式实现分开一个,就像这里的情况一样。 没有这个,单个Draw方法就可以了。

在所有情况下, 除非将引用强制转换为显式实现的接口类型, 否则将调用公共Draw

 interface I1 { void Draw(); } interface I2 { void Draw(); } class A : I1, I2 { // this is just a method in A public void Minstance() { Console.WriteLine("A::MInstance"); } // method in A, also implements I1.Draw. May be overridden in // derived types. public virtual void Draw() { Console.WriteLine("A::Draw"); } // implements I2.Draw, accessible on object a of type A via ((I2)a).Draw() void I2.Draw() { Console.WriteLine("A::I2.Draw"); } } class B : A, I1, I2 { // new method in B, does not override A.Draw, so A.Draw is only // callable on an object b of type B via ((A)b).Draw(). Types // derived from B may override this method, but can't override // A.Draw because it's hidden. Also implements I2.Draw (see notes). public new virtual void Draw() { Console.WriteLine("B::Draw"); } // implements I1.Draw, accessible on object b of type B via ((I1)b).Draw() void I1.Draw() { Console.WriteLine("B::I2.Draw"); } } 

注释和参考:我必须回到标准(ECMA-334),它可以在§20.4.4接口重新实现中找到:

允许inheritance接口实现的类通过将其包含在基类列表中来重新实现接口。

接口的重新实现遵循与接口的初始实现完全相同的接口映射规则。 因此,inheritance的接口映射对于为接口的重新实现而建立的接口映射没有任何影响。 [ 例子 :在声明中

 interface IControl { void Paint(); } class Control: IControl { void IControl.Paint() {…} } class MyControl: Control, IControl { public void Paint() {} } 

ControlIControl.Paint映射到Control.IControl.Paint事实不会影响MyControl的重新实现, MyControlIControl.Paint映射到MyControl.Paint结束例子 ]

inheritance的公共成员声明和inheritance的显式接口成员声明参与重新实现的接口的接口映射过程。 [ 例如

 interface IMethods { void F(); void G(); void H(); void I(); } class Base: IMethods { void IMethods.F() {} void IMethods.G() {} public void H() {} public void I() {} } class Derived: Base, IMethods { public void F() {} void IMethods.H() {} } 

这里, DerivedIMethods的实现将接口方法映射到Derived.FBase.IMethods.GDerived.IMethods.HBase.I 结束例子 ]

当一个类实现一个接口时,它隐式地也实现了该接口的所有基接口。 同样,接口的重新实现也隐含地是所有接口的基接口的重新实现。 [ 例如

 interface IBase { void F(); } interface IDerived: IBase { void G(); } class C: IDerived { void IBase.F() {…} void IDerived.G() {…} } class D: C, IDerived { public void F() {…} public void G() {…} } 

在这里, IDerived重新实现也重新实现IBase ,将IBase.F映射到DF结束例子 ]

除了其他答案,我发布了一个正确的答案:

  A a = new B(); a.Draw(); //A::Draw I1 i1 = new A(); i1.Draw(); //A::Draw I2 i2 = new B(); i2.Draw();// B::Draw B b = (B) a; b.Draw();// B::Draw 

首先是新的和虚拟的解释

  1. new用于新建对象,即创建类的实例或创建类的对象。
  2. new也用于(覆盖字面)新的基类中的现有方法。

例如

 A a2= new B() (creates a new object using constructor of B, because b is of type A, therefore it works ) and a2.draw() will result in execution of A class draw because the type is A. 

虚拟

  1. 这确定了该方法是虚拟的(即不是永久的或物理的)并且应该被覆盖,即它的定义应该在底层类中提供。

覆盖

  1. 用于表示您要覆盖此方法的关键字等。