1. 程式人生 > >IL角度理解C#中欄位,屬性與方法的區別

IL角度理解C#中欄位,屬性與方法的區別

# IL角度理解C#中欄位,屬性與方法的區別 [toc] # 1.欄位,屬性與方法的區別 欄位的本質是變數,直接在類或者結構體中宣告。類或者結構體中會有例項欄位,靜態欄位等(靜態欄位可實現記憶體共享功能,比如數學上的pi就可以存在靜態欄位)。一般來說欄位應該帶有private 或者 protected訪問屬性。一般來說欄位需要通過類中的方法,或者屬性來暴露給其他類。通過限制間接訪問內部欄位,來保護輸入資料的安全。 屬性的本質是類的一個成員,它提供了私有欄位讀,寫,計算值的靈活機制。屬性如果是資料成員能直接被使用,但本質是特殊方法,我們稱之為訪問器。它的作用是使得資料訪問更容易,資料更安全,方法訪問更靈活。屬性使得類暴露了get,set公有方法,它隱藏了實現,get屬性訪問器,用來返回屬性值,set屬性訪問器,用來設定值。綜上,屬性的本質是一對方法的包裝,get,set。 他們是完全不同的語言元素。欄位是類裡儲存資料的基本單元(變數),屬性不能儲存。 需要建立屬性來控制私有變數(欄位)基於物件的讀寫訪問控制。 一個欄位給其他類訪問,只有兩種方法,欄位的訪問屬性修改為public,不建議,因為欄位是可讀可寫的,無法阻止使用者寫某些欄位,比如出生日期,只讀不可寫,使用屬性。 欄位不能丟擲異常,呼叫方法,屬性可以。 在屬性裡, Set 或者 Get 方法由編譯器預定義好了。 # 2. 欄位,屬性與方法的IL程式碼 ## 2.1 C#程式碼 主程式 ``` class Program { static void Main(string[] args) { Person Tom = new Person(); Tom.SayHello(); Console.WriteLine("{0}", Tom.Name); } } ``` Person類 ``` public class Person { private string _name; public string _firstName; public string Name { get { // return _name; return "Hello"; } set { _name = value; } } public int Age{get;private set;} //AutoProperty generates private field for us public void SayHello() { Console.WriteLine("Hello World!"); } } ``` ## 2.2 IL程式碼分析 ### 2.2.1 欄位的IL程式碼 可以看到欄位的IL程式碼的關鍵字是 field。 ``` .field private string _name .field public string _firstName ``` ### 2.2.2 屬性的IL程式碼 #### 2.2.2.1 屬性 屬性的IL關鍵字即是property。 ``` .property instance string Name() { .get instance string FieldPropertyMethod.Person::get_Name() .set instance void FieldPropertyMethod.Person::set_Name(string) } // end of property Person::Name ``` 點到對應的get,set訪問器。 ``` .method public hidebysig specialname instance string get_Name() cil managed { .maxstack 1 .locals init ( [0] string V_0 ) IL_0000: nop IL_0001: ldstr "Hello" IL_0006: stloc.0 // V_0 IL_0007: br.s IL_0009 IL_0009: ldloc.0 // V_0 IL_000a: ret } // end of method Person::get_Name .method public hidebysig specialname instance void set_Name( string 'value' ) cil managed { .maxstack 8 IL_0000: nop IL_0001: ldarg.0 // this IL_0002: ldarg.1 // 'value' IL_0003: stfld string FieldPropertyMethod.Person::_name IL_0008: ret } // end of method Person::set_Name ``` 從上可以看出get,set訪問器的本質就是方法(method).由上屬性就是對get,set兩種方法及其訪問特性的封裝。由此可見,屬性就是對get,set方法的封裝。 #### 2.2.2.2 自動生成屬性 a. 自動生成屬性程式碼 程式碼量小,實用,此語法從C#3.0開始定義自動屬性 ``` public int Age{get;private set;} ``` b. 自動生成屬性的IL程式碼分析 ``` .property instance int32 Age() { .get instance int32 FieldPropertyMethod.Person::get_Age() .set instance void FieldPropertyMethod.Person::set_Age(int32) } // end of property Person::Age } // end of class FieldPropertyMethod.Person ``` 由上可以看出,其IL程式碼證明也是屬性。繼續看get,set欄位屬性方法。 ``` .method public hidebysig specialname instance int32 get_Age() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 IL_0000: ldarg.0 // this IL_0001: ldfld int32 FieldPropertyMethod.Person::'k__BackingField' IL_0006: ret } // end of method Person::get_Age .method private hidebysig specialname instance void set_Age( int32 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 IL_0000: ldarg.0 // this IL_0001: ldarg.1 // 'value' IL_0002: stfld int32 FieldPropertyMethod.Person::'k__BackingField' IL_0007: ret } // end of method Person::set_Age ``` k__BackingField 即是屬性背後的欄位變數,這是編譯器自動生成的後臺欄位。由此自動屬性與我們自己定義的屬性功能一模一樣。 ### 2.2.3 方法的IL程式碼分析 IL程式碼中的關鍵字method即表示方法。 ``` .method public hidebysig instance void SayHello() cil managed { .maxstack 8 IL_0000: nop IL_0001: ldstr "Hello World!" IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method Person::SayHello ``` **備註:本IL程式碼由rider的IL View功能產生** # 3 屬性的功能 ## 3.1 設定只讀屬性 像出生年月這種只讀不能寫的屬性,易用屬性。 ``` public datetime birthday{get;private set;} ``` ## 3.2 呼叫方法 在屬性Count中呼叫CalculateNoOfRows方法; ``` public class Rows { private string _count; public int Count { get { return CalculateNoOfRows(); } } public int CalculateNoOfRows() { // Calculation here and finally set the value to _count return _count; } } ``` ## 3.3 賴載入 有些資料載入的功能可以放在屬性中載入,不放在建構函式中,以此來加快物件建立的速度。 ## 3.4 介面繼承 可以對接口裡的屬性進行繼承,而欄位不行; ## 3.5 屬性做個簡單的校驗 ``` class Name { private string MFullName=""; private int MYearOfBirth; public string FullName { get { return(MFullName); } set { if (value==null) { throw(new InvalidOperationException("Error !")); } MFullName=value; } } public int YearOfBirth { get { return(MYearOfBirth); } set { if (MYearOfBirth<1900 || MYearOfBirth>DateTime.Now.Year) { throw(new InvalidOperationException("Error !")); } MYearOfBirth=value; } } public int Age { get { return(DateTime.Now.Year-MYearOfBirth); } } public string FullNameInUppercase { get { return(MFullName.ToUpper()); } } } ``` 例子而已,ddd中一般來說值物件來定義,校驗也同樣會放在值物件中。 ## 3.6 屬性中呼叫事件 ``` public class Person { private string _name; public event EventHandler NameChanging; public event EventHandler NameChanged; public string Name{ get { return _name; } set { OnNameChanging(); _name = value; OnNameChanged(); } } private void OnNameChanging(){ NameChanging?.Invoke(this,EventArgs.Empty); } private void OnNameChanged(){ NameChanged?.Invoke(this,EventArgs.Empty); } ``` # 4 欄位的優越性 欄位作為屬性的儲存基元功用之外,還有沒有應用場景是效能超越屬性的呢?答案是肯定的,欄位作為ref/out引數時,效能更優異, 下面舉一例。 ##4.1 屬性賦值程式碼 ``` class Program { static void Main(string[] args) { #region 屬性效能測試 Point[] points = new Point[1000000]; Initializ(points); var bigRunTime = DateTime.Now; for (int i = 0; i < points.Length; i++) { int x = points[i].X; int y = points[i].Y; TransformPoint(ref x, ref y); points[i].X = x; points[i].Y = y; } var endRunTime = DateTime.Now; var timeSpend=ExecDateDiff(bigRunTime,endRunTime); Console.WriteLine("變換後首元素座標:{0},{1}",points[0].X,points[0].Y); Console.WriteLine("程式執行花費時間:{0}",timeSpend); #endregion } /// 程式執行時間測試 ///
/// 開始時間 /// 結束時間 /// 返回(秒)單位,比如: 0.00239秒 public static string ExecDateDiff(DateTime dateBegin, DateTime dateEnd) { TimeSpan ts1 = new TimeSpan(dateBegin.Ticks); TimeSpan ts2 = new TimeSpan(dateEnd.Ticks); TimeSpan ts3 = ts1.Subtract(ts2).Duration(); //你想轉的格式 return ts3.TotalMilliseconds.ToString(); } static Point[] Initializ(Point[] points) { for (int i = 0; i < points.Length; i++) { points[i] =new Point(); points[i].X = 1; points[i].Y = 2; } Console.WriteLine("首元素座標:{0},{1}",points[0].X,points[0].Y); return points; } static void TransformPoint(ref int x, ref int y) { x = 3; y = 4; } } ``` ``` public class Point { public int X { get; set; } public int Y { get; set; } } ``` 這裡屬性為什麼不能直接繫結ref引數呢?rider的智慧提示給我們做了解答 ![](https://img2020.cnblogs.com/blog/1606616/202010/1606616-20201022003101299-1640836015.png) 翻譯過來的意思是屬性返回的是臨時變數,ref需要繫結特定的變數,如欄位,陣列元素等。 屬性拷貝需要的時間: ![](https://img2020.cnblogs.com/blog/1606616/202010/1606616-20201022003110196-1475033567.png) 花費時間大約是31ms。 ## 4.2 欄位賦值 ``` class Program { static void Main(string[] args) { #region 欄位效能測試 PointField[] points = new PointField[1000000]; InitializField(points); var bigRunTime = DateTime.Now; for (int i = 0; i < points.Length; i++) { TransformPoint(ref points[i].X, ref points[i].Y); } var endRunTime = DateTime.Now; var timeSpend=ExecDateDiff(bigRunTime,endRunTime); Console.WriteLine("變換後首元素座標:{0},{1}",points[0].X,points[0].Y); Console.WriteLine("欄位賦值執行花費時間:{0}",timeSpend); #endregion } /// 程式執行時間測試 ///
/// 開始時間 /// 結束時間 /// 返回(秒)單位,比如: 0.00239秒 public static string ExecDateDiff(DateTime dateBegin, DateTime dateEnd) { TimeSpan ts1 = new TimeSpan(dateBegin.Ticks); TimeSpan ts2 = new TimeSpan(dateEnd.Ticks); TimeSpan ts3 = ts1.Subtract(ts2).Duration(); //你想轉的格式 return ts3.TotalMilliseconds.ToString(); } static PointField[] InitializField(PointField[] points) { for (int i = 0; i < points.Length; i++) { points[i] =new PointField(); points[i].X = 1; points[i].Y = 2; } Console.WriteLine("首元素座標:{0},{1}",points[0].X,points[0].Y); return points; } static void TransformPoint(ref int x, ref int y) { x = 3; y = 4; } } ``` ``` public class PointField { public int X; public int Y; } ``` ![](https://img2020.cnblogs.com/blog/1606616/202010/1606616-20201022003011524-1259014545.png) 綜上,使用欄位的效能比使用屬性效能提升了38.7%(31-19/31=38.7%),很可觀。 究其原因,屬性開闢了臨時變數作為中轉進行了深拷貝,而欄位則是直接對地址(指標)進行解引用,直接賦值。 出賦值速度提升外,欄位不需開闢臨時記憶體,更加節省記憶體。 # 5 小技巧 在vs中prop 按tab鍵可自動生成屬性 #6 ref引用的本質 寫在文末,也算是本文的彩蛋。該方法的形參通過關鍵字ref將變數設定成了引用。 ``` static void TransformPoint(ref int x, ref int y) { x = 3; y = 4; } ``` 引用ref的IL程式碼 ``` .method private hidebysig static void TransformPoint( int32& x, int32& y ) cil managed ``` 對沒錯,你看到了&,熟悉C語言的道友知道,在這裡是取了傳入整形變數的地址。所以在方法裡進行解引用賦值,就能改變形參的值, 本質就是通過指標(傳入變數的地址)來對形參值的修改。 [gitHub程式碼地址](https://github.com/JerryMouseLi/RefField) 參考文章: [What is the difference between a field and a property?](https://stackoverflow.com/questions/295104/what-is-the-difference-between-a-field-and-a-property?page=1&tab=votes#tab-top)
版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。 本文連結:https://www.cnblogs.com/JerryMouseLi/p/138557