属性(property)是一种用于访问对象或类的内容的机制。可以像使用公共数据成员一样使用属性,但实际上属性是称为“访问器”的一种特殊方法。

属性(property)是一种"高级字段”,它可能带有一个 getter 和一个 setter,它们保护属性的值,使之不会被外部胡乱篡改。

和字段相比,属性实现了对成员的封装。

无参属性

编辑
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;  }

这样创建的属性叫做自动实现的属性。可以直接通过 A.c 访问属性,而非使用 A.getC 和 A.setC 方法了。

实际上,无参属性仅仅是语法糖。通过编译之后使用 iladsm 查看,我们可以发现,编译器自动为我们生成了 get_c 和 sct_c 方法,以及一个支持字段k_BackingField。

只读和只写属性

编辑

可以通过将 get 或 set 设置为 private 获得只读和只写属性。

例如,如果 set 是私有的,则属性就是只读的。不过,属性的值仍然可以被类型内部的成员修改:

public int c { get; private set; }

从 C# 6 开始,允许省略 set 获得真正具有不变性的属性:

public int c { get; }

此时这个字段就真正地具有了不变性,当你初始化了这个字段的值之后,就再也无法更改它的值。

带有逻辑的属性

编辑

通过为属性的 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 方法支持传入一个或更多参数,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;
            }
        }
    }
}

属性的意义

编辑

通过属性的封装,保留了它与外部交互的能力,又实现了一种可靠的读写机制。私有的字段、属性和方法保护了这些成员,使它们不会被外界调用,因为这是外界无需知道的信息。

通过封装,类型只需要向外部提供它应该知道的信息,否则就会出现“你知道的太多了”这种情形。