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

JerryMouseLi發表於2020-10-22

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

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::'<Age>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::'<Age>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
           
        }

        /// 程式執行時間測試
        /// </summary>
        /// <param name="dateBegin">開始時間</param>
        /// <param name="dateEnd">結束時間</param>
        /// <returns>返回(秒)單位,比如: 0.00239秒</returns>
        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的智慧提示給我們做了解答

翻譯過來的意思是屬性返回的是臨時變數,ref需要繫結特定的變數,如欄位,陣列元素等。
屬性拷貝需要的時間:

花費時間大約是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
        }

        /// 程式執行時間測試
        /// </summary>
        /// <param name="dateBegin">開始時間</param>
        /// <param name="dateEnd">結束時間</param>
        /// <returns>返回(秒)單位,比如: 0.00239秒</returns>
        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;
    }

綜上,使用欄位的效能比使用屬性效能提升了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程式碼地址

參考文章:
What is the difference between a field and a property?


版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。 本文連結:https://www.cnblogs.com/JerryMouseLi/p/13855733.html

相關文章