可能你会认为只读属性就只能读取,调用者不可能改变属性值。并非所有的情况都是如此,我们看下面的示例:
1 public class MyBusinessObject 2 { 3 private List<string> listOfData = new List<string> {"COM1","COM2","COM3","COM4","COM5","COM6" }; 4 5 public List<string> Data 6 { 7 get { return listOfData; } 8 } 9 //其他细节省略 10 }
本来我们希望MyBusinessOjbect类的Data属性是只读的,但是现在仍然可以通过其返还的引用来改变其内部状态:
1 MyBusinessObject bo = new MyBusinessObject(); 2 //访问集合 3 List<string> stuff = bo.Data; 4 //不应该这些,但是这里却允许 5 stuff.Clear();
显然,这并不是我们希望的行为,我们为类创建了严格的接口,然后让用户通过接口使用对象。我们不希望用户在我们不知道的情况下,访问或者修改对象的内部状态。我们有四种策略可以防止这种类型的内部数据结构遭受有意或无意的修改:
- 值类型
- 常量类型
- 接口
- 包装器(Wrapper)
1.值类型
当客户代码通过属性来访问值类型的成员时,实际返回的是值类型的副本,而对副本的任何修改都不会影响对象的内部状态。
2.常量类型
常量类型也是安全的(参见:Effective C# 笔记条目20)。我们可以在类型中安全地返回字符串或者其他的常量类型,客户代码不可能对它们做出任何更改,因为我们也可以保证类型内部状态的安全。
3.定义接口
通过定义接口,将客户对内部数据成员的访问权限限制在一部分功能中(参见:Effective C# 笔记条目22)。当我们创建类时,可以创建一组接口来支持类功能的子集。通过接口向外界暴露类的功能,即可尽量地避免内部数据遭到有意或无意的更改。使用IEnumerable接口向外提供List<T>的功能就是这种策略的一个应用。
4.提供包装器对象
提供一个包装器对象,仅暴露该包装器,从而限制对其中对象的访问。System.Collections.ObjectModel.ReadOnlyCollection<T>类型就是对集合的一个标准的只读封装,让其中的数据保持只读:
1 public class MyBusinessObject 2 { 3 private List<string> listOfData = new List<string> {"COM1","COM2","COM3","COM4","COM5","COM6" }; 4 5 public List<string> Data 6 { 7 get { return listOfData; } 8 } 9 10 public ReadOnlyCollection<string> CollectionOfData 11 { 12 get { return new ReadOnlyCollection<string>(listOfData); } 13 } 14 //其他细节省略 15 }
小节
如果将引用类型通过公有借口暴露给外界,那么对象的使用者即可绕过我们定义的方法和属性来更改对象的内部结构。这违反了我们通常的直觉,也会导致常见的错误。考虑到这一点,你应该修改类暴露出的接口。如果只是简单的返回内部数据,那么实际上就给外界赋予了访问内部成员的权限。客户代码可以调用成员中任何可用的方法。通过使用接口、包装器对象或值类型向外部提供私有数据,即可限制外界对这些数据的访问能力。