一、无参属性

  对于字段,强烈建议将所有的字段都设为private。如果允许用户或类型获取或设置状态信息,就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器(accessor)方法。访问器方法可选择对数据的合理性进行检查,确保对象的状态永远不被破坏。如下代码:

  1. private sealed class Employee
  2. {
  3. private String m_Name;
  4. private Int32 m_Age;
  5.  
  6. public String GetName(){
  7. return m_Name;
  8. }
  9.  
  10. public void SetName(String value){
  11. m_Name = value;
  12. }
  13.  
  14. public Int32 GetAge(){
  15. return m_Age;
  16. }
  17.  
  18. public void SetAge(Int32 value){
  19. if (value <= )
  20. throw new ArgumentOutOfRangeException("value", "must be >0");
  21. m_Age = value;
  22. }
  23. }

  想这样的数据封装有两个缺点。第一:不得不实现额外的方法;第二、用户必须调用方法。

  CLR提供了属性(Property)的机制,它缓解了第一个缺点所造成的的影响,同事完全消除了第二个缺点。如下面代码:
  1. private sealed class Employee {
  2. private String m_Name;
  3. private Int32 m_Age;
  4.  
  5. public String Name {
  6. get { return (m_Name); }
  7. set { m_Name = value; } // 关键字'value' 总是代表新值
  8. }
  9.  
  10. public Int32 Age {
  11. get { return (m_Age); }
  12. set {
  13. if (value <= ) // 关键字'value' 总是代表新值
  14. throw new ArgumentOutOfRangeException("value", "must be >0");
  15. m_Age = value;
  16. }
  17. }
  18. }

  于是就可以这样调用:

  1. Employee emp = new Employee();
  2. emp.Name = "Jeffrey Richter";
  3. emp.Age = ;
  4. Console.WriteLine("Employee info: Name = {0}, Age = {1}", emp.Name, emp.Age);

  可将属性想象成智能字段,即背后有额外逻辑的字段。CLR支持静态、实例、抽象和虚属性。属性可以使用任意可访问性修饰符修饰。

  每个属性都有一个名称和一个类型(类型不能为void).属性不能重载。也就是说不能定义名称相同,类型不同的属性。定义属性时,通常要同时指定get和set两个方法。但是,可以省略set方法来定义一个只读属性,或者省略get方法定义一个只写属性。
 
  定义属性时,取决属性的定义,编译器在最后的托管程序集中生成以下两项或三项:
  *)代表属性的get访问器方法的一个方法。仅在属性定义了get访问器方法时生成。
  *)代表属性的set访问器方法的一个方法。仅在属性定义了set访问器方法时生成。
  *)托管程序集元数据中的一个属性定义。这一项是肯定要生成的。
 
  以前面的Employee类为例,编译器将会生成4个方法定义。如图:
  
  编译器在你指定的属性名之前会附加get_或set_前缀,从而自动生成这些方法的名称。C#内建了对属性的支持。当编译器发现代码试图获取或设置一个属性时,它实际会生成对上述某个方法的一个调用。
  1. 自动实现属性
  如果只是为了封装一个私有字段而创建一个属性,C#还提供了一种更简单的语法,称为自动实现的属性(Automatically implemented Property,AIP)。下面是Name属性的一个例子:
  1. private sealed class Employee {
  2. //这是一个自动实现属性
  3. public String Name {get;set;}
  4. }

  2.合理定义属性

  属性和字段的比较:

  1)属性可以是只读或只写的,字段访问确总是可读和可写。如果定义一个属性,最好同时为它提供get和set访问器方法。
  2)一个属性方法可能抛出异常;字段访问永远不会抛出异常。
  3)属性不能作为out或ref参数传给方法;字段却可以。
  4)属性方法可能花费较长时间执行;字段的访问总是立即完成的。
  5)如果连续多次调用,属性方法每次都可能返回一个不同的值;而字段每次调用都返回相同的值。
  6)属性方法可能造成明显的side effect(指访问属性时,除了单纯的设置或获取属性,还会造成对象状态的改变);字段访问永远不会。
  7)属性方法可能需要额外的内存,或者返回一个不正确的引用,指向不属于对象状态一部分的某个东西,这样一来,对返回对象的修改就作用不到原始对象身上了。相反,查询字段返回的总是正确的引用,它指向的东西保证是原始对象状态的一部分。
 
  现在的开发人员对属性的依赖有过之而无不及,经常有没有必要都使用属性,仔细看下上面的比较,你会发现在极少数的情况下,才有必要定义属性。属性的唯一好处就是提供了简化的语法,和调用普通方法相比,属性不仅不会提高代码性能,还会妨碍对代码的理解。建议就是让开发人员老老实实的写Getxxx和Setxxx方法,希望编译器提供一种特殊的,简化的,有别于字段访问的语法,是开发人员知道他们实际上是在调用一个方法。
 
  3.对象和集合初始化器
  我们经常要构造一个对象,然后设置对象的一些公共属性(或字段)。为了简化这个常见的编程模式,C#语言支持一种特殊的对象初始化语法。比如:[()]代表"()"可要可不要。
  1. Employee e = new Employee[()] { Name = "Jeff", Age = }
  对象初始化器语法真正的好处在于,它允许在表达式的上下文(相对于语句的上下文)中编码,允许组合多个函数,进而增强了代码的可读性。于是,就可以这么写了:
  1. string s = new Employee() {Name = "Jeff", Age = }.ToString().ToUpper();
  
  4.匿名类型
  利用C#的匿名类型,可以使用非常简洁的语法来声明一个不可变的元组类型。元组(Tuple)类型是含有一组属性的类型,这些属性通常以某种方式相互关联。
  1. //定义一个类型,后再它的一个实例,并初始化它的属性
  2. var o1 = new { Name = "Jeff", Year = };
  3. Console.WriteLine("Name={0}, Year={1}", o1.Name, o1.Year);

  第一行代码创建了一个匿名类型,没有在new 关键字后制定类型名称,所以编译器会为我自动创建一个类型名称,而且不会告诉我这个名称是什么(这正是匿名类型一词的由来),但编译器是知道的。虽然我不知道变量o1声明的是什么类型,但可以利用C#的"隐式推断类型局部变量"功能(var)。

  编译器支持用另外两种语法声明匿名类型中的属性,它根据变量推断出属性名和类型:

  1. String Name = "Grant";
  2. DateTime dt = DateTime.Now;
  3.  
  4. // 有两个属性的一个匿名类型
  5. // 1. String Name 属性设为"Grant"
  6. // 2. Int32 Year 属性设为dt中的年份
  7. var o2 = new { Name, dt.Year };

  在这个例子中,编译器判断第一个属性名为Name。由于Name是一个局部变量的名称,所以编译器将属性类型设为与局部变量相同的类型:String。对于第二个属性,编译器使用字段/属性的名称:Year。Year是DateTime类的一个Int32属性,所以匿名类型中的Year属性也是一个Int32。

  如果编译器看见你在源代码中定义了多个匿名类型,而且这些类型具有相同的结构,那么它只会创建一个匿名类型定义,但可以创建该类型的多个实例。相同的结构,指在这些匿名类型中,每个属性都有相同的类型和名称,而且这些属性的指定顺序相同。
   匿名类型经常和LINQ技术配合使用。可用LINQ进行查询,从而生成由一组对象构成的集合,这些对象都是相同的匿名类型。然后,可以对结果集中的对象进行处理。所有的这些都在一个方法中完成。
  5.System.Tuple类型
  在System命名空间,Microsoft定义了几个泛型Tuple(元组)类型。它们全部从Object派生,区别只在于元数(泛型参数的个数)。
  在计算机编程中,一个函数或运算的元数是指函数获取的实参或操作数的个数。
  1. //这是最简单的
  2. public class Tuple<T1> {
  3. private T1 m_item1;
  4. public Tuple(T1 item1) { m_Item1 = item1;}
  5. public item1 { get { retuen m_Item1; } }
  6. }

  和匿名类型相似,一旦创建好了一个Tuple,他就不可变了(所有属性都只读)。Tuple类还提供了CompareTo,Equals,GetHashCode和ToString方法,另外还提供了一个Size属性。除此之外,所有Tuple类型都实现了IstruralEquatable,IstructuralComparable和IComparable接口,所以可以比较两个Tuple对象。

