具有常量性的類型很簡(jiǎn)單,它們自創(chuàng)建后便保持不變。如果在構(gòu)造的時(shí)候就驗(yàn)證了參數(shù)的有效性,我們就可以確保從此之后它都處于有效的狀態(tài)。因?yàn)槲覀儾豢赡茉俑钠鋬?nèi)部狀態(tài)。通過(guò)禁止在構(gòu)建對(duì)象之后更改對(duì)象狀態(tài),我們實(shí)際上可以省卻許多必要的錯(cuò)誤檢查。具有常量性的類型同時(shí)也是線程安全的:多個(gè)reader可以訪問(wèn)同樣的內(nèi)容。因?yàn)槿绻麅?nèi)部狀態(tài)不可能改變,那么不同線程也就沒(méi)有機(jī)會(huì)獲得同一數(shù)據(jù)的不同值。具有常量性的類型也可以安全地暴露給外界,因?yàn)檎{(diào)用者不可能改變對(duì)象的內(nèi)部狀態(tài)。具有常量性的類型在基于散列(hash)的集合中也表現(xiàn)得很好,因?yàn)橛蒓bject.GetHashCode()方法返回的值必須是一個(gè)不變量(參見(jiàn)條款10),而具有常量性的類型顯然可以保證這一點(diǎn)。 然而,并非所有類型都可以為常量類型。如果那樣的話,我們將需要克隆對(duì)象來(lái)改變程序的狀態(tài)。這也就是為什么本條款同時(shí)針對(duì)具有常量性和原子性的值類型。我們應(yīng)該將我們的類型分解為各種可以自然形成單個(gè)實(shí)體的結(jié)構(gòu)。比如,Address類型就是這樣的例子。一個(gè)Address對(duì)象是由多個(gè)相關(guān)字段組成的單一實(shí)體。其中一個(gè)字段的更改可能意味著需要更改其他字段。而Customer類型就不具有原子性。一個(gè)Customer類型可能包含許多信息:地址(address)、名稱(name)以及一個(gè)或者多個(gè)電話號(hào)碼(phone number)。這些獨(dú)立信息中的任何一個(gè)都可能更改。一個(gè)Customer對(duì)象可能要更改它的電話號(hào)碼,但并不需要更改地址;也可能更改它的地址,而仍然保留同樣的電話號(hào)碼;也可能更改它的名稱,但保留同樣的電話號(hào)碼和地址。因此,Customer對(duì)象并不具有原子性。但它由各個(gè)不同的原子類型組成:一個(gè)地址、一個(gè)名稱或者一組電話號(hào)碼/類型對(duì)[13]。具有原子性的類型都是單一的實(shí)體:我們通常會(huì)直接替換一個(gè)原子類型的整個(gè)內(nèi)容。但有時(shí)候也有例外,比如更改構(gòu)成它的幾個(gè)字段。 下面是一個(gè)典型的可變類型Address的實(shí)現(xiàn): // 可變結(jié)構(gòu)Address。 public struct Address { private string _line1; private string _line2; private string _city; private string _state; private int _zipCode; // 依賴系統(tǒng)產(chǎn)生的默認(rèn)構(gòu)造器。 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; } } // 忽略其他細(xì)節(jié)。 } // 應(yīng)用示例: Address a1 = new Address( ); a1.Line1 = "111 S. Main"; a1.City = "Anytown"; a1.State = "IL"; a1.ZipCode = 61111 ; // 更改: a1.City ="Ann Arbor"; // ZipCode、State 現(xiàn)在無(wú)效。 a1.ZipCode = 48103; // State 現(xiàn)在仍然無(wú)效。 a1.State = "MI"; // 現(xiàn)在整個(gè)對(duì)象正常。 內(nèi)部狀態(tài)的改變意味著有可能違反對(duì)象的不變式 (invariant)——至少是臨時(shí)性地違反。在我們將City字段更改之后,a1就處于無(wú)效的狀態(tài)了。City更改后便不再與State或者ZipCode匹配。上面的代碼看起來(lái)好像沒(méi)什么問(wèn)題,但是假設(shè)這段代碼是一個(gè)多線程程序的一部分,那么任何在City更改過(guò)程中的上下文切換都可能導(dǎo)致 另一個(gè)線程看到不一致的數(shù)據(jù)視圖。 即使我們并不是在編寫(xiě)多線程應(yīng)用程序,上面的代碼仍然存在問(wèn)題。假設(shè)ZipCode的值無(wú)效,因此拋出了一個(gè)異常。這時(shí)候我們實(shí)際上僅做了一部分改變,對(duì)象將處于一個(gè)無(wú)效的狀態(tài)。為了修復(fù)這個(gè)問(wèn)題,我們需要在Address結(jié)構(gòu)中添加相當(dāng)多的內(nèi)部校驗(yàn)代碼。這無(wú)疑將增加代碼的體積和復(fù)雜性。為了完全實(shí)現(xiàn)異常安全,我們還需要在所有改變多個(gè)字段的代碼塊處放上防御性的代碼。線程安全也要求我們?cè)诿恳粋€(gè)屬性訪問(wèn)器(get和set)上添加線程同步檢查??偠灾?,這將是一個(gè)相當(dāng)可觀的工作——而且我們還要考慮隨著時(shí)間的推移,功能的增加,以及代碼可能的擴(kuò)展。 相反,讓我們將Address結(jié)構(gòu)實(shí)現(xiàn)為常量類型。首先,要將所有的實(shí)例字段都更改為只讀字段: public struct Address { private readonlystring _line1; private readonly string _line2; private readonly string _city; private readonly string _state; private readonly int _zipCode; // 忽略其他細(xì)節(jié)。 } 同時(shí)要?jiǎng)h除每個(gè)屬性的所有set訪問(wèn)器: 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; } } } 現(xiàn)在我們得到了一個(gè)常量類型。為了讓其可用,我們還需要添加必要的構(gòu)造器來(lái)徹底初始化Address結(jié)構(gòu)。目前看來(lái),Address結(jié)構(gòu)只需要一個(gè)構(gòu)造器來(lái)為其每一個(gè)字段賦值。復(fù)制構(gòu)造器就不必要了, 因?yàn)镃#默認(rèn)的賦值操作符已經(jīng)足夠高效了。記住,默認(rèn)的構(gòu)造器仍然是有效的。使用默認(rèn)構(gòu)造器創(chuàng)建的Address對(duì)象中所有的字符串將為null,而 zipCode將為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, stringline2, string city, string state, int zipCode) { _line1 = line1; _line2 = line2; _city = city; _state = state; _zipCode = zipCode; ValidateState( state ); ValidateZip( zipCode ); } // 忽略其他細(xì)節(jié)。 } 要改變常量類型,我們需要?jiǎng)?chuàng)建一個(gè)新對(duì)象,而非在現(xiàn)有的實(shí)例上做修改: // 創(chuàng)建一個(gè)Address: Address a1 = new Address( "111 S. Main", "", "Anytown", "IL",61111 ); //使用重新初始化的方式來(lái)改變對(duì)象: a1 = new Address( a1.Line1, a1.Line2, "Ann Arbor","MI", 48103 ); 現(xiàn)在a1只可能處于以下兩個(gè)狀態(tài)中的一個(gè):原來(lái)位于Anytown的位置,或者位于Ann Arbor的新位置。我們將不可能再像前面的例子中那樣把一個(gè)現(xiàn)有的Address對(duì)象更改為任何無(wú)效的臨時(shí)狀態(tài)。那些無(wú)效的中間態(tài)只可能存在于 Address構(gòu)造器的執(zhí)行過(guò)程中,不可能出現(xiàn)在構(gòu)造器之外。只要一個(gè)Address對(duì)象被構(gòu)造好后,它的值將保持恒定不變。新版的Address也是異常安全的:a1或者為原來(lái)的值,或者為新構(gòu)造的值。如果有異常在新的Address對(duì)象的構(gòu)造過(guò)程中被拋出,那么a1將保持原來(lái)的值。 對(duì)于常量類型,我們還要確保沒(méi)有任何漏洞會(huì)導(dǎo)致其內(nèi)部狀態(tài)被更改。由于值類型不支持派生類型,因此我們不必?fù)?dān)心派生類型會(huì)更改其字段。但我們需要注意常量類型中的可變引用類型字段。當(dāng)我們?yōu)檫@樣的類型實(shí)現(xiàn)構(gòu)造器時(shí),需要對(duì)其中的可變類型進(jìn)行防御性的復(fù)制。下面的例子假設(shè)Phone為一個(gè)具有常量性的值類型,因?yàn)槲覀冎魂P(guān)心值類型的常量性: // 下面的類型為狀態(tài)的改變留下了漏洞。 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]; // 初始化phones PhoneList pl = new PhoneList( phones ); // 改變phones數(shù)組: // 同時(shí)也改變了常量類型的內(nèi)部狀態(tài)。 phones[5] = Phone.GeneratePhoneNumber( ); 我們知道,數(shù)組是一個(gè)引用類型。這意味著 PhoneList結(jié)構(gòu)內(nèi)部引用的數(shù)組和外部的phones數(shù)組引用著同一塊內(nèi)存空間。這樣開(kāi)發(fā)人員就有可能通過(guò)修改phones來(lái)修改常量結(jié)構(gòu) PhoneList。為了避免這種可能性,我們需要對(duì)數(shù)組做一個(gè)防御性的復(fù)制。上面的例子展示的是一個(gè)可變集合類型可能存在的漏洞。如果Phone為一個(gè)可變的引用類型,那么將更具危害性。在這種情況下,即使集合類型可以避免更改,集合中的值仍然可能會(huì)被更改。這時(shí)候,我們就需要對(duì)這樣的類型在所有構(gòu)造器中做防御性的復(fù)制了——事實(shí)上只要常量類型中存在任何可變的引用類型,我們都要這么做: // 常量類型: 構(gòu)造時(shí)對(duì)可變的引用類型進(jìn)行復(fù)制。 public struct PhoneList { private readonly Phone[] _phones; public PhoneList( Phone[] ph ) { _phones = new Phone[ph.Length ]; // 因?yàn)镻hone是一個(gè)值類型,所以可以直接復(fù)制值。 ph.CopyTo(_phones, 0 ); } public IEnumerator Phones { get { return_phones.GetEnumerator(); } } } Phone[] phones = new Phone[10]; // 初始化phones PhoneList pl = new PhoneList( phones ); // 改變phones數(shù)組: // 不會(huì)改變pl中的副本。 phones[5] = Phone.GeneratePhoneNumber( ); 當(dāng)要返回一個(gè)可變的引用類型時(shí),我們也要遵循同樣的規(guī)則。例如,如果我們要添加一個(gè)屬性來(lái)從PhoneList結(jié)構(gòu)中獲取整個(gè)數(shù)組,那么其中的訪問(wèn)器也要?jiǎng)?chuàng)建一個(gè)防御性的復(fù)制。更多細(xì)節(jié)可參見(jiàn)條款23。 初始化常量類型通常有三種策略,選擇哪一種策略依賴于一個(gè)類型的復(fù)雜度。定義一組合適的構(gòu)造器通常是最簡(jiǎn)單的策略。例如,上述的Address結(jié)構(gòu)就是通過(guò)定義一個(gè)構(gòu)造器來(lái)負(fù)責(zé)初始化工作。 我們也可以創(chuàng)建一個(gè)工廠方法(factory method)來(lái)進(jìn)行初始化工作。這種方式對(duì)于創(chuàng)建一些常用的值比較方便。.NET框架中的Color類型就采用了這種策略來(lái)初始化系統(tǒng)顏色。例如,靜態(tài)方法Color.FromKnownColor()和Color.FromName()可以根據(jù)一個(gè)指定的系統(tǒng)顏色名,來(lái)返回一個(gè)對(duì)應(yīng)的顏色值。 最后,對(duì)于需要多個(gè)步驟操作才能完整構(gòu)造出一個(gè)常量類型的情況,我們可以通過(guò)創(chuàng)建一個(gè)可變的輔助類來(lái)解決。.NET中的String類就采用了這種策略,其輔助類為System.Text.StringBuilder。我們可以使用StringBuilder類通過(guò)多步操作來(lái)創(chuàng)建一個(gè)String對(duì)象。在執(zhí)行完所有必要的操作后,我們便可以通過(guò)StringBuilder類來(lái)獲取期望的String對(duì)象。 具有常量性的類型使得我們的代碼更加易于編寫(xiě)和維護(hù)。我們不應(yīng)該盲目地為類型中的每一個(gè)屬性都創(chuàng)建get和set訪問(wèn)器。對(duì)于目的是存儲(chǔ)數(shù)據(jù)的類型來(lái)說(shuō),我們應(yīng)該盡可能地將它們實(shí)現(xiàn)為具有常量性和原子性的值類型。在這些類型的基礎(chǔ)上,我們可以很容易地構(gòu)建更復(fù)雜的結(jié)構(gòu)。 |
|
來(lái)自: 蘭亭文藝 > 《改善C#程序的50種方法》