2011年9月22日 星期四

屬性(Property) 與 欄位(Field)

建立類別的時候,往往會遇到這兩個成員:Property 以及 Field,熟悉物件導向對於物件封裝的人應該是可以區分這兩種成員的差別,但是對於初學者或是對 OO 不熟悉的人是搞不清楚,明明兩種都是可以去儲存以及取得物件中的資料,但是到底兩者有什麼差別呢?又應該在什麼樣的情況下去使用這兩種成員?

所以就看到很多人都是直接在自訂類別中使用 Field,這狀況尤其在 ASP.NET MVC 的 ViewModel 最多,為什麼要捨 Property 而就 Field 呢?因為 Filed 在 Coding 時最快、最方便。反正只要可以存取資料,管他什麼是 Property、Field ……

但是這樣對嗎?

讓我們來認識 Property 與 Field 的差別。


我們先來看看在 MSDN 上面對於 Property 與 Field 的說明:

MSDN - 類別屬性、欄位和方法

MSDN - 屬性程序和欄位:協助您判斷何時使用欄位將資料儲存於類別中,以及何時是使用屬性的較佳時機。

節錄並整理如下:

屬性和欄位都可以儲存和擷取物件中的資訊。它們的相似處讓使用者很難判斷在某個狀況下較適合用哪一個來撰寫程式。

屬性:Property
使用屬性程序的時機:

  • 您需要控制設定或擷取值的時機和方法。
  • 屬性具有定義妥善且需要加以驗證的值集。
  • 設定值造成可察覺的物件狀態變更,例如 IsVisible 屬性。
  • 設定屬性造成其他內部變數或其他屬性值的變更。
  • 在設定或擷取屬性前,必須執行一組步驟。

欄位:Field
使用欄位的時機:

  • 值屬於自我驗證型別。例如,如果將 True 或 False 以外的值指派給 Boolean 變數,會發生錯誤或自動資料轉換。
  • 資料型別所支援範圍內的任何值為有效值。Single 或 Double 型別的許多屬性都適用。
  • 屬性為 String 資料型別,且字串大小或字串值沒有限制。


在MSDN的這一篇文件中「HOW TO:將欄位和屬性加入至類別」有說到:

您可以使用欄位和屬性將資訊儲存於物件中。

雖然欄位和屬性從用戶端應用程式的觀點來看並無分別,但它們在類別中的宣告方式卻不一樣。

欄位只是由類別 (Class) 公開 (Expose) 的公用變數,

而屬性則使用 Property 程序來控制值的設定或傳回方式。



在一開始的時候有說到物件導向的封裝,Field與Property都是屬於物件的資料成員,而Field,除非是定義為一個常數(const)或是固定的變數(readonly),我們才會將此Field修飾為Public(公開),否則一旦將Field的修飾設為Public而且不對其存取加以限制,那麼只要使用此物件時都可以任意的修改這個Field,這就失去了物件封裝的意義,也危及資料的安全。

所以在物件導向裡,Field通常的修飾都是為private(私有),只有開放給物件內部使用,對外則不開放的,而物件資料的存取則是使用Property,Property可以使用get,  set這兩種存取子去對資料的存取再做處理,在.NET Framework 3.0 之後有多增加一個「自動屬性實作」,讓屬性的get, set不必對存取子的內部再做邏輯處理,

   1: public string FullDescription
   2: {
   3:     get;
   4:     set;
   5: }

但是如果有需要去做邏輯處理時,就不能使用自動實作屬性,自動實作屬性是在對資料只有單純存取時才適用。
而通常資料成員的封裝是要Field與Property一起搭配的,
   1: private string _FullDescription;
   2: public string FullDescription
   3: {
   4:     get
   5:     {
   6:         if (string.IsNullOrWhiteSpace(this._FullDescription))
   7:         {
   8:             this._FullDescription = string.Format("{0} - {1}", 
   9:                 this.CategoryName, 
  10:                 this.Description);
  11:         }
  12:         return this._FullDescription;
  13:     }
  14: }

Property:FullDescription為主要資料取得的地方,對外公開;

Field:_FullDescription為物件私有成員,是用來存放屬性的資料。


