在“The C# Programming Language”一书中,Eric Lippert 提到了这一点:
这里的一个微妙点是,被覆盖的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。
这句话的意义何在?为什么重写的虚方法被认为是引入它的类的方法(或以其他方式),因为除非您正在处理派生类,否则永远不会调用重写的方法?
在“The C# Programming Language”一书中,Eric Lippert 提到了这一点:
这里的一个微妙点是,被覆盖的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。
这句话的意义何在?为什么重写的虚方法被认为是引入它的类的方法(或以其他方式),因为除非您正在处理派生类,否则永远不会调用重写的方法?
当您有一个类型的引用指向不同类型的对象时,这很重要。
例子:
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
以下是这本书的完整引用:
这里的一个微妙点是,被覆盖的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。在某些情况下,重载决策规则更喜欢派生类型的成员而不是基类型中的成员;覆盖方法不会“移动”该方法在此层次结构中所属的位置。
在本节的开头,我们注意到 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)
当编译器确定M1
ind.M1(1)
指的是什么时,编译器会看到这两者M1(int)
和M1(float)
都被引入 in Derived
,因此这两个重载都是适用的候选者。编译器选择M1(int)
overM1(float)
作为整数参数的最佳匹配1
。
当编译器确定M2
ind.M2(1)
指的是什么时,编译器会看到M2(float)
in 被引入Derived
并且是适用的候选者。根据重载决议规则,“如果派生类中的任何方法适用,则基类中的方法不是候选方法”。因为M2(float)
适用,所以这条规则防止M2(int)
成为候选人。即使M2(int)
是整数参数的更好匹配,即使它在 中被覆盖Derived
,它仍然被认为是Base
.
了解重写的虚方法属于引入它的类,而不是重写它的类,可以更容易地理解类成员的绑定方式。除了使用dynamic
对象时,C# 中的所有绑定都在编译时解析。如果 aBaseClass
声明了一个虚拟方法foo
并DerivedClass: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.foo
DoFoo<SubDrivedClass>(subDerivedInstance)
SubDerivedClass
foo
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()
. AMegaDerivedClass
具有相同的两种方法。SubDerivedClass()
请注意,尝试覆盖的派生类foo
将覆盖SubDerivedClass.foo()
并且不会影响BaseClass.foo()
;使用上述声明,没有任何派生SubDerivedClass
可以覆盖BaseClass.Foo
. 另请注意,将其实例SubDerivedClass
或其子类强制转换为DerivedClass
或BaseClass
将公开BaseClass.foo
用于调用的虚拟方法。
顺便说一句,如果方法声明SubDerivedClass
已经是friend new virtual void foo() {Console.WriteLine("SubDerived.Foo");
,则同一程序集中的其他类将无法覆盖BaseClass.foo()
(因为任何覆盖尝试foo()
都会覆盖SubDerivedClass.foo()
),但是从程序集外部派生的类SubDerivedClass
将看不到SubDerivedClass.foo()
并因此可以覆盖BaseClass.foo()
。