Effective C# Item 23: Avoid Returning References to Internal Class Objects
我们知道定义只读属性可以让调用者无法修改该属性的值。但是并不是在所有情况下这种做法都能达到目的。如果我们创建了一个返回引用类型的属性,那么调用者可以对这些对象成员进行任何的操作,包括那些我们不期望的修改操作。考虑下面这段例子:
{
private DataSet _ds;
public DataSet Data
{
get
{
return _ds;
}
}
}
MyBusinessObject bizObj = new MyBusinessObject();
DataSet ds = bizObj.Data;
ds.Tables.Clear(); //删除了所有的表
任何MyBusinessObject的客户程序都可以修改它的内部dataset。我们创建只读属性的目的本来是为了隐藏内部成员,但是客户端却可以对它进行任意操作。这个只读属性在我们的类中打开了一个通道将内部成员的引用暴露给了外部。而我们想要的只读属性,并不是这种可读又可写的属性。
这就是引用类型的神奇。任何引用类型返回的都是对象的引用地址。当我们将对象的引用地址返回给调用者后,调用者就可以完全无视类的只读属性而直接操作对应地址上的引用类型。
很明显,我们应当避免类似的这种情况。例如我们为类创建了一个接口并希望所有的使用者遵循,我们一定不会希望使用者获得或者修改类的内部成员。我们有四种策略可以保护类的内部成员不受到这种的威胁:使用值类型,使用不可变类型,通过接口进行功能限制和使用包装器(wrapper)。
值类型通过接口传递的是本身的拷贝。在客户程序中对拷贝的任何修改都不会反映的类的内部成员上来。客户端可以任意处理值类型的拷贝,这不会带来任何问题。
不可变类型同样是安全的,一个典型的例子就是System.String类。我们可以返回一个string或者任何其它的不可变类型,因为它们是不可被修改的。客户端的任何操作同样不会影响到类的内部成员的安全。
第三种方法是定义接口,通过接口客户程序可以使用内部成员的部分功能。当你创建类的时候,可以定义一组包含类中不同功能性的接口。通过暴露这些功能性接口,可以使内部成员暴露最小化。客户程序只能通过接口来获得我们许可的功能,而不是内部成员的全部功能。对应上例中我们可以通过只暴露DataSet类的IListsource接口的方法来避免对dataset的修改。
对于DataSet来说还有最后一种方法:使用包装器。通过DataViewManager类我们可以获得DataSet中的数据,但可以避免对其进行不必要的操作:
{
private DataSet _ds;
public DataView this[string tableName]
{
get
{
return _ds.DefaultViewManager.CreateDataView(_ds.Tables[tableName]);
}
}
}
DataViewManager可以为DataSet中的数据表创建DataView。当我们使用DataViewManager时是没有办法来修改DataSet中的数据表的内容的。我们可以对DataView进行某些操作,但是这些操作都不会反映到数据源上。
在讨论如何创建真正的只读数据之前,我们先谈谈如何响应客户端对公有数据进行的修改。这是很重要的,因为我们经常需要将一些数据展示在用户界面上并接受用户修改。通过数据绑定,我们可以将这些数据提供给用户。而DataTable类中有一系列事件可以让我们轻松的实现观察者模式:我们的类可以响应任何用户发起的任何修改。不论是行或者列的修改,DataSet中的DataTable都可以唤起事件。ColumnChanging和RowChanging事件会在修改提交之前被唤起,ColumnChanged和RowChanged事件会在修改提交之后被唤起。
当我们希望客户端仅能查看数据时,我们可以为DateTable创建一个不可以修改的DataView来达到目的。DataView类中包含了一些属性让我们可以选择是否支持对它的添加删除修改或者存储功能。我们可以创建一个索引器来实现返回指定索引的DataView:
{
private DataSet _ds;
public IList this[string tableName]
{
get
{
DataView view = _ds.DefaultViewManager.CreateDataView(_ds.Tables[tableName]);
view.AllowNew = false;
view.AllowDelete = false;
view.AllowEdit = false;
return view;
}
}
}
最后我们要将特定数据表的视图通过IList接口返回。我们可以在任何集合上使用IList接口,而不仅限于DataSet。我们不能简单的返回一个DataView,用户可以轻易的改变它的功能属性。我们返回的是一个不可修改的对象列表。通过IList接口,我们可以保证DataView对象不会被外部操作修改。
直接将内部成员的引用暴露给用户并接受用户修改的做法是错误的。我们应当注意在接口中返回的内部成员的引用。用户可能会对这些引用做任意的操作,例如调用这些内部成员的公有方法。我们应当通过接口或者包装的方法来限制这种直接的对内部成员的访问。当我们希望客户端可以修改内部数据时,我们应当使用观察者模式(observer pattern),对用户的操作进行验证和应答。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录