Effective C#之7:Prefer Immutable Atomic Value Type

rel="File-List" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml"> rel="themeData" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_themedata.thmx"> rel="colorSchemeMapping" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_colorschememapping.xml">

Item7  Prefer Immutable Atomic Value Type

优先考虑具有不可变性的原子性的值类型

Immutable types are simple: After they are created, they are constant. If you validate the parameters used to construct the object, you know that it is in a valid state from that point forward. You cannot change the object's internal state to make it invalid. You save yourself a lot of otherwise necessary error checking by disallowing any state changes after an object has been constructed. Immutable types are inherently thread safe: Multiple readers can access the same contents. If the internal state cannot change, there is no chance for different threads to see inconsistent views of the data. Immutable types can be exported from your objects safely. The caller cannot modify the internal state of your objects. Immutable types work better in hash-based collections. The value returned by Object.GetHashCode() must be an instance invariant (see Item 10); that's always true for immutable types.

不可变的类型很简单,它们在被创建之后是固定不变的。如果你验证了用来构造一个对象的参数,那么就知道从那之后,该对象处于有效状态。你不能再改变这个对象的内部状态来引发它的无效。一个对象在被构建之后,不允许任何状态的改变,这就节省了很多必须的错误检查。不可变的类型是内在的线程安全的:多个读取者可以访问同样的内容。如果内部状态不能被改变,那么对于不同的线程来说,就没有机会看到这个数据的不同值。不可变类型可以安全的暴露给外界,因为调用者不能修改该对象的内部状态。不可变类型在基于hash的集合中工作的更好,由Object.GetHashCode()返回的值必须是一个不可变的变量;对于不可变类型来说,这一点总是有保证的。

Not every type can be immutable. If it were, you would need to clone objects to modify any program state. That's why this recommendation is for both atomic and immutable value types. Decompose your types to the structures that naturally form a single entity. An Address type does. An address is a single thing, composed of multiple related fields. A change in one field likely means changes to other fields. A customer type is not an atomic type. A customer type will likely contain many pieces of information: an address, a name, and one or more phone numbers. Any of these independent pieces of information might change. A customer might change phone numbers without moving. A customer might move, yet still keep the same phone number. A customer might change his or her name without moving or changing phone numbers. A customer object is not atomic; it is built from many different immutable types using composition: an address, a name, or a collection of phone number/type pairs. Atomic types are single entities: You would naturally replace the entire contents of an atomic type. The exception would be to change one of its component fields.

并非所有的类型都可以是不可变的。如果那样的话,你就得复制对象来修改任何程序状态,这就是为什么该建议同时适用于原子性的和不可变性质的值类型。将你的类型分解成可以自然形成单个实体的结构。Address类型就是这样的,一个address就是一个单一的东西,由多个相关的字段组成,对一个字段的修改很可能意味着要对其他字段进行修改customer类型就不是原子性的类型,可能含有很多条信息:addressname,一个或者多个phone number。任何这些独立的信息都可能改变。一个customer可能变更了电话号码,但是不需要更改地址。一个customer也可能变更了地址,但仍保留了原来的电话号码。一个customer也可能既没有变更地址也没有换电话号码,但是却修改了名字。Customer对象不是原子性的,它由很多不同的不可变类型通过组合形成:addressname,或者一组phone/type对儿的集合。原子类型是单个实体:你可以自然的替换原子类型的整个内容,但有时也有特例,可能改变其中的一个组成字段。

Here is a typical implementation of an address that is mutable:

