# 原則26:避免返回類的內部對象的引用
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
你可能會覺得只讀屬性是只讀的所以調用者不能修改它。不幸的是,這并不總是奏效的方法。如果你的屬性返回引用類型,調用者可以訪問任何 public 的對象成員,包括那些能修改屬性狀態。例如:
```
public class MyBusinessObject
{
// Read Only property providing access to a
// private data member:
private BindingList<ImportantData> listOfData =
new BindingList<ImportantData>();
public BindingList<ImportantData> Data
{
get { return listOfData; }
}
// other details elided
}
// Access the collection:
BindingList<ImportantData> stuff = bizObj.Data;
// Not intended, but allowed:
stuff.Clear(); // Deletes all data.
```
MyBusinessObject 的任何使用者都可以修改你的內部數據集。你可以創建屬性隱藏內部數據結構。你提供方法允許客戶端只能通過這些方法操作數據,因此你可以管理內部狀態的改變。只讀屬性打開類封裝的后門。當你考慮這類問題時,你會認為它不是一個可讀寫的屬性,而是一個只讀屬性。
歡迎來到一個基于引用的精彩系統。任何返回引用的成員都會返回對象的句柄。你給了調用者你的內部結構的句柄,因此調用者不再需要通過對象修改包含的引用。
顯然,你需要阻止這類行為。你構建接口,并且希望使用者使用它。你不希望使用者可以在你不知情的情況下改變對象的內部狀態。你有四種策略包含你的內部數據結構不被任意修改:值類型,不可變類型,接口和包裝器。
值類型會被復制當客戶端通過屬性訪問它們。客服端對復雜的類數據的任何改變,都不會影響你對象內部狀態。客戶端可以根據需求隨意的改變復雜的數據。這不會影響你的內部狀態。
不可變類型,例如 System.String 同樣是安全的(查看原則20)。你返回 string ,或者其他不可變類型,很安全地知道沒有客戶端可以改變字符串。你的內部狀態是安全的。
第三種方案是定義接口,從而允許客服端訪問內部成員的部分功能(查看原則22)。當你創建一個自己的類時,你可以創建一些接口,用來支持對類的部分的功能。通過這些接口來暴露一些功能函數,你可以盡可能的減少一些對數據的無意修改。客戶可以通過你提供的接口訪問類的內部對象,而這個接口并不包含這個類的全部的功能。在 List<T> 中暴露 IEnumerable<T> 接口就是這個策略的例子。聰明的程序可以阻止那些猜測實現接口的對象實際類型并使用強制類型轉換。但是那些那樣做的程序員就是花更多時間去創建 bug ,這是他們應得的。
這個在 BindingList 類會有點小麻煩會引起一些問題。因為沒有泛型版本的 IBindingList ,所以你需要創建兩個不同的 API 方法訪問數據:一個通過 IBindingList 接口支持 DataBinding ,一個通過 ICollection<T> 或其他類似接口編程支持。
```
public class MyBusinessObject
{
// Read Only property providing access to a
// private data member:
private BindingList<ImportantData> listOfData = new
BindingList<ImportantData>();
public IBindingList BindingData
{
get { return listOfData; }
}
public ICollection<ImportantData> CollectionOfData
{
get { return listOfData; }
}
// other details elided
}
```
在我們開始討論如何創建一個完全只讀的數據視圖時以前,讓我先簡單的了解一下你應該如何響應客服端的修改。這是很重要的,因為你可能經常要暴露一個 IBindingList 給 UI 控件,這樣用戶就可以編輯數據。毫無疑問你已經使用過 Windows 表單的數據綁定,用來給用戶提供對象私有數據編輯。BindingList<T> 類實現 IBindingList 接口,所以你響應展示給用戶的集合的任何添加,更新,或者刪除元素的操作。
任何時候,當你期望給客戶端提供修改內部數據的方法時,都可以擴展這個的技術,但你要驗證而且響應這些改變。你的類訂閱對內部數據結構產生改變的事件。事件處理器驗證改變或者響應這些改變以更新其他內部狀態。
回到開頭的問題上,你想讓客戶查看你的數據,但不許做任何的修改。當你的數據存儲在一個 BindingList<T> 里時,
你可以通過強制在 BindingList 上設置一些屬性( AddEdit , AllowNew ,AllowRemove等)。這些屬性的值被 UI 控件控制。UI 控件基于這些屬性值開啟和關閉不同的行為。這些是 public 的屬性,所以你可以修改集合的行為。但是那樣也還沒有作為 public 屬性暴露 BindingList<T> 對象。客戶端可以修改你的屬性并且規避使用只讀綁定集合的意圖。再強調一次,通過接口類型而不是類類型暴露內部存儲可以限制客服端代碼在這個對象上的行為。
最后一個選擇是提供一個包裝器對象并且值暴露這個包裝器實例,這可以減少訪問內部對象。 System.Collections.ObjectModel.ReadOnlyCollection<T> 類就是包裝集合并暴露一個只讀版本的數據的標準方法:
```
public class MyBusinessObject
{
// Read Only property providing access to a
// private data member:
private BindingList<ImportantData> listOfData = new BindingList<ImportantData>();
public IBindingList BindingData
{
get { return listOfData; }
}
public ReadOnlyCollection<ImportantData>CollectionOfData
{
get
{
return new ReadOnlyCollection<ImportantData>(listOfData);
}
}
// other details elided
}
```
通過 public 接口直接暴露引用類型將允許使用者修改對象的內部而不通過你定義的方法或屬性。這看起來不可思議,確實一個常見的錯誤。你應該考慮到你暴露的是引用而不是值,因此需要修改類的接口。如果你只是簡單的返回內部數據,那么你就給了訪問它們包含的常用的權限。客戶端可以調用可訪問的方法。你要限制訪問private 內部數據要通過接口,包裝器對象或值類型。
小結:
這里說的確實是引用類型系統或者很多需要統一管理模型的一個通病,怎么才能做到對引用類型內部改變“一夫當關萬夫莫開”的效果,目前比較好的方法是使用接口!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2086266](/blog/2086266)
更多精彩請關注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:使用動態接收匿名類型參數