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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM