概述:在真正的对象化开发项目中,我们通常会将常用的业务实体抽象为特定的类,如Employee、Customer、Contact等,而多数的类之间会存在着相应的关联或依存关系,如Employee和Customer通过Contact而产生关联、Contact是依赖于Employee和Customer而存在的。在实际的对象应用模块中,可能会有这样的需求:获得一组客户对象(即Customers集合类的实例,如customers),指向其中一个Customer对象(如customers[i]),通过访问这个Customer对象的属性Name(customers[i].Name)和Contacts(如customers[i].Contacts)来查询客户的姓名和与该客户的联络记录,甚至遍历Contacts对象,查找该客户的某次联络摘要(即customers.[i].contacts[x].Summary)。为满足以上集合类的需求,对照.NET Framework 的平台实现,不难发现.NET在Collections命名空间下提供了一系列实现集合功能的类,并且根据适用环境的不同为开发者提供灵活多样的选择性:如通过索引访问使用广泛的ArrayList 和 StringCollection;通常在检索后被释放的先进先出的Queue和后进先出Stack;通过元素键对其元素进行访问Hashtable、SortedList、ListDictionary 和 StringDictionary;通过索引或通过元素键对其元素进行访问的NameObjectCollectionBase 和 NameValueCollection;以及具有集合类的特性而被实现在System.Array下的Array类等。本文将通过实现具有代表性的 “集合类”的两种典型途径,分析对比不同实现方式的差异性与适用环境,让大家了解和掌握相关的一些技术,希望为大家的学习和开发工作起到抛砖引玉的作用(注:作者的调试运行环境为.NET Framework SDK 1.1)。
1.采用从CollectionBase抽象基类继承的方式实现Customers集合类:
首先需要创建为集合提供元素的简单类Customer:

/// <summary>
/// 描述一个客户基本信息的类
/// </summary>
public class Customer
{
/// <summary>
/// 客户姓名
/// </summary>
public string Name;

/// <summary>
/// 描述所有客户联络信息的集合类
/// </summary>
//public Contacts Contacts=new Contacts();

/// <summary>
/// 不带参数的Customer类构造函数
/// </summary>
public Customer()
{
System.Console.WriteLine("Initialize instance without parameter");
}

/// <summary>
/// 带参数的Customer类构造函数
/// </summary>
public Customer(string name)
{
Name=name;
System.Console.WriteLine("Initialize instance with parameter");
}
}

以上就是Customer类的简单框架,实用的Customer类可能拥有更多的字段、属性、方法和事件等。值得注意的是在Customer类中还以公共字段形式实现了对Contacts集合类的内联,最终可形成Customer.Contacts[i]的接口形式,但这并不是最理想的集合类关联方式,暂时将它注释,稍后将详加分析,这个类的代码重在说明一个简单类(相对于集合类的概念范畴)的框架;另外,该类还对类构造函数进行了重载,为声明该类的实例时带name参数或不带参数提供选择性。
接下来看我们的第一种集合类实现,基于从CollectionBase类派生而实现的Customers类:
/// <summary>
/// Customers 是Customer的集合类实现,继承自CollectionBase
/// </summary>
public class Customers: System.Collections.CollectionBase
{
public Customers()
{

}
/// <summary>
/// 自己实现的Add方法
/// </summary>
/// <param name="customer"></param>
public void Add(Customer customer)
{
List.Add(customer);
}
/// <summary>
/// 自己实现的Remove方法
/// </summary>
/// <param name="index"></param>
public void Remove(int index)
{
if (index > Count - 1 || index < 0)
{
System.Console.WriteLine("Index not valid!");
}
else
{
List.RemoveAt(index);
}
}
}

