恆定類型(immutable types)其實很簡單,就是一但它們被創建,它們(的值) 就是固定的。如果你驗證一些准備用於創建一個對象的參數,你知道它在驗證狀 態從前面的觀點上看。你不能修改一個對象的內部狀態使之成為無效的。在一個 對象被創建後,你必須自己小心翼翼的保護對象,否則你不得不做錯誤驗證來禁 止改變任何狀態。恆定類型天生就具有線程完全性的特點:多訪問者可同時訪問 相同的內容。如果內部狀態不能修改,那麼就不能給不同的線程提供查看不一致 的數據視圖的機會。恆定類型可以從你的類上安全的暴露出來。調用者不能修改 對象的內部狀態。恆定類型可以很好的在基於哈希代碼的集合上工作。以 Object.GetHashCode()方法返回的值,對同一個實例是必須相同的(參見原則10) ,而這正是恆定類型總能成功的地方。
並不是所有的類型都能成為恆定 類型的。如果它可以,你需要克隆一個對象用於修改任何程序的狀態了。這就是 為什麼同時推薦使用恆定類型和原子類型數據了。把你的對象分解為自然的單一 實體結構。一個Address類型就是的,它就是一個簡單的事,由多個相關的字段 組成。改變其中一個字段就很可能意味著修改了其它字段。一個客戶類型不是一 個原子類型,一個客戶類型可能包含很多小的信息塊:地址,名字,一個或者多 個電話號碼。任何一個互不關聯的信息塊都可以改變。一個客戶可能會在不搬家 的情況下改變電話號碼。而另一個客戶可能在搬了家的情況下保留原來的電話號 碼。還有可能,一個客戶改變了他(她)的名字,而沒有搬家也沒有改電話號碼。 一個客戶類型就不是原子類型;它是由多個不同的恆定的組成部份構成的:地址 ,名字,以及一個成對出現的電話號碼集合。原子類型是單一實體:你很自然的 用原子類型來取代實體內容。這一例外會改變它其中的一個組成字段。
下面就是一個典型的可變地址類的實現:
// Mutable Address structure.
public struct Address
{
private string _line1;
private string _line2;
private string _city;
private string _state;
private int _zipCode;
// Rely on the default system-generated
// constructor.
public string Line1
{
get { return _line1; }
set { _line1 = value; }
}
public string Line2
{
get { return _line2; }
set { _line2 = value; }
}
public string City
{
get { return _city; }
set { _city= value; }
}
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處於無效狀態。城市的改變使得它與洲字段及以區碼字段 不再匹配。代碼的有害性看上去還不足以致命,但這對於多線程程序來說只是一 小部份。在城市變化以後,洲變化以前的任何內容轉變,都會潛在的使另一個線 程看到一份矛盾的數據視圖。
Okay,所以你不准備去寫多線程程序。你 仍然處於困境當中。想象這樣的問題,區代碼是無效的,並且設置拋出了一個異 常。你只是完成了一些你想做的事,可你卻使系統處於一個無效的狀態當中。為 了修正這個問題,你須要在地址類裡面添加一個相當大的內部驗證碼。這個驗證 碼應該須要相當大的空間,並且很復雜。為了完全實現期望的安全性,當你修改 多個字段時,你須要在你的代碼塊周圍創建一個被動的數據COPY。線程安全性可 能要求添加一個明確的線程同步用於檢測每一個屬性訪問器,包括set和get。總 而言之,這將是一個意義重大的行動--並且這很可能在你添加新功能時被過分的 擴展。
取而代之,把address結構做為一個恆定類型。開始把所有的字段 都改成只讀的吧:
public struct Address
{
private readonly string _line1;
private readonly string _line2;
private readonly string _city;
private readonly string _state;
private readonly int _zipCode;
// remaining details elided
}
你還要移除所有的屬性設置 功能:
public struct Address
{
// ...
public string Line1
{
get { return _line1; }
}
public string Line2
{
get { return _line2; }
}
public string City
{
get { return _city; }
}
public string State
{
get { return _state; }
}
public int ZipCode
{
get { return _zipCode; }
}
}
現在,你就擁有了 一個恆定類型。為了讓它有效的工作,你必須添加一個構造函數來完全初始化 address結構。這個address結構只須要額外的添加一個構造函數,來驗證每一個 字段。一個拷貝構造函數不是必須的,因為賦值運算符還算高效。記住,默認的 構造函數仍然是可訪問的。這是一個默認所有字符串為null,ZIP代碼為0的地址 結構:
public struct Address
{
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;
ValidateState( state );
ValidateZip( zipCode );
}
// etc.
}
在使用這個恆定數據類型時,要求直接用不同的調用來一順的修 改它的狀態。你更寧願創建一個新的對象而不是去修改某個實例:
// Create an address:
Address a1 = new Address( "111 S. Main",
"", "Anytown", "IL", 61111 );
// To change, re-initialize:
a1 = new Address( a1.Line1,
a1.Line2, "Ann Arbor", "MI", 48103 );
a1的值是兩者之一:它的原始位置 Anytown,或者是後來更新後的位置Ann Arbor。你再不用像前面的例子那樣,為 了修改已經存在的地址而使對象產生臨時無效狀態。這裡只有一些在構造函數執 行時才存在的臨時狀態,而在構造函數外是無法訪問內部狀態的。很快,一個新 的地址對象很快就產生了,它的值就一直固定了。這正是期望的安全性:a1要麼 是默認的原始值,要麼是新的值。如果在構造對象時發生了異常,那麼a1保持原 來的默認值不變。
(譯注:為什麼在構造時發生異常不會影響a1的值呢? 因為只要構造函數沒有正確返回,a1都只保持原來的值。因為是那是一個賦值語 句。這也就是為什麼要用構造函數來實現對象更新,而不是另外添加一個函數來 更新對象,因為就算用一個函數來更新對象,也有可能更新到一半時,發生異常 ,也會使得對象處於不正確的狀態當中。大家可以參考一下.Net裡的日期時間結 構,它就是一個典型的恆定常量例子。它沒有提供任何的對單獨年,月,日或者 星期進行修改的方法。因為單獨修改其中一個,可能導致整個日期處於不正確的 狀態:例如你把日期單獨的修改為31號,但很可能那個月沒有31號,而且星期也 可能不同。它同樣也是沒提供任何方法來同時設置所以參數,讀了條原則後就明 白為什麼了吧。參考一下DateTime結構,可以更好的理解為什麼要使用恆定類型 。注:有些書把immutable type譯為不變類型。)
為了創建一個恆定類型 ,你須要確保你的用戶沒有任何機會來修改內部狀態。值類型不支持派生類,所 以你不必定義擔心派生類來修改它的內部狀態。但你須要注意任何在恆定類型內 的可變的引用類型字段。當你為這些類型實現了構造函數後,你須要被動的把可 變的引用類型COPY一遍(譯注:被動COPY,defensive copy,文中應該是指為了 保護數據,在數據賦值時不得不進行的一個COPY,所以被認為是“防守 ”拷貝,我這裡譯為:被動拷貝,表示拷貝不是自發的,而是不得以而為 之的)。
所有這些例子,都是假設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 IEnumerator Phones
{
get
{
return _phones.GetEnumerator();
}
}
}
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內部引用的數組,引用 了分配在對象外的數組存儲空間上。開發人員可以通過另一個引用到這個存儲空 間上的對象來修改你的恆定結構。為了避免這種可能,你須要對這個數組做一個 被動拷貝。前面的例子顯示了可變集合的弊端。如果電話類型是一個可變的引用 類型,它還會有更多危害存在的可能。客戶可以修改它在集合裡的值,即使這個 集合是保護,不讓任何人修改。這個被動的拷貝應該在每個構造函數裡被實現, 而不管你的恆定類型裡是否存在引用對象:
// Immutable: A copy is made at construction.
public struct PhoneList
{
private readonly Phone[] _phones;
public PhoneList( Phone[] ph )
{
_phones = new Phone[ ph.Length ];
// Copies values because Phone is a value type.
ph.CopyTo( _phones, 0 );
}
public IEnumerator Phones
{
get
{
return _phones.GetEnumerator();
}
}
}
Phone[] phones = new Phone[10];
// initialize phones
PhoneList pl = new PhoneList( phones );
// Modify the phone list:
// Does not modify the copy in pl.
phones[5] = Phone.GeneratePhoneNumber( );
當你返回一個 可變類型的引用時,也應該遵守這一原則。如果你添加了一個屬性用於從 PhoneList結構中取得整個數組的鏈表,這個訪問器也必須實現一個被動拷貝。 詳情參見原則23。
這個復雜的類型表明了三個策略,這是你在初始化你 的恆定對象時應該使用的。這個Address結構定義了一個構造函數,讓你的客戶 可以初始化一個地址,定義合理的構造函數通常是最容易達到的。
你同 樣可以創建一個工廠方法來實現一個結構。工廠使得創建一個通用的值型數據變 得更容易。.Net框架的Color類型就是遵從這一策略來初始化系統顏色的。這個 靜態的方法Color.FromKnownColor()和Color.FromName()從當前顯示的顏色中拷 貝一個給定的系統顏色,返回給用戶。
第三,你可以為那些需要多步操 作才能完成構造函數的恆定類型添加一個伴隨類。.Net框架裡的字符串類就遵從 這一策略,它利用了伴隨類System.Text.StringBuilter。你是使用 StringBuliter類經過多步操作來創建一個字符串。在完成了所有必須步驟生成 一個字符串類後,你從StringBuilter取得了一個恆定的字符串。
(譯注 :.net裡的string是一但初始化,就不能再修改,對它的任何改動都會生成新的 字符串。因此多次操作一個string會產生較多的垃圾內存碎片,你可以用 StringBuliter來平衡這個問題。)
恆定類型是更簡單,更容易維護的。 不要盲目的為你的每一個對象的屬性創建get和set訪問器。你對這些類型的第一 選擇是把這些數存儲為恆定類型,原子類型。從這些實體中,你可以可以容易的 創建更多復雜的結構。
=================================
小 結:翻譯了幾篇原則,有些句子確實很難理解,自己也感覺翻譯的七不像八不像 的。如果讀者遇到這樣的一些不清楚的句子,可以跳過去,或者看原文。感覺實 在是能力有限。
而且,對於書中的內容,我也並不是完全清楚,很多東 西我自己也是在學習。所以添加的一些譯注也不見得就是完全正確的。例如這一 原則中的DateTime結構,它是不是一個恆定類型,我不敢確定,但從我讀了這一 原則後,加上我對DataTime以及這一原則的理解,覺得這個DateTime結構確實就 是這一原則的實例。後面的原則我大概翻閱了一下,有的深有的淺,後期的翻譯 也會是有些艱難的,但不管怎樣,我都會盡我最大的能力,盡快翻譯完所有原則 。
返回教程目錄