下面是一个典型的可变Address类型的实现:

  1. //Multable Address structure
  2. public struct Address
  3. {
  4.     private String line1;
  5.     private String line2;
  6.     private String city;
  7.     private String state;
  8.     private Int32 zipCode;
  9.  
  10.     //Rely on the default system-generated
  11.     //constructor
  12.     public String Line1
  13.     {
  14.         get { return line1; }
  15.         set { line1 = value; }
  16.     }
  17.     public String Line2
  18.     {
  19.         get { return line2; }
  20.         set { line2 = value; }
  21.     }
  22.     public String City
  23.     {
  24.         get { return city; }
  25.         set { city = value; }
  26.     }
  27.     public String State
  28.     {
  29.         get { return state; }
  30.         set
  31.         {
  32.             ValidateState(value);
  33.             state = value;
  34.         }
  35.     }
  36.     public String ZipCode
  37.     {
  38.         get { return zipCode; }
  39.         set
  40.         {
  41.             ValidateZip(value);
  42.             zipCode = value;
  43.         }
  44.     }
  45. }
  46.  
  47. //Example usage
  48. Address a1 = new Address();
  49. a1.Line1 = "111 S. Main";
  50. a1.City = "Anytown";
  51. a1.State = "IL";
  52. a1.ZipCode = 61111;
  53. //Modify:
  54. a1.City = "Ann Arbor";//zip,state invalid now
  55. a1.ZipCode = 48103;//state still invalid now
  56. a1.State = "MI";//Now fine
  57.  

Internal state changes means that it's possible to violate object invariants, at least temporarily. After you have replaced the City field, you have placed a1 in an invalid state. The city has changed and no longer matches the state or ZIP code fields. The code looks harmless enough, but suppose that this fragment is part of a multithreaded program. Any context switch after the city changes and before the state changes would leave the potential for another thread to see an inconsistent view of the data.

内部状态的改变意味着,违背对象的不可变性是可能的,至少暂时的改变是可能的。在你已经替换了City字段后,就将a1置于了无效状态。City字段变化了,和state以及zip code字段就不再匹配了。代码看起来是无害的,但是假设这段代码是一个多线程程序的一部分,在city改变后,任何上下文交换中,在state改变前,这时,如果另外一个线程也在访问的话,就会有潜在的危险,可能会看到不一致的数据。

Okay, so you're not writing a multithreaded program. You can still get into trouble. Imagine that the ZIP code was invalid and the set threw an exception. You've made only some of the changes you intended, and you've left the system in an invalid state. To fix this problem, you would need to add considerable internal validation code to the address structure. That validation code would add considerable size and complexity. To fully implement exception safety, you would need to create defensive copies around any code block in which you change more than one field. Thread safety would require adding significant thread-synchronization checks on each property accessor, both sets and gets. All in all, it would be a significant undertaking and one that would likely be extended over time as you add new features.

好吧,即使你没有在写多线程的程序,也有可能陷入问题中。让我们想象zipcode是无效的,抛出了异常。你仅仅做了一部分你希望的更改,便将系统置于了无效的状态。为了修复这个问题,需要为Address结构加入相当多的内部有效性验证代码,会给代码增加相当多的规模和复杂性。为了完全实现在异常上的安全性,在你修改一个或者多个字段的代码块的周围,需要创建保护性的拷贝。线程安全性要求在每个属性访问器那里加入重要的线程同步检查,getset都要加。总之一句话,这会是一个重要的工作——随着时间的改变,当你加入新特性时,还会被扩展。

Instead, make the Address structure an immutable type.Start by changing all instance fields to read-only:

相反呢,将Address结构作为不可变的类型。由将所有的常量字段改成read-only开始:

  1.         public struct Address
  2.         {
  3.             private readonly String line1;
  4.             private readonly String line2;
  5.             private readonly String city;
  6.             private readonly String state;
  7.             private readonly Int32 zipCode;
  8.         }

You'll also want to remove all set accessors to each property:

你也希望移除所有属性的set访问符:

  1. public String Line1
  2. {
  3.     get { return line1; }
  4. }
  5. public String Line2
  6. {
  7.     get { return line2; }
  8. }
  9. public String City
  10. {
  11.     get { return city; }
  12. }
  13. public String State
  14. {
  15.     get { return state; }
  16. }
  17. public String ZipCode
  18. {
  19.     get { return zipCode; }
  20. }

Now you have an immutable type. To make it useful, you need to add all necessary constructors to initialize the Address structure completely. The Address structure needs only one additional constructor, specifying each field. A copy constructor is not needed because the assignment operator is just as efficient. Remember that the default constructor is still accessible. There is a default address where all the strings are null, and the ZIP code is 0:

