# 原則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:使用動態接收匿名類型參數