【问题标题】:Overridden virtual method in C#C#中重写的虚方法
【发布时间】:2012-08-26 15:28:26
【问题描述】:

在“The C# Programming Language”一书中,Eric Lippert 提到了这一点:

这里有个微妙的地方是,被覆盖的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。

这句话的意义是什么?为什么重写的虚方法被认为是引入它的类的方法(或其他方法),因为除非您处理派生类,否则永远不会调用重写的方法?

【问题讨论】:

  • 如果你使用反射可能很重要。
  • @TonyBasile 它与重载决议有关,而不是反射。
  • 我认为@PeterRitchie 认为这与反射无关。当覆盖类中的虚拟方法并使用BindingFlags.DeclaredOnly 反映该类时,仍会返回该覆盖的方法。
  • 完整的语句如下:在这个层次结构中。”

标签: c#


【解决方案1】:

当你有一个类型的引用指向不同类型的对象时,这很重要。

例子:

public class BaseClass {
  public virtual int SomeVirtualMethod() { return 1; }
}

public class DerivedClass : BaseClass {
  public override int SomeVirtualMethod() { return 2; }
}

BaseClass ref = new DerivedClass();
int test = ref.SomeVirtualMethod(); // will be 2

因为虚方法是基类的成员,所以可以使用基类类型的引用来调用覆盖方法。如果不是,则需要派生类型的引用来调用覆盖方法。

当您隐藏一个方法而不是覆盖它时,该隐藏方法是派生类的成员。根据引用的类型,您将调用原始方法或阴影方法:

public class BaseClass {
  public int SomeMethod() { return 1; }
}

public class DerivedClass : BaseClass {
  public new int SomeMethod() { return 2; }
}

BaseClass ref = new DerivedClass();
int test = ref.SomeMethod(); // will be 1

DerivedClass ref2 = ref;
int test2 = ref2.SomeMethod(); // will be 2

【讨论】:

  • 所以他的观点适用于所有使用多态的现代语言,对吗?
  • @Boon:是的,这就是虚拟方法的工作原理,因此它适用于任何面向对象的语言。
  • 他的观点仍然没有意义。如果 BaseClass 引用持有 DerivedClass 实例,从技术上讲,您仍然持有 DerivedClass,因此该方法仍然属于 DerivedClass 实例。他可以说 BaseClass 引入了原始定义或实现,但说“重写的虚拟方法仍然被认为是引入它的类的方法”没有任何意义,这听起来像是 BaseClass 拥有它,或者实现存在于 BaseClass 中。
  • @Boon:将其与阴影方法进行比较,其中方法属于实现它们的类。要调用的方法在编译时根据引用类型确定。对于虚拟方法,除了处理虚拟调用的内部机制之外,覆盖方法基本上对所有内容都隐藏。基类中定义的虚方法是唯一可以调用的,然后在运行时根据对象的实际类型来确定哪个方法来处理调用。
【解决方案2】:

这是本书的完整引述:

这里有个微妙的地方是,被覆盖的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。在某些情况下,重载决策规则更喜欢派生类型的成员而不是基类型中的成员;覆盖方法不会“移动”该方法在此层次结构中所属的位置。

在本节的开头,我们注意到 C# 在设计时考虑了版本控制。这是有助于防止“脆弱基类综合症”导致版本控制问题的功能之一。

完整的引用清楚地表明,Eric Lippert 专门讨论了方法重载,而不仅仅是虚拟方法的工作原理。

例如,考虑以下程序:

class Base
{
    public virtual void M2(int i) { }
}

class Derived : Base
{
    public void M1(int i) { Console.WriteLine("Derived.M1(int)"); }
    public void M1(float f) { Console.WriteLine("Derived.M1(float)"); }

    public override void M2(int i) { Console.WriteLine("Derived.M2(int)"); }
    public void M2(float f) { Console.WriteLine("Derived.M2(float)"); }

    public static void Main()
    {
        Derived d = new Derived();
        d.M1(1);
        d.M2(1);
    }
}

我认为许多开发人员会惊讶于输出是

Derived.M1(int)
Derived.M2(浮点数)

为什么d.M2(1) 会调用Derived.M2(float),即使Derived.M2(int) 是更好的匹配?

当编译器确定d.M1(1) 中的M1 指的是什么时,编译器会看到M1(int)M1(float) 都在Derived 中引入,因此这两个重载都是适用的候选者。编译器选择 M1(int) 而不是 M1(float) 作为整数参数 1 的最佳匹配。