现在,你拥有了一个不可变的类型。为了让它有用,需要加入所有必须的构造器来完全初始化Address结构。Address结构只需要一个额外的构造器,来指定每个字段。复制的构造器是不必要的,因为赋值操作符是高效的。请记住,默认的构造器仍然是可访问的。存在一个默认的Address对象,它所有的字符串都是nullzip编码是0

  1. public struct Address
  2. {
  3.     private readonly String line1;
  4.     private readonly String line2;
  5.     private readonly String city;
  6.     private readonly String state;
  7.     private readonly Int32 zipCode;
  8.  
  9.     public Address(String line1, String line2, String city, String state, String zipCode)
  10.     {
  11.         this.line1 = line1;
  12.         this.line2 = li2;
  13.         this.city = city;
  14.         this.state = state;
  15.         this.zipCode = zipCode;
  16.     }
  17. }

使用不可变类型,要修改它的状态要求有些轻微的不同。需要创建一个新的对象而不是修改已经存在的实例;

  1. //create an address
  2. Address a1 = new Address("111 S. Main","","Anytown","IL",61111);
  3. //To change,re-initialize
  4. a1 = new Address(a1.Line1, a1.Line2, "Ann Arbor""MI", 48103);

The value of a1 is in one of two states: its original location in Anytown, or its updated location in Ann Arbor. You do not modify the existing address to create any of the invalid temporary states from the previous example. Those interim states exist only during the execution of the Address constructor and are not visible outside of that constructor. As soon as a new Address object is constructed, its value is fixed for all time. It's exception safe: a1 has either its original value or its new value. If an exception is thrown during the construction of the new Address object, the original value of a1 is unchanged.

a1的值处于两种状态中的一个:在Anytown的原来的位置,或在Ann Arbor的新位置。不可能像前面的例子那样将现存的地址修改成任何无效的临时状态。这些临时状态仅仅存在于Address构造器的执行过程中,不会出现在构造器以外。一旦一个新的Address对象被构建,它的值就一直是固定的。它对异常来说是安全的:a1或者是原来的值或者是新的值。如果在一个新的Address对象被构建的过程中发生了异常,a1将保持原来的值。

To create an immutable type, you need to ensure that there are no holes that would allow clients to change your internal state. Value types do not support derived types, so you do not need to defend against derived types modifying fields. But you do need to watch for any fields in an immutable type that are mutable reference types. When you implement your constructors for these types, you need to make a defensive copy of that mutable type. All these examples assume that Phone is an immutable value type because we're concerned only with immutability in value types:

为了创建一个不可变的类型,你需要保障没有任何漏洞允许客户修改你的内部状态。值类型不支持派生类型,因此不需要对派生类型会修改字段的情况做出防卫。但是你确实需要观察一个不可变类型中的任何可变的引用类型字段,当这样的类型实现构造器时,需要对这些可变类型作出防护性复制。下面所有这些例子都假设Phone是一个不可变的值类型,因为我们仅仅关注值类型的不可变性。

  1. //Almost immutable: there are holes that would
  2. //allow state changes
  3.     public struct PhoneList
  4.     {
  5.         private readonly Phone[] phones;
  6.         public PhoneList(Phone[] ph)
  7.         {
  8.             phones = ph;
  9.         }
  10.         public IEnumerator Phones
  11.         {
  12.             get
  13.             {
  14.                 return phones.GetEnumerator();
  15.             }
  16.         }
  17.   }
  18.  
  19. Phone[] phones = new Phone[10];
  20. //initialize phones
  21. PhoneList pl = new PhoneList(phones);
  22.  
  23. //modify the phone list
  24. //also modifies the internals of the (supposedly)
  25. //immutable object.
  26. phones[5] = Phone.GeneratePhoneNumber();
  27.  

The array class is a reference type. The array referenced inside the PhoneList structure refers to the same array storage (phones) allocated outside of the object. Developers can modify your immutable structure through another variable that refers to the same storage. To remove this possibility, you need to make a defensive copy of the array. The previous example shows the pitfalls of a mutable collection. Even more possibilities for mischief exist if the Phone type is a mutable reference type. Clients could modify the values in the collection, even if the collection is protected against any modification. This defensive copy should be made in all constructors whenever your immutable type contains a mutable reference type:

