2

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

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

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

4

3 回答 3

3

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

例子:

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
于 2012-08-26T15:38:47.133 回答
1

以下是这本书的完整引用:

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

在本节的开头,我们注意到 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);
    }
}

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

派生的.M1(int)
Derived.M2(浮点数)

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

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

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

于 2012-08-26T16:58:54.483 回答
0

了解重写的虚方法属于引入它的类,而不是重写它的类,可以更容易地理解类成员的绑定方式。除了使用dynamic对象时,C# 中的所有绑定都在编译时解析。如果 aBaseClass声明了一个虚拟方法fooDerivedClass:BaseClass覆盖了foo,那么尝试调用foo类型变量的代码BaseClass将始终绑定到虚拟方法 "slot" BaseClass.foo,而虚拟方法 "slot" 又将指向实际DerivedClass.foo方法。

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

顺便说一句,如果一个类可以同时覆盖一个基类成员并创建一个新成员,那么有时它会很有用。例如,给定一个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(). AMegaDerivedClass具有相同的两种方法。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()

于 2012-08-26T15:54:10.583 回答