泛型.docx
- 文档编号:24297372
- 上传时间:2023-05-26
- 格式:DOCX
- 页数:50
- 大小:41.25KB
泛型.docx
《泛型.docx》由会员分享,可在线阅读,更多相关《泛型.docx(50页珍藏版)》请在冰豆网上搜索。
泛型
12.1泛型的概念
为了介绍泛型的概念,说明它们为什么这么有用,先回忆一下第11章中的集合类。
基本集合可以包含在类似ArrayList这样的类中,但这些集合是没有类型化的,所以需要把object项转换为集合中实际存储的对象类型。
继承自System.Object的任何对象都可以存储在ArrayList中,所以要特别仔细。
假定包含在集合中的某些类型可能导致抛出异常,代码逻辑崩溃。
前面介绍的技术可以处理这个问题,包括检查对象类型所需的代码。
但是,更好的解决办法是一开始就使用强类型化的集合类。
这种集合类派生于CollectioonBase,并可以拥有自己的方法,来添加、输出和访问集合的成员,但它可能把集合成员限制为派生于某种基本类型,或者必须支持某个接口。
这会带来一个问题。
每次创建需要包含在集合中的新类时,就必须执行下述任务之一:
●使用某个集合类,该类已经定义为可以包含新类型的项。
●创建一个新的集合类,它可以包含新类型的项,实现所有需要的方法。
一般情况下,新的类型需要额外的功能,我们常常并不需要新的集合类,创建集合类也会花费大量时间。
另一方面,泛型类大大简化了这个问题。
泛型类是以实例化过程中提供的类型或类为基础建立的,可以毫不费力地对对象进行强类型化。
对于集合,创建“T类型对象的集合”只需把它用一行代码写出来即可。
不使用下面的代码:
CollectionClasscol=newCollectionClass();
col.Add(newItemClass());
而可以使用:
CollectionClass
col.Add(newItemClass());
尖括号语法就是把变量类型传送给泛型类型的方式。
在上面的代码中,应把CollectionClass
当然,本章后面会详细探讨这个语法。
前面的泛型只涉及到集合,实际上泛型非常适合于这个领域,本章在后面介绍System.Collection.Generic命名空间时会提及。
创建一个泛型类,就可以生成一些方法,它们的签名可以强类型化为我们需要的任何类型,该类型甚至可以是值类型或引用类型,处理各自的操作。
还可以把用于实例化泛型类的类型限制为支持某个给定的接口,或派生自某种类型,只允许使用类型的一个子集。
泛型并不限于类,还可以创建泛型接口、泛型方法(可以在非泛型类上定义),甚至泛型委托。
这将大大提高代码的灵活性,正确使用泛型可以显著缩短开发时间。
那么该如何实现泛型呢?
通常,在创建类时,它会编译为一个类型,然后在代码中使用。
读者可能认为,在创建泛型类时,它必须编译为许多类型,才能进行实例化。
幸好并不是这样:
在.NET中,类有无限多个。
在后台,.NET运行库允许在需要时动态生成泛型类。
在通过实例化来请求生成之前,B的某个泛型类A甚至不存在。
注意:
对于熟悉C++或者对C++感兴趣的读者来说,这是C++模板和C#泛型类的一个区别。
在C++中,编译器可以检测出在哪里使用了模板的某个特定类型,例如模板B的A类型,然后编译需要的代码,来创建这个类型。
而在C#中,所有的操作都在运行期间进行。
总之,泛型允许灵活地创建类型,处理一种或多种特定类型的对象,这些类型是在实例化时确定的,否则就使用泛型类型。
下面看看它们的使用。
6.2变量的作用域
在上一节中,读者可能想知道为什么需要利用函数交换数据。
原因是C#中的变量仅能从代码的本地作用域访问。
给定的变量有一个作用域,访问该变量要通过这个作用域来实现。
变量的作用域是一个重要的主题,最好用一个示例来说明。
下面的示例将演示变量在一个作用域中定义,但试图在另一个作用域中使用的情形。
试试看:
定义和使用基本函数
(1)对Ch06Ex01中的Program.cs进行如下修改:
classProgram
{
staticvoidWrite()
{
Console.WriteLine("myString={0}",myString);
}
staticvoidMain(string[]args)
{
stringmyString="StringdefinedinMain()";
Write();
Console.ReadKey();
}
}
(2)编译代码,注意显示在任务列表中的错误和警告:
Thename'myString'doesnotexistinthecurrentcontext
Thevariable'myString'isassignedbutitsvalueisneverused
示例的说明
什么地方出错了?
在应用程序主体(Main()函数)中定义的变量myString不能在Write()函数中访问。
原因是变量有一个作用域,在这个作用域中,变量才是有效的。
这个作用域包括定义变量的代码块和直接嵌套在其中的代码块。
函数中的代码块与调用它们的代码块是不同的。
在Write()中,没有定义myString,在Main()中定义的myString则超出了作用域——它只能在Main()中使用。
实际上,在Write()中可以有一个完全独立的变量myString,修改代码,如下所示:
classProgram
{
staticvoidWrite()
{
stringmyString="StringdefinedinWrite()";
Console.WriteLine("NowinWrite()");
Console.WriteLine("myString={0}",myString);
}
staticvoidMain(string[]args)
{
stringmyString="StringdefinedinMain()";
Write();
Console.WriteLine("\nNowinMain()");
Console.WriteLine("myString={0}",myString);
Console.ReadKey();
}
}
这段代码执行的操作如下:
●Main()定义和初始化字符串变量myString。
●Main()把控制权传送给Write()。
●Write()定义和初始化一个字符串变量myString,它与Main()中定义的myString变量完全不同。
●Write()把一个字符串输出到控制台上,该字符串包含在Write()中定义的myString的值。
●Write()把控制权传送回Main()。
●Main()把一个字符串输出到控制台上,该字符串包含在Main()中定义的myString的值。
作用域以这种方式覆盖一个函数的变量称为局部变量。
还有一种全局变量,其作用域可覆盖几个函数。
修改代码,如下所示:
classProgram
{
staticstringmyString;
staticvoidWrite()
{
stringmyString="StringdefinedinWrite()";
Console.WriteLine("NowinWrite()");
Console.WriteLine("LocalmyString={0}",myString);
Console.WriteLine("GlobalmyString={0}",Program.myString);
}
staticvoidMain(string[]args)
{
stringmyString="StringdefinedinMain()";
Program.myString="Globalstring";
Write();
Console.WriteLine("\nNowinMain()");
Console.WriteLine("LocalmyString={0}",myString);
Console.WriteLine("GlobalmyString={0}",Program.myString);
Console.ReadKey();
}
}
这里添加了另一个变量myString,这次进一步加深了代码中的名称层次。
这个变量定义如下:
staticstringmyString;
注意这里也需要static关键字。
在这种形式的控制台应用程序中,必须使用static或const关键字,来定义这种形式的全局变量。
如果要修改全局变量的值,就需要使用static,因为const禁止修改变量的值。
为了区分这个变量和Main()与Write()中同名的局部变量,必须用一个完整限定的名称为变量名分类,参见第3章。
这里把全局变量称为Program.myString。
注意,在全局变量和局部变量同名时,这是必需的。
如果没有局部myString变量,就可以使用myString表示全局变量,而不需要使用Program.myString。
如果局部变量和全局变量同名,全局变量就会被屏蔽。
全局变量的值在Main()中设置如下:
Program.myString="Globalstring";
在Write()中访问:
Console.WriteLine("GlobalmyString={0}",Program.myString);
为什么不能使用这个技术通过函数交换数据,而要使用前面介绍的参数来交换数据?
有时,这确实是一种交换数据的首选方式,但在许多情况下不应使用这种方式。
是否使用全局变量取决于函数的位置。
使用全局变量的问题在于,它们一般不适合于“常规用途”的函数——这些函数能处理我们所提供的数据,而不仅限于处理特定全局变量中的数据。
详见本章后面的内容。
6.2.1其他结构中变量的作用域
在继续之前,应先注意一下上一节的一个要点总结了上述内容,并超出了函数之间的变量作用域。
前面说过,变量的作用域包含定义它们的代码块和直接嵌套在其中的代码块。
这也可以应用到其他代码块上,例如分支和循环结构的代码块。
考虑下面的代码:
inti;
for(i=0;i<10;i++)
{
stringtext="Line"+Convert.ToString(i);
Console.WriteLine("{0}",text);
}
Console.WriteLine("Lasttextoutputinloop:
{0}",text);
字符串变量text是for循环的局部变量,这段代码不能编译,因为在该循环外部调用的Console.WriteLine()试图使用该变量text,这超出了循环的作用域。
修改代码,如下所示:
inti;
stringtext;
for(i=0;i<10;i++)
{
text="Line"+Convert.ToString(i);
Console.WriteLine("{0}",text);
}
Console.WriteLine("Lasttextoutputinloop:
{0}",text);
这段代码也会失败,原因是变量必须在使用前声明和初始化,而text是在for循环中初始化的。
赋给text的值在循环块退出时就丢失了。
但是还可以进行如下修改:
inti;
stringtext="";
for(i=0;i<10;i++)
{
text="Line"+Convert.ToString(i);
Console.WriteLine("{0}",text);
}
Console.WriteLine("Lasttextoutputinloop:
{0}",text);
这次text是在循环外部初始化的,可以访问它的值。
这段简单代码的结果如图6-6所示。
在循环中最后赋给text的值可以在循环外部访问。
可以看出,这个主题的内容需要花一点时间来掌握。
在前面的示例中,循环之前赋给text空字符串,而在循环之后的代码中,该text就不会是空字符串了,其原因不能立即看出。
这种情况的解释涉及到分配给text变量的内存空间,实际上任何变量都是这样。
只声明一个简单的变量类型,并不会引起其他的变化。
只有在给变量赋值后,这个值才占用一块内存空间。
如果这种占据内存空间的行为在循环中发生,该值实际上定义为一个局部值,在循环的外部会超出了其作用域。
即使变量本身没有局部化到循环上,循环所包含的值也局部化到该循环上。
但是,在循环外部赋值可以确保该值是主体代码的局部值,在循环内部它仍处于其作用域中。
这意味着变量在退出主体代码块之前是没有超出作用域的,所以可以在循环外部访问它的值。
幸而,C#编译器可检测变量作用域的问题,它生成的响应错误信息可以帮助我们理解变量作用域的问题。
最后一个要注意的问题是,应采用“最佳实践”。
一般情况下,最好在声明和初始化所有的变量后,再在代码块中使用它们。
一个例外是把循环变量声明为循环块的一部分,例如:
for(inti=0;i<10;i++)
{
...
}
其中i局部化于循环代码块中,但这是可以的,因为我们很少需要在外部代码中访问这个计数器。
6.2.2参数和返回值与全局数据
本节详细介绍如何通过全局数据以及参数和返回值,与函数交换数据。
先看看下面的代码:
classProgram
{
staticvoidshowDouble(refintval)
{
val*=2;
Console.WriteLine("valdoubled={0}",val);
}
staticvoidMain(string[]args)
{
intval=5;
Console.WriteLine("val={0}",val);
showDouble(refval);
Console.WriteLine("val={0}",val);
}
}
注意:
这段代码与本章前面的代码略有不同,在前面的示例中,在Main()中使用了变量名myNumber,这说明了局部变量可以有相同的名称,且不会相互干涉。
这里列出的两个代码示例比较类似,以便我们集中精力研究它们的区别,而无需担心变量名。
和下面的代码比较:
classProgram
{
staticintval;
staticvoidshowDouble()
{
val*=2;
Console.WriteLine("valdoubled={0}",val);
}
staticvoidMain(string[]args)
{
val=5;
Console.WriteLine("val={0}",val);
showDouble();
Console.WriteLine("val={0}",val);
}
}
这两个showDouble()函数的结果是相同的。
现在,使用哪种方法并没有什么硬性规定,这两种方法都是有效的。
但是,需要考虑一些规则。
首先,在第一次讨论这个问题时,使用全局值的showDouble()版本只使用全局变量val。
为了使用这个版本,必须使用这个全局变量。
这会对该函数的多样性有轻微的限制,如果要存储结果,就必须总是把这个全局变量值复制到其他变量中。
另外,全局数据可以在应用程序的其他地方由代码修改,这会导致预料不到的结果(即使我们没有认识到这一点,值也是可以改变的)。
但是,损失了多样性常常是有好处的。
我们常常希望把一个函数只用于一个目的,使用全局数据存储能减少在函数调用中犯错的可能性,例如把它传递给错误的变量。
当然,也可以说,这种简化实际上使代码更难理解。
显示指定参数可以一眼看出发生了什么改变。
例如myFunction(val1,outval2)函数调用,其中val1和val2都是要考虑的重要变量,在函数执行结束后,val2就会被赋予一个新值。
反之,如果这个函数不带参数,就不能对它处理了什么数据做任何假设。
最后,记住并不总是能使用全局数据。
本书的后面将介绍在不同的文件中编写的代码,以及不同命名空间中的代码如何通过函数彼此通信。
像这样的情况,代码常常要分开编写,显然不能使用全局存储方式。
总之,可以自由选择使用哪种技术来交换数据。
一般情况下,最好使用参数,而不使用全局数据,但有时使用全局数据更合适,使用这个技术并没有错。
6.3Main()函数
前面介绍了创建和使用函数时涉及的大多数简单技术,下面详细论述Main()函数。
Main()是C#应用程序的入口点,执行这个函数就是执行应用程序。
也就是说,在执行过程开始时,会执行Main()函数,在Main()函数执行完毕时,执行过程就结束了。
这个函数有一个参数string[]args,但我们还没有说明这个参数的含义。
本节将介绍该参数,以及如何使用它。
注意:
Main函数可以使用4种签名:
●staticvoidMain()
●staticvoidMain(string[]args)
●staticintMain()
●staticintMain(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中:
classProgram
{
staticvoidMain(string[]args)
{
Console.WriteLine("{0}commandlineargumentswerespecified:
",
args.Length);
foreach(stringarginargs)
Console.WriteLine(arg);
Console.ReadKey();
}
}
(3)打开项目的属性页面(在SolutionExplorer窗口中右击Ch06Ex04项目名称,选择Properties)。
(4)选择Debug页面,在CommandLineArguments设置中添加所希望的命令行参数,如图6-7所示。
(5)运行应用程序,结果如图6-8所示。
示例的说明
这里使用的代码非常简单:
Console.WriteLine("{0}commandlineargumentswerespecified:
",
args.Length);
foreach(stringarginargs)
Console.WriteLine(arg);
使用args参数与使用其他字符串数组类似。
我们没有对参数进行任何异样的操作,只是把指定的信息写到屏幕上。
在本示例中,通过VS中的项目属性提供参数,这是一种很便捷的方式,只要在VS中运行应用程序,就可以使用相同的命令行参数,无需每次都在命令行提示窗口中输入它们。
在项目输出所在的目录(C:
\BegVCSharp\Chapter6\Ch06Ex04\bin\Debug)下打开命令行窗口,输入下述代码,也可以得到相同的结果:
Ch06Ex04256myFile.txt"alongerargument"
注意,每个参数都用空格分隔开,如果参数包含空格,就可以用双引号把参数括起来,这样才不会把这个参数解释为多个参数。
12.2使用泛型
在探讨如何创建自己的泛型之前,先介绍.NETFramework提供的泛型,包括System.Collection.Generic命名空间中的类型,这个命名空间已在前面的代码中出现过多次,因为默认情况下它包含在控制台应用程序中。
我们还没有使用过这个命名空间中的类型,但下面就要使用了。
本节将讨论这个命名空间中的类型,以及如何使用它们创建强类型化的集合,提高已有集合的功能。
首先论述另一个较简单的泛型类型(nullabletype):
可空类型,解决值类型的一个小问题。
12.2.1可空类型
在前面的章节中,介绍了值类型(大多数基本类型,例如int、double和所有的结构)区别于引用类型(string和所有的类)的一种方式:
值类型必须包含一个值,它们可以在声明之后、赋值之前,在未赋值的状态下存在,但不能以任何方式使用。
而引用类型可以是null。
有时让值类型为空是很有用的,泛型使用System.Nullable
例如:
System.Nullable
这行代码声明了一个变量nullableInt,它可以拥有int变量能包含的任意值,还可以拥有值null。
所以可以编写下面的代码:
nullableInt=null;
如果nullableInt是一个int类型的变量,上面的代码是不能编译的。
前面的赋值等价于:
nullableInt=newSystem.Nullable
与其他变量一样,无论是初始化为null(使用上面的语法),还是通过给它赋值来初始化,都不能在初始化之前使用它。
可以像测试引用类型一样,测试可空类型,看看它们是否为null:
if(nullableInt==null)
{
...
}
另外,可以使用HasValue属性:
if(nullableInt.HasValue)
{
...
}
这不适用于引用类型,即使引用类型有一个HasValue属性,也不能使用这种方法,因为引用类型的变量值为null,就表示不存在对象,当然就不能通过对象来访问这个属性,此时会抛出一个异常。
使用Value属性可以查看引用类型的值。
如果HasValue是true,就说明Value属性有一
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 泛型