zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    android问题及其解决-优化listView卡顿和怎样禁用ListView的fling
    平安科技移动开发二队技术周报(第三期)
    机房重构(个人版)——类图
    php-wamp环境搭建
    ajax 通过return 返回data值
    cocos2d-x中六种持续性动作
    Android SimpleAdapter
    jquery 判断当前上传文件大小限制上传格式 搭配thinkphp实现上传即预览(模拟异步上传)
    【转】我的第一个Python小程序
    python官网
  • 原文地址:https://www.cnblogs.com/JerryMouseLi/p/13855733.html
Copyright © 2011-2022 走看看