在探讨如何创建自己的泛型之前,先介绍.NET Framework提供的泛型,包括System. Collection.Generic命名空间中的类型,这个命名空间已在前面的代码中出现过多次,因为默认情况下它包含在控制台应用程序中。我们还没有使用过这个命名空间中的类型,但下面就要使用了。本节将讨论这个命名空间中的类型,以及如何使用它们创建强类型化的集合,提高已有集合的功能。
首先论述另一个较简单的泛型类型(nullable type):可空类型,解决值类型的一个小问题。
12.2.1 可空类型
在前面的章节中,介绍了值类型(大多数基本类型,例如int、double和所有的结构)区别于引用类型(string和所有的类)的一种方式:值类型必须包含一个值,它们可以在声明之后、赋值之前,在未赋值的状态下存在,但不能以任何方式使用。而引用类型可以是null。
有时让值类型为空是很有用的,泛型使用System.Nullable<T>类型提供了使值类型为空的一种方式。例如:
System.Nullable<int> nullableInt;
这行代码声明了一个变量nullableInt,它可以拥有int变量能包含的任意值,还可以拥有值null。所以可以编写下面的代码:
nullableInt = null;
如果nullableInt是一个int类型的变量,上面的代码是不能编译的。
前面的赋值等价于:
nullableInt = new System.Nullable<int>();
与其他变量一样,无论是初始化为null(使用上面的语法),还是通过给它赋值来初始化,都不能在初始化之前使用它。
可以像测试引用类型一样,测试可空类型,看看它们是否为null:
if (nullableInt == null)
{
...
}
另外,可以使用HasValue属性:
if (nullableInt.HasValue)
{
...
}
这不适用于引用类型,即使引用类型有一个HasValue属性,也不能使用这种方法,因为引用类型的变量值为null,就表示不存在对象,当然就不能通过对象来访问这个属性,此时会抛出一个异常。
使用Value属性可以查看引用类型的值。如果HasValue是true,就说明Value属性有一个非空值。但如果HasValue是false,就说明变量被赋予了null,访问Value属性会抛出System. InvalidOperationException类型的异常。
可空类型要注意的一点是,它们非常有用,以致于修改了C#语法。上面可空类型的变量不使用上述语法,而是使用下面的语法:
int? nullableInt;
int ?是System.Nullable<int>的缩写,但可读性更高。在后面的章节中就使用这个语法。
1. 运算符和可空类型
对于简单类型如int,可以使用+、–等运算符来处理值。而对于可空类型,这是没有区别的:包含在可空类型中的值会隐式转换为需要的类型,使用适当的运算符。这也适用于结构和自己提供的运算符。例如:
int? op1 = 5;
int? result = op1 * 2;
注意其中result变量的类型也是int?。下面的代码不会编译:
int? op1 = 5;
int result = op1 * 2;
为了使上面的代码正常工作,必须进行显式转换:
int? op1 = 5;
int result = (int)op1 * 2;
只要op1有一个值,上面的代码就可以正常运行,如果op1是null,就会生成System.Invalid OperationException类型的异常。
这就引出了下一个问题:当运算等式中的一个或两个值是null时,例如上面代码中的op1,会发生什么情况?答案是:对于除了bool?之外的所有简单可空类型,该操作的结果是null,可以把它解释为“不能计算”。对于结构,可以定义自己的运算符来处理这种情况(详见本章后面的内容)。对于bool?,为&和 | 定义的运算符会得到非空返回值,如表12-1所示。
表 12-1
|
op1 |
op2 |
op1 & op2 |
op1 | op2 |
|
true |
true |
true |
true |
|
true |
false |
false |
true |
|
true |
null |
null |
true |
|
false |
true |
false |
true |
|
false |
false |
false |
false |
|
false |
null |
false |
null |
|
null |
true |
null |
true |
|
null |
false |
false |
null |
|
null |
null |
null |
null |
这些运算符的结果与我们想像的一样,如果不需要知道其中一个操作数的值,就可以计算出结果,则该操作数是否为null就不重要。
2. ??运算符
为了进一步减少处理可空类型所需的代码量,使可空变量的处理变得更简单,可以使用??运算符。这个运算符允许提供可空类型是null和不是null时的默认值,其用法如下:
int? op1 = null;
int result = op1 * 2 ?? 5;
在这个示例中,op1是null,所以op1*2也是null。但是,??运算符检测到这个情况,并把值5赋予result。这里特别要注意,在结果中放入int类型的变量result不需要显式转换。??运算符会自动处理这个转换。可以把??等式的结果放在int?中:
int? result = op1 * 2 ?? 5;
在处理可空变量时,??运算符有许多用途,它也是提供默认值的一种方便方式,不需要使用if结构中的代码块。
在下面的示例中,将介绍可空类型Vector。
试试看:可空类型
(1) 在目录C:\BegVCSharp\Chapter12下创建一个新控制台应用程序项目Ch12Ex01。
(2) 使用VS快捷方式,在文件Vector.cs中添加一个新类Vector。
(3) 修改Vector.cs中的代码,如下所示:
public class Vector
{
public double? R = null;
public double? Theta = null;
public double? ThetaRadians
{
get
{
// Convert degrees to radians.
return (Theta * Math.PI / 180.0);
}
}
public Vector(double? r, double? theta)
{
// Normalize.
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
// Assign fields.
R = r;
Theta = theta;
}
public static Vector operator +(Vector op1, Vector op2)
{
try
{
// Get (x, y) coordinates for new vector.
double newX = op1.R.Value * Math.Sin(op1.ThetaRadians.Value)
+ op2.R.Value * Math.Sin(op2.ThetaRadians.Value);
double newY = op1.R.Value * Math.Cos(op1.ThetaRadians.Value)
+ op2.R.Value * Math.Cos(op2.ThetaRadians.Value);
// Convert to (r, theta).
double newR = Math.Sqrt(newX * newX + newY * newY);
double newTheta = Math.Atan2(newX, newY) * 180.0 / Math.PI;
// Return result.
return new Vector(newR, newTheta);
}
catch
{
// Return "null" vector.
return new Vector(null, null);
}
}
public static Vector operator -(Vector op1)
{
return new Vector(-op1.R, op1.Theta);
}
public static Vector operator -(Vector op1, Vector op2)
{
return op1 + (-op2);
}
public override string ToString()
{
// Get string representation of coordinates.
string rString = R.HasValue ? R.ToString() : "null";
string thetaString = Theta.HasValue ? Theta.ToString() : "null";
// Return (r, theta) string.
return string.Format("({0}, {1})", rString, thetaString);
}
}
(4) 修改Program.cs中的代码,如下所示:
class Program
{
public static void Main(string[] args)
{
Vector v1 = GetVector("vector1");
Vector v2 = GetVector("vector1");
Console.WriteLine("{0} + {1} = {2}", v1, v2, v1 + v2);
Console.WriteLine("{0} - {1} = {2}", v1, v2, v1 - v2);
Console.ReadKey();
}
public static Vector GetVector(string name)
{
Console.WriteLine("Input {0} magnitude:", name);
double? r = GetNullableDouble();
Console.WriteLine("Input {0} angle (in degrees):", name);
double? theta = GetNullableDouble();
return new Vector(r, theta);
}
public static double? GetNullableDouble()
{
double? result;
string userInput = Console.ReadLine();
try
{
result = double.Parse(userInput);
}
catch
{
result = null;
}
return result;
}
}
(5) 执行应用程序,给两个矢量(vector)输入值,结果如图12-1所示。

