属性,允许源代码用简化语法来调用方法。CLR支持两种属性:无参属性和有参属性(索引器)。
1.1无参属性特征
由于某些不恰当使用字段会破坏对象的状态,所以一般会将所有字段都设为private。要允许用户或类型获取或设置状态信息,需要提供封装了字段访问的方法(访问器)。
public class Employee { private string name; public string GetName(){return name;} public string SetName(string value){name = value;} }
public class Employee { private string name; public string Name
{ get{return name;} set{name=value;} } }
每个属性都有名称和类型(类型不能是void)。属性不能重载。
C#编译器发现获取或设置属性时,实际会生成如下代码,在属性名之前自动附加get_,set_前缀。
public class Employe { private string name; public string get_Name() { return n_Name; } pubic void set_Name() { n_Name=value; } }
针对源代码中定义的每个属性,编译器会在托管程序集的元数据中生成一个属性定义项,包含一些标志以及属性类型。同时还引用了get和set访问器方法。这些工作将属性这种抽象概念与它的访问器方法之间建立起一个联系。
1.2 自动实现属性
如果仅为了封装一个支持字段而创建属性,C#提供了称为自动属性的简洁语法。public string Name{get;set;}C#会在内部自动声明一个私有字段。
优点:访问该属性的任何代码实际都会调用get和set方法。如果以后决定自己实现get和set方法,则访问属性的任何代码都不必重新编译。然而将Name声明为字段,以后想改为属性,那么访问字段的所有代码都必须重新编译才能访问属性方法。
缺点:AIP的支持字段名称由编译器决定,每次重新编译都可能更改名称。因此任何类型含有一个AIP,就没办法对该类型的实例进行反序列化。而运行时序列化引擎会将字段名持久存储到序列化流中。任何想要序列化或反序列化的类型中都不要使用AIP功能。另外使用AIP不能加断点,手动实现的属性则是可以,方便调试。使用AIP属性必然是可读可写的,只写字段不能读取值有什么用?只读肯定是有默认值,所以AIP作用于整个属性的,不能显式实现一个访问器方法,而让另一个自动实现。
1.3 属性使用时需注意的点:
- 如果定义属性,最好同时为它提供get,set访问器方法
- 属性方法可能抛出异常;字段访问永远不会
- 属性不能作为out和ref参数传给方法;字段可以
- 属性方法可能花较长时间执行,可能需要额外内存
- 属性只是简化了语法,不会提升代码的性能,最好老老实实实现GetXXX和SetXXX方法。
1.4 对象和集合初始化器
C#支持一种特殊的对象初始化语法 Employee e = new Employee(){Name="abc",Age = 45}。这句话将构造一个对象,调用它的无参构造函数,将公共Name属性设为"abc"等,等同于e.Name = "abc";e.Age = 45;两种方式生成的IL代码相同。同时C#还允许组合多个函数,比如
Employee e = new Employee(){Name="abc",Age = 45}.ToString().ToUpper();
如果属性类型实现了IEnumerable或IEnumerable<T>接口,属性就被认为是集合,而集合的初始化是一种相加操作。如下:
public class ClassRoom { private List<string> students = new List<string>(); public List<string> Students{get{return students;}} //初始化,实际上是调用集合自带的Add方法,将数据添加到集合中 ClassRoom classRoom = new ClassRoom{Students = {"sd","sdf","fgd"};}
}
//对于字典类型初始化
var table = new Dictionary<string,int>{ {"abc",1}, {"def",2} }
1.5 匿名类型
匿名类型的功能可以自动声明不可变的元组类型。元组类型含有一组属性的类型。var o1 = new {Name="abc",Year = 1990}
编译器会推断每个表达式的类型,创建推断类型的私有字段,为每个字段创建公共只读属性,并创建一个构造器来接受所有表达式,在其中会用传给他的表达式求值结果来初始化私有字段。还会重写Object的Equals,GetHashCode和ToString方法,生成所有这些方法的代码。
如果在源代码中定义了多个匿名类型,而且这些类型具有相同的结构(每个属性都有相同的类型和名称,而且这些属性的指定顺序相同),那么它只会创建一个匿名类型定义,并创建该类型多个实例。
一般匿名类型经常和LINQ配合使用。方法原型不能接受匿名类型参数,因为不知道是什么类型,也不能返回对匿名类型的引用,想要传递元组,可以使用System.Tuple类型。
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); var query = from pathname in Directory.GetFiles(myDocuments) let LastWriteTime = File.GetLastWriteTime(pathname) where LastWriteTime>(DateTime.Now - TimeSpan.FromDays(7)) orderby LastWriteTime select new {Path = pathname,LastWriteTime};
1.6 有参属性
C#使用数组风格的语法公开有参属性(索引器),可将索引器是C#开发人员对[]操作符的重载。比如定义一个类Student
public class Student { public string this[int n] { get { return Name; } set { Name = n > 0 ? "大于0" : "小于0"; } } private string Name; }
当执行Student stu = new Student();stu[4] = "NNN";时执行set方法,将Name设为大于0;执行stu[4]时输出大于0字样。
1.7 调用属性访问器方法时的性能
对于简单的get和set访问器方法,JIT编译器会将代码内联(将方法也就是访问器方法的代码直接编译到调用它的方法中),避免在运行时发出调用所产生的开销,代价是编译好的方法变得更大。注意:JIT在调试代码时不会内联属性方法,因为这将难以调试,发行版本会使用内联,性能会快。
1.8 既然属性本质上是方法,而C#和CLR支持泛型方法,但是C#不允许属性引入自己的泛型类型参数,最主要原因属性本来应该表示可供查询或设置某个对象特征。一旦引入泛型类型参数就意味可能改变查询/设置行为,但属性不应该和行为沾边,公开对象的行为无论是不是泛型,都应该定义方法而非属性。