谜题

上一篇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. JSON字符串和Dictionary字典类型的相互转换

    在开发过程中,往往会遇到数据类型转换的情况,根据自己的业务,可能转换类型有多种,下面就说一下json字符串和字典类型的转换. public static class JsonUntity { /// ...

  2. 0010《SQL必知必会》笔记06-表的修改与删除

    1.表的修改: 1.1 删除列:ALTER TABLE 表名 DROP COLUMN 列名 1.2 添加列:ALTER TABLE 表名 ADD(列名 数据类型) 1.3 修改列名:ALTER TAB ...

  3. linux 源码安装mysql 5.5

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

  4. C++之STL一般总结

    重新复习一下STL 什么是STL? STL(模板和标准模板库),实现与类型无关的算法和数据类型,需要将实现中的类型参数化,允许用户根据它的需要制定不同的类型. 一.一般介绍 STL(Standard ...

  5. Centos 内存占满 释放内存

    free -m 查看内存使用情况 top,然后按下shift+m,按内存占用百分比排序 centos 为了提高效率,把部分使用过的文件缓存到了内存里.如果不需要这样的文件性能,那就可以释放. 如下两个 ...

  6. [uwsgi] no request plugin is loaded, you will not be able to manage requests.

    Problem: *** Operational MODE: preforking+threaded no app loaded. going in full dynamic mode uWSGI i ...

  7. 【2016-10-12】【坚持学习】【Day3】【责任链模式】

    今天学习责任链模式 例子: 采购审批系统 采购单需要经过不同人审批 采购价格<500 部门经理审批 采购价格<1000 部门主任审批 采购价格<2000 副总审批 采购价格<5 ...

  8. Java注解的使用

    概念:java提供了一种原程序中的元素关联任何信息和任何元数据的途径和方法. Java中的常见注解 JDK自带注解: @Override//覆盖父类的方法 @Deprecated//表示方法过时了 @ ...

  9. 关于OATUH中的AUTHRAZITON CODE和TOKEN的关系,实际上就是这么回事

    关于OATUH中的AUTHRAZITON CODE和TOKEN的关系,实际上就是这么回事 每回要拿AUTHRAZITON CODE换取TOKEN,然后才能正常通信, 为什么要多一步呢?直接给TOKEN ...

  10. Hibernate延迟加载Lazy

    Hibernate延迟加载Lazy 延迟加载(lazy load)又称为懒加载,延迟加载的机制是为了避免一些无谓性能的开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作 如 ...