第12章 泛 型

 

 

 C#第1版中最受垢病的一个方面是缺乏对泛型(generics)的支持。C++中的泛型(在该语言中称为模板)很早就被公认为是完成任务的最佳方式。它可以在编译期间由一个类型定义派生出许多特定的类型,这节省了大量的时间和精力。不知道什么原因,泛型没有被纳入C#的第1版,C#也因此也备受批评。也许是因为泛型是一种很难掌握的技术,也许开发人员觉得不需要泛型。幸好,C# 2.0版中加入了泛型。泛型并不是真的很难掌握,只是需要用略微不同的方式处理而已。只要努力,就一定有收获。
本章的主要内容:
● 介绍泛型的概念,先学习泛型的抽象术语,因为学习泛型的概念对高效使用它是至关重要的。
● 讨论.NET Framework中的一些泛型类型,这有助于更好地理解其功能和强大之处,以及在代码中需要使用的新语法。
● 定义自己的泛型类型,包括泛型的类、接口、方法和委托。还要介绍进一步定制泛型类型的其他技术:default关键字和类型约束。

 

12.1 泛型的概念

 

 

 为了介绍泛型的概念,说明它们为什么这么有用,先回忆一下第11章中的集合类。基本集合可以包含在类似ArrayList这样的类中,但这些集合是没有类型化的,所以需要把object项转换为集合中实际存储的对象类型。继承自System.Object的任何对象都可以存储在ArrayList中,所以要特别仔细。假定包含在集合中的某些类型可能导致抛出异常,代码逻辑崩溃。前面介绍的技术可以处理这个问题,包括检查对象类型所需的代码。但是,更好的解决办法是一开始就使用强类型化的集合类。这种集合类派生于CollectioonBase,并可以拥有自己的方法,来添加、输出和访问集合的成员,但它可能把集合成员限制为派生于某种基本类型,或者必须支持某个接口。
这会带来一个问题。每次创建需要包含在集合中的新类时,就必须执行下述任务之一:
● 使用某个集合类,该类已经定义为可以包含新类型的项。
● 创建一个新的集合类,它可以包含新类型的项,实现所有需要的方法。
一般情况下,新的类型需要额外的功能,我们常常并不需要新的集合类,创建集合类也会花费大量时间。
另一方面,泛型类大大简化了这个问题。泛型类是以实例化过程中提供的类型或类为基础建立的,可以毫不费力地对对象进行强类型化。对于集合,创建“T类型对象的集合”只需把它用一行代码写出来即可。不使用下面的代码:
CollectionClass col = new CollectionClass();
col.Add(new ItemClass());
而可以使用:
CollectionClass<ItemClass> col = new CollectionClass<ItemClass>();
col.Add(new ItemClass());
尖括号语法就是把变量类型传送给泛型类型的方式。在上面的代码中,应把CollectionClass <ItemClass>看作ItemClass的CollectionClass。当然,本章后面会详细探讨这个语法。
前面的泛型只涉及到集合,实际上泛型非常适合于这个领域,本章在后面介绍System. Collection.Generic命名空间时会提及。创建一个泛型类,就可以生成一些方法,它们的签名可以强类型化为我们需要的任何类型,该类型甚至可以是值类型或引用类型,处理各自的操作。还可以把用于实例化泛型类的类型限制为支持某个给定的接口,或派生自某种类型,只允许使用类型的一个子集。泛型并不限于类,还可以创建泛型接口、泛型方法(可以在非泛型类上定义),甚至泛型委托。
这将大大提高代码的灵活性,正确使用泛型可以显著缩短开发时间。
那么该如何实现泛型呢?通常,在创建类时,它会编译为一个类型,然后在代码中使用。读者可能认为,在创建泛型类时,它必须编译为许多类型,才能进行实例化。幸好并不是这样:在.NET中,类有无限多个。在后台,.NET运行库允许在需要时动态生成泛型类。在通过实例化来请求生成之前,B的某个泛型类A甚至不存在。
注意:
对于熟悉C++或者对C++感兴趣的读者来说,这是C++模板和C#泛型类的一个区别。在C++中,编译器可以检测出在哪里使用了模板的某个特定类型,例如模板B的A类型,然后编译需要的代码,来创建这个类型。而在C#中,所有的操作都在运行期间进行。
总之,泛型允许灵活地创建类型,处理一种或多种特定类型的对象,这些类型是在实例化时确定的,否则就使用泛型类型。下面看看它们的使用。

 

6.2 变量的作用域

 

 

  


