在探讨如何创建自己的泛型之前,先介绍.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,它和上一章的客户代码包含在本章的下载代码中。
|