二、有参属性
 
  编程语言还支持所谓的有参属性,它的get访问器方法接收一个或多个属性,set访问器方法接收两个或多个参数。C#语言把它们称为索引器。VB称为默认属性。
  C#使用数组风格的语法来公开有参属性(索引器)。换句话说,可将索引器看作C#开发人员重载[]操作符的一种方式。下面是一个实例BitArray类,它允许用数组风格的语法来索引由该类的一个实例维护的一组二进制位。
  1. internal sealed class BitArray {
  2. // 容纳了二进制位的私有字节数组
  3. private Byte[] m_byteArray;
  4. private Int32 m_numBits;
  5.  
  6. // 下面的构造器用于分配字节数组,并将所有位设为 0
  7. public BitArray(Int32 numBits) {
  8. // 先验证实参
  9. if (numBits <= )
  10. throw new ArgumentOutOfRangeException("numBits must be > 0");
  11.  
  12. // 保留位的个数
  13. m_numBits = numBits;
  14.  
  15. // 为位数组分配字节
  16. m_byteArray = new Byte[(m_numBits + ) / ];
  17. }
  18.  
  19. // 下面是索引器(有参属性)
  20. public Boolean this[Int32 bitPos] {
  21.  
  22. // 下面是索引器的get访问器方法
  23. get {
  24. // 先验证实参
  25. if ((bitPos < ) || (bitPos >= m_numBits))
  26. throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);
  27.  
  28. // 返回指定索引处的位的状态
  29. return ((m_byteArray[bitPos / ] & ( << (bitPos % ))) != );
  30. }
  31.  
  32. // 下面是索引器的set访问器方法
  33. set {
  34. if ((bitPos < ) || (bitPos >= m_numBits))
  35. throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);
  36.  
  37. if (value) {
  38. // 将指定索引处的位设为true
  39. m_byteArray[bitPos / ] = (Byte)
  40. (m_byteArray[bitPos / ] | ( << (bitPos % )));
  41. } else {
  42. // 将指定索引处的位设为false
  43. m_byteArray[bitPos / ] = (Byte)
  44. (m_byteArray[bitPos / ] & ~( << (bitPos % )));
  45. }
  46. }
  47. }
  48. }

  BitArray类的调用也非常简单:

  

  1. private static void BitArrayTest() {
  2. // 分配含有14个位的bitArray数组
  3. BitArray ba = new BitArray();
  4.  
  5. // 调用set访问器方法,将编号为偶数的所有为设为true
  6. for (Int32 x = ; x < ; x++) {
  7. ba[x] = (x % == );
  8. }
  9.  
  10. // 调用get访问器方法显示所有为的状态
  11. for (Int32 x = ; x < ; x++) {
  12. Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
  13. }
  14. }
  CLR本身并不区分无参属性和有参属性。对CLR来说,每个属性都只是类型中定义的一对方法和一些元数据。将this[...]作为表达一个索引器的语法,这纯粹是C#团队自己的选择,正因如此,C#只允许在对象的实例上定义索引器,C#没有提供定义静态索引器属性的语法,虽然CLR是支持静态有参属性的。
 