在上一节中,读者可能想知道为什么需要利用函数交换数据。原因是C#中的变量仅能从代码的本地作用域访问。给定的变量有一个作用域,访问该变量要通过这个作用域来实现。
变量的作用域是一个重要的主题,最好用一个示例来说明。下面的示例将演示变量在一个作用域中定义,但试图在另一个作用域中使用的情形。
试试看:定义和使用基本函数
(1) 对Ch06Ex01中的Program.cs进行如下修改:
class Program
{
static void Write()
{
Console.WriteLine("myString = {0}", myString);
}

static void Main(string[] args)
{
string myString = "String defined in Main()";
Write();
Console.ReadKey();
}
}
(2) 编译代码,注意显示在任务列表中的错误和警告:
The name 'myString' does not exist in the current context
The variable 'myString' is assigned but its value is never used
示例的说明
什么地方出错了?在应用程序主体(Main()函数)中定义的变量myString不能在Write()函数中访问。
原因是变量有一个作用域,在这个作用域中,变量才是有效的。这个作用域包括定义变量的代码块和直接嵌套在其中的代码块。函数中的代码块与调用它们的代码块是不同的。在Write()中,没有定义myString,在Main()中定义的myString则超出了作用域—— 它只能在Main()中使用。
实际上,在Write()中可以有一个完全独立的变量myString,修改代码,如下所示:
class Program
{
static void Write()
{
string myString = "String defined in Write()";
Console.WriteLine("Now in Write()");
Console.WriteLine("myString = {0}", myString);
}

static void Main(string[] args)
{
string myString = "String defined in Main()";
Write();
Console.WriteLine("\nNow in Main()");
Console.WriteLine("myString = {0}", myString);
Console.ReadKey();
}
}
这段代码就可以编译,结果如图6-4所示。

图 6-4
这段代码执行的操作如下:
● Main()定义和初始化字符串变量 myString。
● Main() 把控制权传送给Write()。
● Write()定义和初始化一个字符串变量myString,它与Main()中定义的myString变量完全不同。
● Write()把一个字符串输出到控制台上,该字符串包含在Write()中定义的myString的值。
● Write()把控制权传送回Main()。
● Main()把一个字符串输出到控制台上,该字符串包含在Main()中定义的myString的值。
作用域以这种方式覆盖一个函数的变量称为局部变量。还有一种全局变量,其作用域可覆盖几个函数。修改代码,如下所示:
class Program
{
static string myString;

static void Write()
{
string myString = "String defined in Write()";
Console.WriteLine("Now in Write()");
Console.WriteLine("Local myString = {0}", myString);
Console.WriteLine("Global myString = {0}", Program.myString);
}

static void Main(string[] args)
{
string myString = "String defined in Main()";
Program.myString = "Global string";
Write();
Console.WriteLine("\nNow in Main()");
Console.WriteLine("Local myString = {0}", myString);
Console.WriteLine("Global myString = {0}", Program.myString);
Console.ReadKey();
}
}
结果如图6-5所示。

图 6-5
这里添加了另一个变量myString,这次进一步加深了代码中的名称层次。这个变量定义如下:
static string myString;
注意这里也需要static关键字。在这种形式的控制台应用程序中,必须使用static 或 const关键字,来定义这种形式的全局变量。如果要修改全局变量的值,就需要使用static,因为const禁止修改变量的值。
为了区分这个变量和Main()与Write()中同名的局部变量,必须用一个完整限定的名称为变量名分类,参见第3章。这里把全局变量称为Program.myString。注意,在全局变量和局部变量同名时,这是必需的。如果没有局部myString变量,就可以使用myString表示全局变量,而不需要使用Program.myString。如果局部变量和全局变量同名,全局变量就会被屏蔽。
全局变量的值在Main()中设置如下:
Program.myString = "Global string";
在Write()中访问:
Console.WriteLine("Global myString = {0}", Program.myString);
为什么不能使用这个技术通过函数交换数据,而要使用前面介绍的参数来交换数据?有时,这确实是一种交换数据的首选方式,但在许多情况下不应使用这种方式。是否使用全局变量取决于函数的位置。使用全局变量的问题在于,它们一般不适合于“常规用途”的函数—— 这些函数能处理我们所提供的数据,而不仅限于处理特定全局变量中的数据。详见本章后面的内容。
6.2.1 其他结构中变量的作用域
在继续之前,应先注意一下上一节的一个要点总结了上述内容,并超出了函数之间的变量作用域。前面说过,变量的作用域包含定义它们的代码块和直接嵌套在其中的代码块。这也可以应用到其他代码块上,例如分支和循环结构的代码块。考虑下面的代码:

int i;
for (i = 0; i < 10; i++)
{
string text = "Line " + Convert.ToString(i);
Console.WriteLine("{0}", text);
}
Console.WriteLine("Last text output in loop: {0}", text);
字符串变量text是for循环的局部变量,这段代码不能编译,因为在该循环外部调用的Console.WriteLine()试图使用该变量text,这超出了循环的作用域。修改代码,如下所示:
int i;
string text;
for (i = 0; i < 10; i++)
{
text = "Line " + Convert.ToString(i);
Console.WriteLine("{0}", text);
}
Console.WriteLine("Last text output in loop: {0}", text);
这段代码也会失败,原因是变量必须在使用前声明和初始化,而text是在for循环中初始化的。赋给text的值在循环块退出时就丢失了。但是还可以进行如下修改:
int i;
string text = "";
for (i = 0; i < 10; i++)
{
text = "Line " + Convert.ToString(i);
Console.WriteLine("{0}", text);
}
Console.WriteLine("Last text output in loop: {0}", text);
这次text是在循环外部初始化的,可以访问它的值。这段简单代码的结果如图6-6所示。

