日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

條款7:將值類型盡可能實(shí)現(xiàn)為具有常量性和原子性的類型

 蘭亭文藝 2018-01-17
具有常量性的類型很簡(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)。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多