10.1 无参属性
面向对象设计和编程的重要原则之一就是数据封装,意味着类型的字段永远不应该公开,否则很容易因为不恰当使用字段而破坏对象的状态。
建议将所有字段都设为private。要允许用户或类型设置、获取状态信息,就公开一个针对该用途的方法。
封装了字段访问的方法通常称为访问器accessor方法。
CLR提供了属性property的机制,减少了访问器方法的缺点
private String m_Name; public String Name { get{return (m_name);} set{m_name=value;} }
可将属性想象成智能字段,即背后有额外逻辑的字段。
CLR支持静态、实例、抽象和虚属性。另外,属性可以认有可访问性修饰符来标记。
属性的特点
每个属性都有名称和类型(不能是void)。
属性不能重载,即不能定义名称相同类型不同的两个属性。
定义属性时通常同时指定get和set两个方法。但可以省略其中之一实现只读、只写。
利用属性的set、get操纵类型定义的私有字段。
私有字段通常称为支持字段 backing field。
但get、set方法并非一定要访问支持字段。
属性本质是方法
以前面的Name属性为例,由于有get、set访问器方法,
所以编译器在类型中生成两个方法定义,就像原始代码是这样写的一样
private String m_Name; public String GetName(){return m_name} public void set_Name(String value){m_Name=value;}
编译器在你指定的属性名之前自动附加get_或者set_前缀来生成方法名,C#内建了对属性的支持。
当C#编译器发现代码试图获取设置属性时,实际会生成对上述某个方法的的调用。
CLR只认访问器方法
除了生成访问器方法,针对源代码中定义的每一个属性,编译器还会在托管程序集中的元数据中生成一个属性定义项。
在这个记录项中,包含了一些标志flags以及属性的类型。
另外还引用了get、set访问器方法。这些信息的唯一作用是在属性这种抽象概念与他的访问器方法之间建立一个联系。
编译器和其它工具可以利用这种元数据,使用System.Reflection.PropertyInfo类来获得。
CLR不使用这种元数据信息,运行时只需要访问器方法。
10.1.1 自动实现的属性
如果只是为了封装一个支持字段而创建属性。C#提供了一种更简洁的语法,称为自动实现的属性 Automatically Implemented Property,AIP。
public String Name{get;set;}
升麻属下而不提供get/set方法的实现,C#自动为你声明一个私有字段。编译器会自动实现get_Name,setName方法,分别返回和设置字段中的值。
AIP的特点
使用AIP,意味着你已经创建了一个属性,访问该属性的任何代码实际都会调用get,set方法。
如果以后要自己实现get、set方法,而不是接受编译器的默认实现,访问属性的任何代码都不必重新编译。
然而,如果将Name声明为字段,以后又想把他改为属性,那么访问字段的所有代码都必须重新编译才能访问属性方法。
AIP的缺点
1.作者不喜欢AIP,避免使用AIP。因为字段声明语法可能包含初始化,
2.运行时序列化引擎将字段名持久存储到序列化的流中。AIP的支持字段名由编译器决定,每次重新编译都可能更改这个名称。因此,任何类型只要含有一个AIP,就没办法对该类型进行反序列化。
3.调试时不能在AIp的get、set上添加断点。
如果使用AIP,属性必然是可读和可写的。AIP作用域整个属性,要么都用要么都不用。
10.1.2 合理定义属性
作者不喜欢属性,看起来像字段,本质是方法,使人误解,程序员在看到貌似访问字段的代码时,会做出一些对属性来说不成立的假定,具体如下。
1.属性可以只读或只写,而字段访问总是可读和可写的,除了1readonly仅在构造器中可写。如果定义属性,最好同时为他提供get、set。
2.属性方法可能抛出异常,字段不会。
3.属性不能作为out、ref参数传给方法,字段可以。
4.属性方法可能花费较长时间执行,字段访问总是立即完成。许多人使用属性是为了线程同步,这就可能造成线程永远终止。所以,要线程同步就不要使用舒徐而要使用方法。
5.连续多次调用,属性方法每次都可能返回不同的值,字段则每次都返回相同的值。
6.属性方法可能需要额外内存,或者返回的引用并非指向对象状态一部分,造成对返回对象的修改做用不到原始对象身上。而查询字段返回的引用总是只想原始对象状态的一部分。使用会返回一个拷贝属性很容易引起混淆。
10.1.3 对象和集合初始化器
经常要构造一个对象并设置对象的一些公共属性或字段,C#提供了一种特殊的对象初始化语法来简化这种常见的编程模式。
Employee e=new Employee(){Name="Jeff",Age=45};
这一句代码构造了一个Employee对象,调用它的无参构造器,将其公共属性Name设为"Jeff",Age属性设为45。实际上这等价于以下几行代码。
Employee e=new Employee(); e.Name="Jeff"; e.Age=45;
对象初始化器的好处
对象初始化器语法真正的好处在于,允许在表达式的上下文中编码,允许组合多个函数。
String s=new Employee(){Name="Jeff",Age=45}.ToString().ToUpper();
这个语句首先构造一个Employee对象,调用它的构造器,再初始化两个公共属性,然后在结果表达式上,先调用ToString,再调用ToUpper。
补充一下,如果想调用的本来就是一个无参构造器,C#允许省略起始大括号之前的圆括号,下面这行代码与上一行生成相同的IL。
String s=new Employee{Name="Jeff",Age=45}.ToString().ToUpper();
如果属性的类型实现了IEnumerable或IEnumerable<T>接口,属性就被认为是集合,而集合的初始化是一种相加操作而非替换操作。比如像下面初始化Classroom对象的Student集合。
Classroom classroom=new Classroom{Students={"Jeff","Tom","Kim"}};
假定List<String>类型提供了名为Add的方法,上述代码会被编译器转换成这样。
Classroom classroom=new Classroom(); classroom.Students.Add("Jeff"); classroom.Students.Add("Tom"); classroom.Students.Add("Kim");
如果属性的类型实现了IEnumerable或IEnumerable<T>,但未提供Add方法,编译器就不允许使用集合初始化语法向集合中添加数据项。
使用集合初始化器语法初始化Dictionary
有的集合的Add方法要获取多个实参,比如Dictionary的Add
public void Add(TKey key,TValue value);
通过在集合初始化器中嵌套大括号的方式可向Add方法传递多个实参,如下所示。
var table=new Dictionary<String,Int32>{{"Jeffrey",1},{"Kristin",2}}
它等价于以下代码
var table=new Dictionary<String,Int32>(); table.Add("Jeffrey",1); table.Add("Kristin",1);
10.1.4 匿名类型
利用C#的匿名类型功能,可以用很简洁的语法来自动声明不可变的元组类型。
元组类型是含有一组属性的类型,这些属性通常以某种方式互相关联。
以下代码定义了一个含有两个属性的类,构造了该类型的实例。
var o1=new {Name="Jeff",Year=1964};
这行代码创建了匿名类型,没在new关键字后指定类型名称,编译器会自动创建类型名称,而且不会告诉你这个名称是什么,这正是匿名的含义。
这行代码使用对象初始化器语法来声明属性同时初始化。
编译器如何处理匿名类型
编译器会推断每个表达式的类型,创建类型的私有字段,为每个字段创建公共只读属性,并创建一个构造器来接受所有这些表达式。在构造器的代码在,会用传给他的表达式的求值结果来初始化私有只读字段。
除此之外,编译器还会重写Object的Equals,GetHashCode和ToString方法,并生成所有这些方法中的代码。
因此匿名类型的实例能放到哈希表集合中。
属性是只读的,目的是防止对象的哈希码发生改变。如果对象在哈希表中作为键来使用,更改他的哈希码会再也找不到她。
多个相同结构的匿名类型
如果你定义多个匿名类型,而这些类型具有相同的结构,那么编译器只会创建一个匿名类型定义,但创建多个实例。
两个匿名类型实例可以检查两个对象是否包含相等的值,将一个对象的引用赋给正在指向另一个对象的变量。
由于这种类型的同一性,可以创建一个隐式类型的数组,在其中包含一组匿名类型的对象
匿名类型的其他注意事项
匿名类型经常与LINQ配合使用。
匿名类型的实例不能泄露到方法外部,方法原型不接受匿名类型的参数。防范也不能返回对匿名类型的引用。