原则20:更倾向于使用不可变原子值类型

优质
小牛编辑
130浏览
2023-12-01

不可变类型是很简单的:一旦被创建,它们就是常量。如果你验证构造对象参数,你知道从那以后它们就是有效的状态。你不可能改变对象的内部状态让它失效。一旦对象构造好,如果不允许任何状态改变,你会省去很多必须的错误的检查。不可变类型本质上是线程安全的:多个读取者可以访问相同的内容。如果内部状态不会改变,不同线程就没有机会读取到不一致的数据。不可变类型可以让你的对象安全地暴露。调用者不能修改你的对象的内部状态。不可变类型在基于哈希的集合中工作的更好。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 访问器。你的第一选择是存储数据需要不可变,原子的值类型。你可以轻易从这些实体构建更复杂的结构体。

小结:

本节对实现不可变原子值类型给了很好的方案,当希望数据是不可变或者保持原子性的,就可以派上用场了。

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!