# 原则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:使用动态接收匿名类型参数