访问重写方法

有一个练习“OverloadResolutionOverride”

以下代码的输出结果如何:

class Foo { public virtual void Quux(int a) { Console.WriteLine("Foo.Quux(int)"); } } class Bar : Foo { public override void Quux(int a) { Console.WriteLine("Bar.Quux(int)"); } public void Quux(object a) { Console.WriteLine("Bar.Quux(object)"); } } class Baz : Bar { public override void Quux(int a) { Console.WriteLine("Baz.Quux(int)"); } public void Quux(params T[] a) { Console.WriteLine("Baz.Quux(params T[])"); } } void Main() { new Bar().Quux(42); new Baz().Quux(42); } 

答案是:

 Bar.Quux(object) Baz.Quux(params T[]) 

该网站有一个解释 :

如果编译器在“当前”类中为方法调用找到了合适的签名,则编译器将不会查找父类。

是否认为重载的Quux(int)方法在基类中,而不是在当前的方法中? 如果是这样,我如何在当前类中调用Quux(int)方法?

这绝对是编译器执行方法解析时会发生的有趣影响。

让我们看看规范 ,看看我们可以从中得到什么(这是我的理解,我绝不是阅读规范的专家,所以如果我出错了,请告诉我!)。

第一步是找到具有适用于第一个子弹读数的参数的方法:

A [其中A是参数列表]中的每个参数对应于函数成员声明中的参数,如对应参数中所述,并且无参数对应的任何参数都是可选参数。

现在我们来看看对应的参数是什么,我们得到:

对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收器的静态类型开始,并搜索其基类。

我们也有

对于所有其他函数成员和委托,只有一个参数列表,即使用的参数列表。

因此,对于Bar类,我们找到两种适合该法案的方法:

  1. Bar.Quux(object) 。 这是来自第二段,因为它是直接在类型上定义的。
  2. Foo.Quux(int) 。 这是通过跟随覆盖virtual方法声明来从派生类型。

对于Baz课,我们得到3:

  1. Baz.Quux(int[]) 。 这是在类型上定义的。
  2. Bar.Quux(object) 。 这是在父类中定义的,并且对Baz的范围可见。
  3. Foo.Quux(int) 。 这是覆盖的虚拟声明。

这给了我们Bar 2个方法匹配,以及Baz 3个可能的方法匹配。 这意味着我们需要使用下面的下一组(Emphasis mine)进一步剔除参数集:

候选方法集合被简化为仅包含来自大多数派生类型的方法:对于集合中的每个方法CF,其中C是声明方法F的类型, 所有在基本类型C中声明的方法都从集合 。 此外,如果C是除object之外的类类型,则从集合中删除在接口类型中声明的所有方法。 (后一条规则仅在方法组是对具有除object之外的有效基类和非空有效接口集的类型参数进行成员查找的结果时才会生效。)

因此,对于Bar ,我们将剔除Foo.Quux(int)因为它是在基类型中声明的,因此从集合中删除。

对于Baz ,我们删除了以下两种方法,因为它们都是在基类型中声明的:

  1. Bar.Quux(object)
  2. Foo.Quux(int)

现在,每个集合只有一个方法,我们可以执行两个方法Bar.Quux(object)Baz.Quux(int[])

选择正确的方法

所以这引出了一个问题,我们可以强制调用正确的方法吗? 并且基于第二个解决步骤,它使用最派生的类型,答案是我们可以。

如果我们想调用Foo的方法,我们需要将调用者的类型设置为Foo 。 如果我们想让Baz调用Bar的方法,那么我们需要将Baz设置为Bar

考虑以下方法调用:

 new Bar().Quux(42); new Baz().Quux(42); ((Foo)new Bar()).Quux(42); ((Foo)new Baz()).Quux(42); ((Bar)new Baz()).Quux(42); 

我们得到以下输出:

 Bar.Quux(object) Baz.Quux(params T[]) Bar.Quux(int) Baz.Quux(int) Bar.Quux(object) 

并且能够针对我们想要使用如上所述的类似方法分辨率的特定方法。

更进一步

如果我们将Baz的定义更改为:

 class Baz : Bar { public override void Quux(int a) { Console.WriteLine("Baz.Quux(int)"); } } 

然后调用方法: new Baz().Quux(42); 我们的输出仍然是: Bar.Quux(object) 。 这看起来很奇怪,因为我们直接在Baz定义了一个方法覆盖。 但是,该方法的原始类型是Foo ,其特性不如Bar 。 因此,当我们匹配我们的参数列表时,我们最终得到了Bar.Quux(object)Foo.Quux(int)因为int参数列表是在Foo上定义的。 因此, Foo.Quux(int)在第二步中被淘汰,因为BarFoo派生更多,我们称之为Bar.Quux(object)

我认为这里的故事的道德是不要命名方法与覆盖相同!

另外注意,调用你想要的方法的方法是(Bar)(new Baz).Quux(42);

但是为什么编译器选择generics方法呢? 在解决方法解析时,是不是优先于通用方法的非通用匹配?

是的,但另一条规则也适用于此; 方法解析将更喜欢最近的适用方法,最接近的意思是声明方法与呼叫站点相关的位置。

嗯… Quux(int)Baz宣布,所以我不确定……

没有! Overriden方法属于实现虚方法的类; 在这种情况下,就编译器而言, Quux的“所有者”是Foo ,而不是Baz

为什么编译器会应用这样的规则? 好吧,假设以下情况:

Alpha公司发布了A类:

 class A { ... } 

公司Beta继续前进并通过扩展它来消耗A

 class B: A { public void Frob(T frobbinglevel) { ... } ... } 

一切都是骄傲的,并且Beta凭借其精彩的表现取得了巨大的成功。 阿尔法公司决定自己动手,并发布新版A

 class A { public virtual void Frob(int frobbinglevel) { ... } ... } 

Beta使用Alpha的更新重新编译其库,现在应该发生什么? 如果编译器开始在A.Frob(1)上选择A.Frob(1) B.Frob(1)A.Frob是非通用的完全匹配吗?

但是嘿…… Beta没有改变任何东西,现在它的类没有按预期工作! 突然间,在没有任何代码更改的情况下,方法解析就是选择不同的方法,打破客户……这似乎不对。 这就是规则存在的原因。

要从Bar或Baz调用Quux(int),你应该将Bar或Baz转换为Foo:

 Foo bar = new Bar(); Foo baz = new Baz(); bar.Quux(42); baz.Quux(42); 

输出:

 Bar.Quux(int) Baz.Quux(int)