谜题

上一篇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. su认证失败&文件夹里打开终端的方法&atom安装

    很久没用笔记本上的ubuntu,用不顺手,比在公司调教了半年多的电脑差远了.一步一步来.先解决最不顺手的三件事 1.su认证失败. 新安装的ubuntu系统是无法切换到root账户的,得做一番修改 s ...

  2. linux 源码安装mysql 5.5

         今天在ubuntu和CentOS下,用源码反复安装了许多次mysql,趁还没忘记,赶紧记下来...      在ubuntu和CentOS下安装过程倒是没什么差别.      0.下载源码, ...

  3. C++杂谈(三)产生随机数与time函数

    产生随机数在程序中很有用,这篇文章简单介绍一下产生随机数的方法. 伪随机数 使用标准库<cstdlib>中的rand()函数产生随机数. #include<iostream> ...

  4. 修改/etc/profile文件

    通常情况下,/etc/profile文件是只读的,直接用vi或gedit打开修改后是无法保存的.要修改profile,需要取得root权限,(使用gedit编辑)应该如下: $sudo gedit / ...

  5. 单片机实现60s定时器

    单片机573+数码管+按钮 实现60秒的定时器 知识: IE寄存器 TCON寄存器 TMOD 寄存器 /***************** 2个定时中断,2个按钮中断 **************** ...

  6. 一个PHP混淆后门的分析

    洒家的朋友的公司的某个站发现最近被上传了一个后门程序.为了取证我们抓取了HTTP请求流量,看到了一堆莫名其妙看似经过混淆的请求,响应也是看似base64的乱码.洒家用了2个小时静态分析了一遍,并写了利 ...

  7. Hadoop Kernel tunning

    /etc/security/limits.conf @ochadoop soft nofile 102642 @ochadoop hard nofile 102642 @ochadoop soft n ...

  8. Pairs Forming LCM(素因子分解)

    http://acm.hust.edu.cn/vjudge/contest/view.action?cid=109329#problem/B    全题在文末. 题意:在a,b中(a,b<=n) ...

  9. Notes on <Assembly Language step by step>

    By brant-ruan Yeah, I feel very happy When you want to give up, think why you have held on so long. ...

  10. poj 2278 DNASequnce AC自动机

    地址:http://poj.org/problem?id=2778 题目: DNA Sequence Time Limit: 1000MS   Memory Limit: 65536K Total S ...