实际上,本书前面的每个应用程序都有如下命名空间:
using System;
using System.Collections.Generic;
using System.Text;
System命名空间包含.NET应用程序使用的大多数基本类型。System.Text命名空间包含与字符串处理和编码相关的类型,但System.Collections.Generic命名空间包含什么类型?为什么要在默认情况下把它包含在控制台应用程序中?
这个命名空间包含用于处理集合的泛型类型,使用得非常频繁,用using语句配置它,使用起来就不必添加限定符了。
如本章前面所述,下面就介绍这些泛型类型,它们可以使工作更容易完成,可以毫不费力地创建强类型化的集合类。表12-2描述了本节要介绍的类型,本章后面还会详细阐述这些类型。
表 12-2
类 型 | 说 明 |
List<T> | T类型对象的集合 |
Dictionary<K, V> | V类型的项与K类型的键值相关的集合 |
后面还会介绍和这些类一起使用的各种接口和委托。
1. List<T>
使用这个泛型的集合类型会更快捷、更简单,而不是像上一章那样,从CollectionBase中派生一个类,实现需要的方法。它的另一个好处是正常情况下需要实现的许多方法(例如Add())已经自动实现了。
创建T类型对象的集合需要如下代码:
List<T> myCollection = new List<T>();
这就足够了。没有定义类、实现方法和进行其他操作。还可以把List<T>对象传送给构造函数,在集合中设置项的起始列表。
使用这个语法实例化的对象将支持表12-3中的方法和属性(其中,提供给List<T>泛型的类型是T)。
表 12-3
成 员 | 说 明 |
int Count | 该属性给出集合中项的个数 |
void Add(T item) | 把item添加到集合中 |
void AddRange(IEnumerable<T>) | 把多个项添加到集合中 |
IList<T> AsReadOnly() | 给集合返回一个只读接口 |
int Capacity | 获取或设置集合可以包含的项数 |
void Clear() | 删除集合中的所有项 |
bool Contains(T item) | 确定item是否包含在集合中 |
void CopyTo(T[] array, int index) | 把集合中的项复制到数组array中,从数组的索引index开始 |
IEnumerator<T> GetEnumerator() | 获取一个IEnumerator<T>实例,用于迭代集合。注意返回的接口强类型化为T,所以在foreach循环中不需要类型转换 |
int IndexOf(T item) | 获取item的索引,如果项没有包含在集合中,就返回-1 |
void Insert(int index, T item) | 把item插入到集合的指定索引上 |
bool Remove(T item) | 从集合中删除第一个item,并返回true;如果item不包含在集合中,就返回false |
void RemoveAt(int index) | 从集合中删除索引index处的项 |
List<T>还有一个Item属性,可以进行类似于数组的访问,如下所示:
T itemAtIndex2 = myCollectionOfT[2];
这个类还支持其他几个方法,但上述知识已足以开始使用该类了。
下面的示例介绍如何使用Collection<T>。
试试看:使用Collection<T>
(1) 在目录C:/BegVCSharp/Chapter12下创建一个新控制台应用程序项目Ch12Ex02。
(2) 在Solution Explorer窗口中右击项目名称,选择Add | Add Existing Item...选项。
(3) 在C:/BegVCSharp/Chapter11/Ch11Ex01/Ch11Ex01目录下选择Animal.cs、Cow.cs和Chicken.cs文件,单击Add。
(4) 修改这3个文件中的命名空间声明,如下所示:
namespace Ch12Ex02
(5) 修改Program.cs中的代码,如下所示:
static void Main(string[] args)
{
List<Animal> animalCollection = new List<Animal>();
animalCollection.Add(new Cow("Jack"));
animalCollection.Add(new Chicken("Vera"));
foreach (Animal myAnimal in animalCollection)
{
myAnimal.Feed();
}
Console.ReadKey();
}
(6) 执行应用程序,结果与上一章的Ch11Ex02相同。
示例的说明
这个示例与Ch11Ex02只有两个区别。第一个区别是下面的代码:
Animals animalCollection = new Animals();
被替换为:
List<Animal> animalCollection = new List<Animal>();
第二个区别比较重要:项目中不再有Animals集合类。前面为创建这个类所做的工作现在用一行代码即可完成,即使用泛型的集合类。
获得相同效果的另一个方法是不修改Program.cs中的代码,使用Animals的如下定义:
public class Animals : List<Animal>
{
}
这么做的优点是,能比较容易看懂Program.cs中的代码,还可以在合适时给Animals类添加额外的成员。
为什么不从CollectionBase中派生类?这是一个很好的问题。实际上,在许多情况下,我们都不会从CollectionBase中派生类。知道内部工作原理肯定是件好事,因为List<T>以相同的方式工作,但CollectionBase是向后兼容的。使用CollectionBase的惟一场合是要更多地控制向类的用户展示的成员。如果希望集合类的Add()方法使用内部访问修饰符,则使用CollectionBase是最佳选择。
注意:
也可以把要使用的初始容量(作为int)传递给List<T>的构造函数,或者传递使用IEnumerable<T>接口的初始项列表。支持这个接口的类包括List<T>。
2. 对泛型列表进行排序和搜索
给泛型列表进行排序与对其他列表进行排序是一样的。在上一章中,介绍了如何使用IComparer和IComparable接口比较两个对象,然后对该类型的对象列表排序。这里惟一的区别是,可以使用泛型接口IComparer<T>和IComparable<T>,它们略有区别、且针对特定类型的方法。表12-4列出了它们的区别。
表 12-4
泛 型 方 法 | 非泛型方法 | 区 别 |
int IComparable<T>. CompareTo(T otherObj) | int IComparable. CompareTo( object, otherObj) | 泛型版本中是强类型化的 |
bool IComparable<T>. Equals(T otherObj) | N/A | 在非泛型接口中不存在,可以使用object.Equals()替代 |
int IComparer<T>. Compare(T objectA, T objectB) | int IComparer. Compare(object objectA, object objectB) | 泛型版本中是强类型化的 |
bool IComparer<T>. Equals(T objectA, T objectB) | N/A | 在非泛型接口中不存在,可以使用object.Equals()替代 |
int IComparer<T>. GetHashCode (T objectA) | N/A | 在非泛型接口中不存在,可以使用object. GetHashCode()替代 |
要对List<T>排序,可以在要排序的类型上提供IComparable<T>接口,或者提供IComparer<T>接口。另外,还可以提供泛型委托,作为排序方法。从了解工作原理的角度来看,这非常有趣,因为实现上述接口并不比实现其非泛型版本更麻烦。
一般情况下,给列表排序需要一个方法,来比较T类型的两个对象。要在列表中搜索,也需要一个方法来检查T类型的对象,看看它是否满足某个条件。定义这样的方法很简单,这里给出两个可以使用的泛型委托:
● Comparison<T>:这个委托类型用于排序方法,其签名是int method (T objectA, T objectB)。
● Predicate<T>:这个委托类型用于搜索方法,其签名是bool method (T targetObject)。
可以定义任意个这样的方法,使用它们实现List<T>的搜索和排序方法。下面的示例进行了演示。
试试看:List<T>的搜索和排序
(1) 在目录C:/BegVCSharp/Chapter12下创建一个新控制台应用程序项目Ch12Ex03。
(2) 在Solution Explorer窗口中右击项目名称,选择Add | Add Existing Item...选项。
(3) 在C:/BegVCSharp/Chapter12/Ch12Ex01/Ch12Ex01目录下选择Vector.cs文件,单击Add。
(4) 修改这个文件中的命名空间声明,如下所示:
namespace Ch12Ex03
(5) 添加一个新类Vectors。
(6) 修改Vectors.cs中的代码,如下所示:
public class Vectors : List<Vector>
{
public Vectors()
{
}
public Vectors(IEnumerable<Vector> initialItems)
{
foreach (Vector vector in initialItems)
{
Add(vector);
}
}
public string Sum()
{
StringBuilder sb = new StringBuilder();
Vector currentPoint = new Vector(0.0, 0.0);
sb.Append("origin");
foreach (Vector vector in this)
{
sb.AppendFormat(" + {0}", vector);
currentPoint += vector;
}
sb.AppendFormat(" = {0}", currentPoint);
return sb.ToString();
}
}
(7) 添加一个新类VectorDelegates。
(8) 修改VectorDelegates.cs中的代码,如下所示:
public static class VectorDelegates
{
public static int Compare(Vector x, Vector y)
{
if (x.R > y.R)
{
return 1;
}
else if (x.R < y.R)
{
return -1;
}
return 0;
}
public static bool TopRightQuadrant(Vector target)
{
if (target.Theta >= 0.0 && target.Theta <= 90.0)
{
return true;
}
else
{
return false;
}
}
}
(9) 修改Program.cs中的代码,如下所示:
static void Main(string[] args)
{
Vectors route = new Vectors();
route.Add(new Vector(2.0, 90.0));
route.Add(new Vector(1.0, 180.0));
route.Add(new Vector(0.5, 45.0));
route.Add(new Vector(2.5, 315.0));
Console.WriteLine(route.Sum());
Comparison<Vector> sorter = new Comparison<Vector>(VectorDelegates.Compare);
route.Sort(sorter);
Console.WriteLine(route.Sum());
Predicate<Vector> searcher =
new Predicate<Vector>(VectorDelegates.TopRightQuadrant);
Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher));
Console.WriteLine(topRightQuadrantRoute.Sum());
Console.ReadKey();
}
(10) 执行应用程序,结果如图12-4所示。
图 12-4
示例的说明
在这个示例中,为Ch12Ex01中的Vector类创建了一个集合类Vectors。可以只使用List <Vector>类型的变量,但因为需要其他功能,所以使用了一个新类Vectors,它派生自List <Vector>,允许添加需要的其他成员。
该类有一个成员Sum(),依次返回每个矢量的字符串列表,并在最后把它们加在一起(使用源类Vector的重载+运算符)。每个矢量都可以看作“方向+距离”,所以这个矢量列表构成了一条有端点的路径。
public string Sum()
{
StringBuilder sb = new StringBuilder();
Vector currentPoint = new Vector(0.0, 0.0);
sb.Append("origin");
foreach (Vector vector in this)
{
sb.AppendFormat(" + {0}", vector);
currentPoint += vector;
}
sb.AppendFormat(" = {0}", currentPoint);
return sb.ToString();
}
这个方法使用System.Text命名空间中的StringBuilder类来构建响应字符串。这个类包含Append()和AppendFormat()等成员(这里使用),所以很容易构建字符串,其性能也高于连接各个字符串。使用这个类的ToString()方法即可获得最终的字符串。
本例还创建了两个用作委托的方法,作为VectorDelegates的静态成员。Compare()用于比较(排序),TopRightQuadrant()用于搜索。下面在讨论Program.cs中的代码时介绍它们。
Main()中的代码首先初始化Vectors集合,给它添加几个Vector对象:
Vectors route = new Vectors();
route.Add(new Vector(2.0, 90.0));
route.Add(new Vector(1.0, 180.0));
route.Add(new Vector(0.5, 45.0));
route.Add(new Vector(2.5, 315.0));
如前所述,Vectors.Sum()方法用于输出集合中的项,这次是按照其初始顺序输出:
Console.WriteLine(route.Sum());
接着,创建第一个委托sorter,这个委托是Comparison<Vector>类型的,因此可以赋予带如下签名的方法:
int method(Vector objectA, Vector objectB)
它匹配VectorDelegates.Compare(),该方法就是赋予委托的方法。
Comparison<Vector> sorter = new Comparison<Vector>(VectorDelegates.Compare);
Compare()比较两个矢量的大小,如下所示:
public static int Compare(Vector x, Vector y)
{
if (x.R > y.R)
{
return 1;
}
else if (x.R < y.R)
{
return -1;
}
return 0;
}
这样就可以按大小对矢量排序了:
route.Sort(sorter);
Console.WriteLine(route.Sum());
应用程序给出了我们期望的结果—— 汇总的结果是一样的,因为“矢量路径”的端点顺序与执行各个步骤的顺序相同。
然后,进行搜索,获取集合中的一个矢量子集。这需要使用VectorDelegates.TopRight Quadrant()来实现:
public static bool TopRightQuadrant(Vector target)
{
if (target.Theta >= 0.0 && target.Theta <= 90.0)
{
return true;
}
else
{
return false;
}
}
如果方法的Vector参数值是介于0到90°之间的Theta值,该方法就返回true,也就是说,它在前面的排序图中指向上或右。
在主函数体中,通过Predicate<Vector>类型的委托使用这个方法,如下所示:
Predicate<Vector> searcher =
new Predicate<Vector>(VectorDelegates.TopRightQuadrant);
Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher));
Console.WriteLine(topRightQuadrantRoute.Sum());
这需要在Vectors中定义构造函数:
public Vectors(IEnumerable<Vector> initialItems)
{
foreach (Vector vector in initialItems)
{
Add(vector);
}
}
其中,使用IEnumerable<Vector>的实例初始化了一个新的Vectors集合,这是必须的,因为List<Vector>.FindAll()返回一个List<Vector>实例,而不是Vectors实例。
搜索的结果是,只返回Vector对象的一个子集,所以汇总的结果不同(这正是我们希望的)。
2. Dictionary<K, V>
这个类型可以定义键/值对的集合。与本章前面介绍的其他泛型集合类型不同,这个类需要实例化两个类型,分别用于键和值,以表示集合中的各个项。
实例化Dictionary<K, V>对象后,就可以对它执行与继承自DictionaryBase的类相同的一些操作,但要使用已有的类型安全的方法和属性。例如,可以使用强类型化的Add()方法添加键/值对。
Dictionary<string, int> things = new Dictionary<string, int>();
things.Add("Green Things", 29);
things.Add("Blue Things", 94);
things.Add("Yellow Things", 34);
things.Add("Red Things", 52);
things.Add("Brown Things", 27);
可以使用Keys和Values属性迭代集合中的键和值:
foreach (string key in things.Keys)
{
Console.WriteLine(key);
}
foreach (int value in things.Values)
{
Console.WriteLine(value);
}
还可以迭代集合中的各个项,把每个项作为一个KeyValuePair<K, V>实例来获取,这与上一章介绍的DictionaryEntry对象相同:
foreach (KeyValuePair<string, int> thing in things)
{
Console.WriteLine("{0} = {1}", thing.Key, thing.Value);
}
对于Dictionary<K, V>要注意的一点是,每个项的键都必须是惟一的。如果要添加的项的键与已有项的键相同,就会抛出ArgumentException异常。所以,Dictionary<K, V>允许把IComparer<K>接口传递给其构造函数,如果要把自己的类用作键,且它们不支持IComparable或IComparable<K>接口,或者要使用非默认的过程比较对象,就必须把IComparer<K>接口传递给其构造函数。例如,在上面的示例中,可以使用不区分大小写的方法比较字符串键:
Dictionary<string, int> things =
new Dictionary<string, int>(StringComparer.CurrentCultureIgnoreCase);
如果使用下面的键,就会得到一个异常:
things.Add("Green Things", 29);
things.Add("Green things", 94);
也可以给构造函数传递初始容量(使用int)或项的集合(使用IDictionary<K,V>接口)。
3. 修改CardLib,以使用泛型集合类
对前几章创建的CardLib项目可以进行简单的修改,即修改Cards集合类,以使用一个泛型集合类,这将减少许多代码。对Cards的类定义需要进行如下修改:
public class Cards : List<Card>, ICloneable
{
...
}
还可以删除Cards的所有方法,但Clone()和CopyTo()除外,因为Clone()是ICloneable需要的方法,而List<Card>提供的CopyTo()版本处理的是Card对象数组,而不是Cards集合。
这里没有列出代码,因为这是很简单的修改,CardLib的更新版本为Ch12CardLib,它和上一章的客户代码包含在本章的下载代码中。