图 6-6
在循环中最后赋给text的值可以在循环外部访问。
可以看出,这个主题的内容需要花一点时间来掌握。在前面的示例中,循环之前赋给text空字符串,而在循环之后的代码中,该text就不会是空字符串了,其原因不能立即看出。
这种情况的解释涉及到分配给text变量的内存空间,实际上任何变量都是这样。只声明一个简单的变量类型,并不会引起其他的变化。只有在给变量赋值后,这个值才占用一块内存空间。如果这种占据内存空间的行为在循环中发生,该值实际上定义为一个局部值,在循环的外部会超出了其作用域。
即使变量本身没有局部化到循环上,循环所包含的值也局部化到该循环上。但是,在循环外部赋值可以确保该值是主体代码的局部值,在循环内部它仍处于其作用域中。这意味着变量在退出主体代码块之前是没有超出作用域的,所以可以在循环外部访问它的值。
幸而,C#编译器可检测变量作用域的问题,它生成的响应错误信息可以帮助我们理解变量作用域的问题。
最后一个要注意的问题是,应采用“最佳实践”。一般情况下,最好在声明和初始化所有的变量后,再在代码块中使用它们。一个例外是把循环变量声明为循环块的一部分,例如:
for (int i = 0; i < 10; i++)
{
...
}
其中i局部化于循环代码块中,但这是可以的,因为我们很少需要在外部代码中访问这个计数器。
6.2.2 参数和返回值与全局数据
本节详细介绍如何通过全局数据以及参数和返回值,与函数交换数据。先看看下面的代码:
class Program
{
static void showDouble(ref int val)
{
val *= 2;
Console.WriteLine("val doubled = {0}", val);
}

static void Main(string[] args)
{
int val = 5;
Console.WriteLine("val = {0}", val);
showDouble(ref val);
Console.WriteLine("val = {0}", val);
}
}
注意:
这段代码与本章前面的代码略有不同,在前面的示例中,在Main()中使用了变量名myNumber,这说明了局部变量可以有相同的名称,且不会相互干涉。这里列出的两个代码示例比较类似,以便我们集中精力研究它们的区别,而无需担心变量名。
和下面的代码比较:
class Program
{
static int val;

static void showDouble()
{
val *= 2;
Console.WriteLine("val doubled = {0}", val);
}

static void Main(string[] args)
{
val = 5;
Console.WriteLine("val = {0}", val);
showDouble();
Console.WriteLine("val = {0}", val);
}
}
这两个showDouble()函数的结果是相同的。
现在,使用哪种方法并没有什么硬性规定,这两种方法都是有效的。但是,需要考虑一些规则。
首先,在第一次讨论这个问题时,使用全局值的showDouble()版本只使用全局变量val。为了使用这个版本,必须使用这个全局变量。这会对该函数的多样性有轻微的限制,如果要存储结果,就必须总是把这个全局变量值复制到其他变量中。另外,全局数据可以在应用程序的其他地方由代码修改,这会导致预料不到的结果(即使我们没有认识到这一点,值也是可以改变的)。
但是,损失了多样性常常是有好处的。我们常常希望把一个函数只用于一个目的,使用全局数据存储能减少在函数调用中犯错的可能性,例如把它传递给错误的变量。
当然,也可以说,这种简化实际上使代码更难理解。显示指定参数可以一眼看出发生了什么改变。例如myFunction(val1, out val2)函数调用,其中val1和val2都是要考虑的重要变量,在函数执行结束后,val2就会被赋予一个新值。反之,如果这个函数不带参数,就不能对它处理了什么数据做任何假设。
最后,记住并不总是能使用全局数据。本书的后面将介绍在不同的文件中编写的代码,以及不同命名空间中的代码如何通过函数彼此通信。像这样的情况,代码常常要分开编写,显然不能使用全局存储方式。
总之,可以自由选择使用哪种技术来交换数据。一般情况下,最好使用参数,而不使用全局数据,但有时使用全局数据更合适,使用这个技术并没有错。

 

6.3 Main()函数

 

 

前面介绍了创建和使用函数时涉及的大多数简单技术,下面详细论述Main()函数。
Main()是C#应用程序的入口点,执行这个函数就是执行应用程序。也就是说,在执行过程开始时,会执行Main()函数,在Main()函数执行完毕时,执行过程就结束了。这个函数有一个参数string[] args,但我们还没有说明这个参数的含义。本节将介绍该参数,以及如何使用它。

注意:
Main函数可以使用4种签名:
● static void Main()
● static void Main(string[] args)
● static int Main()
● static int Main(string[] args)
如果需要,可以忽略这里讨论的args。直到现在还在使用这个参数的原因,就是在VS中创建控制台应用程序时自动生成的Main()版本。
上面的第三、四个版本返回一个int值,它们可以用于表示应用程序如何终止,通常用作一种错误提示(但这不是强制的),一般情况下,返回0反映了“正常”的终止(即应用程序执行完毕,并安全地终止)。
Main()的参数args是从应用程序的外部接受信息的方法,这些信息在运行期间指定,其形式是命令行参数。
前面已经遇到了命令行参数,在从命令行上执行应用程序时,通常可以直接指定信息,如在执行应用程序时加载一个文件。例如,考虑Windows中的Notepad应用程序。在命令行窗口中输入notepad,或者在Windows的Start菜单中选择Run选项,再在打开的窗口中输入notepad,就可以运行该应用程序。也可以输入notepad "myfile.txt",结果是Notepad在运行时将加载文件myfile.txt,如果该文件不存在,Notepad也会创建该文件。这里myfile.txt是一个命令行参数。利用args参数,可以编写以相同的方式工作的控制台应用程序。
在执行控制台应用程序时,指定的任何命令行参数都放在这个args数组中,接着可以根据需要在应用程序中使用这些参数。
下面用一个示例来说明。这个示例可以指定任意数量的命令行参数,每个参数都输出到控制台上。
试试看:命令行参数
(1) 在目录C:\BegVCSharp\Chapter6下创建一个新控制台应用程序Ch06Ex04。
(2) 把下述代码添加到Program.cs中:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("{0} command line arguments were specified:",
args.Length);
foreach (string arg in args)
Console.WriteLine(arg);
Console.ReadKey();
}
}
(3) 打开项目的属性页面(在Solution Explorer窗口中右击Ch06Ex04项目名称,选择Properties)。