当编译器确定d.M2(1) 中的M2 指的是什么时,编译器会看到M2(float) 是在Derived 中引入的,并且是适用的候选者。根据重载决议规则,“如果派生类中的任何方法适用,则基类中的方法不是候选方法”。因为M2(float) 是适用的,所以这条规则会阻止M2(int) 成为候选人。尽管M2(int) 是整数参数的更好匹配,即使它在Derived 中被覆盖,它仍然被认为是Base 的方法。

【讨论】:

  • +1 哇。很好的答案,我当然很惊讶!反对票是怎么回事?
  • @Cristi:在较早的版本中,我不小心写了“完整的引用清楚地表明 Eric Lippert 专门谈论方法重载,而不是覆盖”而不是“......不是只是 覆盖。”我在投票后编辑了我的答案。
【解决方案3】:

理解被覆盖的虚方法属于引入它的类,而不是覆盖它的类,这样就更容易理解类成员的绑定方式。除了使用dynamic 对象时,C# 中的所有绑定都在编译时解析。如果BaseClass 声明了一个虚拟方法foo,并且DerivedClass:BaseClass 覆盖foo,则尝试在BaseClass 类型的变量上调用foo 的代码将始终绑定到虚拟方法“slot”@987654328 @,这又将指向实际的DerivedClass.foo 方法。

这种理解在处理泛型时尤其重要,因为在 C# 中,与 C++ 不同,泛型类型的成员是根据泛型的约束来绑定的,而不是根据具体的泛型类型。例如,假设有一个SubDerivedClass:DerivedClass 创建了一个new virtual 方法foo(),并且定义了一个方法DoFoo<T>(T param) where T:BaseClass {param.foo();}param.foo() 调用将绑定到 BaseClass.foo,即使该方法被调用为 DoFoo<SubDrivedClass>(subDerivedInstance)。如果在调用foo 之前将参数强制转换为SubDerivedClass,则调用将绑定到SubDrivedClass.foo(),但编译器在生成DoFoo<T> 时无法判断T 将比BaseClass 更具体, 它不能绑定到BaseClass 中不存在的任何东西。

顺便说一句,如果一个类可以同时覆盖一个基类成员并创建一个新成员,那么有时它会很有用;例如,给定一个具有一些抽象只读属性的抽象基类ReadableFoo,如果一个类MutableFoo 既可以为该属性提供覆盖并定义一个具有相同名称的读写属性,那将很有帮助。不幸的是,.net 不允许这样做。鉴于这样的限制,最好的方法可能是ReadableFoo 提供一个具体的非虚拟只读属性,该属性调用具有不同名称的protected abstract 方法来获取值。这样一来,派生类就可以用读写属性(调用相同的虚拟方法进行读取,或调用新的(可能是虚拟的)写入方法)隐藏只读属性。

(以下未测试)

class BaseClass
{
    public virtual void foo() {Console.WriteLine("BaseClass.Foo");
}
class DerivedClass:BaseClass
{
    public override void foo() {Console.WriteLine("Derived.Foo");
}
class SubDerivedClass:DerivedClass
{
    public new virtual void foo() {Console.WriteLine("SubDerived.Foo");
}
class MegaDerivedClass:SubDerivedClass
{
    public override void foo() {Console.WriteLine("MegaDerived.Foo");
}
void DoFoo1<T>(T param) where T:BaseClass 
    {
        param.foo();
}
void DoFoo1<T>(T param) where T:SubDerivedClass 
    {
        param.foo();
}
void test(void)
{
    var megaDerivedInstance = new MegaDerivedClass();
    DoFoo1<MegaDerivedClass>(megaDerivedInstance);
    DoFoo2<MegaDerivedClass>(megaDerivedInstance);
}

SubDerivedClass 有两个虚拟的 foo() 方法:BaseClass.foo()SubDerivedClass.foo()MegaDerivedClass 具有相同的两种方法。请注意,从SubDerivedClass() 派生的类尝试覆盖foo 将覆盖SubDerivedClass.foo(),并且不会影响BaseClass.foo();通过上述声明,SubDerivedClass 的任何派生项都不能覆盖BaseClass.Foo。另请注意,将SubDerivedClass 或其子类的实例强制转换为DerivedClassBaseClass 将公开BaseClass.foo 用于调用的虚拟方法。

顺便说一句,如果SubDerivedClass 中的方法声明是friend new virtual void foo() {Console.WriteLine("SubDerived.Foo");,同一程序集中的其他类就不可能覆盖BaseClass.foo()(因为任何覆盖foo() 的尝试都会覆盖SubDerivedClass.foo()) ,但从 SubDerivedClass 派生的程序集外部的类不会看到 SubDerivedClass.foo(),因此可以覆盖 BaseClass.foo()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-05
    • 1970-01-01
    • 2013-05-06
    • 2014-05-23
    • 1970-01-01
    • 1970-01-01
    • 2016-10-22
    相关资源
    最近更新 更多