三、调用属性访问器方法时的性能

  对于简单的get和set访问器方法,JIT编译器会将代码内联。这样一来,使用属性(而不使用字段)就没有性能上的损失。

  内联是指将一个方法的代码直接编译到它的方法中。这样能避免在运行时发出调用所产生的开销,代价是编译好的方法的额代码会变得更大。
  由于属性访问器方法通常只包含及少量代码,所以对它们进行内联,反而会使最终生成的本地代码更小,执行更快。
  JIT编译器在调试代码时不会内联属性,因为这会变得难以调试。

四、属性访问器的可访问性

  我们有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。如下:

  1. public class SomeType {
  2. private String m_name;
  3. public String Name {
  4. get { return m_name;}
  5. protected set { m_name = value;}
  6. }
  7. }

  定义一个属性时,如果两个访问器方法需要具有不同的可访问性,C#语法要求必须为属性本身指定限制最不大的那一种可访问性。然后,在两个访问器中,只能选择一个来应用限制较大的那一种可访问性。如前面例子中,属性本身声明为public,set访问器方法声明为protected(限制比public大)。

五、泛型属性访问器方法

既然属性本质是方法,而且C#和CLR允许方法是泛型的,但是C#不允许定义泛型属性。这从概念上讲不通。属性本用来表示一项可供查询的或设置的对象特征。从概念上讲,属性是不具有行为的。

