最近在閱讀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>();<span style="color: blue;">public </span><span style="color: #2b91af;">List</span><<span style="color: #2b91af;">Address</span>> Addresses { <span style="color: blue;">get </span>{ <span style="color: blue;">return </span>addresses; } }}
假設這個是我們對外提供的一個API的一個類。那么就違反了之前討論的這一原則,不應該把成員以List<T>的形式對外暴露。可以初步修改為:
public class Person { private Collection<Address> addresses = new Collection<Address>();<span style="color: blue;">public </span><span style="color: #2b91af;">Collection</span><<span style="color: #2b91af;">Address</span>> Addresses { <span style="color: blue;">get </span>{ <span style="color: blue;">return </span>addresses; } }}
現在,假設有一個需求,當用戶的地址發生改變了,需要通知另外一個系統,給用戶發提醒,或者其他操作。現在就需要在集合發生改變的時候對外提供事件提醒了,比如當用戶修改地址后,需要發郵件通知用戶是否需要修改信用卡電子賬單寄送地址。
現在我們需要自定義我們的AddressCollection如:
public class AddressCollection : Collection<Address> { public event EventHandler<AddressChangedEventArgs> AddressChanged;<span style="color: blue;">protected override void </span>InsertItem(<span style="color: blue;">int </span>index, <span style="color: #2b91af;">Address </span>item) { <span style="color: blue;">base</span>.InsertItem(index, item); <span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>> temp = AddressChanged; <span style="color: blue;">if </span>(temp != <span style="color: blue;">null</span>) { temp(<span style="color: blue;">this</span>, <span style="color: blue;">new </span><span style="color: #2b91af;">AddressChangedEventArgs</span>(<span style="color: #2b91af;">ChangeType</span>.Added, item, <span style="color: blue;">null</span>)); } } <span style="color: blue;">protected override void </span>SetItem(<span style="color: blue;">int </span>index, <span style="color: #2b91af;">Address </span>item) { <span style="color: #2b91af;">Address </span>replaced = Items[index]; <span style="color: blue;">base</span>.SetItem(index, item); <span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>> temp = AddressChanged; <span style="color: blue;">if </span>(temp != <span style="color: blue;">null</span>) { temp(<span style="color: blue;">this</span>, <span style="color: blue;">new </span><span style="color: #2b91af;">AddressChangedEventArgs</span>(<span style="color: #2b91af;">ChangeType</span>.Replaced, replaced, item)); } } <span style="color: blue;">protected override void </span>RemoveItem(<span style="color: blue;">int </span>index) { <span style="color: #2b91af;">Address </span>removedItem = Items[index]; <span style="color: blue;">base</span>.RemoveItem(index); <span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>> temp = AddressChanged; <span style="color: blue;">if </span>(temp != <span style="color: blue;">null</span>) { temp(<span style="color: blue;">this</span>, <span style="color: blue;">new </span><span style="color: #2b91af;">AddressChangedEventArgs</span>(<span style="color: #2b91af;">ChangeType</span>.Removed, removedItem, <span style="color: blue;">null</span>)); } } <span style="color: blue;">protected override void </span>ClearItems() { <span style="color: blue;">base</span>.ClearItems(); <span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>> temp = AddressChanged; <span style="color: blue;">if </span>(temp != <span style="color: blue;">null</span>) { temp(<span style="color: blue;">this</span>, <span style="color: blue;">new </span><span style="color: #2b91af;">AddressChangedEventArgs</span>(<span style="color: #2b91af;">ChangeType</span>.Cleared, <span style="color: blue;">null</span>, <span style="color: blue;">null</span>)); } }}
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;<span style="color: blue;">public </span>Person() { addresses = <span style="color: blue;">new </span><span style="color: #2b91af;">AddressCollection</span>(); addresses.AddressChanged += <span style="color: blue;">new </span><span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>>(addresses_Changed); } <span style="color: blue;">public </span><span style="color: #2b91af;">Collection</span><<span style="color: #2b91af;">Address</span>> Addresses { <span style="color: blue;">get </span>{ <span style="color: blue;">return </span>addresses; } } <span style="color: blue;">void </span>addresses_Changed(<span style="color: blue;">object </span>sender, <span style="color: #2b91af;">AddressChangedEventArgs </span>e) { <span style="color: #2b91af;">EventHandler</span><<span style="color: #2b91af;">AddressChangedEventArgs</span>> temp = AddressChanged; <span style="color: blue;">if </span>(temp != <span style="color: blue;">null</span>) { temp(<span style="color: blue;">this</span>, e); } } <span style="color: blue;">public void </span>AddAddress(<span style="color: #2b91af;">Address </span>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的時候遵循這一規范使得系統具有更好的擴展性。
轉載來源:https://www.cnblogs.com/yangecnu/archive/2014/05/23/Do-not-expose-generic-lists.html

