# 原则20:更倾向于使用不可变原子值类型
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
不可变类型是很简单的:一旦被创建,它们就是常量。如果你验证构造对象参数,你知道从那以后它们就是有效的状态。你不可能改变对象的内部状态让它失效。一旦对象构造好,如果不允许任何状态改变,你会省去很多必须的错误的检查。不可变类型本质上是线程安全的:多个读取者可以访问相同的内容。如果内部状态不会改变,不同线程就没有机会读取到不一致的数据。不可变类型可以让你的对象安全地暴露。调用者不能修改你的对象的内部状态。不可变类型在基于哈希的集合中工作的更好。Object.GetHashCode() 的返回值是实例不变(查看原则7);对于不可变类型这一直是正确的。
在实践中,很难让每个类型都是不可变的。你需要克隆对象去修改任何程序状态。那就是为什么推荐使用院子并且不可变的值类型。分解你的类型为自然单一的结构体实体。 Address 类型就是这样的。一个地址就是一个简单的实体,有多个相关域构成。一个域的改变很大程度上意味着改变其他域。消费者的类型不具有原子性。消费者类型通常包含很多信息:地址,名字和一个或多个电话号码。这些独立的信息都可能改变。一个消费者可能改变电话号码而没有搬家。另一个消费者可能只改变他的货她的名字。消费者对象不是原子的;它由很多不同的不可变类型构成:地址,名字,或者电话号码的集合。原子类型是单一的实体:你可以替换原子类型的整个内容。如果改变它的一个构成域可能会出现异常。
这是地址不可变类型的典型实现:
```
// Mutable Address structure.
public struct Address
{
private string state;
private int zipCode;
// Rely on the default system-generated
// constructor.
public string Line1
{
get;
set;
}
public string Line2
{
get;
set;
}
public string City
{
get;
set;
}
public string State
{
get { return state; }
set
{
ValidateState(value);
state = value;
}
}
public int ZipCode
{
get { return zipCode; }
set
{
ValidateZip(value);
zipCode = value;
}
}
// other details omitted.
}
// Example usage:
Address a1 = new Address();
a1.Line1 = "111 S. Main";
a1.City = "Anytown";
a1.State = "IL";
a1.ZipCode = 61111;
// Modify:
a1.City = "Ann Arbor"; // Zip, State invalid now.
a1.ZipCode = 48103; // State still invalid now.
a1.State = "MI"; // Now fine.
```
内部状态的改变意味着可能破坏对象的不可变性,至少它是暂时的。你更改了 City 域,你就已经使 a1 变为无效状态了。城市改变了不可能再和州或邮政编码域匹配。这段代码看起来是无害的,但是假设它是多线程程序的一部分就不会这么认为了。在城市域改变之后和州域改变之前上下文的切换可能潜在使另外一个线程看到的是不一致的数据。
好的,所以你会觉得你写的不是多线程程序。你仍然会有麻烦。想象邮政编码是无效的,就会抛出异常。你根据你的意图做了写改变,同时使得系统就变成无效状态。为了修复这个问题,你可以在地址结构体中增加内部验证码。验证码会增加相当大的规模和复杂性。为了实现全部异常安全,你需要被动的复制改变状态一个或多个域的代码块。线程安全需要在属性 set 和 get 访问器上增加大量的线程同步检查。总之,这是一个重大的工作,随着时间的推移可能还会扩展到你新增加的特性里面。
另外,你需要将 Address 定义为 struct 类型,使它不可变。开始让所有实例域对外部使用者变为只读:
```
public struct Address2
{
// remaining details elided
public string Line1
{
get;
private set;
}
public string Line2
{
get;
private set;
}
public string City
{
get;
private set;
}
public string State
{
get;
private set;
}
public int ZipCode
{
get;
private set;
}
}
```
现在,你已经得到一个基于 public 接口的不可变类型。为了使它有用,你需要添加初始化 Address 结构的构造函数。 Address 结构只需要一个构造函数,指定每个域。不需要复杂构造函数,因为赋值操作足够的高效。记住默认构造函数仍然是可用的。有一个默认的地址,它的所有字符串为 null ,而且邮政编码为0:
```
public Address2(string line1, string line2, string city, string state, int zipCode) :
this()
{
Line1 = line1;
Line2 = line2;
City = city;
ValidateState(state);
State = state;
ValidateZip(zipCode);
ZipCode = zipCode;
}
```
使用不可变类型需要一个稍微不同的调用次序去改变它的状态。你穿件一个新的对象而不是修改已存在的实例:
```
// Create an address:
Address2 a2 = new Address2("111 S. Main", "", "Anytown", "IL", 61111);
// To change, re-initialize:
a2 = new Address2(a1.Line1,a1.Line2, "Ann Arbor", "MI", 48103);
```
a1 的值有两个州:一个是原来的位置在 Anytown ,或者是后面更新的位置 Ann Arbor。你不会像之前的例子修改已存在的地址导致变为无效的临时状态。这些临时状态只存在 Address 构造器的执行过程中,在外部是不可见的。一旦新的 Address 对象构造好,它的值在任何时候都是固定不变的。这是例外的安全: a1 要么是就得值要么就是新的值。如果在构造新的 Address 对象是抛出异常,旧的值 a1 还是不会改变的。
第二个 Address 类型不是严格的不可变。带有 private set 的隐式属性仍包含方法改变内部的状态。如果想要一个真实的不可变类型,你需要做更多改变。你需要改变隐式属性为显示属性,并且修改它背后的域为 readonly :
```
public struct Address3
{
// remaining details elided
public string Line1
{
get { return Line1; }
}
private readonly string line1;
public string Line2
{
get { return line2; }
}
private readonly string line2;
public string City
{
get { return city; }
}
private readonly string city;
public string State
{
get { return state; }
}
private readonly string state;
public int ZipCode
{
get { return zip; }
}
private readonly int zip;
public Address3(string line1, string line2, string city, string state, int zipCode) :
this()
{
this.line1 = line1;
this.line2 = line2;
this.city = city;
ValidateState(state);
this.state = state;
ValidateZip(zipCode);
this.zip = zipCode;
}
}
```
为了创建不可变类型,你需要区别没有任何漏洞让使用者改变你的内部状态。值类型不支持继承,所有你不需要防御子类修改基类的域。但是你需要注意不可变类中的可变引用类的域,你需要防御型复制可变类型。这个例子假设 Phone 是不可变的值类型因为我们只关心值类型的域的不可变性:
```
// Almost immutable: there are holes that would
// allow state changes.
public struct PhoneList
{
private readonly Phone[] phones;
public PhoneList(Phone[] ph)
{
phones = ph;
}
public IEnumerable<Phone> Phones
{
get
{
return phones;
}
}
}
Phone[] phones = new Phone[10];
// initialize phones
PhoneList pl = new PhoneList(phones);
// Modify the phone list:
// also modifies the internals of the (supposedly)
// immutable object.
phones[5] = Phone.GeneratePhoneNumber();
```
数组类是引用类型。PhoneList 结构内部引用的是在对象外分配的同一存储( Phone )数组。开发者可以通过引用同一存储的另外引用修改这个不可变结构。为了排除这种可能,你需要被动复杂数组。前面例子就暴露了可变集合的缺陷。甚至更多糟糕的可能性存在, Phone 类是可变的引用类型。使用者可以修改集合的值,即使集合是 protected 防止任何修改。任何不可变类包含的可变引用类型都需要在构造函数中被动地复制:
```
// Immutable: A copy is made at construction.
public struct PhoneList2
{
private readonly Phone[] phones;
public PhoneList2(Phone[] ph)
{
phones = new Phone[ph.Length];
// Copies values because Phone is a value type.
ph.CopyTo(phones, 0);
}
public IEnumerable<Phone> Phones
{
get
{
return phones;
}
}
}
Phone[] phones2 = new Phone[10];
// initialize phones
PhoneList p2 = new PhoneList(phones);
// Modify the phone list:
// Does not modify the copy in pl.
phones2[5] = Phone.GeneratePhoneNumber();
```
当你返回可变引用类型同样要遵循这个规则。如果你在 PhoneList 结构体中增加一个属性检索整个数组,这个访问器仍然需要被动地复制。更多细节请查看原则27。
类的复杂性决定你使用三种中的哪一种初始化不可变类。 Address 结构体定义一个构造器允许使用者初始化地址。定义合理的构造函数通常是最简单的方法。
你还可以使用工厂方法初始化这个结构体。工厂使得很容易创建常用的值。.NET 框架 Color 类就是按照这个策略初始化系统颜色。静态方法 Color.FromKnowColor() 和 Color.FromName() 返回当前系统给的颜色值的复制。
第三 ,对于那些需要多不操作构造不可变类的对象,你可以创建一个可变辅助类。.NET string 类遵从这个策略就有辅助类 System.Text.StringBuilder 。你使用 StringBuilder 进行多个操作创建 string 。在所有必须操作都执行之后就构建了一个 string 对象,你从 StringBuilder 获得这个不可变字符串。
不可变类是更简单,更容易维护的。不要盲目地为你的每个属性都创建 get 和 set 访问器。你的第一选择是存储数据需要不可变,原子的值类型。你可以轻易从这些实体构建更复杂的结构体。
小结:
本节对实现不可变原子值类型给了很好的方案,当希望数据是不可变或者保持原子性的,就可以派上用场了。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2079804](/blog/2079804)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数