IL角度理解C#中字段,属性与方法的区别

  • A+
所属分类:.NET技术
摘要

字段的本质是变量,直接在类或者结构体中声明。类或者结构体中会有实例字段,静态字段等(静态字段可实现内存共享功能,比如数学上的pi就可以存在静态字段)。一般来说字段应该带有private 或者 protected访问属性。一般来说字段需要通过类中的方法,或者属性来暴露给其他类。通过限制间接访问内部字段,来保护输入数据的安全。


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的智能提示给我们做了解答

IL角度理解C#中字段,属性与方法的区别

翻译过来的意思是属性返回的是临时变量,ref需要绑定特定的变量,如字段,数组元素等。
属性拷贝需要的时间:

IL角度理解C#中字段,属性与方法的区别

花费时间大约是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;     } 

IL角度理解C#中字段,属性与方法的区别

综上,使用字段的性能比使用属性性能提升了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