MSDN - 使用屬性 (C# 程式設計手冊)

http://msdn.microsoft.com/zh-tw/library/w86s7x04.aspx

節錄整理:

屬性 (Property) 是欄位和方法的綜合體;

如果是物件的使用者,屬性會以欄位出現,存取屬性需要完全相同的語法。

而對於類別的實作者,屬性則是一或兩個程式碼區塊,代表 get 存取子 (Accessor) 和 (或) set 存取子。

get 存取子的程式碼區塊會於讀取屬性時執行,而且 set 存取子的程式碼區塊會在指派新值給該屬性時執行。

不含 set 存取子的屬性會被視為唯讀,而不含 get 存取子的屬性會被視為唯寫;同時具有這兩種存取子的屬性則為可讀寫。

屬性並不會歸類為變數,這點與欄位不同。因此,不可能將屬性當做 ref (C# 參考)out (C# 參考) 參數來傳遞。

屬性能有許多用途:

  • 可以在允許變更之前驗證資料;
  • 可以在實際從其他某些來源 (例如資料庫) 擷取資料時,透明地在某一類別上公開資料;
  • 也可以在資料變更 (例如引發事件,或是變更其他欄位的值) 時採取動作。

屬性在類別區塊內被宣告的方式是指定欄位存取層級、接著指定屬性的型別、再指定屬性的名稱,然後是宣告 get 存取子和 (或) set 存取子的程式碼區塊。

屬性可以標記為 publicprivateprotectedinternalprotected internal,這些存取修飾詞 (Modifier) 將定義類別使用者如何存取屬性。

相同屬性的 getset 存取子可能具有不同的存取修飾詞。

例如,get 可能具有 public,以允許來自型別外部的唯讀存取,而 set 則可能具有 privateprotected。如需詳細資訊,請參閱存取修飾詞 (C# 程式設計手冊)

您可以使用 static 關鍵字,將屬性宣告為靜態屬性。這讓呼叫端即使沒有類別的執行個體,也可以隨時使用屬性。如需詳細資訊,請參閱靜態類別和靜態類別成員 (C# 程式設計手冊)

屬性可以使用 virtual 關鍵字標記為虛擬屬性。如此一來,衍生類別就可以使用 override 關鍵字覆寫屬性行為。如需這些選項的詳細資訊,請參閱繼承 (C# 程式設計手冊)

用來覆寫虛擬屬性的屬性也可以是 sealed,對於衍生類別來說,該屬性即不再為虛擬的。

最後,您可以將屬性宣告為 absrtact,表示類別中不會有實作,衍生類別必須撰寫本身的實作。

如需這些選項的詳細資訊,請參閱抽象和密封類別以及類別成員 (C# 程式設計手冊)


MSDN - 欄位

http://msdn.microsoft.com/zh-tw/library/ms173118.aspx

節錄整理如下:

「欄位」(Field) 是一個任意型別的變數,直接在類別建構中宣告。 欄位是其包含型別 (Containing Type) 的「成員」(Member)。

類別 (Class) 或結構 (Struct) 可能會有執行個體 (Instance) 欄位或靜態 (Static) 欄位,或者兩個都有。 執行個體欄位專屬於某個型別的執行個體。

如果您有類別 T 搭配執行個體欄位 F,則您可以建立兩個型別 T 的物件,然後修改每個物件中 F 的值,而不會影響到另一個物件中的值。

相較之下,靜態欄位屬於類別本身所有,在該類別的所有執行個體之間共用。對執行個體 A 所做的變更,執行個體 B 和 C 只要存取該欄位就會馬上看到。

一般來說,欄位只應用在具有 private 或 protected 存取範圍的變數上。

類別公開 (Expose) 給用戶端程式碼的資料應透過方法屬性索引子來提供。

透過以這些建構來間接存取內部欄位,您可以防範無效的輸入值。

儲存由公用屬性公開之資料的私用欄位稱為「支援存放區」(Backing Store) 或「支援欄位」(Backing Field)。

欄位通常用來儲存必須由一個以上類別方法存取的資料,以及其儲存時間比任何單一方法的存留期 (Lifetime) 都還要長的資料。

例如,表示行事曆日期的類別有三個整數欄位,分別為月、日和年。 不會在單一方法以外範圍使用的變數,應在方法主體當中宣告為「區域變數」(Local Variable)。

您必須依序指定欄位的存取層級、欄位型別和欄位名稱,以在類別區塊中宣告欄位。


dustin - [大話設計模式]物件導向 筆記1

http://www.dotblogs.com.tw/dustin/archive/2011/01/17/20848.aspx

屬性:是一個方法或一對方法,但在調用它的程式碼看來,它是一個欄位,即屬性適合以欄位的方式使用方法調用的場合。

欄位:儲存類別要滿足其設計所需要的資料,欄位是與類別相關的變數。


另外在「Effective C#: 50 Specific Ways to Improve Your C# (Effective C#:改善C#程序的50種方法)」中也有提到,

「Always Use Properties Instead of Accessible Data Members.」
使用属性,以避免將資料成員直接暴露外界。

詳細說明可參閱:Level Up - [C#]Effective C# 條款一: 使用屬性代替公有欄位

 

因為在專案當中時常看到會有人用Reflection去取得Field的資料,但是正確的作法應該是要去取用Property的資料,尤其是ADO.NET Entity Framework去映射資料庫所產出的類別,例如:

   1: [EdmEntityTypeAttribute(NamespaceName="NorthwindModel", Name="Category")]
   2: [Serializable()]
   3: [DataContractAttribute(IsReference=true)]
   4: public partial class Category : EntityObject
   5: {
   6:     #region Factory 方法
   7:  
   8:     /// <summary>
   9:     /// 建立新 Category 物件。
  10:     /// </summary>
  11:     /// <param name="categoryID">CategoryID 屬性的初始值。</param>
  12:     /// <param name="categoryName">CategoryName 屬性的初始值。</param>
  13:     public static Category CreateCategory(global::System.Int32 categoryID, global::System.String categoryName)
  14:     {
  15:         Category category = new Category();
  16:         category.CategoryID = categoryID;
  17:         category.CategoryName = categoryName;
  18:         return category;
  19:     }
  20:  
  21:     #endregion
  22:     #region 基本屬性
  23:  
  24:     /// <summary>
  25:     /// 沒有可用的中繼資料文件。
  26:     /// </summary>
  27:     [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
  28:     [DataMemberAttribute()]
  29:     public global::System.Int32 CategoryID
  30:     {
  31:         get
  32:         {
  33:             return _CategoryID;
  34:         }
  35:         set
  36:         {
  37:             if (_CategoryID != value)
  38:             {
  39:                 OnCategoryIDChanging(value);
  40:                 ReportPropertyChanging("CategoryID");
  41:                 _CategoryID = StructuralObject.SetValidValue(value);
  42:                 ReportPropertyChanged("CategoryID");
  43:                 OnCategoryIDChanged();
  44:             }
  45:         }
  46:     }
  47:     private global::System.Int32 _CategoryID;
  48:     partial void OnCategoryIDChanging(global::System.Int32 value);
  49:     partial void OnCategoryIDChanged();
  50:  
  51:     /// <summary>
  52:     /// 沒有可用的中繼資料文件。
  53:     /// </summary>
  54:     [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
  55:     [DataMemberAttribute()]
  56:     public global::System.String CategoryName
  57:     {
  58:         get
  59:         {
  60:             return _CategoryName;
  61:         }
  62:         set
  63:         {
  64:             OnCategoryNameChanging(value);
  65:             ReportPropertyChanging("CategoryName");
  66:             _CategoryName = StructuralObject.SetValidValue(value, false);
  67:             ReportPropertyChanged("CategoryName");
  68:             OnCategoryNameChanged();
  69:         }
  70:     }
  71:     private global::System.String _CategoryName;
  72:     partial void OnCategoryNameChanging(global::System.String value);
  73:     partial void OnCategoryNameChanged();
  74:  
  75:     /// <summary>
  76:     /// 沒有可用的中繼資料文件。
  77:     /// </summary>
  78:     [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
  79:     [DataMemberAttribute()]
  80:     public global::System.String Description
  81:     {
  82:         get
  83:         {
  84:             return _Description;
  85:         }
  86:         set
  87:         {
  88:             OnDescriptionChanging(value);
  89:             ReportPropertyChanging("Description");
  90:             _Description = StructuralObject.SetValidValue(value, true);
  91:             ReportPropertyChanged("Description");
  92:             OnDescriptionChanged();
  93:         }
  94:     }
  95:     private global::System.String _Description;
  96:     partial void OnDescriptionChanging(global::System.String value);
  97:     partial void OnDescriptionChanged();
  98:  
  99:     /// <summary>
 100:     /// 沒有可用的中繼資料文件。
 101:     /// </summary>
 102:     [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
 103:     [DataMemberAttribute()]
 104:     public global::System.Byte[] Picture
 105:     {
 106:         get
 107:         {
 108:             return StructuralObject.GetValidValue(_Picture);
 109:         }
 110:         set
 111:         {
 112:             OnPictureChanging(value);
 113:             ReportPropertyChanging("Picture");
 114:             _Picture = StructuralObject.SetValidValue(value, true);
 115:             ReportPropertyChanged("Picture");
 116:             OnPictureChanged();
 117:         }
 118:     }
 119:     private global::System.Byte[] _Picture;
 120:     partial void OnPictureChanging(global::System.Byte[] value);
 121:     partial void OnPictureChanged();
 122:  
 123:     #endregion
 124:  
 125:     #region 導覽屬性
 126:  
 127:     /// <summary>
 128:     /// 沒有可用的中繼資料文件。
 129:     /// </summary>
 130:     [XmlIgnoreAttribute()]
 131:     [SoapIgnoreAttribute()]
 132:     [DataMemberAttribute()]
 133:     [EdmRelationshipNavigationPropertyAttribute("NorthwindModel", "FK_Products_Categories", "Products")]
 134:     public EntityCollection<Product> Products
 135:     {
 136:         get
 137:         {
 138:             return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Product>("NorthwindModel.FK_Products_Categories", "Products");
 139:         }
 140:         set
 141:         {
 142:             if ((value != null))
 143:             {
 144:                 ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Product>("NorthwindModel.FK_Products_Categories", "Products", value);
 145:             }
 146:         }
 147:     }
 148:  
 149:     #endregion
 150: }

在所有屬性的get, set 都是去對私有的Field欄位做資料的存取。

 

延伸閱讀:

MSDN - 封裝欄位重構 (C#)

http://msdn.microsoft.com/zh-tw/library/a5adyhe9(v=VS.100).aspx

[封裝欄位] (Encapsulate Field) 重構作業可讓您從現有欄位快速建立屬性,然後以新屬性的參考順利地更新程式碼。

欄位public (公用) 時,可以直接存取該欄位的其他物件,都能在擁有該欄位的物件無法偵測到的情況下修改該欄位。 藉由使用屬性來封裝該欄位,您就可以禁止直接存取該欄位。


另外補充兩篇MSDN對於屬性設計與欄位設計的設計方針:

屬性設計

http://msdn.microsoft.com/zh-tw/library/ms229006.aspx

一般而言,方法表示動作,而屬性則表示資料。 屬性的使用與欄位一樣,這表示屬性在運算方面來說不應該是複雜的,或不應該產生副作用。 如需屬性設計的詳細資訊,請參閱索引屬性設計屬性變更通知事件

下列方針可協助您確保屬性的設計正確。

如果呼叫端不應該能夠變更屬性的值,一定要建立唯讀屬性。

請注意,屬性型別的可變動性會影響使用者可以變更的內容。 例如,如果您定義會傳回讀取 / 寫入集合的唯讀屬性,則使用者將無法為此屬性指派不同的集合,但是可以修該集合中的元素。

請勿提供僅 set 屬性。

如果無法提供屬性 getter,請改用一個方法來實作此功能。 此方法名稱應該以 Set 做為開頭,後面接著原本的屬性名稱。 例如,AppDomain 有一個稱為 SetCachePath 的方法,而沒有稱為 CachePath 的僅 set 屬性。

一定要為所有屬性提供合理的預設值,以確保預設值不會產生安全性漏洞或極無效率的設計。

一定要允許屬性依任何順序來設定,即使這樣會產生暫時無效的物件狀態時亦然。

如果屬性 setter 擲回例外狀況,一定要保留之前的值。

避免從屬性 getter 擲回例外狀況。

屬性 getter 應該是沒有任何先前條件的簡單作業; 如果 getter 可能擲回例外狀況,請考慮將此屬性重新設計為方法; 但是,這項建議不適用於索引子, 索引子可能會因為無效的引數擲回例外狀況。

從屬性 setter 擲回例外狀況是有效且可接受的。

 

欄位設計

http://msdn.microsoft.com/zh-tw/library/ms229057.aspx

欄位可保存與物件有關的資料。 在大多數的情況下,程式庫中的任何非靜態欄位應該不會讓開發人員看到。
下列方針可協助您在程式庫設計中正確使用欄位。

不要提供公用或受保護的執行個體欄位。

公用和受保護的欄位在版本控制上並不理想,因此不受程式碼存取安全性要求的保護。
請不要使用公開可見的欄位,而是要使用私用欄位,並透過屬性來進行公開。

要針對絕對不會變更的常數使用常數欄位。

例如,Math 類別可將 EPI 定義為靜態常數。

編譯器會將 const 欄位的值直接插入呼叫程式碼中,這表示在變更 const 值時,一定會遇到引入相容性問題所帶來的風險。

一定要針對預先定義的物件執行個體使用公用靜態唯讀欄位。

例如,DateTime 類別會提供靜態唯讀欄位,您可使用此欄位來取得設定為最大或最小時間值的 DateTime 物件。 請參閱MaxValueMinValue

請不要將可變動型別的執行個體指派給唯讀欄位。

使用可變動型別建立的物件可以在建立之後修改。 例如,陣列和大多數集合都是可變動型別,而 Int32, UriString 則是不可變動型別。 對於保存可變動參考型別的欄位而言,唯讀修飾詞可避免欄位值被覆寫,但是並不會防止可變動型別被修改。


屬性與欄位要好好認識並區分,然後了解應該如何的使用,建立類別時,凡是要公開的資料成員應該使用 Property而不是用 Field,要把 Field 修飾為 Public 時,應該限定為 const 或是 readonly。

使用 Reflection 對物件的資料成員進行存取,應該是對 Property 而不是 Field。

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。