谜题

在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. Java的String.valueOf 转换 与、空串+类型变量转换与封装类(Integer)的toString方式转换比较。

    1.空串+类型变量方式转换 int i=20; String s=""+i; 这种方式实际上经过了两个步骤,首先进行了i.ToString()把 i 转换为 字符串,然后再进行加法 ...

  2. linux 查看静态库,动态库是32位还是64位

    动态库: file xxx.so 静态库 objdump -a xxx.a

  3. Linux磁盘管理之日志文件系统和非日志文件系统08

    略. 查看linux支持的文件系统命令: ls /lib/module/`uname -r`/x86/fs blkid查看文件系统的类型 mkfs.ext2  == mkfs –t ext2

  4. Linux信号类型说明

    说明 在Linux系统开发中经常要使用到信号来实现异步通知机制.而在Linux系统中信号有很多种,也不用全部记住,学习几种常见的信号,学会使用即可:当然也要知道用哪种方式能够发送这样的信号. 查看li ...

  5. Java里面的转义字符

    转义字符是指,用一些普通字符的组合来代替一些特殊字符,由于其组合改变了原来字符表示的含义,因此称为“转义”. 常见的转义字符: \n 回车(\u000a) \t 水平制表符(\u0009) \b 空格 ...

  6. Sqlserver2008R2 数据库镜像配置步骤

    Sqlserver2008镜像功能可以保障数据库的高可用性.数据库镜像维护着数据库的两个副本,这两个副本必须分别放置在不同的SQL Server数据库实例中.可以用两台服务器也可以用一台服务器的不同实 ...

  7. C# 中的占位符本质

    占位符本质 1.占位符是相对于String字符串类型而言的. 2.占位符其实就是调用String.Format()方法.把指定的变量拼接到定义好的字符串模板中组成新的字符串.

  8. mysql 二进制文件增量备份

    1.首先在my.cnf下添加二进制文件路径(windows下文件名称为my.ini) 在[mysqld]下添加 log-bin=mysql-bin 2.centos下默认安装mysql 5.6,数据默 ...

  9. NET Core中实现一个Token base的身份认证

    NET Core中实现一个Token base的身份认证 注:本文提到的代码示例下载地址> How to achieve a bearer token authentication and au ...

  10. 给IIS添加CA证书以支持https

    一.在IIS中生成Certificate Signing Request (CSR) 个人理解:生成CSR就是生成“私钥/公钥对”之后从中提取出公钥. 1. 打开IIS Manager,在根节点中选择 ...