集合是一组组合在一起的类似的类型化对象。对于集合中的每一个对象一般称为项或者元素。集合类定义在 System.Collections 或 System.Collections.Specialized命名空间中。System.Collections命名空间包含接口和类,这些接口和类定义了各种对象(如列表、队列、位数组、哈希表和字典)的集合以及这些集合应该具备的功能。System.Collections.Specialized命名空间中还定义了一些专用的集合,比如集合ListDictionary,它是使用单链接列表实现的键/值对的集合,一般使用于集合中的项少于10个或10个以下的情况,它比Hashtable小,且更快。如果用于大量元素时性能很重要,就不应使用这种实现方法。所以专用集合的使用面比较狭窄,只能适用于某种特定的情况。
10.2.1System.Collections命名空间的接口
图10-2显示了System.Collections命名空间的接口。
图10-2
下面描述一下各个接口要求实现的功能以及成员。在本空间中实现的接口中IEnumerable、IEnumerator、IComparer、IEqualityComparer都没有从其他接口派生,这里暂时称其为一级接口。而ICollection从IEnumerable派生,在ICollection接口又派生出接口IDictionary与IList。接口IDictionaryEnumerator从IEnumerator派生,这些接口的功能随着派生而被带入下一级接口。
l 接口IEnumerator的定义如图10-3所示:
图10-3
在System.Collections命名空间IEnumerator与IDictionaryEnumerator接口的关系如图10-4所示:
图10-4
从图10-4可以看出,接口IEnumerator中有一个Current属性,MoveNext()与Reset()两个方法。属性Current能够获取集合中的当前元素,方法将枚举数推进到集合的下一个元素。如果枚举数成功地推进到下一个元素,则为 true;如果枚举数越过集合的结尾,则为 false。而Reset()将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。因此接口IEnumerator的功能能够支持集合简单的迭代。由于IDictionaryEnumerator从IEnumerator派生而出。所以具有IEnumerator的迭代功能,只是增加了针对键/值这种集合中一些特定的操作,比如得到键的属性Key与得到值的属性Value。
l 接口IEnumerable的定义如图10-5所示:
图10-5
在System.Collections命名空间与其他接口的关系如图10-6所示:
图10-6
在接口IEnumerable中只有一个方法GetEnumerator(),该方法返回一个循环访问集合的枚举数(属于IEnumerator接口)。由于“枚举数”这个名称,使很多程序员认为它只是一个数字,其实枚举数是一个对象,这个对象实现了接口IEnumerator的所有功能,因此实现了IEnumerable的所有对象,都支持简单迭代的操作。
l 接口ICollection从IEnumerable派生而来,所以具有简单迭代功能,而且还添加了得到集合中元素的个数的属性Count。
l IDictionary与IList从ICollection派生而出,分别添加了适合键/值集合与列表集合相对应的一些方法与属性。
l 接口IComparer的定义如图10-6所示:
图10-6
在接口IComparer中只有一个方法Compare。主要是提供一种比较两个对象的方法。图10-7是方法Compare的表示:
图10-7
该方法的两个参数是Object类型,返回一个整型的结果,如果x>y返回正值,如果x<y返回负数,如果相等返回0。为什么必须实现这样的接口呢?现在假设在一个整型数组IntArray,在数组中存放有若干整型的数据。如果比较数组中的两个数据,应该没有什么问题,因为整型数据支持直接的比较。可是,现在有一个对象的数组,每个对象中都有若干个属性,那么现在我们比较数组中的两个元素,程序到底该用那一个属性来进行比较呢?程序当然不知道,于是,程序员必须实现接口IComparer中的Compare方法,用来告诉程序到底该如何比较。
例子ComparePoint用来比较一个由类Point的若干对象组成的数组,用来实现数组的逆序排列,现在规定,比较两个点,如果点p1的横坐标大于点p2的横坐标,认为点p1比点p2大,如果两个点的横坐标相同,接着比较点的纵坐标。有了这个规则就可以比较两个点了。
Point类的代码:
namespace ComparePoint
{
class Point
{
public int X { get; set; }
public int Y { get; set; }
}
}
用来提供比较规则的类MyCompare的代码:
using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace ComparePoint
{
public class MyCompare:IComparer
{
public int Compare(Object o1, Object o2)
{
Point pp1 = (Point)o1;
Point pp2 = (Point)o2;
if (pp1.X != pp2.X)
return -(pp1.X - pp2.X);
else
return -(pp1.Y-pp2.Y);
}
}
}
因为要实现逆序排列,所以返回点坐标差值的相反值。注意代码中加边框的命名空间,如果你在VS2008中实现这个代码,必须去掉命名空间System.Collections.Generic;而使用System.Collections;实现数组排序的代码:
namespace ComparePoint
{
class Program
{
static void Main(string[] args)
{
//生成一个实现了IComparer接口的类
MyCompare mycompare = new MyCompare();
Point p1 = new Point { X = 2, Y = 3 };
Point p2 = new Point { X = 2, Y = 5 };
Point p3 = new Point { X = 3, Y = 5 };
Point p4 = new Point { X = 3, Y = 1 };
Point[] p = new Point[] { p1, p2, p3, p4 };
foreach (Point temp in p)
{
Console.WriteLine("点的坐标是({0},{1})", temp.X, temp.Y);
}
Console.WriteLine("<<<<<<<<<<<<<<<降序排列>>>>>>>>>>>>>>>>>>>>");
//使用Sort方法,实现数组排序的代码
Array.Sort(p, mycompare);
foreach (Point temp in p)
{
Console.WriteLine("点的坐标是({0},{1})", temp.X, temp.Y);
}
Console.ReadKey();
}
}
}
图10-8是程序的运行结果
图10-8
接口IEqualityComparer的定义如图10-9所示:
图10-9
在接口IEqualityComparer中,有Equals、GetHashCode两个方法。重点解释Equals方法,这个方法跟接口IComparer中的Compare方法有点类似,主要是用来提供判断两个对象相等的方法。该方法返回bool类型,两个参数都是Object类型。实现这个方法的时候必须注意,Equals 方法是自反的、对称的和可传递的。也就是说,如果此方法用于将某个对象与其自身比较,则它将返回 true;如果对 y 和 x 执行此方法返回 true,则对 x 和 y 这两个对象也返回 true;如果对 x 和 y 执行此方法返回 true,并且对 y 和 z 执行此方法也返回 true,则对 x 和 z 这两个对象也返回 true。
本小节中接口之间的类图保存在CollectionsStruct工程的类视图中。
10.2.2 System.Collections的类
图10-10显示了System.Collections命名空间的类。
图10-10
在System.Collections命名空间一共提供了12个类,除了已经过时的CaseInsensitiveHashCodeProvider类不进行讲解以外,其余的类,按照类的作用或者功能分批给大家介绍。在本小节中对于11个类主要分为3个小的部分。这三个部分分别是(一)比较类部分,(二)集合类部分,(三)强类型集合基类部分
10.2.2.1用于比较的类
这里所说的比较是一个广义的比较,即包含对对象判断相等的操作。用于比较的类在System.Collections命名空间有两个,分别是CaseInsensitiveComparer类与Comparer类。前者可以用于比较两个对象,比较时忽略字符串的大小写。后者也是可以用于比较两个对象,但是比较时对字符串的大小写敏感。
比较两个对象有两种实现的办法:
1) 该对象本身具有比较方法。
2) 该对象本身没有比较方法,这个时候可以使用专门用来进行比较的类来进行。CaseInsensitiveComparer类与Comparer类就是专门用来比较的类。它们把传入的对象进行比较,并返回比较的结果。
对于c#中的基本类型,提供了很多的比较运算符,可以直接进行比较,并得到比较的结果,可是对于类的对象,比较运算符可能不能使用。比如定义了一个三维点的类Point3D,它有三个字段,分别是x,y,z,如想比较两个三维点的大小或者是否相等,显示使用运算符“>”,“<”,“==”是不能得到正确的结果的。这个时候需要给对象提供比较的方法。例子CompaerPoint3D就是一个比较具体的实例。本例子中是按照分别比较点的x,y,z坐标的顺序来确定点的大小的。
类Point3D的代码是:
namespace CompaerPoint3D
{
class Point3D
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
/// <summary>
/// 比较两个三维点,分别按照x,y,z的顺序进行比较
/// </summary>
/// <param name="p">Point3D</param>
/// <returns></returns>
public int Compare(Point3D p)
{
if(X!=p.X)
return X - p.X;
else
if(Y!=p.Y)
return Y - p.Y;
else
return Z - p.Z;
}
/// <summary>
/// 显示点的坐标
/// </summary>
public void Show3D()
{
Console.Write("点{0},{1},{2}",X,Y,Z);
}
}
}
对点进行比较的代码是:
namespace CompaerPoint3D
{
class Program
{
static void Main(string[] args)
{
Point3D p1 = new Point3D { X = 4, Y = 5, Z = 6 };
Point3D p2 = new Point3D { X = 5, Y = 6, Z = 7 };
Point3D p3 = new Point3D { X = 5, Y = 7, Z = 6 };
Point3D p4 = new Point3D { X = 5, Y = 7, Z = 8 };
Point3D p5 = new Point3D { X = 5, Y = 7, Z = 8 };
MainCompare(p1, p2);
MainCompare(p2, p3);
MainCompare(p3, p2);
MainCompare(p3, p4);
MainCompare(p4, p3);
MainCompare(p4, p5);
Console.ReadKey();
}
/// <summary>
/// 输出比较的结果
/// </summary>
/// <param name="p1">Point3D</param>
/// <param name="p2">Point3D</param>
public static void MainCompare(Point3D p1,Point3D p2)
{
p1.Show3D();
p2.Show3D();
Console.WriteLine("比较的结果是{0}",p1.Compare(p2));
}
}
}
程序运行的结果
Object类提供了一个静态的成员方法Equals,用来判断两个对象是否相等,注意Equals的默认实现支持引用相等性(对于引用类型)和按位相等性(对于值类型)。所以对于对象来说,方法Equals只能判断两个对象是不是同一个引用,而不能提供真实的对象的比较功能,如果想判断两个对象的值是否相等,那么在Object类的派生类中必须重写Equals方法。
既然对象可以具有自己的比较方法来比较两个对象,那么为什么还要专门写一个功能是比较的类呢?好像多此一举。其实原因有多个,也许你得到了一个第三方的类,该类没有提供用于比较的方法,这个时候可以使用扩展方法扩展该类,或者从该类中再派生出一个子类,在子类中添加一个用于比较的方法。
可是在数组或者集合(后面讲解)中,有一个排序的方法Sort(),这个方法在内部需要调用比较两个对象的算法,进而实现排序。该算法中为了避免碰到本身没有比较方法的对象传入内部,所以要求传入到Sort()方法中的对象实现IComparer接口。或者直接给Sort()方法提供一个实现了IComparer接口的类的对象,算法将会直接调用传入的具有比较功能的对象来作用到传入的需要排序的对象上。例子ComparePoint就是给数组的Sort()方法传入了一个实现了IComparer接口的对象。
因为在例子ComparePoint中,使用了实现接口IComparer的类MyCompare来比较两个对象。所以Sort()方法才能对数组进行排序。如果不使用类MyCompare,而直接在类Point里面直接实现接口IComparer,Sort()是否也能对对象进行排序操作呢?例子SortPoints就是在类Point里直接实现了接口IComparer,不过因为是在类里面直接实现,所以实现的接口IComparer不是System.Collections.IComparable接口,而是System.IComparable,在该接口中有一个方法CompareTo需要实现。
例子SortPoints中类Point的代码如下:
using System;
using System.Collections;//注意这里包含的空间名
using System.Linq;
using System.Text;
namespace SortPoints
{
public class Point : IComparable
{
public int X { get; set; }
public int Y { get; set; }
//在类Point里面实现System.IComparable接口里面的CompareTo
public int CompareTo(Object p1)
{
Point tp1 = (Point)p1;
if (X != tp1.X)
return X - tp1.X;
else
return Y - tp1.Y;
}
}
}
直接调用Point类对象数组排序的代码:
namespace SortPoints
{
class Program
{
static void Main(string[] args)
{
Point p1 = new Point { X = 6, Y = 8 };
Point p2 = new Point { X = 6, Y = 5 };
Point p3 = new Point { X = 4, Y = 9 };
Point p4 = new Point { X = 4, Y = 7 };
Point p5 = new Point { X = 2, Y = 28 };
Point[] p = new Point[] { p1, p2, p3, p4, p5 };
//排序前数组的输出
foreach (Point tp in p)
{
Console.WriteLine("点({0},{1})",tp.X,tp.Y);
}
//对数组进行排序
Array.Sort(p);
//排序后数组的输出
Console.WriteLine("<<<<<<<<<<<<<<<<<<<排序后>>>>>>>>>>>>>>>>>>");
foreach (Point tp in p)
{
Console.WriteLine("点({0},{1})", tp.X, tp.Y);
}
Console.ReadKey();
}
}
}
程序运行的结果如图10-11所示
图10-11
带目前为止,基本把c#中提供比较操作的内容以及原理分析完毕,现在看一下类Comparer,类Comparer的定义如图10-12所示:
图10-12
该类从接口IComparable、ISerializable中派生,所以类中有一个Compare方法,用来比较两个对象,其中字符串比较是区分大小写的。并且该类被定义成sealed,说明该类不能被继承。该类的构造函数被声明为public Comparer(CultureInfo culture),其中参数CultureInfo说明该类能够根据特定区域性的信息来进行比较。下面就是一个根据不同区域进行字符串比较时的例子SamplesComparer。
namespace SamplesComparer
{
class Program
{
public static void Main()
{
//两个用来比较的字符串
String str1 = "llegar";
String str2 = "lugar";
Console.WriteLine("比较/"{0}/" 和 /"{1}/" ...", str1, str2);
//采用简体默认区域规则比较
Console.WriteLine("默认区域规则比较: {0}", Comparer.DefaultInvariant.Compare(str1, str2));
//采用西班牙语(西班牙)区域规则比较
Comparer myCompIntl = new Comparer(new CultureInfo("es-ES", false));
Console.WriteLine("采用西班牙语区域规则{0}", myCompIntl.Compare(str1, str2));
//采用西班牙语传统区域规则比较
Comparer myCompTrad = new Comparer(new CultureInfo("es-ES_tradnl", false));
Console.WriteLine("采用西班牙语传统区域规则: {0}", myCompTrad.Compare(str1, str2));
Console.ReadKey();
}
}
}
程序的运行结果
类CaseInsensitiveComparer与类Comparer的作用基本相同,只是在字符串比较时,不区分大小写。