一 前言
周五晚回來,照例打開博客園給自己充充電,然后到博問中回答問題,幫助下他人 也給讓自己鞏固下自己所了解的知識,
然后就遇到了這么一個問題 關於類字段的初始化 因為本人對 .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指令的理解是沒有問題的,錯在整個運行過程理解的錯誤,但即使錯了,我得到的收獲還是相當多的,在此感謝
@亂舞春秋 指出我的錯誤!技術人員解決問題,看重的不是結果,而是解決問題的這個過程,與他人思維的碰撞。
如果您覺得本文有給您帶來一點收獲,不妨點個推薦,為我的付出支持一下,謝謝~
如果希望在技術的道路上能有更多的朋友,那就關注下我吧,讓我們一起在技術的路上奔跑