[CLR via C#]10. 属性的更多相关文章

  1. CLR类型设计之属性

    在之前的随笔中,我们探讨了参数,字段,方法,我们在开始属性之前回顾一下,之前的探讨实际上串联起来就是OOP编程的思想,在接下来的文章中,我们还会讨论接口(就是行为),举个例子:我们如果要做一个学生档案 ...

  2. 【CLR in c#】属性

    1.无参属性 1.为什么有字段还需要属性呢? 因为字段很容易写出不恰当的代码,破坏对象的状态,比如Age=-1.人的年纪不可能为负数.使用属性后你可以缓存某些值或者推迟创建一些内部对象,你可以以线程安 ...

  3. 【C#进阶系列】10 属性

    属性分为无参属性和有参属性(即索引器). 属性相对于字段的优点不仅仅是为了封装,还可以在读写的时候做一些额外操作,缓存某些值或者推迟创建一些内部对象,也适用于以线程安全的方式访问字段. 话说最基本的属 ...

  4. 重温CLR(七 ) 属性和事件

    无参属性 许多类型都定义了能被获取或更高的状态信息.这种状态信息一般作为类型的字段成员实现.例如一下类型包含两个字段: public sealed class Employee{ public str ...

  5. CLR via C#深解笔记四 - 方法、参数、属性

    实例构造器和类(引用类型) 构造器(constructor)是允许将类型的实例初始化为良好状态的一种特殊方法.构造器方法在“方法定义元数据表”中始终叫.ctor. 创建一个引用类型的实例时: #1, ...

  6. CLR VIA C# 学习笔记

    第19章 可空类型 1)使用Nullable<T>可将int32的值类型设置为Null,CLR会在Null时默认赋值为0; 如:Nullable<T> x=null; //使用 ...

  7. 【CLR VIA C#】读书笔记

    工作几年了才看,记录下笔记备忘. 章节 笔记 1.CLR的执行模型 公共语言运行时(Common Language Runtime,CLR) 源代码-->编译器检查语法和分析源代码-->托 ...

  8. CLR总览

    Contents 第1章CLR的执行模型... 4 1.1将源代码编译成托管代码模块... 4 1.2 将托管模块合并成程序集... 6 1.3加载公共语言运行时... 7 1.4执行程序集的代码.. ...

  9. CLR 完全介绍

    From: http://msdn.microsoft.com/zh-cn/magazine/cc164193.aspx http://msdn.microsoft.com/en-us/magazin ...

随机推荐

  1. PHP - 如何使用XDEBUG来远程调试?

    开发的时候我都是使用XDebug在本地调试,但是最近加入一些项目中去,环境太复杂了,要在本地搭建一个开发环境真的太麻烦了,那么我们怎么使用xdebug来远程调试呢? 我这里使用虚拟机搭建了一个模拟环境 ...

  2. [GraphQL] Use GraphQL's Object Type for Basic Types

    We can create the most basic components of our GraphQL Schema using GraphQL's Object Types. These ty ...

  3. State状态设计模式

    1.状态模式:改变对象的行为 一个用来改变类的(状态的)对象. 2:问题:当你自己实现 State 模式的时候就会碰到很多细节的问题,你必须根据自己的需要选择合适的实现方法, 比如用到的状态(Stat ...

  4. Android手机的 storage

    老外的一段解释 -------------------------------------------------------------------------------------------- ...

  5. 几种常用远程通信技术(RPC,Webservice,RMI,JMS)的区别

    原文链接:http://blog.csdn.net/shan9liang/article/details/8995023 RPC(Remote Procedure Call Protocol) RPC ...

  6. AD10长方形通孔焊盘的画法

    1.点击工具栏中[放置焊盘]按钮 2.按键盘Tab键弹出[焊盘]对话框 3.设置[空洞信息]相关尺寸(根据自己所需实际设置) 这里左边的单选按钮选择“槽”,通孔尺寸输入20mil,长度为80mil,旋 ...

  7. 解决Visual Studio 2010新建工程时出现『1>LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt』错误

    VS2010在经历一些更新后,建立Win32 Console Project时会出"error LNK1123" 错误.   解决方案为: 第一步:将:项目|项目属性|配置属性|清 ...

  8. ODAC (V9.5.15) 学习笔记(二十一)数据复制

    用TVirtualTable在内存中缓存TOraQuery中的数据,主要应用场景是参照其他数据,需要将TOraQuery中的数据复制到TVirtualTable,由于没有类似于TClientDataS ...

  9. windows7 中开启无线热点

    我用的是移动的 CMCC-EDU 上网,但是这个只能在一个设备上登陆,那么问题就来了,当我电脑需要用网,手机也想要用网(不用 2/3/4G)该怎么办? 电脑操作系统:windows7 接下来是开启 w ...

  10. SVN分支与合并

    分支的基本概念就正如它的名字,开发的一条线独立于另一条线,如果回顾历史,可以发现两条线分享共同的历史,一个分支总是从一个备份开始的,从那里开始,发展自己独有的历史(如下图所示) ⑴创建分支 假设目前我 ...