以Customers集合类为例,结合集合辅助技术,希望大家能了解掌握以下知识:
从CollectionBase继承实现集合类
Customers类采用从CollectionBase继承的方式,不再需要在类内声明一个作为Customer集合容器的List对象,因为CollectionBase类已经内置了一个List对象,并已经实现了Count、Clear、RemoveAt等等IList的重要接口(具体请参照MSDN中的CollectionBase 成员),只需要用户显示实现Add、Remove、IndexOf、Insert等等接口,代码中仅简单实现了Add方法和Remove方法的整参数版本作为示例。这种集合类的实现具有简单高效的特点,CollectionBase已经实现了较为完善的功能,实施者只要在其基础上扩展自己所需的功能即可。

索引器的简单实现
我们惯于操作数组的形式通常为array[i],集合类可以看作是“对象的数组”,在C#中,帮助集合类实现数组式索引功能的就是索引器:
public Customer this[int index]
{
get
{
return (Customer) List[index];
}
}
将以上代码加入到Customers类后,就实现了以整形index为参数,以List[index]强制类型转换后的Customer类型返回值的Customers类只读索引器,使用者以Customers[i].Name的方式,就可以访问Customers集合中第i个Customer对象的姓名字段,是不是很神奇呢?文中的索引器代码并未考虑下标越界的问题,越界的处理方式应参照与之类似的Remove方法。作者在此只实现了索引器的get访问,没有实现set访问的原因将在下文中讨论。

Item的两种实现方式
用过VB的朋友们一定都很熟悉Customers.Itme(i).Name的形式,它实现了与索引器相同的作用,即通过一个索引值来访问集合体中的特定对象,但Item在C#当中应该以怎样的形式实现呢?首先想到的实现途径应该是属性,但你很快就会发现C#的属性是不支持参数的,所以无法把索引值作为参数传入,折中的办法就是以方法来实现:
public Customer Item (int Index)
{
return (Customer) List[Index];
}
这个Item方法已经可以工作了,但为什么说是折中的办法呢,因为对Item的访问将是采用Customers.Item(i).Name的语法形式,与C#‘[]’作数组下标的风格不统一,显的有些突兀,但如果希望在语法上做到统一,哪怕是性能受一些影响也无所谓的话有没有解决之道呢?请看以下代码:
public Customers Item
{
get
{
return this;
}
}
这是以属性形式实现的Item接口,但是由于C#的属性不支持参数,所以我们返回Customers对象本身,也就是在调用Customers对象Item属性时会引发对Customers索引器的调用,性能有所下降,但是的确实现了Customers.Item[i].Name的语法风格统一。对比这两种Item的实现,不难得出结论:以不带参数的属性形式实现的Item依赖于类的索引器,如果该类没有实现索引器,该属性将无法使用;并且由于对Item的访问重定向到索引器性能也会下降;唯一的理由是:统一的C#索引下标访问风格;采用方法实现的裨益正好与之相反,除了语法风格较为别扭外,不存在依赖索引器、性能下降的问题。鱼与熊掌难以兼得,如何取舍应依据开发的实际需求决定。
中间语言的编译缺省与Attribute的应用
如果你既实现了标准的索引器,又想提供名为“Item”的接口,编译时就会出现错误“类‘WindowsApplication1.Customers’已经包含了“Item”的定义”,但除了建立索引器外,你什么也没有做,问题到底出在哪里?我们不得不从.NET中间语言IL来寻找答案了,在.NET命令行环境或Visual Studio .NET 命令提示环境下,输入ILDASM,运行.NET Framework MSIL 反汇编工具,通过主菜单中的‘打开’加载只有索引器没有Item接口实现的可以编译通过的.NET PE执行文件,通过直观的树状结构图找到Customers类,你将意外地发现C#的索引器被解释成了一个名为Item的属性,以下是IL反编译后的被定义为Item属性的索引器代码:
.property instance class WindowsApplication1.Customer
Item(int32)
{
.get instance class WindowsApplication1.Customer WindowsApplication1.Customers::get_Item(int32)
} // end of property Customers::Item
问题总算水落石出,就是C#编译器‘自作聪明’地把索引器解释成了一个名为Item的属性,与我们期望实现的Item接口正好重名,所以出现上述的编译错误也就在所难免。那么,我们有没有方法告知编译器,不要将索引器命名为缺省Item呢?答案是肯定的。
解决方法就是在索引器实现之前声明特性:
[System.Runtime.CompilerServices.IndexerName("item")]
定义这个IndexerName特性将告知CSharp编译器将索引器编译成item而不是默认的Item ,修改之后的索引器IL反汇编代码为:
.property instance class WindowsApplication1.Customer
item(int32)
{
.get instance class WindowsApplication1.Customer WindowsApplication1.Customers::get_item(int32)
} // end of property Customers::item
当然你可以将索引器的生成属性名定义成其它名称而不仅限于item,只要不是IL语言的保留关键字就可以。经过了给索引器命名,你就可以自由地加入名为“Item”的接口实现了。

