C# virtual 关键字
virtual 使用
c#的方法,默认为非虚方法,如果一个方法被声明为 virtual (虚方法),则继承该方法的任何类都可以实现它自己的版本。
public class BaseEngineer
{
public virtual void Work()
{
Console.WriteLine("BaseEngineer.Work");
}
}
virtual 方法可以通过 Override 和 new 关键字来进行版本控制、重写.
Override 关键字
Override 用来重写基类的虚方法。如下例子中,override 关键字可以确保派生类 JuniorEngineer / SeniorEngineer 的任何对象将使用 work 的派生类版本,同时又可以通过 base 关键字访问基类的版本。
public class JuniorEngineer : BaseEngineer
{
public override void Work()
{
base.Work();
Console.WriteLine("=>JuniorEngineer.Work");
}
}
public class SeniorEngineer : BaseEngineer
{
public override void Work()
{
base.Work();
Console.WriteLine("=>SeniorEngineer.Work");
}
}
var juniorEngineer = new JuniorEngineer();
juniorEngineer.Work();
var seniorEngineer = new SeniorEngineer();
seniorEngineer.Work();
----------------------Output---------------------------
BaseEngineer.Work
=>JuniorEngineer.Work
BaseEngineer.Work
=>SeniorEngineer.Work
New 关键字
派生类的方法前面带有 new 关键字,则该方法被定义为独立于基类中的方法,用 new 关键字可以隐藏基类中的虚方法。
假如派生类中的方法前面没有 new / override 关键字,那么编译期会发出 Warning,并将该方法作为有 new 关键字去执行。
public class SuperEngineer : BaseEngineer
{
public new void Work()
{
base.Work();
Console.WriteLine("=>Eat a bug!");
Console.WriteLine("=>Create lots of bugs!");
}
}
var superEngineer = new SuperEngineer();
superEngineer.Work();
----------------------Output---------------------------
BaseEngineer.Work
=>Eat a bug!
=>Create lots of bugs!
何时使用 Override / New 关键字?
从上面可以看到,无论使用 Override / New 中哪一个,派生类的实例似乎都可以重新定义方法内容,也可以调用base的方法,那么为何要有2个关键字呢?
上面的例子,我们稍微改动一点,所有 override / new 的实现中,都不再调用 base 的虚方法,并通过基类访问派生类实例:
public class JuniorEngineer : BaseEngineer
{
public override void Work()
{
Console.WriteLine("=>JuniorEngineer.Work");
}
}
public class SeniorEngineer : BaseEngineer
{
public override void Work()
{
Console.WriteLine("=>SeniorEngineer.Work");
}
}
public class SuperEngineer : BaseEngineer
{
public new void Work()
{
Console.WriteLine("=>Eat a bug!");
Console.WriteLine("=>Create lots of bugs!");
}
}
var enginners = new List<BaseEngineer>
{
juniorEngineer,
seniorEngineer,
superEngineer
};
foreach (var enginner in enginners)
{
enginner.Work();
}
--------------Output----------------------
=>JuniorEngineer.Work
=>SeniorEngineer.Work
BaseEngineer.Work
Junior / Senior 都如期工作,调用了派生类中的方法,而最后的 SuperEngineer 并没有执行派生类的方法,而是执行了基类的虚方法。
因为数组的类型为 BaseEngine,且派生类 SuperEngineer 使用了 new 关键字重新定义方法,因此最终执行的是基类虚方法。
不要在构造函数里调用虚函数
这一条是引自《Effective c#》,在构建对象的过程中调用虚方法会使程序表现出奇怪的行为,因为这个时候对象并没有完全构造好。将书中的例子改的更为详尽一些:
public class VirtualB
{
protected VirtualB()
{
Console.WriteLine("VritualB constructor start");
VFunc();
Console.WriteLine("VritualB constructor end");
}
protected virtual void VFunc()
{
Console.WriteLine("VirtualB.VFunc");
}
}
public class Derived : VirtualB
{
private readonly string msg = "Set by initializer";
public Derived(string msg)
{
Console.WriteLine("Derived constructor start");
this.msg = msg;
VFunc();
Console.WriteLine("Derived constructor end");
}
protected override void VFunc()
{
Console.WriteLine($"Derived: {msg}");
}
}
var d = new Derived("Constructed in main");
----------------Output---------------------------
VritualB constructor start
Derived: Set by initializer
VritualB constructor end
Derived constructor start
Derived: Constructed in main
Derived constructor end
基类的构造函数调用了一个定义在本类中的虚函数,于是派生类实例在运行时调用的就是派生类的版本,即 Derived 的派生版本。
C# 在进入构造函数体之前,已经把该对象的所有成员变量初始化好了,即开发者声明每一个成员变量时写的所有初始化语句都得到了执行。
整个流程如下:
- msg 首先通过初始化语句赋值为 "Set by initializer"
- 开始执行构造函数,先执行 base 构造函数
- base 构造函数调用派生类中的 VFunc 中的方法输出 "Derived: Set by initializer"
- 执行派生类 Derived 构造函数
- msg 通过构造函数入参赋值为 "Constructed in main"
- 再次调用 VFunc
可以看到,构造函数中调用虚方法,可能会导致程序结果不是我们所期望的,除非你清晰理解c#语言规范,否则这样会令程序可读性降低,甚至引起数据混乱。