深入浅出NET泛型编程.docx
《深入浅出NET泛型编程.docx》由会员分享,可在线阅读,更多相关《深入浅出NET泛型编程.docx(16页珍藏版)》请在冰豆网上搜索。
![深入浅出NET泛型编程.docx](https://file1.bdocx.com/fileroot1/2022-12/7/99c58e3b-7960-466a-bdca-62c9b86bc9b0/99c58e3b-7960-466a-bdca-62c9b86bc9b01.gif)
深入浅出NET泛型编程
深入浅出.NET泛型编程
前言
.NET2.0中泛型的出现是一个令人激动的特征。
但是,什么是泛型?
你需要它们吗?
你会在自己的应用软件中使用它们?
在本文中,我们将回答这些问题并细致地分析泛型的使用,能力及其局限性。
类型安全
.NET中的许多语言如C#,C++和VB.NET(选项strict为on)都是强类型语言。
作为一个程序员,当你使用这些语言时,总会期望编译器进行类型安全的检查。
例如,如果你把对一个Book类型的引用转换成一个Vehicle型的引用,编译器将告诉你这样的cast是无效的。
然而,当谈到.NET1.0和1.1中的集合时,它们是无助于类型安全的。
请考虑一个ArrayList的例子,它拥有一个对象集合--这允许你把任何类型的对象放于该ArrayList中。
让我们看一下例1中的代码。
例1.缺乏类型安全的ArrayList
usingSystem;
usingSystem.Collections;
namespaceTestApp
{
classTest
{
[STAThread]
staticvoidMain(string[]args)
{
ArrayListlist=newArrayList();
list.Add(3);
list.Add(4);
//list.Add(5.0);
inttotal=0;
foreach(intvalinlist)
{
total=total+val;
}
Console.WriteLine("Totalis{0}",total);
}
}
}
本例中,我们建立了一个ArrayList的实例,并把3和4添加给它。
然后我循环遍历该ArrayList,从中取出整型值然后把它们相加。
这个程序将产生结果"Totalis7"。
现在,如果我注释掉下面这句:
list.Add(5.0);
程序将产生如下的运行时刻异常:
UnhandledException:
System.InvalidCastException:
Specifiedcastisnotvalid.
AtTestApp.Test.Main(String[]args)in:
\workarea\testapp\class1.cs:
line17
哪里出错了呢?
记住ArrayList拥有一个集合的对象。
当你把3加到ArrayList上时,你已把值3装箱了。
当你循环该列表时,你是把元素拆箱成int型。
然而,当你添加值5.0时,你在装箱一个double型值。
在第17行,那个double值被拆箱成一个int型。
这就是失败的原因。
注意:
上面的实例,如果是用VB.NET书写的话,是不会失败的。
原因在于,VB.NET不使用装箱机制,它激活一个把该double转换成整型的方法。
但是,如果ArrayList中的值是不能转换成整型的,VB.NET代码还会失败。
作为一个习惯于使用语言提供的类型安全的程序员,你希望这样的问题在编译期间浮出水面,而不是在运行时刻。
这正是泛型产生的原因。
3.什么是泛型?
泛型允许你在编译时间实现类型安全。
它们允许你创建一个数据结构而不限于一特定的数据类型。
然而,当使用该数据结构时,编译器保证它使用的类型与类型安全是相一致的。
泛型提供了类型安全,但是没有造成任何性能损失和代码臃肿。
在这方面,它们很类似于C++中的模板,不过它们在实现上是很不同的。
4.使用泛型集合
.NET2.0的System.Collections.Generics命名空间包含了泛型集合定义。
各种不同的集合/容器类都被"参数化"了。
为使用它们,只需简单地指定参数化的类型即可。
请看例2:
例2.类型安全的泛型列表
List<int>aList=newList<int>();
aList.Add(3);
aList.Add(4);
//aList.Add(5.0);
inttotal=0;
foreach(intvalinaList)
{
total=total+val;
}
Console.WriteLine("Totalis{0}",total);
在例2中,我编写了一个泛型的列表的例子,在尖括号内指定参数类型为int。
该代码的执行将产生结果"Totalis7"。
现在,如果我去掉语句doubleList.Add(5.0)的注释,我将得到一个编译错误。
编译器指出它不能发送值5.0到方法Add(),因为该方法仅接受int型。
不同于例1,这里的代码实现了类型安全。
5.CLR对于泛型的支持
泛型不仅是一个语言级上的特征。
.NETCLR能识别出泛型。
在这种意义上说,泛型的使用是.NET中最为优秀的特征之一。
对每个用于泛型化的类型的参数,类也同样没有脱离开微软中间语言(MSIL)。
换句话说,你的配件集仅包含你的参数化的数据结构或类的一个定义,而不管使用多少种不同的类型来表达该参数化的类型。
例如,如果你定义一个泛型类型MyList<T>,仅仅该类型的一个定义出现在MSIL中。
当程序执行时,不同的类被动态地创建,每个类对应该参数化类型的一种类型。
如果你使用MyList<int>和MyList<double>,有两种类即被创建。
当你的程序执行时,让我们进一步在例3中分析这一点。
例3.创建一个泛型类
//MyList.cs
#regionUsingdirectives
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
#endregion
namespaceCLRSupportExample
{
publicclassMyList<T>
{
privatestaticintobjCount=0;
publicMyList()
{objCount++;}
publicintCount
{
get
{returnobjCount;}
}
}
}
//Program.cs
#regionUsingdirectives
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
#endregion
namespaceCLRSupportExample
{
classSampleClass{}
classProgram
{
staticvoidMain(string[]args)
{
MyList<int>myIntList=newMyList<int>();
MyList<int>myIntList2=newMyList<int>();
MyList<double>myDoubleList=newMyList<double>();
MyList<SampleClass>mySampleList=newMyList<SampleClass>();
Console.WriteLine(myIntList.Count);
Console.WriteLine(myIntList2.Count);
Console.WriteLine(myDoubleList.Count);
Console.WriteLine(mySampleList.Count);
Console.WriteLine(newMyList<sampleclass>().Count);
Console.ReadLine();
}
}
}
该例中,我创建了一个称为MyList泛型类。
为把它参数化,我简单地插入了一个尖括号。
在<>内的T代表了实际的当使用该类时要指定的类型。
在MyList类中,定义了一个静态字段objCount。
我在构造器中增加它的值。
因此我能发现使用我的类的用户共创建了多少个那种类型的对象。
属性Count返回与被调用的实例同类型的实例的数目。
在Main()方法,我创建了MyList<int>的两个实例,一个MyList<double>的实例,还有两个MyList<SampleClass>的实例--其中SampleClass是我已定义了的类。
问题是:
Count(上面的程序的输出)的值该是多少?
在你继阅读之前,试一试回答这个问题。
解决了上面的问题?
你得到下列的答案了吗?
2
2
1
1
2
前面两个2对应MyList<int>,第一个1对应MyList<double>,第二个1对应MyList<SampleClass>--在此,仅创建一个这种类型的实例。
最后一个2对应MyList<SampleClass>,因为代码中又创建了这种类型的另外一个实例。
上面的例子说明MyList<int>是一个与MyList<double>不同的类,而MyList<double>又是一个与MyList<SampleClass>不同的类。
因此,在这个例中,我们有四个类:
MyList:
MyList<T>,MyList<int>,MyList<double>和MyList<X>。
注意,虽然有4个MyList类,但仅有一个被存储在MSIL。
怎么能证明这一点?
请看图1显示出的使用工具ildasm.exe生成的MSIL代码。
图1.例3的MSIL
6.泛型方法
除了有泛型类,你也可以有泛型方法。
泛型方法可以是任何类的一部分。
让我们看一下例4:
例4.一个泛型方法
publicclassProgram
{
publicstaticvoidCopy<T>(List<T>source,List<T>destination)
{
foreach(Tobjinsource)
{
destination.Add(obj);
}
}
staticvoidMain(string[]args)
{
List<int>lst1=newList<int>();
lst1.Add
(2);
lst1.Add(4);
List<int>lst2=newList<int>();
Copy(lst1,lst2);
Console.WriteLine(lst2.Count);
}
}
Copy()方法就是一个泛型方法,它与参数化的类型T一起工作。
当在Main()中激活Copy()时,编译器根据提供给Copy()方法的参数确定出要使用的具体类型。
7.无限制的类型参数
如果你创建一个泛型数据结构或类,就象例3中的MyList,注意其中并没有约束你该使用什么类型来建立参数化类型。
然而,这带来一些限制。
如,你不能在参数化类型的实例中使用象==,!
=或<等运算符,如:
if(obj1==obj2)…
象==和!
=这样的运算符的实现对于值类型和引用类型都是不同的。
如果随意地允许之,代码的行为可能很出乎你的意料。
另外一种限制是缺省构造器的使用。
例如,如果你编码象newT(),会出现一个编译错,因为并非所有的类都有一个无参数的构造器。
如果你真正编码象newT()来创建一个对象,或者使用象==和!
=这样的运算符,情况会是怎样呢?
你可以这样做,但首先要限制可被用于参数化类型的类型。
读者可以自己先考虑如何实现之。
8.约束机制及其优点
一个泛型类允许你写自己的类而不必拘泥于任何类型,但允许你的类的使用者以后可以指定要使用的具体类型。
通过对可能会用于参数化的类型的类型施加约束,这给你的编程带来很大的灵活性--你可以控制建立你自己的类。
让我们分析一个例子:
例5.需要约束:
代码不会编译成功
publicstaticTMax<T>(Top1,Top2)
{
if(op1.CompareTo(op2)<0)
returnop1;
returnop2;
}
例5中的代码将产生一个编译错误:
Error1’T’doesnotcontainadefinitionfor’CompareTo’
假定我需要这种类型以支持CompareTo()方法的实现。
我能够通过加以约束--为参数化类型指定的类型必须要实现IComparable接口--来指定这一点。
例6中的代码就是这样:
例6.指定一个约束
publicstaticTMax<T>(Top1,Top2)whereT:
IComparable
{
if(op1.CompareTo(op2)<0)
returnop1;
returnop2;
}
在例6中,我指定的约束是,用于参数化类型的类型必须继承自(实现)Icomparable。
下面的约束是可以使用的:
whereT:
struct类型必须是一种值类型(struct)
whereT:
class类型必须是一种引用类型(class)
whereT:
new()类型必须有一个无参数的构造器
whereT:
class_name类型可以是class_name或者是它的一个子类
whereT:
interface_name类型必须实现指定的接口
你可以指定约束的组合,就象:
whereT:
IComparable,new()。
这就是说,用于参数化类型的类型必须实现Icomparable接口并且必须有一个无参构造器。
9.继承与泛型
一个使用参数化类型的泛型类,象MyClass1<T>,称作开放结构的泛型。
一个不使用参数化类型的泛型类,象MyClass1<int>,称作封闭结构的泛型。
你可以从一个封闭结构的泛型进行派生;也就是说,你可以从另外一个称为MyClass1的类派生一个称为MyClass2的类,就象:
publicclassMyClass2<T>:
MyClass1<int>
你也可以从一个开放结构的泛型进行派生,如果类型被参数化的话,如:
publicclassMyClass2<T>:
MyClass2<T>
是有效的,但是
publicclassMyClass2<T>:
MyClass2<Y>
是无效的,这里Y是一个被参数化的类型。
非泛型类可以从一个封闭结构的泛型类进行派生,但是不能从一个开放结构的泛型类派生。
即:
publicclassMyClass:
MyClass1<int>
是有效的,但是
publicclassMyClass:
MyClass1<T>
是无效的。
10.泛型和可代替性
当我们使用泛型时,要小心可代替性的情况。
如果B继承自A,那么在使用对象A的地方,可能都会用到对象B。
假定我们有一篮子水果(aBasketofFruits(Basket<Fruit>)),而且有继承自Fruit的Apple和Banana(皆为Fruit的种类)。
一篮子苹果--BasketofApples(Basket<apple>)可以继承自BasketofFruits(Basket<Fruit>)?
答案是否定的,如果我们考虑一下可代替性的话。
为什么?
请考虑一个aBasketofFruits可以工作的方法:
publicvoidPackage(Basket<Fruit>aBasket)
{
aBasket.Add(newApple());
aBasket.Add(newBanana());
}
如果发送一个Basket<Fruit>的实例给这个方法,这个方法将添加一个Apple对象和一个Banana对象。
然而,发送一个Basket<Apple>的实例给这个方法时,会是什么情形呢?
你看,这里充满技巧。
这解释了为什么下列代码:
Basket<Apple>anAppleBasket=newBasket<Apple>();
Package(anAppleBasket);
会产生错误:
Error2Argument’1’:
cannotconvertfrom’TestApp.Basket<testapp.apple>’
to’TestApp.Basket<testapp.fruit>’
编译器通过确保我们不会随意地传递一个集合的派生类(此时需要一个集合的基类),保护了我们的代码。
这不是很好吗?
这在上面的例中在成功的,但也存在特殊情形:
有时我们确实想传递一个集合的派生类,此时需要一个集合的基类。
例如,考虑一下Animal(如Monkey),它有一个把Basket<Fruit>作参数的方法Eat,如下所示:
publicvoidEat(Basket<Fruit>fruits)
{
foreach(FruitaFruitinfruits)
{
//将吃水果的代码
}
}
现在,你可以调用:
Basket<Fruit>fruitsBasket=newBasket<Fruit>();
…//添加到Basket对象中的对象Fruit
anAnimal.Eat(fruitsBasket);
如果你有一篮子(aBasketof)Banana-一Basket<Banana>,情况会是如何呢?
把一篮子(aBasketof)Banana-一Basket<Banana>发送给Eat方法有意义吗?
在这种情形下,会成功吗?
真是这样的话,编译器会给出错误信息:
Basket<Banana>bananaBasket=newBasket<Banana>();
//…
anAnimal.Eat(bananaBasket);
编译器在此保护了我们的代码。
我们怎样才能要求编译器允许这种特殊情形呢?
约束机制再一次帮助了我们:
publicvoidEat<t>(Basket<t>fruits)whereT:
Fruit
{
foreach(FruitaFruitinfruits)
{
//将吃水果的代码
}
}
在建立方法Eat()的过程中,我要求编译器允许一篮子(aBasketof)任何类型T,这里T是Fruit类型或任何继承自Fruit的类。
11.泛型和代理
代理也可以是泛型化的。
这样就带来了巨大的灵活性。
假定我们对写一个框架程序很感兴趣。
我们需要提供一种机制给事件源以使之可以与对该事件感兴趣的对象进行通讯。
我们的框架可能无法控制事件是什么。
你可能在处理某种股票价格变化(doubleprice),而我可能在处理水壶中的温度变化(temperaturevalue),这里Temperature可以是一种具有值、单位、门槛值等信息的对象。
那么,怎样为这些事件定义一接口呢?
让我们通过pre-generic代理技术细致地分析一下如何实现这些:
publicdelegatevoidNotifyDelegate(Objectinfo);
publicinterfaceISource
{
eventNotifyDelegateNotifyActivity;
}
我们让NotifyDelegate接受一个对象。
这是我们过去采取的最好措施,因为Object可以用来代表不同类型,如double,Temperature,等等--尽管Object含有因值类型而产生的装箱的开销。
ISource是一个各种不同的源都会支持的接口。
这里的框架展露了NotifyDelegate代理和ISource接口。
让我们看两个不同的源码:
publicclassStockPriceSource:
ISource
{
publiceventNotifyDelegateNotifyActivity;
//…
}
publicclassBoilerSource:
ISource
{
publiceventNotifyDelegateNotifyActivity;
//…
}
如果我们各有一个上面每个类的对象,我们将为事件注册一个处理器,如下所示:
StockPriceSourcestockSource=newStockPriceSource();
stockSource.NotifyActivity
+=newNotifyDelegate(stockSource_NotifyActivity);
//这里不必要出现在同一个程序中
BoilerSourceboilerSource=newBoilerSource();
boilerSource.NotifyActivity
+=newNotifyDelegate(boilerSource_NotifyActivity);
在代理处理器方法中,我们要做下面一些事情:
对于股票事件处理器,我们有:
voidstockSource_NotifyActivity(objectinfo)
{
doubleprice=(double)info;
//在使用前downcast需要的类型
}
温度事件的处理器看上去会是:
voidboilerSource_NotifyActivity(objectinfo)
{
Temperaturevalue=infoasTemperature;
//在使用前downcast需要的类型
}
上面的代码并不直观,且因使用downcast而有些凌乱。
借助于泛型,代码将变得更易读且更容易使用。
让我们看一下泛型的工作原理:
下面是代理和接口:
publicdelegatevoidNotifyDelegate<t>(Tinfo);
publicinterfaceISource<t>
{
eventNotifyDelegate<t>NotifyActivity;
}
我们已经参数化了代理和接口。
现在的接口的实现中应该能确定这是一种什么类型。
Stock的源代码看上去象这样:
publicclassStockPriceSource:
ISource<double>
{
publiceventNotifyDelegate<double>NotifyActivity;
//…
}
而Boiler的源代码看上去象这样:
publicclassBoilerSource:
ISource<temperature>
{
publiceventNotifyDelegate<temperature>NotifyActivity;
//…
}
如果我们各有一个上面每种类的对象,我们将象下面这样来为事件注册一处理器: