谜题

上一篇C#解惑中,我们提到了对象的初始化顺序。当我们创建一个子类的实例时,总是会先执行基类的构造函数,然后再执行子类的构造函数。那么实例字段是什么时候初始化的呢?静态构造函数和静态字段呢?今天我们就来研究一下这个话题。

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

class Foo
{
public Foo(string s)
{
Console.WriteLine(s);
}
public void Bar() { }
}
class Base
{
readonly Foo baseFoo1 = new Foo("Base initializer");
static readonly Foo baseFoo2 = new Foo("Base static initializer");
static Base()
{
Console.WriteLine("Base static constructor");
}
public Base()
{
Console.WriteLine("Base constructor");
}
}
class Derived : Base
{
readonly Foo derivedFoo1 = new Foo("Derived initializer");
static readonly Foo derivedFoo2 = new Foo("Derived static initializer");
static Derived()
{
Console.WriteLine("Derived static constructor");
}
public Derived()
{
Console.WriteLine("Derived constructor");
}
}
static class Program
{
static void Main()
{
new Derived();
Console.Read();
}
}

猜一猜它的输出结果是什么?如果猜不出来,就运行一下看看吧。

Derived static initializer
Derived static constructor
Derived initializer
Base static initializer
Base static constructor
Base initializer
Base constructor
Derived constructor

是不是有点出乎你的意料?没关系,我们来一步一步解释。

解惑

上期已经介绍了构造函数的初始化顺序,所以这次略过不谈,直接来看看实例成员的初始化器。一般来说,我们在构造一个类型的实例时,会先初始化成员,然后初始化构造函数(编译器会把初始化成员的代码编译到构造函数代码的最顶部)。但初始化一个子类的时候,父类的成员、构造函数的初始化,和子类的成员、构造函数的初始化顺序是什么样的呢?

实例初始化器和实例构造函数的执行顺序

我们把上面的代码简化一下,去掉静态构造函数和静态初始化器。

class Base
{
readonly Foo baseFoo = new Foo("Base initializer");
public Base()
{
Console.WriteLine("Base constructor");
}
}
class Derived : Base
{
readonly Foo derivedFoo = new Foo("Derived initializer");
public Derived()
{
Console.WriteLine("Derived constructor");
}
}

结果如下所示:

Derived initializer
Base initializer
Base constructor
Derived constructor

这可能会有点出乎你的意料,因为直观上来说,似乎应该是先初始化父类的成员和构造函数,再初始化子类的成员和构造函数:

Base Initializers
Base Constructor
Derived Initializers
Derived Constructor

但实际上为什么会先初始化子类的成员呢?这是因为,按照这样的初始化顺序,所有引用类型的只读字段(注意这里的readonly并不是随手写写的)都能确保在调用时不为null。而如果先初始化基类的成员和构造函数,就无法给出这样的保证。

比如下面的代码:

internal class Base
{
public Base()
{
Console.WriteLine("Base constructor");
if (this is Derived) (this as Derived).N();
// would deref null if we are constructing an instance of Derived
M();
// would deref null if we are constructing an instance of MoreDerived
} public virtual void M()
{
}
}
internal class Derived : Base
{
private readonly Foo derivedFoo = new Foo("Derived initializer"); public void N()
{
derivedFoo.Bar();
}
}
internal class MoreDerived : Derived
{
public override void M()
{
N();
}
}

如注释所示,在构造Derived类型的实例时,如果先初始化Base的构造函数,后初始化Derived的成员,那么在Base的构造函数中调用DerivedN时,derivedFoo就会为null,因为它还没有初始化。试想一下,你正在调用一个对象的方法,但这个对象的字段没有初始化,构造函数也还没有执行,这显然是不合理的。

同样,在构造MoreDerived时,在Base的构造函数中调用M(进而调用N)也会得到空引用,因为DerivedderivedFoo仍然没有初始化。

注意 尽管类似if (this is Derived) (this as Derived).N();这样的代码是合法的,但是一定注意不要这样写。在基类的构造函数中,把“自己”转换为自己的子类,想想都不可思议……

因此,类型的初始化顺序必须是这样的:

Derived initializer
Base initializer
Base constructor
Derived constructor

静态初始化器和静态构造函数的初始化顺序

我们都知道,静态构造函数是一个特殊的构造函数,它在该类型的所有成员(包括实例构造函数)第一次被访问之前执行。而与实例的初始化器会在实例构造函数之前执行类似,静态初始化器会在静态构造函数之前执行。结合这两点,我们来看看本文最初的谜题。在执行new Derived()时,是第一次访问Derived类,此时会率先执行它的静态构造函数,而在执行静态构造函数之前,会执行静态初始化器。因此打印的结果应该为:

Derived static initializer
...
Derived static constructor
...
Derived constructor

现在问题来了,基类的静态构造函数会被执行吗?如果会,是在什么时候执行的呢?会和实例构造函数一样,在子类的静态初始化器之后吗?

Derived static initializer
Base static initializer
Base static constructor
Derived static constructor

稍加思考我们就能得出答案。由于静态初始化器和静态构造函数都是静态的,所以在执行的时候并不会出发基类的任何行为(记住我们前面说的,只有当类的成员被调用的时候,才会执行静态初始化器和静态构造函数)。因此在它们之后应该继续执行子类的实例初始化器。而在这之后,按顺序该执行基类的实例初始化器了,这时基类的成员第一次被调用,会出发基类的静态初始化器和静态构造函数,此后再执行基类的实例初始化器,并按顺序继续执行下去。

