编译器对私有字段初始化的优化

今天Review时看到自己写的这样一段类似的代码:

internal class TestClass
{
    private bool _inited = false;
    private int _count = 0;
}

简单地说就是一个类中的私有字段被显示地赋上了默认值。根据我们惯有的经验,这么做不仅多此一举,而且画蛇添足。因为false和0分别是bool型和int型的默认值,也即是如果我们没有显示赋值,它们仍然会是false和0.那么这样其实会在执行时多走一步,是丑陋的代码。

 

果真如此吗?

 

事实上,在某些情况下,上面的代码确实会多执行一次,也即CLR在给所有的字段赋上默认值后,我们的逻辑又会把这些字段赋一遍默认值,而且这个赋值的过程是在构造函数中完成的。但是这个情况有一个大大的前提:Debug模式

 

查看Debug模式下编译的TestClass的构造器IL指令,可以看到这里确实生成了为字段赋默认值的指令,部分证实了我们的猜想。

image

 

那么相同的代码打开Release模式再编译一遍,再来看看结果有什么不一样的吧。

image

 

让我们欣慰的是,C#编译器已经聪明地识别出了这是一段完全多余的代码,并且帮助我们做了优化,在构造函数中并不会对赋了默认值的私有字段做任何操作。在任何情况下,我们发布出去的dll都应该是经过Release编译的,所以之前的担心是完全没有必要的。而且鉴于这么做并没有任何性能的损失,显示地给字段赋初值会让阅读者更容易理解作者的用途,不至于迷惑于“是开发忘记了赋初值还是他确实想这么做?”。

 

明白了这一点之后我们再来看一个更进一步的:

internal class TestClass
{
    public TestClass()
    {
        _inited = false;
        _count = 0;
    }
    private bool _inited;
    private int _count;
}
这样一段代码,同样在Release模式下编译。都说直接给字段赋值只是通过构造器赋值的一个语法糖,那么上述代码会否被优化呢?编译器有如此聪明么?
 
image

很遗憾,看来果然不如我们想象地那般神奇。而且和第一张IL代码图进行比较会发现两个构造器对私有字段初始化的时间点是不一样的,直接赋值的初始化点在调用父类的构造器之前,而在构造器中赋值的初始化点在调用父类的构造器之后。通过这里我们可以大致猜出一个实例的构造顺序,即是先有字段,再调用父类构造器,再调用自身构造器。可以通过如下代码做个简单的验证:

internal class TestClass
{
    public TestClass()
    {
        _inited = false;
        _count = 0;
    }
    private bool _inited = true;
    private int _count = 1;
}
我们并没有给字段赋上默认值,这样可以强迫编译器生成这一段IL指令。查看它的Release编译的IL指令:
 

image

 

结果证明了我们的猜想,其中红圈赋值1的操作是我们直接赋值的IL代码,在调用Object的构造器之前执行。而蓝色方框标出的赋值0的操作是我们构造器中显示赋值的IL代码,在调用Object的构造器之后。

posted @ 2011-05-04 23:35  我就是砖家  阅读(283)  评论(0编辑  收藏  举报