(4) 选择Debug页面,在Command Line Arguments设置中添加所希望的命令行参数,如图6-7所示。

图 6-7
(5) 运行应用程序,结果如图6-8所示。

图 6-8
示例的说明
这里使用的代码非常简单:
Console.WriteLine("{0} command line arguments were specified:",
args.Length);
foreach (string arg in args)
Console.WriteLine(arg);
使用args参数与使用其他字符串数组类似。我们没有对参数进行任何异样的操作,只是把指定的信息写到屏幕上。
在本示例中,通过VS中的项目属性提供参数,这是一种很便捷的方式,只要在VS中运行应用程序,就可以使用相同的命令行参数,无需每次都在命令行提示窗口中输入它们。在项目输出所在的目录(C:\BegVCSharp\Chapter6\Ch06Ex04\bin\Debug)下打开命令行窗口,输入下述代码,也可以得到相同的结果:
Ch06Ex04 256 myFile.txt "a longer argument"
注意,每个参数都用空格分隔开,如果参数包含空格,就可以用双引号把参数括起来,这样才不会把这个参数解释为多个参数。

 

 

12.2 使用泛型

 

 

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


 

  12.3 定义泛型

 

前面介绍的泛型知识足以创建自己的泛型了,其中许多代码都涉及到泛型类型,还讨论了泛型语法的使用。本节将定义如下内容:
● 泛型类
● 泛型接口
● 泛型方法
● 泛型委托
在定义泛型类型的过程中,还将讨论处理如下问题的一些更高级技术:
● default关键字
● 约束类型
● 从泛型类中继承
● 泛型运算符
12.3.1 定义泛型类
要创建泛型类,只需在类定义中包含尖括号语法:
class MyGenericClass<T>
{
...
}
其中T可以是任意标识符,只要遵循通常的C#命名规则即可,例如不以数字开头等。
泛型类可以在其定义中包含任意多个类型,它们用逗号分隔开,例如:
class MyGenericClass<T1, T2, T3>
{
...
}
定义了这些类型之后,就可以在类定义中像使用其他类型那样使用它们。可以把它们用作成员变量的类型、属性或方法等成员的返回类型,方法变元的参数类型等。例如:
class MyGenericClass<T1, T2, T3>
{
private T1 innerT1Object;

public MyGenericClass(T1 item)
{
innerT1Object = item;
}

public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}
其中,类型T1的对象可以传递给构造函数,这个对象只能通过InnerT1Object属性进行只读访问。
注意,不能假定类提供了什么类型。例如,下面的代码就不会编译:
class MyGenericClass<T1, T2, T3>
{
private T1 innerT1Object;

public MyGenericClass()
{
innerT1Object = new T1();
}

public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}
我们不知道T1是什么,也就不能使用它的构造函数,它甚至可能没有可公共访问的默认构造函数。如果不使用涉及本节后面介绍的高级技术的复杂代码,则可以安全地对T1进行如下假设:
● 可以把它看作继承自System.Object的类型或封箱到System.Object中。
显然,这意味着不能对这个类型的实例进行非常有趣的操作,或者对为MyGenericClass泛型类提供的其他类型进行有趣的操作。不使用反射(这是用于在运行期间检查类型的高级技术,本章不介绍它),就只能使用下面的代码:
public string GetAllTypesAsString()
{
return "T1 = " + typeof(T1).ToString()
+ ", T2 = " + typeof(T2).ToString()
+ ", T3 = " + typeof(T3).ToString();
}
可以做一些其他工作,尤其是对集合进行操作,因为处理对象组是非常简单的,不需要对对象类型进行任何假设,这是存在本章前面介绍的泛型集合类的一个原因。
需要注意的另一个限制是,在比较为泛型类型提供的类型值和null时,只能使用运算符==和!=。例如,下面的代码会正常工作:
public bool Compare(T1 op1, T1 op2)
{
if (op1 != null && op2 != null)
{
return true;
}
else
{
return false;
}
}
其中,如果T1是一个值类型,则总是假定它是非空的,于是在上面的代码中,Compare总是返回true。
但是,试图比较两个变元op1和op2,下面的代码将不能编译:
public bool Compare(T1 op1, T1 op2)
{
if (op1 == op2)
{
return true;
}
else
{
return false;
}
}
其原因是这段代码假定T1支持==运算符。
这说明,要对泛型进行实际的操作,需要更多地了解类中使用的类型。
1. default关键字
要确定用于创建泛型类实例的类型,需要了解一个最基本的情况:它们是引用类型还是值类型。若不知道这个情况,不能用下面的代码赋予null值:
public MyGenericClass()
{
innerT1Object = null;
}
如果T1是值类型,则innerT1Object不能是null,所以这段代码不会编译。
幸好,开发人员考虑到了这个问题,使用default关键字(本书前面在switch结构中使用过它)的新用法解决了它。该新用法如下:
public MyGenericClass()
{
innerT1Object = default(T1);
}
其结果是,如果innerT1Object是引用类型,就给它赋予null,如果它是值类型,就给它赋予默认值。对于数字类型,这个默认值是0;而结构根据其各个成员的类型,以相同的方式初始化为0或null。
default关键字允许对要使用的类型进行更多的操作,但为了更进一步,还需要限制所提供的类型。
2. 约束类型
前面用于泛型类的类型称为无绑定(unbounded)类型,因为没有对它们进行任何约束。而通过约束类型,可以把类型限制为用于实例化泛型类,这有许多方式。例如,可以把类型限制为继承自某个类型。下面复习前面使用的Animal、Cow和Chicken类,把一个类型限制为继承自Animal,则下面的代码是正确的:
MyGenericClass<Cow> = new MyGenericClass<Cow>();
但下面的代码不能编译:
MyGenericClass<string> = new MyGenericClass<string>();
在类定义中,这可以使用where关键字来实现:
class MyGenericClass<T> where T : constraint
{
...
}
其中constraint定义了约束。
可以用这种方式提供许多约束,各个约束间用逗号分隔开:
class MyGenericClass<T> where T : constraint1, constraint2
{
...
}
还可以使用多个where语句,定义泛型类需要的任意类型或所有类型上的约束:
class MyGenericClass<T1, T2> where T1 : constraint1 where T2 : constraint2
{
...
}
约束必须出现在继承说明符的后面:
class MyGenericClass<T1, T2> : MyBaseClass, IMyInterface
where T1 : constraint1 where T2 : constraint2
{
...
}
表12-5列出了一些可用的约束。
表 12-5
约 束 定 义 示 例
struct 类型必须是值类型 在类中,需要值类型才能起作用,例如,类中T类型的成员变量是0,表示某种含义
class 类型必须是引用类型 在类中,需要引用类型才能起作用,例如,类中T类型的成员变量是null,表示某种含义
base class 类型必须是基类或继承自基类 在类中,需要继承自基类的某种基本功能,才能起作用
interface 类型必须是接口或实现了接口 在类中,需要接口提供的某种基本功能,才能起作用
new() 类型必须有一个公共的无参构造函数 在类中,需要能实例化T类型的变量,例如在构造函数中实例化
注意:
如果new()用作约束,它就必须是为类型指定的最后一个约束。
可以把一个类型参数用作另一个类型参数的约束,如下所示:
class MyGenericClass<T1, T2> where T2 : T1
{
...
}
其中,T2必须与T1的类型相同,或者继承自T1。这称为裸类型约束(naked type constraint),表示一个泛型类型参数用作另一个类型参数的约束。
类型约束不能循环,例如:
class MyGenericClass<T1, T2> where T2 : T1 where T1 : T2
{
...
}
这段代码不能编译。
在下面的示例中,将定义和使用一个泛型类,该类使用前面几章介绍的Animal类系列。
试试看:定义泛型类
(1) 在C:\BegVCSharp\Chapter12目录下创建一个新的控制台应用程序项目Ch12Ex04。
(2) 在Solution Explorer窗口中右击项目名称,选择Add | Add Existing Item…选项。
(3) 从C:\BegVCSharp\Chapter12\Ch12Ex02\Ch12Ex02目录下选择Animal.cs、Cow.cs和Chicken.cs文件,单击Add。
(4) 在这3个文件中修改命名空间声明,如下所示:
namespace Ch12Ex04
(5) 修改Animal.cs,如下所示:
public abstract class Animal
{
...

public abstract void MakeANoise();
}
(6) 修改Chicken.cs,如下所示:
public class Chicken : Animal
{
...

public override void MakeANoise()
{
Console.WriteLine("{0} says 'cluck!'", name);
}
}
(7) 修改Cow.cs,如下所示:
public class Cow : Animal
{
...

public override void MakeANoise()
{
Console.WriteLine("{0} says 'moo!'", name);
}
}
(8) 添加一个新类SuperCow.cs,如下所示:
public class SuperCow : Cow
{
public void Fly()
{
Console.WriteLine("{0} is flying!", name);
}

public SuperCow(string newName) : base(newName)
{
}

public override void MakeANoise()
{
Console.WriteLine("{0} says 'here I come to save the day!'", name);
}
}

