在“C#编程语言”一书中,Eric Lippert提到了这一点:
这里的一个细微之处在于,重写的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。
这句话有什么意义?为什么重写的虚方法被认为是引入它的类的方法(或其他方法),因为除非你处理派生类,否则永远不会调用重写的方法?
答案 0 :(得分: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
因为virtual方法是基类的成员,所以可以使用基类类型的引用来调用重写方法。如果不是,则需要派生类型的引用来调用覆盖方法。
当您遮蔽方法而不是覆盖它时,阴影方法是派生类的成员。根据您将调用原始方法或阴影方法的引用类型:
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
答案 1 :(得分:0)
了解被重写的虚方法属于引入它的类,而不是覆盖它的类,这使得更容易理解类成员的绑定方式。除了使用dynamic
个对象外,C#中的所有绑定都在编译时解析。如果BaseClass
声明虚拟方法foo
,DerivedClass:BaseClass
覆盖foo
,则尝试在foo
类型的变量上调用BaseClass
的代码将始终绑定到虚拟方法“slot”BaseClass.foo
,这将反过来指向实际的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()
。 MegaDerivedClass
具有相同的两种方法。请注意,尝试覆盖SubDerivedClass()
的{{1}}派生的类将覆盖foo
,不会影响SubDerivedClass.foo()
;如上所述,BaseClass.foo()
的衍生物不能覆盖SubDerivedClass
。另请注意,将BaseClass.Foo
或其子类的实例强制转换为SubDerivedClass
或DerivedClass
将公开BaseClass
虚拟方法以进行调用。
顺便说一句,如果BaseClass.foo
中的方法声明为SubDerivedClass
,则同一程序集中的其他类将无法覆盖friend new virtual void foo() {Console.WriteLine("SubDerived.Foo");
(因为任何覆盖{{1}的尝试都是如此}}会覆盖BaseClass.foo()
),但是从foo()
派生的程序集之外的类将看不到SubDerivedClass.foo()
,因此可以覆盖SubDerivedClass
。
答案 2 :(得分:0)
以下是该书的完整引用:
这里的一个细微之处在于,重写的虚方法仍然被认为是引入它的类的方法,而不是覆盖它的类的方法。在某些情况下,重载决策规则更喜欢更多派生类型的成员和基类型的成员;覆盖方法不会“移动”该方法属于此层次结构的位置。
在本节的最开始,我们注意到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(float)
为什么d.M2(1)
会调用Derived.M2(float)
即使Derived.M2(int)
更匹配?
当编译器确定M1
中d.M1(1)
引用的内容时,编译器会发现M1(int)
中引入了M1(float)
和Derived
,所以两个重载都是适用的候选人。编译器选择M1(int)
超过M1(float)
作为整数参数1
的最佳匹配。
当编译器确定M2
中d.M2(1)
引用的内容时,编译器会发现M2(float)
中引入了Derived
并且是适用的候选者。根据重载决策规则,“如果派生类中的任何方法适用,则基类中的方法不是候选者”。由于M2(float)
适用,此规则会阻止M2(int)
成为候选人。即使M2(int)
更适合整数参数,即使它在Derived
中被覆盖,它仍然被认为是Base
的方法。