属性提供灵活的机制来读取、编写或计算私有字段的值。
属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来。
可以像使用公共数据成员一样使用属性,但实际上属性是称为“访问器”的一种特殊方法,这使得数据在被轻松访问的同时,仍能提供方法的安全性和灵活性。
无参属性
一般看来,常量和字段已经足够刻画实际生活中对象的各种参数。实际上也确实是这样。属性(property)是一种"高级字段”,它可能带有一个 getter 和一个 setter,它们保护属性的值,使之不会被外部胡乱篡改。
和字段相比,属性实现了对成员的封装。
Java 中有相似的概念(JavaBeans),它要求类中的成员都是私有的,并且提供一个 public 的 getter 和一个 setter,用于让外界访问和修改私有字段的值。
这两个方法的名称以 get 和 set 开头,需要自己手写。
在 C# 中,也可以通过字段和一对读写方法,自己手动实现属性:
class A { private int c; public int getC() { return c; } public void setC(int value) { c = value; } }在这里的私有字段称为支持字段(Backing Field)。
不过,这样做有两个明显缺点,一是必须手打这些代码;二是在访问属性时,必须调用方法,而不能直接使用点号加属性名。
CLR 提供了称为属性的机制,解决了这两个缺点。下面的写法是经过简化了的写法:
private int c { get; set; }
如果不想属性有任何特殊行为,从 C# 3 开始,可以使用简易语法get; set;
。这样创建的属性叫做自动实现的属性。另外,我们可以直接通过 A.c 访问属性,而非使用
A.getC
和 A.setC
方法了。实际上,无参属性仅仅是语法糖。通过编译之后使用 iladsm 查看,我们可以发现,编译器自动为我们生成了 get_c 和 sct_c 方法,以及一个支持字段 <c>k_BackingField,如下图所示。
1) 只读和只写属性
可以通过将 get 或 set 设置为 private 获得只读和只写属性。例如,如果 set 是私有的,则属性就是只读的。不过,属性的值仍然可以被类型内部的成员修改:
public int c { get; private set; }
从 C# 6 开始,允许省略 set 获得真正具有不变性的属性:public int c { get; }
此时这个字段就真正地具有了不变性,当你初始化了这个字段的值之后,就再也无法更改它的值。2) 带有逻辑的属性
通过为属性的 get 和 set 中加入代码,我们可以控制属性的取值范围。例如,要实现一个永远为非负整数的属性:private int age; public int Age { get { return age; } set { if (value < 0) { throw new ArgumentOutOfRangeException("Age",value,"Age必须大于等于0"); } age = value; } }在这段代码中,关键字 value 代表赋值时传入的新值。
有参属性
无参属性的 get 方法不支持参数,而有参属性的 get 方法支持传入一个或更多参数,set 方法支持传入两个或更多参数。C# 中的有参属性又叫索引器,顾名思义,它是重载
[]
操作符的一种方式。虽然有参属性(索引器)很少被使用,不过,它有一个常用场景是这样的:一个类的成员包括一个集合。
比如,记录每天 24 小时温度的 DayTemperature 类,具有一个长度为 24 的 double 集合成员 Temperatures。
当拿到这个类的一个实例 d 时,访问它的成员需要 d.Temperatures[6](代表早上 6 点的温度)。
如果我们对这个类实现有参属性,可以直接使用 d[6] 获得相同的值,省去了集合成员名称 Temperatures。
这种表示法不仅简化了客户端应用程序的语法,还使其他开发人员能够更加直观地理解类及其用途。
索引器至少要定义一个访问方法(即 get 或 set)。
class Program { static void Main(string[] args) { var d = new DayTemperature(); d.temperature[0] = 20.5; d.temperature[1] = 22; //使用类索引器访问 Console.WriteLine(d[1]); //22 Console.ReadKey(); } } class DayTemperature { public double[] temperature = new double[24]; //类的索引器 public double this[int index] { get { //检查索引范围 if (index < 0 || index >= temperature.Length) { return -1; } else { return temperature[index]; } } set { if (!(index < 0 || index >= temperature.Length)) { temperature[index] = value; } } } }
属性的意义
通过属性的封装,我们保留了它与外部交互的能力,又实现了一种可靠的读写机制。私有的字段、属性和方法保护了这些成员,使它们不会被外界调用,因为这是外界无需知道的信息。
生活中一个典型的封装例子就是 ATM 机,我们通过它暴露的有限功能来实现存钱取钱,查询余额等操作,但对于它是如何实现的(例如,钱到底放在了哪儿)一无所知。
通过封装,类型只需要向外部提供它应该知道的信息,否则就会出现“你知道的太多了”这种情形。