以下为Customer类和Customers类的调试代码,在作者的Customers类中,为说明问题,同时建立了以item为特性名的索引器、一个Items方法和一个Item属性来实现对集合元素的三种不同访问方式,实际的项目开发中,一个类的索引功能不需要重复实现多次,可能只实现索引器或一个索引器加上一种形式的Item就足够了:
public class CallTest
{
public static void Main()
{
Customers custs=new Customers();
System.Console.WriteLine(custs.Count.ToString());//Count属性测试

Customer aCust=new Customer();//将调用不带参数的构造函数
aCust.Name ="Peter";
custs.Add(aCust);//Add方法测试

System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Item[0].Name);//调用Item属性得到
custs.Items(0).Name+="Hu";//调用Items方法得到
System.Console.WriteLine(custs[0].Name);//调用索引器得到

custs.Add(new Customer("Linnet"));//将调用带name参数的构造函数
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs.Items(1).Name);//调用Items方法得到
custs.Item[1].Name+="Li";//调用Items方法得到
System.Console.WriteLine(custs[1].Name);//调用索引器得到

custs.Remove(0);//Remove方法测试
System.Console.WriteLine(custs.Count.ToString());
System.Console.WriteLine(custs[0].Name);//Remove有效性验证
custs[0].Name="Test passed" ;//调用索引器得到
System.Console.WriteLine(custs.Item[0].Name);
custs.Clear();
System.Console.WriteLine(custs.Count.ToString());//Clear有效性验证

}
}
输出结果为:
0
Initialize instance without parameter
1
Peter
PeterHu
Initialize instance with parameter
2
Linnet
LinnetLi
1
LinnetLi
Test passed
0

2.采用内建ArrayList对象的方式实现集合类:
或许有经验的程序员们早已经想到,可以在一个类中内建一个数组对象,并在该类中通过封装对该对象的访问,一样能够实现集合类。以下是采用这种思路的Contact元素类和Contacts集合类的实现框架:

public class Contact
{
protected string summary;

/// <summary>
/// 客户联系说明
/// </summary>
public string Summary
{
get
{
System.Console.WriteLine("getter access");
return summary;//do something, as get data from data source
}
set
{
System.Console.WriteLine("setter access");
summary=value;// do something , as check validity or Storage
}
}

public Contact()
{

}
}

