程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則7: 選擇恆定的原子值類型數據

Effective C#原則7: 選擇恆定的原子值類型數據

編輯:關於C#

恆定類型(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結構確實就 是這一原則的實例。後面的原則我大概翻閱了一下,有的深有的淺,後期的翻譯 也會是有些艱難的,但不管怎樣,我都會盡我最大的能力,盡快翻譯完所有原則 。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved