一 前言
周五晚回来,照例打开博客园给自己充充电,然后到博问中回答问题,帮助下他人 也给让自己巩固下自己所了解的知识,
然后就遇到了这么一个问题 关于类字段的初始化 因为本人对 .net Framework 的运行机制比较感兴趣,也写过几篇关于 IL指令的文章 地址如下: 读懂IL代码就这么简单 (一) 然后就进去大至看了下问题与回答都从自己的角度表达了自己的看法,其中的一位园友 @乱舞春秋 贴出了IL 的详细指令并附上了注释,我仔细看完后觉得与我的理解不同 然后发表了自己的看法,很巧的是 @乱舞春秋 又针对我的回答提出了不同的看法,然后周末的两天,我们两都互相质疑对方的说法直到周末的下午真像终于出来了!
至于讨论过程大家可以从博问链接进入 详细了解 关于类字段的初始化
真像虽然已大白,但是追寻真像的过程我觉得很有必要分享给大家,也许有园友会觉得,对于这么小的一个问题至于这么较真吗? 我的理解是 很有必要,既然选择了当技术人员当然要把原理弄明白,而且我相信做为技术人员 对于某一个知识点不清楚时,自己心里是很没底的。也反应技术人员对技术保持的一种态度。
二 问题分析
2.1 问题描述
namespace StaticDemo { class Program { static void Main(string[] args) { var a = new A(); } } class A { int x = 1; public A( ) { x = 4; } } }
//问:int x=1;这个赋值操作时在构造函数执行前的啥时候执行的?
下面是编译成IL指令后的结果 而之后的IL指令出都是图中选中状态下的详情
2.2 @乱舞春秋 观点
先看@乱舞春秋 的回答
所以他认为 字段是在 A类的构造函数中 完成了对字段的初始化
2.3 我的观点
因为当时不够细心,把注意力都集中在了 IL指令的解释上所以出现了我的观点
ldarg.0 是指将图2中Call Stack中索引为0的参数加载到栈中
ldc.i4.1 是指将整数 1 作为int32类型加载到栈中
stfld 做一个赋值的操作,也就是完成 int x=1;这一个过程
此时 变量x的值就是1了 然后调用
call 指令 注意最后的 .ctor() 这个是调用基类Object构造函数的指令
之后的操作就与前面一至了对x 进行赋值 x=4
从上面的过程来看 x=1 肯定是在构造函数之前就以经完成了初始化,而并不是在构造函数中完成初始化的
单从IL的指令来看 字段的初始化是位于构造函数前的 所以我认为是在构造函数前完成了初始化 而且将代码编译成IL后就不存在什么语法糖之类的东西 CPU会按 IL转成的JIT直接执行。
本以为这个问题就此告一段落,但是剧情不是这么演滴~~
春秋兄 依然认为我的理解有误,于是我要求给出相关资料来证明我的错误,给力的春秋兄啊 还真列出来了,还附带了链接,点个赞!
三 问题证明
我按照料春秋兄给出的资料 仔细阅读与理解着每一个字 试图从中找出可以证我是正确的证据,
3.1 参数资料
10.11
10.11.1
10.11.2
10.11.3
我例出重点的地方
10.11.2 实例变量初始值设定项
当实例构造函数没有构造函数初始值设定项时,或仅具有 base(...) 形式的构造函数初始值设定项时,该构造函数就会隐式地执行在该类中声明的实例字段的初始化操作,这些操作由对应的字段声明中的variable-initializer 指定。这对应于一个赋值序列,它们会在进入构造函数时,在对直接基类的构造函数进行隐式调用之前立即执行。这些变量初始值设定项按它们出现在类声明中的文本顺序执行。
10.11.3 构造函数执行
变量初始值设定项被转换为赋值语句,而这些语句将在对基类实例构造函数进行调用之前执行。这种排序确保了在执行任何访问该实例的语句之前
,所有实例字段都已按照它们的变量初始值设定项进行了初始化。给定示例
using System; class A { public A() { PrintFields(); } public virtual void PrintFields() {} } internal class B : A { private int x = 1; private int y; public B() { y = -1; } public override void PrintFields() { Console.WriteLine("x = {0}, y = {1}", x, y); } }
当使用 new B() 创建 B 的实例时,产生如下输出:x = 1, y = 0 x 的值为 1,
这是由于变量初始值设定项是在调用基类实例构造函数之前执行的。但是,y 的值为 0(int 型变量的默认值),这是因为对 y 的赋值直到基类构造函数返回之后才执行。可以这样设想来帮助理解:将实例变量初始值设定项和构造函数初始值设定项视为自动插入到
constructor-body 之前的语句
问题解决的重点就是这里
其中每个注释指示一个自动插入的语句(用于自动插入的构造函数调用的语法是无效的,而只是用来阐释此机制)。
书中为了使读者列容易懂用代码换了一种写法来阐释此机制,书中说 你在构造函数外定义的字段 如 int x=2 其实最终是在 构造函数中 做的初始化
using System.Collections; internal class A { private int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object (); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object (); // Invoke object() constructor count = n; } }
3.2 问题解决
我试着用这套理论来论证我的想法,但是发现无法走下去,因为IL是代码编译之后的 指令也就是说 不管你代码怎么写怎么变只要编译成IL指令后,那只要看代码对应的IL 指令就可以知道代码执行的顺序,这个肯定是不会错的,我再看看春秋兄提供的资料 印了几个硕大的Microsoft肯定是不会出错的,那难道是自己的问题,然后开始把 图中IL指令中出现的指令又细看了一遍,类实例化的过程又了解了一遍,然后不再局限在IL的指令上去理解时,终于想通了 (其实也是自己当时不够细心)
问题的解决与IL的一个指令 .ctor有很大关系 .ctor:它表示构造函数
当实例化A类时 最先调用的方法当然是构造函数,那我只用看A类构造函数的IL指令不就行了
图中选中部分就是 A类的构造函数,查看详情 我惊呆了,顿时茅塞顿开啊 所有问题一瞬间都懂了......
这不就是我开始回答问题时贴出的那张IL指令图嘛 它其实就是A类的构造函数IL 指令 然后结合 春秋兄提供的资料 完全对上了啊
字段的初化果然是在类的构造函数中完成的
1 ldarg.0 是指将图2中Call Stack中索引为0的参数加载到栈中
2 ldc.i4.1 是指将整数 1 作为int32类型加载到栈中
3 stfld 做一个赋值的操作,也就是完成 int x=1;这一个过程
而且注意 最后一段 //end of method A ::.ctor 构造函数结束的标志
四 收获与总结
至此 问题的真像大白了,显然我的理解是错误的,但是对于 IL指令的理解是没有问题的,错在整个运行过程理解的错误,但即使错了,我得到的收获还是相当多的,在此感谢
@乱舞春秋 指出我的错误!技术人员解决问题,看重的不是结果,而是解决问题的这个过程,与他人思维的碰撞。
如果您觉得本文有给您带来一点收获,不妨点个推荐,为我的付出支持一下,谢谢~
如果希望在技术的道路上能有更多的朋友,那就关注下我吧,让我们一起在技术的路上奔跑