谜题

在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什么事儿了?

其实这个警告就是提醒我们不要在非封闭类型的构造函数内调用虚方法或虚属性。但为什么这样做不合适呢?在解惑之前,我们先来了解两个概念。

类型的初始化顺序

我们先来看这样一段代码:

class Base
{
public Base()
{
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

我们在初始化一个对象时,总是会先执行基类的构造函数,然后再执行子类的构造函数

虚方法调用

我们再来看一段代码:

class Base
{
public void M()
{
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();
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)。

[C#解惑] #1 在构造函数内调用虚方法的更多相关文章

  1. C# 构造函数中调用虚方法的问题

    请看下面代码: using System; public class A{ public A(){ M1(); } public virtual void M1(){} } public class ...

  2. 避免在构造函数中调用虚方法(Do not call overridable methods in constructors)

    CLR中说道,不要在构造函数中调用虚方法,原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现.但在这个时候,尚未完成对继承层次结构中所有字段的初始化.所以,调用虚方法会导致不可预测的 ...

  3. 关于在C#中构造函数中调用虚函数的问题

    在C#中如果存在类的继承关系,应避免在构造函数中调用虚函数.这是由于C#的运行机制造成的,原因如下: 新建一个类实例时,C#会先初始化该类(对类变量赋值,并将函数记在函数表中),然后再初始化父类.构造 ...

  4. C++ 构造函数中调用虚函数

    我们知道:C++中的多态使得可以根据对象的真实类型(动态类型)调用不同的虚函数.这种调用都是对象已经构建完成的情况.那如果在构造函数中调用虚函数,会怎么样呢? 有这么一段代码: class A { p ...

  5. C++中构造函数能调用虚函数吗?(答案是语法可以,输出错误),但Java里居然可以

    环境:XPSP3 VS2005 今天黑总给应聘者出了一个在C++的构造函数中调用虚函数的问题,具体的题目要比标题复杂,大体情况可以看如下的代码: class Base { public: Base() ...

  6. Spring AOP开发时如何得到某个方法内调用的方法的代理对象?

    Spring AOP开发时如何得到某个方法内调用的方法的代理对象? 问题阅读起来拗口,看代码 在方法中调用其他方法很常见,也经常使用,如果在一个方法内部调用其他方法,比如 public class U ...

  7. 【C++】不要在构造函数或析构函数内调用虚函数

    这个问题来自于<Effective C++>条款9:永远不要在构造函数或析构函数中调用虚函数 . 假设有如下代码: class Transaction {// 所有交易的基类 public ...

  8. 微信小程序填坑,wx.request() 内调用setData()方法错误的解决办法

    再方法内添加一行代码,把this对象赋值给给一个变量供success()方法内调用 核心代码: var v = this.txt; 完整示例 abc:function(e){//该函数用于和后台交互 ...

  9. CLR如何调用虚方法、属性和事件

    方法代表在类型或类型的实例上执行某些操作的代码.在类型上执行操作,称为静态方法:在类型的实例上执行操作,称为非静态方法.任何方法都有一个名称.一个签名和一个返回值(可以是void). CLR允许一个类 ...

随机推荐

  1. html5+css3实现跑动的爱心/动态水滴效果[原创][5+3时代]

    大风起兮云飞扬,安得猛士兮走四方!html5+css3,不学不行. 做web开发已经有好几年了,见证了太多语言的崛起和陨落. 其实作为一个程序员最苦逼的事情莫过于每天要不停的追赶各大公司新出的框架和语 ...

  2. InnoDB源码分析--缓冲池(二)

    转载请附原文链接:http://www.cnblogs.com/wingsless/p/5578727.html 上一篇中我简单的分析了一下InnoDB缓冲池LRU算法的相关源码,其实说不上是分析,应 ...

  3. x01.os.12: 在 windows 中写 OS

    在 windows 中写操作系统,需要一系列的辅助工具.在此,要感谢川谷秀实!所有工具,都在 z_tools 文件夹中.有了大师的帮助,不妨也来尝试在 windows 中写一把 OS. 源代码及工具可 ...

  4. vmware 10 e1000e e1000e_cyclecounter_read 挂机解法

    http://ehc.ac/p/e1000/mailman/message/34100875/In the e1000e_cyclecounter_read function, if incvalue ...

  5. gcc编译参数-fPIC的一些问题

    gcc编译参数-fPIC的一些问题 (2012-07-26 15:41:08) 转载▼ 标签: linux compiler gcc -fpic it 分类: NSN_BspDriver ppc_85 ...

  6. TCP的关闭,到底是几次握手,每次的标志位到底是什么!

    做题的时候遇到一个问题,TCP关闭的时候到底是三次还是四次握手,如果是三次,少了哪部分?   按照 <计算机网络> -第五版-谢希仁       然而对于TCP关闭, 有的地方能找到   ...

  7. [转]Membership三步曲之入门篇 - Membership基础示例

    本文转自:http://www.cnblogs.com/jesse2013/p/membership.html Membership三步曲之入门篇 - Membership基础示例   Members ...

  8. MyBatis使用总结+整合Spring

    MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis .20 ...

  9. OAuth2学习及DotNetOpenAuth部分源码研究

    OAuth2学习及DotNetOpenAuth部分源码研究 在上篇文章中我研究了OpenId及DotNetOpenAuth的相关应用,这一篇继续研究OAuth2. 一.什么是OAuth2 OAuth是 ...

  10. poj 3468

    A Simple Problem with Integers Time Limit: 5000MS Memory Limit: 131072K Total Submissions: 90736 Acc ...