[C#解惑]#1在构造函数内调⽤虚⽅法
谜题
在C#中,⽤virtual关键字修饰的⽅法(属性、事件)称为虚⽅法(属性、事件),表⽰该⽅法可以由派⽣类重写(override)。虚⽅法是.NET中的重要概念,可以说在某种程度上,虚⽅法使得多态成为可能。
然⽽虚⽅法的使⽤却存在着很⼤学问,如果滥⽤的话势必对程序产⽣很⼤的负⾯影响。⽐如下⾯这个例⼦:
public class Puzzle
{
public Puzzle()
{
Name = "Virtual member call in constructor";
Solve();
}
public virtual string Name { get; set; }
public virtual void Solve()
{
}
}
如果您的Visual Studio没有安装ReSharper,那么上⾯的代码不会有任何异常。但如果安装了,在构造函数内部给Name赋值和调
⽤Solve时就会在下⾯产⽣⼀个波浪线,即警告:virtual member call in constructor。
这是什么原因呢?我们在构造函数中调⽤虚⽅法,碍着ReSharper什么事⼉了?
其实这个警告就是提醒我们不要在⾮封闭类型的构造函数内调⽤虚⽅法或虚属性。但为什么这样做不合适呢?在解惑之前,我们先来了解两个概念。
类型的初始化顺序
我们先来看这样⼀段代码:
Console.WriteLine("Base constructor");
}
}
class Derived : Base
{
public Derived()
{
Console.WriteLine("Derived constructor");
}
}
static class Program
{
static void Main()
{
new Derived();
Console.Read();
}
}
猜⼀猜它的输出结果是什么?
你也许已经猜到了,它的结果是:
Base constructor
Derived constructor
我们在初始化⼀个对象时,总是会先执⾏基类的构造函数,然后再执⾏⼦类的构造函数。虚⽅法调⽤
我们再来看⼀段代码:
Console.WriteLine("Base.M");
}
public virtual void V()
{
Console.WriteLine("Base.V");
}
}
class Derived : Base
{
public new void M()
{
Console.WriteLine("Derived.M");
}
public override void V()
{
Console.WriteLine("Derived.V");
}
}
static class Program
{
static void Main()
{
var d = new Derived();
Base b = d;
b.M();
b.V();
d.M();writeline函数
d.V();
Console.Read();
}
}
再来猜⼀猜输出结果吧。
貌似应该是:
Base.M
Base.V
Derived.M
Derived.V
但运⾏⼀下会发现,真正的结果是这样的:
Base.M
Derived.V
Derived.M
Derived.V
这是为什么呢?
原来对于⾮虚⽅法调⽤,编译器会进⾏⼀些额外的“动作”。⽐如出所调⽤对象的实际类型,以访问正确的⽅法表(调⽤b.V()的时候就会到变量b的实际类型Derived,从⽽输出Derived.V)。
解惑
现在回到我们最初的谜题,virtual member call in constructor。结合以上两个知识点,会有哪些发现?
我们稍微改造⼀下虚⽅法调⽤的那个例⼦。
class Foo
{
public Foo(string s)
{
Console.WriteLine(s);
}
public void Bar() { }
}
class Base
{
public Base()
{
V(); // Virtual member call in constructor
}
public virtual void V()
{
Console.WriteLine("Base.V");
}
}
class Derived : Base
{
private Foo foo;
public Derived()
{
foo = new Foo("foo in Derived");
}
public override void V()
{
Console.WriteLine("Derived.V");
foo.Bar(); // will throw NullReferenceException
}
}
在Base的构造函数中调⽤虚⽅法V()时,ReSharper会给出virtual member call in constructor的警告。这是因为V可以在Base的任意⼦类中被改写(override),⽽这种改写,很有可能使得它依赖于⾃⼰的构造函数,如上例所⽰。⽽由于之前提到的类型初始化顺序,在执⾏Base b = new Derived();这样的代码时,Base的构造函数要早于Derived的构造函数执⾏,因此在执⾏到foo.Bar()时foo还是个空引⽤。
明⽩了吗?我们来简单总结⼀下。Virtual member call in constructor的警告是因为,对于Base b = new Derived();这样的代码:
1. 基类构造函数的执⾏要早于⼦类构造函数
2. 基类构造函数中对于虚⽅法的调⽤,实际调⽤的是⼦类中重写的虚⽅法
因此,ReSharper会警告我们,这么做存在隐患。
我们能完全避免这么做吗?很遗憾,答案是不能。⽐如如果项⽬中使⽤了NHibernate,框架本⾝要求ORM实体类中,所有与数据库列具有对应关系的属性都必须为虚属性。这是因为NHibernate为了实现延迟加载,会为每个实体类⽣成proxy,这些proxy需要重写实体类中属性的getter/setter。⽽有些时候,
为了业务需要,我们不得不在实体类的构造函数中对这些属性进⾏某些操作(⽐如初始化)。
我认为这么做是技术选型所致的必然结果,是完全可以接受的。但我们要注意,在代码中保证那些可能会被继承的实体,在⼦类中重写那些虚属性时,不要依赖于⼦类⾃⾝的构造函数(这⼏乎是可以保证的,因为与数据库列映射的属性,只能是最简单的getter/setter)。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论