2020年10月21日星期三

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

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

目录
  • IL角度理解C#中字段,属性与方法的区别
  • 1.字段,属性与方法的区别
  • 2. 字段,属性与方法的IL代码
    • 2.1 C#代码
    • 2.2 IL代码分析
      • 2.2.1 字段的IL代码
      • 2.2.2 属性的IL代码
        • 2.2.2.1 属性
        • 2.2.2.2 自动生成属性
      • 2.2.3 方法的IL代码分析
  • 3 属性的功能
    • 3.1 设置只读属性
    • 3.2 调用方法
    • 3.3 赖加载
    • 3.4 接口继承
    • 3.5 属性做个简单的校验
    • 3.6 属性中调用事件
  • 4 字段的优越性
    • 4.1 属性赋值代码
    • 4.2 字段赋值
  • 5 小技巧
  • 6 ref引用的本质

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);   

没有评论:

发表评论