2.【.Net底層剖析】2.stfld指令-給對象的字段賦值
未完待續......
概述:
我們經常在code中用到屬性,但是我們真的知道屬性和字段的區別嗎?為什么會有屬性這個用法?帶着這兩個問題,我們來用IL中間語言剖析一下屬性(Property)
C#中如何定義一個屬性
public string name { get; set; }
編譯之后,用ildasm.exe工具看下IL代碼
ildasm.exe 路徑:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools 不同的機器,ildasm.exe路徑可能不同
在我們分析IL代碼之前,我們需要了解下IL的符號代表的含義:
Student類
1.查看更多信息
雙擊進去就可以看到詳細的IL代碼:
- .class 表明了Student是一個類。該類繼承自外部程序集mscorlib的System.Object類,且Student作為另一個類的子類
- auto表明程序加載時內存的布局是由CLR決定的,而不是程序本身。
- ansi標識為了實現托管代碼和非托管代碼間的無縫轉換。
- public為訪問控制權限。
- Beforefieldinit 屬性為Student提供了一個附加信息,用於標記運行庫可以在任何時候執行類型構造函數方法,只要該構造方法在第一次訪問其靜態字段之前執行即可。如果沒有beforefieldinit則運行庫必須在某個精確時間執行類型構造函數方法,從而影響性能優化。
- extends 繼承
- [mscorlib]多語言標准通用對象運行時庫
2.構造函數
雙擊進去查看IL代碼
- .method 表明.ctor為一個方法
- public訪問權限
- hidebysig屬性用於表示表示如果當前Student類作為父類時,類中的標記了hidebysig的方法不會被子類繼承,因此該構造函數不會被繼承
- specialname MSDN英文解釋Method is special. Name describes how. 方法是特殊的,名字描述是怎樣特殊。
- rtspecialname MSDN英文解釋Runtime should check name encoding. 運行時應該檢查名字的編碼
- void .ctor() 返回值為void的無參方法.ctor
- cil managed 說明方法體中為IL代碼,指示編譯器編譯為托管代碼
- .maxstack表明執行構造函數.ctor期間的評估堆棧(Evaluatuion Stack)可容納數據項的最大個數。評估堆棧:用於保存方法所需變量的值,並在方法執行結束時清空,或者存儲一個返回值。
- IL_xxxx 標記代碼行,一般來說,IL標記之前的部分為變量的聲明和初始化。
- Ldarg.0(load argument)裝載第一個成員參數,在實例方法中指的是當前實例的引用,該實例引用將用於在基類構造函數中調用。
- call instance void [mscorlib]System.Object::.ctor()調用Student的父類Object的構造函數.ctor()
- ret(return)方法返回
屬性Name
1.隱藏字段
私有字段<Name>K__BackingField,string類型,
2.get_Name方法
ldarg要特別注意一個問題:如果是實例方法的話ldarg.0加載的是本身,也就是this,ldarg.1加載的才是函數的第一個參數;如果是靜態函數,ldarg.0就是第一個參數。
所以get_Name的作用就是得到隱藏字段<Name>k__BackingField的值
s.Name相當於得到了隱藏字段<Name>k__BackingField的值
3.set_Name方法
所以set_Name的作用就是將set_Name中的參數value賦值給隱藏字段<Name>k__BackingField
s.Name = "Jackson" 相當於value="Jackson",將value賦值給<Name>k__BackingField
4.屬性Name
Main方法
newobj指令:
- 從托管堆分配指定類型所需要的全部內存空間。
- 從調用執行構造函數初始化之前,首先初始化對象的附加成員:
- 指向該類型方法表的指針
- SyncBlockIndex,用於進行線程同步。
所有的對象都包含這兩個附加成員,用於管理對象。
- 調用構造函數ctor,進行初始化操作,並返回新建對象的引用地址,將引用地址加入Execution Stack
所以s.Name就是調用set_Name(string value)方法,將"Jackson"的值傳給set_Name,setName方法中將value的值賦值給隱藏字段<Name>k__BackingField
實現get,set方法
上面的Name屬性是一個自動實現的屬性
下面我們來顯示實現Name的get和set訪問器方法
查看get_Name和set_Name的IL代碼,與自動屬性的get_Name和set_Name方法的IL的代碼的區別是多了一行IL_0000: nop和代碼大小。
那么顯示實現getter和setter訪問器有什么好處呢?
1.如果開始使用自動實現的屬性public string Name { get; set; },訪問該屬性的任何代碼實際都會調用get和set方法。如果以后決定自己實現get方法和/或set方法,而不是接收編譯器的默認實現,訪問屬性的任何代碼都不必重新編譯。然后,如果將Name什么為字段,以后又想它更改為屬性,那么訪問字段的所有代碼都必須重新編譯,以便訪問屬性的方法。
2.自動實現的屬性,不能再get和set方法上添加一個斷電,所以不好檢測應用程序在什么時候獲取或設置這個屬性。相反,手動實現的屬性可設置斷點,查錯時顯得非常方便。但是調試時需要注意,如果對屬性Name添加了監視,則可能會引入bug,比如在get訪問器中遞增一個字段count,那么每單步執行一行代碼,監視器都會重新去調用get方法,從而造成字段的遞增。
看下面的code,如果不進行debug,count=1,如果進行了debug,count值會隨着監視Name屬性而調用get方法的次數不同而不同。
解決辦法是在Visual Studio中關閉屬性求值,工具->選項->調試->常規->不勾選啟用屬性求值和其他隱式函數調用。
如果需要對監視器中的屬性求值,可以手動強制屬性求值:
注意:
get和set方法要么都顯示實現,要么都自動實現
性能
1.簡單的get和set訪問其方法,release版本中,JIT將代碼內聯(inline)。使用屬性就沒有性能上的損失。
2.JIT編譯器在調試代碼時不會內聯屬性方法,因為內聯的代碼回變得難以調試。
3.在程序的release版本中,訪問屬性時的性能可能比較快,在程序的調試版本中,則可能比較慢。
4.字段訪問無論在調試還是release版本中很快
訪問權限
通過上面的IL代碼分析,我們已經對屬性這個語法糖的本質更加清楚了。那么既然屬性就是兩個方法構成的,那么我們可以設置get_Name和get_Name方法的訪問權限嗎?
答案是肯定的。如下圖所示,當我們想要對象s的Name屬性時,提示get訪問器不能訪問
那么我們是否能刪掉其中一個get或set方法呢?
答案是不能。如下圖所示:提示必須定義get和set訪問器
注意:
1.屬性的訪問級別可以是指定為任意訪問級別
2.get和set訪問器的訪問級別必須等於屬性的訪問級別或比屬性的訪問級別更大,如屬性Name的訪問級別是public,則get和set訪問器的訪問級別必須是public,或是protected,private等
3.get和set訪問器中只能有一個訪問級別高於屬性的訪問級別,另外一個必須等於屬性的訪問級別
4.get和set訪問器的訪問級別如果等於屬性的訪問級別,則必須省略訪問性的關鍵字。如屬性Name的訪問級別是public,get和set的訪問級別是public,則必須省略get和set前面的public,否則VS編譯不通過。
回到最開始提出的問題
1.屬性和字段的區別?
屬性其實是由兩個訪問器方法get_Name()和set_Name()和一個隱藏字段<Name>k__BackingField構成。
2.為什么會有屬性這個用法?
1.屬性中的get_Name和set_Name方法我們可以自己實現,從而可以在方法中加一些對數據的合理性檢查,確保對象的狀態永遠不被破壞。其他的用法如:在WPF可以利用屬性實現動態綁定。
2.封裝了字段的訪問性。可以設置get方法是public的,set方法是private的,那么這個屬性就是只讀的。
參考資料
《你必須知道的.NET》
《CLR via C#》
作 者: Jackson0714
出 處:http://www.cnblogs.com/jackson0714/
關於作者:專注於微軟平台的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回復。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信我
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!