数组类是一个引用类型。在PhoneList结构内部的数组引用指向了在对象外部分配的同样的数组存储空间(phones)。开发者可以通过指向同样存储空间的变量(phone)来修改不可变结构(phonelist)。为了移除这种可能性,需要对这个数组做一个防御性的复制。前面的例子展现了一个可变集合的缺陷。如果Phone类型是一个可变的引用类型,那么就可能存在更多的缺陷。即使这个集合类型被保护起来防止更改,客户也可能会修改这个集合中的值。无论什么时候你的不可变类型中包含一个可变的引用类型时,这个保护性的复制应该在所有的构造器中被使用。

  1.     //Immutable : A copy is made at construction
  2.     public struct PhoneList
  3.     {
  4.         private readonly Phone[] phones;
  5.         public PhoneList(Phone[] ph)
  6.         {
  7.             phones = new Phone[ph.Length];
  8.             //copies values because Phone is a value type
  9.             ph.CopyTo(phones, 0);
  10.         }
  11.         public IEnumerator Phones
  12.         {
  13.             get
  14.             {
  15.                 return phones.GetEnumerator();
  16.             }
  17.         }
  18.   }
  19.  
  20. Phone[] phones = new Phone[10];
  21. //initialize phones
  22. PhoneList pl = new PhoneList(phones);
  23.  
  24. //modify the phone list
  25. //Does not modify the copy in pl
  26. phones[5] = Phone.GeneratePhoneNumber();

You need to follow the same rules when you return a mutable reference type. If you add a property to retrieve the entire array from the PhoneList struct, that accessor would also need to create a defensive copy. See Item 23 for more details.

当返回一个可变的引用类型时,需要遵守同样的规则。如果增加一个属性来从PhoneList结构中获取整个数组,访问器同样需要创建一个保护性的拷贝。参看Item23获得更多细节。

The complexity of a type dictates which of three strategies you will use to initialize your immutable type. The Address structure defined one constructor to allow clients to initialize an address. Defining the reasonable set of constructors is often the simplest approach.

一个类型的复杂性指示了你应该使用这三个策略里面的哪一个来初始化不可变类型。Address结构定义了一个构造器来允许客户初始化这个地址。通过定义合理的构造器集合通常是最简单的途径。

You can also create factory methods to initialize the structure. Factories make it easier to create common values. The .NET Framework Color type follows this strategy to initialize system colors. The static methods Color.FromKnownColor() and Color.FromName() return a copy of a color value that represents the current value for a given system color.

你同样可以创建工厂方法来初始化该结构。工厂使得创建一般的值很容易。.Net框架的Color类型就采用了这种策略来初始化系统颜色。静态方法Color.FromKnownColor()Color.FromName()返回了一个颜色值的拷贝,这个颜色值表示了特定系统颜色的当前值。

Third, you can create a mutable companion class for those instances in which multistep operations are necessary to fully construct an immutable type. The .NET string class follows this strategy with the System.Text.StringBuilder class. You use the StringBuilder class to create a string using multiple operations. After performing all the operations necessary to build the string, you retrieve the immutable string from the StringBuilder.

第三,在一些实例里面,需要一些必要的多步操作来完全的构建一个不可变类型,这时,可以为这些实例创建一个可变的辅助类。.Netstring类遵循了这个策略,使用了System.Text.StringBuilder辅助类。可以使用StringBuilder类通过多步操作来创建一个字符串。在执行完所有的构建字符串必须的操作之后,就可以从StringBuilder获得一个不可变的字符串。

Immutable types are simpler to code and easier to maintain. Don't blindly create get and set accessors for every property in your type. Your first choice for types that store data should be immutable, atomic value types. You easily can build more complicated structures from these entities.

不可变类型在代码上是简单的,也容易维护。不要在你的类型里面盲目的为每个属性创建getset访问符。用来存储数据的类型,首要的选择是不可变性的、原子性的类型。在这些实体的基础上可以轻松的创建更加复杂的结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值