(9) 添加一个新类Farm.cs,如下所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Ch12Ex04
{
public class Farm<T> : IEnumerable<T>
where T : Animal
{
private List<T> animals = new List<T>();

public List<T> Animals
{
get
{
return animals;
}
}

public IEnumerator<T> GetEnumerator()
{
return animals.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return animals.GetEnumerator();
}

public void MakeNoises()
{
foreach (T animal in animals)
{
animal.MakeANoise();
}
}
public void FeedTheAnimals()
{
foreach (T animal in animals)
{
animal.Feed();
}
}
public Farm<Cow> GetCows()
{
Farm<Cow> cowFarm = new Farm<Cow>();
foreach (T animal in animals)
{
if (animal is Cow)
{
cowFarm.Animals.Add(animal as Cow);
}
}
return cowFarm;
}
}
}
(10) 修改Program.cs,如下所示:
static void Main(string[] args)
{
Farm<Animal> farm = new Farm<Animal>();
farm.Animals.Add(new Cow("Jack"));
farm.Animals.Add(new Chicken("Vera"));
farm.Animals.Add(new Chicken("Sally"));
farm.Animals.Add(new SuperCow("Kevin"));
farm.MakeNoises();

Farm<Cow> dairyFarm = farm.GetCows();
dairyFarm.FeedTheAnimals();

foreach (Cow cow in dairyFarm)
{
if (cow is SuperCow)
{
(cow as SuperCow).Fly();
}
}
Console.ReadKey();
}
(11) 执行应用程序,结果如图12-5所示。