public class Contacts
{
protected ArrayList List;

public void Add(Contact contact)
{
List.Add(contact);
}

public void Remove(int index)
{
if (index > List.Count - 1 || index < 0)
{
System.Console.WriteLine("Index not valid!");
}
else
{
List.RemoveAt(index);
}
}

public int Count
{
get
{
return List.Count;
}
}

public Contact this[int index]
{
get
{
System.Console.WriteLine("indexer getter access");
return (Contact) List[index];
}
set
{
List[index]=value;
System.Console.WriteLine("indexer setter access ");
}

}

public Contacts()
{
List=new ArrayList();
}
}
通过这两个类的实现,我们可以总结以下要点:
采用ArrayList的原因
在Contacts实现内置集合对象时,使用了ArrayList类,而没有使用大家较为熟悉的Array类,主要的原因有:在现有的.NET v1.1环境中,Array虽然已经暴露了IList.Add、IList.Insert、IList.Remove、IList.RemoveAt等典型的集合类接口,而实际上实现这些接口总是会引发 NotSupportedException异常,Microsoft是否在未来版本中实现不得而知,但目前版本的.NET显然还不支持动态数组,在MS推荐的更改Array大小的办法是,将旧数组通过拷贝复制到期望尺寸的新数组后,删除旧数组,这显示是费时费力地在绕弯路,无法满足集合类随时添加删除元素的需求;ArrayList已经实现了Add、Clear、Count、IndexOf、Insert、Remove、RemoveAt等集合类的关键接口,并且有支持只读集合的能力,在上边的Contacts类中,只通过极少的封装代码,就轻松地实现了集合类。另一个问题是我们为什么不采用与Customers类似的从System.Collections.ArrayList继承的方式实现集合类呢?主要是由于将ArrayList对象直接暴露于类的使用者,将导致非法的赋值,如用户调用arraylist.Add方法,无论输入的参数类型是否为Contact,方法都将被成功执行,类无法控制和检查输入对象的类型与期望的一致,有悖该类只接纳Contact类型对象的初衷,也留下了极大的安全隐患;并且在Contact对象获取时,如不经过强制类型转换,Contacts元素也无法直接以Contact类型形式来使用。
集合类中的Set
在集合类的实现过程中,无论是使用索引器还是与索引器相同功能的“Item”属性,无可避免地会考虑是只实现getter形成只读索引器,还是同时实现getter和setter形成完整的索引器访问。在上文的示例类Customers中就没有实现索引器的setter,形成了只读索引器,但在Customer类和Customers类的调试代码,作者使用了容易令人迷惑的“custs[0].Name="Test passed"”的访问形式,事实上,以上这句并不会进入到Customers索引器的setter而是会先执行Customers索引器的getter得到一个Customer对象,然后设置这个Customer的Name字段(如果Name元素为属性的话,将访问Customer类Name属性的setter)。那么在什么情况下索引器的setter才会被用到呢?其实只有需要在运行时动态地覆盖整个元素类时,集合类的setter才变得有意义,如“custs [i]=new Customer ()”把一个全新的Customer对象赋值给custs集合类的已经存在的一个元素,这样的访问形式将导致Customers的setter被访问,即元素对象本身进行了重新分配,而不仅仅是修改现有对象的一些属性。也就是说,由于Customers类没有实现索引器的setter 所以Customers类对外不提供“覆盖”客户集合中既有客户的方法。与此形成鲜明对照的是Contacts类的索引器既提供对集合元素的getter,又提供对集合元素的setter,也就是说Contacts类允许使用者动态地更新Contact元素。通过对Contacts和Contact两个类运行以下测试可以很明确说明这个问题:
public class CallTest
{
public static void Main()
{
Contacts cons=new Contacts();
cons.Add(new Contact());
cons[0]=new Contact();//trigger indexer setter
cons[0].Summary="mail contact about ticket";
System.Console.WriteLine(cons[0].Summary);
}
}
理所当然的输出结果为:
indexer setter access
indexer getter access
setter access
indexer getter access
getter access
mail contact about ticket
明确认识到了索引器setter的作用后,在类的实现中就应当综合实际业务特点、存取权限控制和安全性决定是否为索引器建立setter机制。
属性-强大灵活的字段 合二为一的方法
在最初实现Customer类时,我们使用了一个公共字段Name,用作存取客户的姓名信息,虽然可以正常的工作,但我们却缺乏对Name字段的控制能力,无论类的使用者是否使用了合法有效的字段赋值,字段的值都将被修改;并且没有很好的机制,在值改变时进行实时的同步处理(如数据存储,通知相关元素等);另外,字段的初始化也只能放在类的构造函数中完成,即使在整个对象生命周期内Name字段都从未被访问过。对比我们在Contact类中实现的Summary属性,不难发现,属性所具有的优点:属性可以在get时再进行初始化,如果属性涉及网络、数据库、内存和线程等资源占用的方式,推迟初始化的时间,将起到一定的优化作用;经过属性的封装,真正的客户联系说明summary被很好地保护了起来,在set时,可以经过有效性验证再进行赋值操作;并且在getter和setter前后,可以进行数据存取等相关操作,这一点用字段是不可能实现的。所以我们可以得出结论,在字段不能满足需求的环境中,属性是更加强大灵活的替代方式。
另外,属性整合了“get”和“set”两个“方法”,而采用统一自然的接口名称,较之JAVA语言的object.getAnything和object.setAnything语法风格更加亲和(事实上,C#中的属性只不过是对方法的再次包装,具有getter和setter的Anything属性在.NET IL中,依然会被分解成一个由Anything属性调用的get_Anything和set_Anything两个方法)。
集合类内联的方式
在文章最初的Customer类中使用了公共字段public Contacts Contacts=new Contacts()实现了customer. Contacts[]形式的集合类内联接口,这是一种最为简单但缺乏安全性保护的集合类集成方式,正如以上所述属性的一些优点,采用属性形式暴露一个公共的集合类接口,在实际存取访问时,再对受封状保护的集合类进行操作才是更为妥当完善的解决方案,如可以把Customer类内联的集合Contacts的接口声明改为:
protected Contacts cons; //用于类内封装的真正Contacts对象
public Contacts Contacts//暴露在类外部的Contacts属性
{
get
{
if (cons == null) cons=new Contacts();
return cons;
}
set
{
cons=value;
}
}
最终,customers[i].Contacts[x].Summary的形式就被成功地实现了。
实例化的最佳时机
.NET的类型系统是完全对象化的,所有的类型都是从System.Object派生而来,根据类型的各自特点,可以分为值类型和引用类型两大阵营。值类型包括结构(简单的数值型和布尔型也包括在内)和枚举,引用类型则包括了类、数组、委托、接口、指针等,对象化的一个特点是直到对象实例化时才为对象分配系统资源,也就是说灵活适时地实例化对象,对系统资源的优化分配将产生积极意义。在一些文章中所建议的“Lazy initialization”倡导在必要时才进行对象的实例化,本着这样的原则,从类的外部来看,类可以在即将被使用时再进行初始化;在类的内部,如属性之类的元素,也可以不在构造函数中初始化,而直到属性的getter被真正访问时才进行,如果属性一直没有被读取过,就不必要无意义地占用网络、数据库、内存和线程等资源了。但是也并不是初始化越晚越好,因为初始化是需要时间的,在使用前才进行初始化可能导致类的响应速度过慢,无法适应使用者的实时需求。所以在资源占用和初始化耗时之间寻求一个平衡点,才是实例化的最佳时机。

总结
本文围绕实现集合类的两种途径-从CollectionBase继承实现和内建ArrayList对象实现,为大家展示了部分集合、索引器、属性、特性的应用以及.NET环境中的类构造函数、对象优化、类关联等其它相关知识。通过本文浅显的示例和阐述,希望可以启发读者的灵感,推出更加精辟合理的基础理论和应用模型。

通过C#实现集合类纵览.NET Collections及相关技术相关推荐

  1. 【原创】IP摄像头技术纵览(七)---P2P技术—UDP打洞实现内网NAT穿透

    [原创]IP摄像头技术纵览(七)-P2P技术-UDP打洞实现内网NAT穿透 本文属于<IP摄像头技术纵览>系列文章之一: Author: chad Mail: linczone@163.c ...

  2. [知识库分享系列] 二、.NET(ASP.NET)

    最近时间又有了新的想法,当我用新的眼光在整理一些很老的知识库时,发现很多东西都已经过时,或者是很基础很零碎的知识点.如果分享出去大家不看倒好,更担心的是会误人子弟,但为了保证此系列的完整,还是选择分享 ...

  3. [NHibernate]集合类(Collections)映射

    系列文章 [Nhibernate]体系结构 [NHibernate]ISessionFactory配置 [NHibernate]持久化类(Persistent Classes) [NHibernate ...

  4. .Net Framework System.Collections 集合类

    本文内容 集合类 性能 最近复习了一下集合,C# 关于集合的类蛮多,但我除了 List 那几个经常用之外,其他的用得还真不多(只在小范围使用),但其实,每个集合类都各有自己适用的场景,功能也很强大.尤 ...

  5. Java 集合类(一)

    今天我们先讲一下Collection: Collection和Collections的区别: java.util.Collection是一种java集合接口,它提供了对集合对象的基本操作通用接口方法, ...

  6. C#编程利器之五:集合对象(Collections)

    C#里面的集合对象,是一个很重要的知识点.可以说没有人编程不使用集合.这里我不打算过多的去介绍理论相关的知识,下面和大家分享和学习一下在平时开发中的常用集合对象,以及他们之间的关系. 记得教科书上有这 ...

  7. 40个Java Collections面试问答

    Java Collections Framework是Java编程语言的基本方面. 这是Java面试问题的重要主题之一. 在这里,我列出了Java集合框架的一些重要问题和解答. 什么是Java Col ...

  8. Java Collections list()方法与示例

    集合类list()方法 (Collections Class list() method) list() method is available in java.util package. list( ...

  9. c#中常用集合类和集合接口之接口系列【转】

    常用集合接口系列:http://www.cnblogs.com/fengxiaojiu/p/7997704.html 常用集合类系列:http://www.cnblogs.com/fengxiaoji ...

最新文章

  1. 自定义报错返回_Keras编写自定义层--以GroupNormalization为例
  2. 《spring 2.0技术手册》入门不错!
  3. ProxyFactoryBean(代码)
  4. 关于c语言中合法的数值常量
  5. matlab 增加图像对比度_计算机视觉学习笔记6 图像直方图与直方图均衡化
  6. JSON指针:JSON-P 1.1概述系列
  7. EasyCode.Net代码生成器使用心得
  8. android 使用动态的svg资源,在Android中使用SVG作为资源 – victor
  9. Hadoop入门进阶步步高(四)-测试Hadoop
  10. sizeof(class)分析
  11. 如何能把 fastdfs-client-java的jar包安装到本地的仓库中
  12. python numpy模块玩转矩阵与科学计算
  13. 用剪映将无字幕的英文视频翻译成中文字幕(附教程+软件)
  14. 迷你迅雷(官方版)不含广告,不用会员,多线程急速
  15. win10运行程序提示“为了对电脑进行保护,已经阻止此应用” 解决方法
  16. MIT线性代数笔记七 列空间和零空间求解 Ax=0:主变量和特解
  17. 杜利特尔分解法Doolittle(LU分解法)_解线性方程组的直接解法
  18. torch 显存管理
  19. (离散)设函数 f:A→B,g:B→C,证明:若g °f是满射,则g是满射.
  20. 扬帆际海:如何成为一个合格的跨境电商运营?

热门文章

  1. linux分区通俗讲解,linux硬盘分区基础及设备号的解释
  2. tableau两个不同的图合并_Tableau可视化分析-业务常用图形绘制1
  3. populate_dir
  4. 【面向对象编程】(1) 类实例化的基本方法
  5. windows下opencv安装及配置(vs2010环境)
  6. java gif 帧_在Java中修复动画gif的帧速率
  7. linux怎么开启samba服务,LINUX开启SAMBA服务
  8. define的多行定义
  9. Unity空间射击游戏开发教程
  10. 一文入门 Zookeeper