图 12-1
(6) 再次执行应用程序,这次跳过四个值中的至少一个,结果如图12-2所示。

图 12-2
示例的说明
在这个示例中,创建了一个类Vector,它表示带极坐标(有一个幅值和一个角度)的矢量,如图12-3所示。

图 12-3
坐标r和_在代码中用公共字段R和Theta表示,其中Theta的单位是度(°)。ThetaRad用于获取Theta的弧度值,这是必须的,因为Math类在其静态方法中使用弧度。R和Theta的类型都是double?,所以它们可以为空。
public class Vector
{
public double? R = null;
public double? Theta = null;
public double? ThetaRadians
{
get
{
// Convert degrees to radians.
return (Theta * Math.PI / 180.0);
}
}
Vector的构造函数标准化R和Theta的初始值,然后赋予公共字段。
public Vector(double? r, double? theta)
{
// Normalize.
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
// Assign fields.
R = r;
Theta = theta;
}
Vector类的主要功能是使用运算符重载对矢量进行相加和相减,这需要一些非常基本的三角函数知识,这里不解释它们。在代码中,重要的是,如果在获取R或ThetaRad的Value属性时抛出了异常,即其中一个是null,就返回“空”矢量。
public static Vector operator +(Vector op1, Vector op2)
{
try
{
// Get (x, y) coordinates for new vector.
...
}
catch
{
// Return "null" vector.
return new Vector(null, null);
}
}
如果组成矢量的坐标是null,该矢量就是无效的,这里用R和Theta都可为null的Vector类来表示。
Vector类的其他代码重写了其他运算符,把相加的功能扩展到相减上,再重写ToString(),获取Vector对象的字符串表示。
Program.cs中的代码测试Vector类,让用户初始化两个矢量,再对它们进行相加和相减。如果用户省略了一个值,该值就解释为null,应用前面提及的规则。
12.2.2 System.Collections.Generic命名空间
实际上,本书前面的每个应用程序都有如下命名空间:
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,它和上一章的客户代码包含在本章的下载代码中。





