若一个实例方法的声明中含有 virtual 修饰符,则称该方法为虚拟方法。若其中没有 virtual 修饰符,则称该方法为非虚拟方法。
非虚拟方法的实现是不会变的:无论是在声明它的类的实例上调用该方法还是在派生类的实例上调用,实现都是相同的。与此相反,一个虚拟方法的实现可以由派生类取代。取代所继承的虚拟方法的实现的过程称为重写该方法(第 10.5.4 节)。
在一个虚拟方法调用中,该调用所涉及的那个实例的运行时类型确定了要被调用的究竟是该方法的哪一个实现。在非虚拟方法调用中,相关的实例的编译时类型是决定性因素。准确地说,当在具有编译时类型 C 和运行时类型 R 的实例(其中 R 为 C 或者从 C 派生的类)上用参数列表 A 调用名为 N 的方法时,调用按下述规则处理:
- 首先,将重载决策应用于 C、N 和 A,以从在 C 中声明的和由 C 继承的方法集中选择一个特定的方法 M。第 7.5.5.1 节对此进行了描述。
- 然后,如果 M 为非虚拟方法,则调用 M。
- 否则(M 为虚拟方法),就会调用就 R 而言 M 的派生程度最大的那个实现。
对于在一个类中声明的或者由类继承的每个虚拟方法,存在一个就该类而言的派生程度最大的实现。就类 R 而言虚拟方法 M 的派生度最大的实现按下述规则确定:
- 如果 R 中含有关于 M 的 virtual 声明,则这是 M 的派生程度最大的实现。
- 否则,如果 R 中含有关于 M 的 override 声明,则这是 M 的派生程度最大的实现。
- 否则,就 R 而言 M 的派生程度最大的实现与就 R 的直接基类而言 M 的派生程度最大的实现相同。
下列实例阐释虚拟方法和非虚拟方法之间的区别:
class A
{
public void F() { Console.WriteLine("A.F"); }
public virtual void G() { Console.WriteLine("A.G"); }
}
class B: A
{
new public void F() { Console.WriteLine("B.F"); }
public override void G() { Console.WriteLine("B.G"); }
}
class Test
{
static void Main() {
B b = new B();
A a = b;
a.F();
b.F();
a.G();
b.G();
}
}
在该示例中,A
引入一个非虚拟方法 F
和一个虚拟方法 G
。类 B
引入一个新的非虚拟方法 F
,从而隐藏了继承的 F
,并且还重写了继承的方法 G
。此例产生输出:
.F
B.F
B.G
B.G
请注意,语句 a.G()
实际调用的是 B.G
而不是 A.G
。这是因为,对调用哪个实际方法实现起决定作用的是该实例的运行时类型(即 B
),而不是该实例的编译时类型(即 A
)。
由于一个类中声明的方法可以隐藏继承来的方法,因此同一个类中可以包含若干个具有相同签名的虚拟方法。这不会造成多义性问题,因为除派生程度最大的那个方法外,其他方法都被隐藏起来了。在下面的示例中
using System;
class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() { Console.WriteLine("D.F"); }
}
class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}
C
类和 D
类均含有两个具有相同签名的虚拟方法:A
引入的虚拟方法和 C
引入的虚拟方法。但是,由 C
引入的方法隐藏了从 A
继承的方法。因此,D
中的重写声明所重写的是由 C
引入的方法,D
不可能重写由 A
引入的方法。此例产生输出:
B.F
B.F
D.F
D.F
请注意,通过访问 D
的实例(借助一个派生程度较小的类型,它的方法没有被隐藏起来),可以调用被隐藏的虚拟方法。