最近在阅读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的时候遵循这一规范使得系统具有更好的扩展性。

不要对外公开泛型List成员的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  2. 来了!公开揭密团队成员开发鸿蒙 OpenHarmony 的完整过程(收获官方7000奖金和开发板等,1w字用心总结)

    背景 随着 OpenHarmony 组件开发大赛结果公布,我们的团队成员被告知获得了二等奖,在开心之余也想将我们这段时间宝贵的开发经验写下来与大家分享,当我们看到参赛通知的时候已经是 9 月中旬的时候 ...

  3. Web Service 一些对外公开的网络服务接口

    商业和贸易: 1.股票行情数据 WEB 服务(支持香港.深圳.上海基金.债券和股票:支持多股票同时查询) Endpoint: http://webservice.webxml.com.cn/WebSe ...

  4. Java学习笔记(七):内部类、静态类和泛型

    内部类 在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类.广泛意义上的内部类一般来说包括这四种:成员内部类.局部内部类.匿名内部类和静态内部类.下面就先来了解一下这四种 ...

  5. 十一、C# 泛型

    为了促进代码重用,尤其是算法的重用,C#支持一个名为泛型的特性. 泛型与模块类相似. 泛型使算法和模式只需要实现一交.而不必为每个类型都实现一次.在实例化的时候,传入相应的数据类型便可. 注:可空值类 ...

  6. 【转】C++类中对同类对象private成员访问

    私有成员变量的概念,在脑海中的现象是,以private关键字声明,是类的实现部分,不对外公开,不能在对象外部访问对象的私有成员变量. 然而,在实现拷贝构造函数和赋值符函数时,在函数里利用对象直接访问了 ...

  7. [C++参考]私有成员变量的理解

    私有成员变量的概念,在脑海中的现象是,以private关键字声明,是类的实现部分,不对外公开,不能在对象外部访问对象的私有成员变量. 然而,在实现拷贝构造函数和赋值符函数时,在函数里利用对象直接访问了 ...

  8. public类型中internal成员

    今天遇到一问题,找到下面的两篇文章,研究比较深入,特转了一下, 最近除了搞ASP.NET MVC之外,我也在思考一些编程实践方面的问题.昨天在回家路上,我忽然对一个问题产生了较为清晰的认识.或者说,原 ...

  9. 类中的internal成员可能是一种坏味道

    前言 最近除了搞ASP.NET MVC之外,我也在思考一些编程实践方面的问题.昨天在回家路上,我忽然对一个问题产生了较为清晰的认识.或者说,原先只是有一丝细微的感觉,而现在将它和一些其他的方面进行了联 ...

随机推荐

  1. 开发常用技巧之css字体编码

    简介: 当我们写css时,通常需要设置字体名称,我们可以直接写中文,这样没错,但是文件编码为GB2312.UTF-8等不匹配将会出现乱码.因此将中文字体名称转为unicode编码来避免出现这些错误. ...

  2. 转:MYSQL连接字符串参数解析(解释)

    被迫转到MySQL数据库,发现读取数据库时,tinyint类型的值都被转化为boolean了,这样大于1的值都丢失,变成true了.查阅资料MySQL中无Boolean类型,都是存储为tinyint了 ...

  3. apache 服务器配制

    简介:Apache 是世界上使用量第一的Web服务器软件,可用于linux,unix,windows等平台,尤其是对Linux支持完美 Apache的优点: 功能强大,自带很多功能模块,可根据需求编译 ...

  4. 2-Sat问题

    二分+2-Sat 判断是否可行 输出字典序最小的解 输出字典序可行解 其实这些都是小问题,最重要的是建图,请看论文. 特殊的建边方式,如果a b是一对,a必须选,那么就是b->a建边. HDU ...

  5. web.xml配置错误导致applicationContext.xml配置重复加载

    web.xml相关配置 <context-param><param-name>log4jRefreshInterval</param-name><param- ...

  6. .net工具

    程序名称 作者 说明 文件结构与元数据查看看 AssemblyView1.0   可以查看.net平台下exe,dll源代码的类结构,比如变量,属性,函数,事件的定义. Anakrino   源代码开 ...

  7. C++ activemq CMS 学习笔记.

    很早前就仓促的接触过activemq,但当时太赶时间.后面发现activemq 需要了解的东西实在是太多了. 关于activemq 一直想起一遍文章.但也一直缺少自己的见解.或许是网上这些文章太多了. ...

  8. Nginx反向代理和负载均衡

    一.Nginx反向代理设置 从80端口转向其他端口反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的 ...

  9. Ejabberd导入到eclipse

    ejabberd 在eclipse(erlide)中的配置.调试.运行   最近在折腾ejabberd,将ejabberd项目配置到eclipse中进行编译.调试等,现在将过程记下来,希望能帮助到需要 ...

  10. JQuery学习笔记

    注:以下资料来源W3School.COM.CN jQuery 语法 jQuery 语法是为 HTML 元素的选取编制的,可以对元素执行某些操作. 基础语法是:$(selector).action() ...