最近在阅读Framework Design Guidelines,本着现学现用的原则,于是就用FxCop工具对代码进行规范性检查时,发现了很多问题,其中包括命名以及一些设计上的规范。
其中,Do not expose generic lists 这条设计规范引起了我的注意。该规范指出“不要在对象模型中对外暴露List<T>,应该考虑使用Collection<T>,ReadOnlyCollection<T>或者KeyedCollection<K,V>,List<T>是原先ArrayList的泛型实现,是最基础的、性能最好和功能最强大的“动态数组”,对性能进行了优化,但是相对较“封闭”,入口较多。比如,如果奖List<T>对象返回给客户端,那么就不能实现诸如当客户端对该集合进行更改进行通知的功能”
本文首先讨论Collection和List泛型的区别,使用场景,然后演示了一个使用Collection对象作为类的属性的例子,并展现了如何在Collection的操作中触发事件。
Collection<T>和List<T>的主要区别和使用场景
刚开始还不太理解这个设计规范的意思,于是查了下资料,在Why we don’t recommend using List<T> in public APIs 一文中,简要介绍了原因:
- List<T>类型并不是为可扩展性而设计的,他优化了性能,但是丢失了可扩展性。比如,它没有提供任何可以override的成员。这样就不能获得诸如集合改变时获取通知的功能。Collection<T>集合允许我们重写受保护的SetItem方法,这样当我们向集合中添加或者修改集合中的记录,调用SetItem方法的时候就可以自定义一些事件来通知对象。
- List<T>对象有太多的与“场景”不相关的属性和成员,将其作为成员类型对外暴露在一些情况下显得过“重”,比如在WindowsForm中ListView.Items并没有返回一个List对象,而是一个 ListViewItemCollection对象,该对象的签名为:
// Summary: // Represents the collection of items in a System.Windows.Forms.ListView control // or assigned to a System.Windows.Forms.ListViewGroup. [ListBindable(false)] public class ListViewItemCollection : IList, ICollection, IEnumerable
是一个实现ICollection接口的对象。
还有在我们常用的DataTable的Rows对象是一个DataRowCollection对象,该对象继承自InternalDataCollectionBase
// Summary: //Represents a collection of rows for a System.Data.DataTable. public sealed class DataRowCollection : InternalDataCollectionBase public class InternalDataCollectionBase : ICollection, IEnumerable
而InternalDataCollectionBase则实现ICollection接口。
public class InternalDataCollectionBase : ICollection, IEnumerable
可以看到微软的.NET BCL中没有直接暴露List类型的成员。该规则建议我们使用Collection<T>。
List<T>通常用来作为类的内部实现,因为它对性能进行过优化,具有一些丰富的功能,而Collection<T>则是提供了更多的可扩展性。在编写公共API的时候,我们应该避免接受或者返回List<T>类型的对象,而是使用List的基类或者Collection接口。
Collection<T>虽然可以直接使用,但是通常作为自定义集合的基类来使用,一般的我们应该使用Collection<T>类型的对象来对外暴露功能,除非需要一些List<T>中特有的属性。
实践
这里举个简单的例子来说明,我们有一个Person表示客户信息的类,然后这个类中有个Addresses属性,该属性表示该客户的住址,通常地址有很多个,比如公司地址,住宅地址等等。一般的,我们的设计如下。
public class Person { private List<Address> addresses = new List<Address>(); public List<Address> Addresses { get { return addresses; } } }
假设这个是我们对外提供的一个API的一个类。那么就违反了之前讨论的这一原则,不应该把成员以List<T>的形式对外暴露。可以初步修改为:
public class Person { private Collection<Address> addresses = new Collection<Address>(); public Collection<Address> Addresses { get { return addresses; } } }
现在,假设有一个需求,当用户的地址发生改变了,需要通知另外一个系统,给用户发提醒,或者其他操作。现在就需要在集合发生改变的时候对外提供事件提醒了,比如当用户修改地址后,需要发邮件通知用户是否需要修改信用卡电子账单寄送地址。
现在我们需要自定义我们的AddressCollection如:
public class AddressCollection : Collection<Address> { public event EventHandler<AddressChangedEventArgs> AddressChanged; protected override void InsertItem(int index, Address item) { base.InsertItem(index, item); EventHandler<AddressChangedEventArgs> temp = AddressChanged; if (temp != null) { temp(this, new AddressChangedEventArgs(ChangeType.Added, item, null)); } } protected override void SetItem(int index, Address item) { Address replaced = Items[index]; base.SetItem(index, item); EventHandler<AddressChangedEventArgs> temp = AddressChanged; if (temp != null) { temp(this, new AddressChangedEventArgs(ChangeType.Replaced, replaced, item)); } } protected override void RemoveItem(int index) { Address removedItem = Items[index]; base.RemoveItem(index); EventHandler<AddressChangedEventArgs> temp = AddressChanged; if (temp != null) { temp(this, new AddressChangedEventArgs(ChangeType.Removed, removedItem, null)); } } protected override void ClearItems() { base.ClearItems(); EventHandler<AddressChangedEventArgs> temp = AddressChanged; if (temp != null) { temp(this, new AddressChangedEventArgs(ChangeType.Cleared, null, null)); } } }
public class AddressChangedEventArgs : EventArgs { public readonly Address ChangeItem; public readonly ChangeType ChangeType; public readonly Address ReplaceWith; public AddressChangedEventArgs(ChangeType changeType, Address item, Address replacement) { ChangeType = changeType; ChangeItem = item; ReplaceWith = replacement; } } public enum ChangeType { Added, Removed, Replaced, Cleared };
我们重写了InsertItem, SetItem, RemoveItem, ClearItems这四个方法,并且在这个四个方法中Raise了事件。
现在,我们的Person类变为:
public class Person { private AddressCollection addresses; public event EventHandler<AddressChangedEventArgs> AddressChanged; public Person() { addresses = new AddressCollection(); addresses.AddressChanged += new EventHandler<AddressChangedEventArgs>(addresses_Changed); } public Collection<Address> Addresses { get { return addresses; } } void addresses_Changed(object sender, AddressChangedEventArgs e) { EventHandler<AddressChangedEventArgs> temp = AddressChanged; if (temp != null) { temp(this, e); } } public void AddAddress(Address address) { addresses.Add(address); } }
我们在Person类中,定义了一个私有的之前重写的AddressCollection类表示用户的地址集合的字段addresses,然后通过Get方法定义了一个只读的Collection<Address>集合,该集合内部返回addresses。
在Person类中,我们提供一个AddressChanged事件来通知用户地址信息发生改变。在Person的构造函数中,我们初始化addresses类,然后注册AddressChanged事件,在该事件中,我们直接再次调用Person类暴露给用户的AddressChanged事件。
最后在Persion类中添加了一个AddAddress方法,该方法调用address的Add方法。该方法里面就会触发事件。
在使用的时候,我们只需要实例化一个Person对象,然后注册地址修改变化的事件,这样当我们添加地址的时候,就会触发事件通知了。
static void Main(string[] args) { Person p = new Person(); p.AddressChanged += new EventHandler<AddressChangedEventArgs>(p_AddressChanged); p.AddAddress(new Address { Street= "南京东路100号" }); Console.ReadKey(); } static void p_AddressChanged(object sender, AddressChangedEventArgs e) { switch (e.ChangeType) { case ChangeType.Added: Console.WriteLine("Address: Add {0}", e.ChangeItem.Street); break; case ChangeType.Removed: Console.WriteLine("Address: Removed {0}", e.ChangeItem.Street); break; case ChangeType.Replaced: Console.WriteLine("Address: Replaced {0} with {1}", e.ChangeItem, e.ReplaceWith); break; case ChangeType.Cleared: Console.WriteLine("Address: clear address "); break; default: break; } }
总结
本文简要介绍了Framework Design Guidelines 中的 Do not expose generic lists 这条设计规范, 一般我们在设计通用的系统框架的API的时候遵循这一规范使得系统具有更好的扩展性。