谜题

上一篇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. 【Python】-【类解析】--【脚本实例】

    通过脚本事例,解析下Python中类的几个概念在脚本中的应用 脚本如下: ++++++++++++++++++++++++++++++++++++++++ #!/usr/bin/env python# ...

  2. 【hbase】——Java操作Hbase进行建表、删表以及对数据进行增删改查,条件查询

    1.搭建环境 新建JAVA项目,添加的包有: 有关Hadoop的hadoop-core-0.20.204.0.jar 有关Hbase的hbase-0.90.4.jar.hbase-0.90.4-tes ...

  3. Webview加载本地js、图片的方法

    在项目开发中经常会将比较大的js.图片.css等放到app中,而html放服务器,这样在使用时流量较少,加载也比都放服务器上快,其实方法也比较多,网上搜了很久都没结果. 一种是获取服务器返回的html ...

  4. MongoDB3.0.x版本用户授权配置(单机环境)

    MongoDB数据库默认情况下是没有做权限控制的,只要能够连接所开放的端口就能进行访问,而且拥有root级别的权限:对于生产环境而言是极不安全的,所以需要建立用户,进行授权控制. 单机环境下的用户授权 ...

  5. 描述Linux shell中单引号,双引号及不加引号的简单区别(计时2分钟)

    简要总结: 单引号: 可以说是所见即所得:即将单引号内的内容原样输出,或者描述为单引号里面看到的是什么就会输出什么. 双引号: 把双引号内的内容输出出来:如果内容中有命令.变量等,会先把变量.命令解析 ...

  6. android canvas d

    (以下转自:http://blog.csdn.net/longyi_java/article/details/6930480) 1.基本的绘制图片方法 //Bitmap:图片对象,left:偏移左边的 ...

  7. 【零基础学习iOS开发】【转载】

    原文地址:http://www.cnblogs.com/mjios/archive/2013/04/24/3039357.html 本文目录 一.什么是iOS 二.主流手机操作系统 三.什么是iOS开 ...

  8. 【转帖】4412ARM开发板学习笔记(一)

    本文转自迅为论坛:http://www.topeetboard.com 新手在进行开发学习前,建议先看01-迅为电子开发板入门视频.对开发板和开发环境有一定的了解后,不要盲目接线开机.以下是个人的一点 ...

  9. [转]怎么在MVC中使用自定义Membership

    本文转自:http://www.cnblogs.com/angelasp/p/4078244.html 首先我们来看看微软自带的membership: 我们打开系统下aspnet_regsql.exe ...

  10. Zookeeper C API 指南四(C API 概览)(转)

    上一节<Zookeeper C API 指南三(回调函数)>重点讲了 Zookeeper C API 中各种回调函数的原型,本节将切入正题,正式讲解 Zookeeper C API.相信大 ...