Item7: Prefer Immutable Atomic Value Types
不可变类型很简单:当他们被创建后,它们的值就不可改变。这样可以为我们减少很多负担,否则你必需时时小心这些对象在创建后是否会因修改而改变。不可变类型对于多线程来说也是安全的,它们只能得到相同的结果。不可变类型可以让对象更安全,调用者不能改变它们的内容。不可变类型在基于哈希的集合中工作的很好,Object.GetHashCode()返回的值就是不可变类型的。
并不是所有的类型都是不可变的,如果是那样的话,我们每修改任何数据内容就都要为其创建新的克隆了。这也是为什么本篇的题目是原子不可变值类型的原因。应当将比较复杂的类型合理划分为简单的原子。例如我们定义一个存储地址的类型,其中包含了几个有联系的字段。对于这些字段,我们改变其中一个就可能会影响到另一个也需要变化(例如改变了地址则对应的邮编也要改变)。这种类型就不是原子类型。再比如我们定义一个存储用户信息的类型,它中间包含了很多字段来存储不同的信息,例如姓名,地址,一个或多个电话等,这些信息之间是独立的,可以随意发生变化,这样的类型也不是原子类型,它是若干原子类型的集合。我们应当使用原子类型来代替这些类型,这样会避免一些发生在不同字段之间的异常。
{
private string _line1;
private string _line2;
private string _city;
private string _state;
private int _zipCode;
public string Line1
{
get
{
return _line1;
}
set
{
_line1 = value;
}
}
//line2,city,state属性同上
public int ZipCode
{
get
{
return _zipCode;
}
set
{
//zipcode需要一个校验来检验输入是否合法
ValidateZip(value);
_zipCode = value;
}
}
}
当我们改变了City字段时,a1就可能出现问题。改变了City,原本的State或Zip code就可能不再匹配了。虽然这看起来并没有什么,但是在多线程访问时很有可能在尚未完全修改完毕时访问引发错误的结果。
即便不是多线程访问,上面的写法一样会造成问题。例如Zip Code一项填写不合法并抛出异常。这样会使得我们可能只修改了一部分数据,系统中存储的数据是错误的。为了解决这个问题,我们需要为adress结果内加入大量的校验代码,而这些代码可能会很复杂。为了彻底解决安全问题,当用户修改一个以上的字段时,我们还需要为这些字段创建备份。为了保证多线程安全还要为属性的get和set设置标记。这些对于上面的例子来说都是非常必要的。
下面我们来把Adress结构变成不可变类型,首先我们把所有的内置字段声明为readonly:
{
private readonly string _line1;
private readonly string _line2;
private readonly string _city;
private readonly string _state;
private readonly int _zipCode;
//其他略
}
再将属性中的set去掉:
{
set
{
_line1 = value;
}
}
//line2,city,state属性同上
这样我们就得到不可变类型了。当然为了让它有用,我们需要在构造函数中初始化它。我们为Address类型创建一个构造函数,为每个内部成员赋值。另外我们要注意它还是有默认的构造函数的,会为所有string置null,Zip code置0
{
private readonly string _line1;
private readonly string _line2;
private readonly string _city;
private readonly string _state;
private readonly int _zipCode;
public Address(string line1, string line2, string city, string state, int zipCode)
{
_line1 = line1;
_line2 = line2;
_city = city;
_state = state;
_zipCode = zipCode;
ValidateZip(zipCode);
}
//其他略
}
当我们修改Adress型对象的值时,我们不是修改了原有的对象而是创建了一个新的对象。
//修改 其实是创建了一个新的
a1 = new Address(a1.Line1,a1.Line2,"cc","dd", 000002);
上例中我们并没有修改已经存在的adress。当新的Adress对象被创建后,它的值就被新对象替换了。这样做会保证安全性:a1并没有修改内部成员的值。如果在构造函数期间有异常抛出,原始值也不会发生改变。
要创建一个不可变类型,我们必须保证客户程序没有任何可能修改内部成员的值。由于值类型不支持派生类型,我们不必担心可能会因为派生类型而修改。但是我们必须注意那些容易发生改变的引用类型成员。下例中我们希望将Phone设置成一个不可变类型,其中我们仅使用了不可变的值类型。
{
private readonly Phone[] _phones;
public PhoneList(Phone[] ph)
{
_phones = ph;
}
public IEnumerator Phones
{
get
{
return _phones.GetEnumerator();
}
}
}
Phone[] phones = new Phone[10];
PhoneList p1 = new PhoneList(phones);
//这样就修改了phones也就修改了PhoneList中的内部成员
phones[5] = Phone.GetNewPhone();
数组是引用类型,它从PhoneList结构中引用了对象外部的Phone的存储空间。我们可以通过修改同样指向这些空间的引用的值来改变PhoneList的内部成员,这样本应为不可变的类型内部成员也发生了改变。为了解决这个问题,我们需要为数组创建一个拷贝。这个例子展示了数组的陷阱。如果连Phone也是个引用类型的话那就会造成更多的麻烦。即便我们将数组保护起来使其不被修改,但是用户还是可能修改到数组中对象的值。每当我们的结构中包含引用类型,我们就要在构造函数中为其创建保护性的拷贝。
{
private readonly Phone[] _phones;
public PhoneList(Phone[] ph)
{
_phones = new Phone[ph.Length];
//将其拷贝至_phones
ph.CopyTo(_phones,0);
}
public IEnumerator Phones
{
get
{
return _phones.GetEnumerator();
}
}
}
Phone[] phones = new Phone[10];
PhoneList p1 = new PhoneList(phones);
//修改了phones数组,但是并未修改PhoneList内部成员
phones[5] = Phone.GetNewPhone();
在返回引用类型时我们也需要遵循这个原则了。如果我们要在属性中返回PhoneList中的数组,也需要为其创建一个保护性的拷贝。
类型的复杂性告诉我们当初始化不可变类型时可以使用下面三种策略。第一,我们可以为其定义统一的构造函数,用户通过构造函数初始化结构。第二,我们也可以创建工厂方法(factory method)来初始化不可变类型。工厂方法使得我们更容易创建基本的值类型。在.Net Framework中Color类型就是这样初始化系统颜色的。静态方法Color.FromKnownColor()和Color.FromName()返回的都是系统预定义颜色的值的拷贝。(这里我就不太清楚了,看不出他举的Color的例子和factory method有什么关系... 明白的还请不吝赐教,谢谢)第三,当我们确实需要通过多步修改不可变类型的内容时,我们可以为其创建一个额外的类来完成这个工作。这就好像.Net中的string和StringBuilder类的关系。我们通过StringBuilder可以多步创建一个字符串,它修改字符串而不创建新的对象。当所有操作完毕后,它将返回一个不可变的string。
不可变类型代码简单,维护方便。我们不应当盲目的为我们类型中的每个属性都设置get和set。如果只是要存储数据,那我们应当将其构建为不可变原子值类型。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录
P.S. 不可变类型是恒定的,创建后就不能对它的值进行修改。从某一个角度讲,当我们要频繁修改值的时候,操作不可变类型的效率就会比较低,因为每次都要重复创建新的实例。上面所讲的第三条策略也是为此而提出的。