因此最终的结果为:

Derived static initializer
Derived static constructor
Derived initializer
Base static initializer
Base static constructor
Base initializer
Base constructor
Derived constructor

[C#解惑] #2 对象的初始化顺序的更多相关文章

  1. [百度空间] [转] 在 Visual C++ 中控制全局对象的初始化顺序

    from: http://blog.csdn.net/classfactory/archive/2004/08/07/68202.aspx 在 C++ 中,同一个翻译单位(.cpp文件)里的全局对象的 ...

  2. Java对象的初始化顺序

    new一个对象时,该对象的初始化顺序如下 : 父类中的静态成员变量 父类中的静态代码块 子类中的静态成员变量 子类中的静态代码块 父类中的非静态变量 父类中的非静态代码块 父类构造函数 子类中的非静态 ...

  3. C++从静态对象的初始化顺序理解static关键字

    问题 首先考虑一个全局变量的初始化顺序问题 在头文件1中: extern int b; ; 在头文件2中: extern int a; ; 源文件中包含了头文件1和头文件2,这种情况下a和b可能的值是 ...

  4. C++ 类对象的初始化顺序 ZZ

    C++构造函数调用顺序 1.     创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类): 2.    如果类里面有成员类,成员类的构造函数优先被调用:(也优先于该类本身的构造函数 ...

  5. java类对象的初始化顺序

    在下面这个例子中,我们分别在父类和子类中测试了静态代码块.普通代码块.静态成员变量.普通成员变量.构造器.静态内部类. 一:代码块及变量测试 class Field{ public static St ...

  6. C++——类继承以及类初始化顺序

    对于类以及类继承, 几个主要的问题:1) 继承方式: public/protected/private继承. 这是c++搞的, 实际上继承方式是一种允许子类控制的思想. 子类通过public继承, 可 ...

  7. java中静态代码块初始化顺序

    (一)java 静态代码块 静态方法区别    一般情况下,如果有些代码必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的;需要在项目启动的时候就初始化,在不创建对象的情况下, ...

  8. C#多态;父类引用指向子类对象;new和override的区别;new、abstract、virtual、override,sealed关键字区别和使用代码示例;c#类的初始化顺序

    关于父类引用指向子类对象 例如: 有以下2个类 public class Father { public int age = 70; public static string name = " ...

  9. Java对象初始化顺序

    最近我发现了一个有趣的问题,这个问题的答案乍一看下骗过了我的眼睛.看一下这三个类: package com.ds.test;   public class Upper { String upperSt ...

随机推荐

  1. Ubuntu13.04安装历险记--Mono,Nginx,Asp.Net一个都不能少

    ----Ubuntu13.04安装历险记--新人新手新作------------------------------------------------- 注:以下操作均省略权限获取操作,如有需要,请 ...

  2. 9、FTP封杀用户、限制传输速率、限制访问目录、为匿名用户提供下载资源

    一.封杀某些用户访问FTP服务器 例如  封杀 yanji [root@localhost root]#   vi   /etc/vsftpd.ftpusers     (用户控制配置文件,主要用于限 ...

  3. mysql 性能优化方案 (转)

    网 上有不少mysql 性能优化方案,不过,mysql的优化同sql server相比,更为麻烦与复杂,同样的设置,在不同的环境下 ,由于内存,访问量,读写频率,数据差异等等情况,可能会出现不同的结果 ...

  4. WPF之全局快捷键

    目录 1.WPF快捷键实现方式 2.全局快捷键设置界面 3.Windows API调用 4.注册全局快捷键 5.快捷键触发 WPF快捷键实现方式 WPF快捷键实现主要有自定义快捷键命令和全局快捷键两种 ...

  5. Java实现点击一个Jlabel增加一个Jlabel的小功能

    当界面生成以后,自己想做一个点击一个Jlabel增加一个Jlabel,即类似于QQ的添加好友以后可以及时的加进一个好友.自己做了好久,发现不能及时刷新.在网上查了一下,然后自己研究了一小会.发现需要v ...

  6. 增量式PID计算公式4个疑问与理解

    一开始见到PID计算公式时总是疑问为什么是那样子?为了理解那几道公式,当时将其未简化前的公式“活生生”地算了一遍,现在想来,这样的演算过程固然有助于理解,但假如一开始就带着对疑问的答案已有一定看法后再 ...

  7. GitHub注册流程(中英对比)

    GitHub注册流程(中英对比) 注明:为方便完成注册,将以中英文网页截图对应展示. 1. 注册邮箱:        点击GitHub官网进行邮箱注册. 2. 制定个人计划:        页面跳转至 ...

  8. SSM三大框架整合详细教程(Spring+SpringMVC+MyBatis)(转)

    使用 SSM ( Spring . SpringMVC 和 Mybatis )已经有三个多月了,项目在技术上已经没有什么难点了,基于现有的技术就可以实现想要的功能,当然肯定有很多可以改进的地方.之前没 ...

  9. Java的注解机制——Spring自动装配的实现原理

    http://www.cnblogs.com/Johness/archive/2013/04/17/3026689.html

  10. [No000050]练习一万小时便能成为天才

    练习一万小时便能成为天才 世界上顶尖的记忆高手都是训练出来的! 加拿大畅销书作家麦尔坎·葛拉威尔在<异数>一书中指出:"人们眼中的天才之所以卓越非凡,并非天资超人一等,而是付出了 ...