图 12-5
示例的说明
在这个示例中,创建了一个泛型类Farm<T>,它没有继承泛型列表类,而是把泛型列表类提供为一个公共属性。这个列表的类型由传递给Farm<T>的类型参数T确定,且约束为Animal,或者继承自Animal。
public class Farm<T> : IEnumerable<T>
where T : Animal
{
private List<T> animals = new List<T>();

public List<T> Animals
{
get
{
return animals;
}
}
Farm<T>还实现了IEnumerable<T>,其中,T传递给这个泛型接口,因此也以相同的方式进行了约束。实现这个接口,就可以迭代包含在Farm<T>中的项,而无需显式迭代Farm<T>.Animals。这是很容易做到的,只需返回Animals提供的枚举即可,该枚举是一个List<T>类,也实现了IEnumerable<T>。
public IEnumerator<T> GetEnumerator()
{
return animals.GetEnumerator();
}
因为IEnumerable<T>继承自IEnumerable,所以还需要实现IEnumerable.GetEnumerator():
IEnumerator IEnumerable.GetEnumerator()
{
return animals.GetEnumerator();
}
之后,Farm<T>包含的两个方法利用了抽象类Animal的方法:
public void MakeNoises()
{
foreach (T animal in animals)
{
animal.MakeANoise();
}
}

public void FeedTheAnimals()
{
foreach (T animal in animals)
{
animal.Feed();
}
}
T约束为Animal,所以这段代码会正确编译—— 无论T是什么,都可以访问这些方法。
下一个方法GetCows()比较有趣。这个方法提取了集合中类型为Cow(或继承自Cow,例如新的SuperCoe类)的所有项:
public Farm<Cow> GetCows()
{
Farm<Cow> cowFarm = new Farm<Cow>();
foreach (T animal in animals)
{
if (animal is Cow)
{
cowFarm.Animals.Add(animal as Cow);
}
}
return cowFarm;
}
有趣的是,这个方法似乎有点浪费。如果以后希望有同一系列的其他方法,例如GetChickens(),也需要显式实现它们。在有许多类型的系统中,需要更多的方法。一个较好的解决方案是使用泛型方法,详见本章后面的内容。
Program.cs中的客户代码测试了各个方法,它并没有包含前面的许多代码,所以不需要深入探讨这些代码。
3. 从泛型类中继承
上面示例中的Farm<T>类以及本章前面介绍的其他几个类都继承自一个泛型类型。在Farm<T>中,这个类型是一个接口IEnumerable<T>。这里Farm<T>在T上提供的约束也会在IEnumerable<T>中使用的T上添加一个额外的约束。这可以用于限制未约束的类型,但是需要遵循一些规则。
首先,如果某个类型在它所继承的基类型中受到了约束,该类型就不能“解除约束”。也就是说,类型T在所继承的基类型中使用时,该类型必须受到至少与基类型相同的约束。例如,下面的代码是正确的:
class SuperFarm<T> : Farm<T>
where T : SuperCow
{
}
因为T在Farm<T>中约束为Animal,把它约束为SuperCow,就是把T约束为这些值的一个子集,所以这段代码可以正常运行。但是,不会编译下面的代码:
class SuperFarm<T> : Farm<T>
where T : struct
{
}
可以肯定地说,提供给SuperFarm<T>的类型T不能转换为可由Farm<T>使用的T,所以代码不会编译。甚至约束为超集的情况也会出现相同的问题:
class SuperFarm<T> : Farm<T>
where T : class
{
}

即使SuperFarm<T>允许有像Animal这样的类型,Farm<T>中也不允许有满足类约束的其他类型。否则编译就会失败。
这个规则适用于本章前面介绍的所有约束类型。
另外,如果继承了一个泛型类型,就必须提供所有必须的类型信息,这可以使用其他泛型类型参数的形式来提供,如上所述,也可以显式提供。这也适用于继承了泛型类型的非泛型类。例如:
public class Cards : List<Card>, ICloneable
{
}
这是可行的,但下面的代码会失败:
public class Cards : List<T>, ICloneable
{
}
没有提供T的信息,所以不能编译。
注意:
如果给泛型类型提供了参数,例如上面的List<Card>,就可以把类型引用为“关闭”。同样,继承List<T>,就是继承一个“打开”的泛型类型。
4. 泛型运算符
在C#中,可以像其他方法一样进行运算符的重写,这也可以在泛型类中实现。
例如,可以在Farm<T>中定义如下隐式的转换运算符:
public static implicit operator List<Animal>(Farm<T> farm)
{
List<Animal> result = new List<Animal>();
foreach (T animal in farm)
{
result.Add(animal);
}
return result;
}
这样,如果需要,就可以在Farm<T>中把Animal对象直接作为List<Animal>来访问。例如使用下面的运算符添加两个Farm<T>实例,这是很方便的:
public static Farm<T> operator +(Farm<T> farm1, List<T> farm2)
{
Farm<T> result = new Farm<T>();
foreach (T animal in farm1)
{
result.Animals.Add(animal);
}
foreach (T animal in farm2)
{
if (!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
return result;
}

public static Farm<T> operator +(List<T> farm1, Farm<T> farm2)
{
return farm2 + farm1;
}
接着,就可以添加Farm<Animal>和Farm<Cow>的实例,如下所示:
Farm<Animal> newFarm = farm + dairyFarm;
在这行代码中,dairyFarm(是Farm<Cow>的实例)隐式转换为List<Animal>,List<Animal>可以在Farm<T>中由重载运算符+使用。
读者可能认为,使用下面的代码也可以做到:
public static Farm<T> operator +(Farm<T> farm1, Farm<T> farm2)
{
Farm<T> result = new Farm<T>();
foreach (T animal in farm1)
{
result.Animals.Add(animal);
}
foreach (T animal in farm2)
{
if (!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
return result;
}
但是,Farm<Cow>不能转换为Farm<Animal>,所以汇总会失败。为了更进一步,可以使用下面的转换运算符来解决这个问题:
public static implicit operator Farm<Animal>(Farm<T> farm)
{
Farm <Animal> result = new Farm <Animal>();
foreach (T animal in farm)
{
result.Animals.Add(animal);
}
return result;
}

使用这个运算符,Farm<T>的实例如Farm<Cow>就可以转换为Farm<Animal>的实例,这解决了上面的问题。所以,可以使用上面列出的两种方法,但是后者更适合,因为它比较简单。这两种方法都包含在本章的示例代码中。
5. 泛型结构
前几章说过,结构实际上与类相同,只是有一些微小的区别,而且结构是值类型,不是引用类型。所以,泛型结构可以用与泛型类相同的方式来创建。例如:
public struct MyStruct<T1, T2>
{
public T1 item1;
public T2 item2;
}
12.3.2 定义泛型接口
前面介绍了几个泛型接口,它们都位于Systems.Collections.Generic命名空间中,例如上一个示例中使用的IEnumerable<T>。
定义泛型接口与定义泛型类所使用的技术相同,例如:
interface MyFarmingInterface<T>
where T : Animal
{
bool AttemptToBreed(T animal1, T animal2);

T OldestInHerd
{
get;
}
}
其中,泛型参数T用作AttemptToBreed()的两个变元的类型和OldestInHerd属性的类型。
其继承规则与类相同。如果继承了一个基泛型接口,就必须遵循“保持基接口泛型类型参数的约束”等规则。
12.3.3 定义泛型方法
在上一个示例中提到了方法GetCows(),在讨论这个示例时也提到,可以使用泛型方法得到这个方法的更一般形式。本节就说明如何达到这一目标。在泛型方法中,返回类型和参数类型由泛型类型参数来确定。例如:
public T GetDefault<T>()
{
return default(T);
}
这个小示例使用本章前面介绍的default关键字,为类型T返回默认值。
这个方法的调用如下所示:
int myDefaultInt = GetDefault<T>();
在调用该方法时提供了类型参数T。
注意,这个T与用于给类提供泛型类型参数的类型完全不同。实际上,泛型方法可以通过非泛型类来实现:
public class Defaulter
{
public T GetDefault<T>()
{
return default(T);
}
}
但如果类是泛型的,就必须为泛型方法类型使用不同的标识符。下面的代码不会编译:
public class Defaulter<T>
{
public T GetDefault<T>()
{
return default(T);
}
}
必须重命名方法或类使用的T。
泛型方法参数可以以与类相同的方式使用约束,在此可以使用任意类类型参数,例如:
public class Defaulter<T1>
{
public T2 GetDefault<T2>()
where T2 : T1
{
return default(T2);
}
}
其中,为方法提供的类型T2必须与给类提供的T1相同,或者继承自T1。这是约束泛型方法的常用方式。
在前面的Farm<T>类中,可以包含下面的方法(在Ch12Ex04的下载代码中包含它们,但加上了注释)。
public Farm<U> GetSpecies<U>() where U : T
{
Farm<U> speciesFarm = new Farm<U>();
foreach (T animal in animals)
{
if (animal is U)
{
speciesFarm.Animals.Add(animal as U);
}
}
return speciesFarm;
}
这可以替代GetCows()和相同类型的其他方法。这里使用的泛型类型参数U由T约束,T又由Farm<T>类约束为Animal。因此,如果愿意,可以把T的实例看作Animal的实例。
在Ch12Ex04的客户代码Program.cs中,使用这个新方法需要进行一处修改:
Farm<Cow> dairyFarm = farm.GetSpecies<Cow>();
也可以编写如下代码:
Farm<Chicken> dairyFarm = farm.GetSpecies<Chicken>();
或者继承了Animal的其他类。
这里要注意,方法有泛型类型参数,会改变该方法的签名。也就是说,该方法有几个重载,它们仅在泛型类型参数上有区别。例如:
public void ProcessT<T>(T op1)
{
...
}

public void ProcessT<T, U>(T op1)
{
...
}
使用哪个方法取决于调用方法时指定的泛型类型参数的个数。
12.3.4 定义泛型委托
最后一个要介绍的泛型类型是泛型委托。本章前面在介绍如何排序和搜索泛型列表时就介绍过它们,即分别为排序和搜索使用了Comparison<T>和Predicate<T>委托。
第7章介绍了如何使用方法的签名、delegate关键字和委托名来定义委托,例如:
public delegate int MyDelegate(int op1, int op2);
要定义泛型委托,只需声明和使用一个或多个泛型类型参数,例如:
public delegate T1 MyDelegate<T1, T2>(T2 op1, T2 op2) where T1 : T2;
可以看出,这里也可以使用约束,其规则也与前面一样。
下一章将更详细地介绍委托,了解在常见的C#编程技术即“事件”中如何使用它们。

 

            12.4 小结

 

本章学习了:
● 如何在C#中使用泛型类型。
● 如何使用结构,包括创建可空类型,使用System.Collecitons.Generic命名空间中的类。
● 如何创建自己的泛型类型,包括类、接口、方法和委托。

泛型是C#中一项非常有用的新技术,使用它们创建的类可以同时达到多种目的,并可以在许多不同的情况下使用。即使没有必要创建自己的泛型类型,也可以使用泛型集合类。
下一章将研究其他基本知识,探讨事件,完成基本C#语言的讨论。
12.5 练习
(1) 下面哪个是泛型?
a. 类
b. 方法
c. 属性
d. 运算符重载
e. 结构
f. 枚举
(2) 扩展Ch12Ex01中的Vector类,使*运算符返回两个矢量的点积。
注意:
两个矢量的点积定义为两个矢量的大小与两个矢量之间夹角余弦的乘积。
(3) 下面的代码有什么错误?修改它。
public class Instantiator<T>
{
public T instance;

public Instantiator()
{
instance = new T();
}
}
(4) 下面的代码有什么错误?修改它。
public class StringGetter<T>
{
public string GetString<T>(T item)
{
return item.ToString();
}
}
(5) 创建一个泛型类ShortCollection<T>,它实现了IList<T>,包含一个项集合及集合最大的容量。这个最大的容量应是一个整数,并可以提供给ShortCollection<T>的构造函数,或者默认为10。构造函数还应通过List<T>参数获取项的最初列表。该类与Collection<T>的功能一样,但如果试图给集合添加太多的项,或者传递给构造函数的List<T>包含太多的项,就会抛出IndexOutOfRangeException类型的异常。

 

 6.7 小结

 

本章相当全面地介绍了C#代码中函数的使用。函数提供的许多其他特性(特别是委托)比较抽象,我们将在面向对象编程中讨论它们,面向对象编程将在第8章讨论。
本章的主要内容总结如下:
● 在控制台应用程序中定义和使用函数
● 通过返回值和参数,与函数交换数据
● 给函数传送参数数组
● 按引用或按值传递参数
● 为其他返回值指定参数
● 变量作用域的概念,变量在不需要它们的代码块中可以隐藏起来
● Main()函数的细节,包括命令行参数的用法
● 在结构类型中使用函数
● 函数的重载,可以为同一个函数提供不同的参数,获得其他功能
● 委托以及如何在运行期间动态地选择函数
如何使用函数的知识是将来要完成的所有编程工作的中心。后面的章节,特别是学习OOP(从第8章开始)的部分,将介绍函数的形式结构,以及如何把它们应用于类。从现在开始,把代码放在可重用块中将成为C#编程中最有用的部分。
6.8 练习
(1) 下面两个函数都有错误,请指出这些错误。
static bool Write()
{
Console.WriteLine("Text output from function.");
}

static void myFunction(string label, params int[] args, bool showLabel)
{
if (showLabel)
Console.WriteLine(label);
foreach (int i in args)
Console.WriteLine("{0}", i);
}
(2) 编写一个应用程序,该程序使用两个命令行参数,分别把值放在一个字符串和一个整型变量中,然后显示这些值。
(3) 创建一个委托,在请求用户输入时,使用它模拟Console.ReadLine()函数。
(4) 修改下面的结构,使之包含一个返回订单总价格的函数。
struct order
{
public string itemName;
public int unitCount;
public double unitCost;
}
(5) 在order结构中添加另一个函数,该结构返回一个格式化的字符串,以合适的值替换用尖括号括起来的斜体条目。
Order Information: <unit count> <item name> items at $<unit cost> each, total cost $<total cost>

 

 

http://www.welan.com/zhuanti/060513/NO4.asp
posted on 2006-07-05 11:30  JusticFu  阅读(1658)  评论(0编辑  收藏  举报