# 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
**By D.S.Qiu**
尊重他人的勞動, **支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
在 C# 語言里,屬性已經是“第一類公民(first-class citizens)”。自從C#1.0的發布以來,多個強化,使得屬性有了更多的表現力。你可以使用 getter 和 setter 函數指定不同的訪問限制。使用隱式(implicit)屬性實現代替數據成員可以極大減少打字的功夫。如果你還是使用 public 變量(variables)在你的類中,請停下來!屬性可以暴露你的數據成員像 public 接口(interface)一樣,更提供你想要面向對象的封裝。屬性是語言基本元素,可以和訪問數據成員的方式一樣訪問,但卻是用方法實現的。
一個類的有些成員最好表示為數據:客戶的名字,一個的坐標(x,y),或者去年的收入。屬性是可以讓你創建一個表現和數據訪問一樣又有所有方法的優勢的接口。客服端代碼可以像訪問 public 域(fiedls)訪問屬性。但是實際是使用方法去定義屬性訪問的行為。
.NET 框架假定你會使用屬性來訪問你的 public 數據成員。實際上,在 .NET 框架數據綁定(data binding)類支持屬性而不是 public 數據成員。這可以從所有數據綁定類庫: WPF , Windows Forms , Web Forms ,和 Silverlight 。數據綁定就是綁定對象(object)到用戶界面(user interface)控制上。數據綁定的使用反射(refelction)去查找一個類的已知屬性:
```
textBoxCitye.DataBindings.Add("Text",address,"City");
```
上面的代碼實現綁定 textBoxCity 的屬性 Text 到對象 address 的屬性 City 。如果使用 public 數據成員 City 是不能工作的,這些框架類庫設計不支持這樣的實踐。 public 數據成員是一個不好的實踐,所以沒有被支持。這樣的決定只是給出另一個理由——按照正確的面向對象計算取實踐。
你也許會說,數據綁定只是應用于那些含有要在用戶界面顯示的元素的類。但那不意味著屬性專門使用在用戶界面的邏輯中。你應該使用屬性在其他類和結構體(structures)。屬性能最快捷地適應因新的需求或行為的改變。你能很快覺得你自定義的類的 name 不能為空。如果你使用 public 屬性 Name,你可以很快修復這個問題在一個地方:
```
public class Customer
{
private string name;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(
"Name cannot be blank",
"Name");
name = value;
}
// More Elided.
}
}
```
如果你使用 public 數據成員,你會很困難地去找遍你的程序,找到每一處并且修復。這花費了太多時間,太多時間了。
因為屬性是用方法實現的,增加多線程的支持是很容易的。你可以加強 get 和 set 訪問器(accessors)的實現來提供數據訪問的同步。
```
public class Customer
{
private object syncHandle = new object();
private string name;
public string Name
{
get
{
lock (syncHandle)
return name;
}
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(
"Name cannot be blank",
"Name");
lock (syncHandle)
name = value;
}
}
// More Elided.
}
```
屬性有所有方法的語言特性。屬性可以是 virtual :
```
public class Customer
{
public virtual string Name
{
get;
set;
}
}
```
你也許注意到上一個例子使用 C# 3.0 的隱式屬性語法。創建屬性包裹一個備份存儲(backing store)是一個常見的模式。一般,你不需要驗證邏輯在屬性的 getters 或 setters 。 C# 語言支持最簡化的隱式屬性語法來極大減少需要作為屬性來暴露簡單域的打字功夫。編譯器會創建一個 private 成員域(通常被稱作備份存儲)并且實現 get 和 set 放棄的邏輯。
你可以使用簡化的隱式屬性拓展屬性為 abstract 和定義屬性作為接口的一部分。下面的例子演示了在泛型接口(generic interface)定義了屬性。可以注意到下面定義的接口沒有包含任何實現并且語法和隱式屬性是一致的。每個實現這個接口的類都要實現這個規范。
```
public interface INameValuePair<T>
{
string Name
{
get;
}
T Value
{
get;
set;
}
}
```
屬性是全面的(full-fledeged),語言第一類元素:擴展方法以訪問或修改內部數據。任何成員函數能做的,你都可以使用屬性。
屬性的兩個訪問器會被編譯成類的兩個單獨的方法。在你的 C# 程序中可以指定 get 和 set 訪問器不同訪問修飾符。這讓你可以很好控制通過屬性暴露數據成員的可訪問性。
```
public class Customer
{
public virtual string Name
{
get;
protected set;
}
// remaining implementation omitted
}
```
屬性的語法擴展遠超過簡單的數據域。如果你的類包含可索引項(indexed items)作為它的接口,你可以使用索引器(indexers)(參數化屬性(parameterized properties))。創建屬性實現返回序列(sequence)中的項是非常有用的:
```
public int this[int index]
{
get { return theValues[index]; }
set { theValues[index] = value; }
}
// Accessing an indexer:
int val = someObject[i];
```
索引器能和單一項(single-item)屬性一樣被語言全面支持:索引器可以像你自己寫的方法一樣實現,里面可以應用你的校驗和計算。索引器可以是 virtual 或 abstract ,可以在接口內聲明,并且可以是只讀或讀和寫。參數是數字的一維索引器可以參與數據綁定。其他使用非整數參數的索引器用來定義 maps 和 dictionaries :
```
public Address this[string name]
{
get { return adressValues[name]; }
set { adressValues[name] = value; }
}
```
為了和多維數組保持一致,你可以創建多維索引器,在不同軸(axis)使用相同或不同類型:
```
public int this[int x, int y]
{
get { return ComputeValue(x, y); }
}
public int this[int x, string name]
{
get { return ComputeValue(x, name); }
}
```
值得注意的是所有索引器必須使用 this 關鍵字聲明。在 C# 中你不能自己命名索引器。所以一個類型的索引器必須有不同的參數列表來避免歧義。幾乎所有的屬性的功能都適用索引器。索引器可以是 virtual 或 abstract ;索引器的 setters 和 getters 可以不同的訪問限制。不過,你不能像創建隱式屬性一樣創建隱式索引器。
屬性的功能都非常好,是一個非常不錯的改進。如果你還蠢蠢欲動使用數據成員的初始實現,然后等到你需要使用到屬性的優勢的時候,再用屬性替換數據成員。這聽起來像是一個合理的策略——但這是錯誤的。考慮下面的類的定義:
```
// using public data members, bad practice:
public class Customer
{
public string Name;
// remaining implementation omitted
}
```
類 Customer 有一個數據成員 Name 。你可以使用熟悉的成員記號(member notation) get 或 set 這個 Name :
```
string name = customerOne.Name;
customerOne.Name = "This Company, Inc.";
```
這個簡單有直觀。你會覺得你以后使用屬性替換數據成員 Name ,而且代碼不做改動能照常工作。好吧,確實是那樣的。屬性就是訪問起來跟數據成員一樣。這個語法的目的就在于此。但是屬性不是數據。屬性訪問和數據訪問會產生不同的微軟中間語言(Microsoft Intermediate Language)指令。
盡管屬性和數據成員在代碼上兼容的。但在二進制上是不兼容的。一個很明顯的案例,當你把一個 public 數據成員改為等同的 public 屬性,意味著你要重新編譯所有使用這個 public 數據成員的代碼。 C# 把二進制程序集(assemblies)看做“第一類公民”。語言的一個目的就是你可以發布單一更新的程序集,而不要更新整個應用。把數據成員改為屬性這么簡單的行為卻破壞二進制的兼容性。這個行為使得部署更新單一程序集變得更困難。
如果你看了 IL 的屬性實現,你可能很想知道屬性和數據成員的相對性能。屬性并不比數據成員快,但也不會慢多少。 JIT 編譯器對屬性的訪問器做了 inline 的優化。如果 JIT 編譯器做了 inline 屬性訪問器優化, 數據成員和屬性的性能是一樣的。即使沒有屬性訪問器沒有置為 inline ,性能差別也只是可以忽略不計的一個函數調用話費。這差別只有在少數情況下才可以被測量出來。
屬性是像數據一樣被調用的方法。調用者就會有一些訪問權限期望。他們把訪問屬性當做數據訪問。畢竟,那就是屬性。你的屬性訪問器要能滿足這些期望。 Get 訪問器不能有其他作用。 Set 訪問器改變了狀態,調用者要能看到改變。
屬性訪問器同時滿足使用者的性能期望。屬性訪問跟數據域訪問很類似。這不能導致屬性和簡單數據訪問有著顯著不同的性能特點。屬性訪問不能在長計算,或跨平臺調用(比如執行數據庫查詢),或其他長操作和使用者期望保持一致。
無論什么時候,你要在類中暴露數據作為 public或 protected 接口,請使用屬性。對于序列或 dictionaries 是使用索引器。所有數據成員應該無一例外地使用 private 修飾符。你會立即在數據綁定得到支持,而且在以后會很容易方法中的實現。把變量封裝在屬性里所有的打字功夫加起來就一到兩分鐘。你會發現在使用屬性替換之前的設計會花費數小時。現在花一點時間,以后節約你大量時間。
小結:
萬事開頭難,終于寫下了翻譯 Effective C# : 50 Specific Ways to Improve Your C# 2nd Edition 的第一篇,花的時間有點心疼,感覺頸椎都熬出問題了(太專注了)。之前工作都沒覺得什么,看來還是工作不要那么專注,以后把時間擠出來干這個,加油!發現最后一句話說的蠻好的:Spend a little time now, and save yourself lots of time later. 分享給大家.....
因為是第一次翻譯,歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在 **文首** 注明出處:[http://dsqiu.iteye.com/blog/1976256](/blog/1976256)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數