7. 无限制的类型参数
假如你创建一个泛型数据结构或类,就象例3中的MyList,注重其中并没有约束你该使用什么类型来建立参数化类型。然而,这带来一些限制。如,你不能在参数化类型的实例中使用象==,!=或<等运算符,如:
象==和!=这样的运算符的实现对于值类型和引用类型都是不同的。假如随意地答应之,代码的行为可能很出乎你的意料。另外一种限制是缺省构造器的使用。例如,假如你编码象new T(),会出现一个编译错,因为并非所有的类都有一个无参数的构造器。假如你真正编码象new T()来创建一个对象,或者使用象==和!=这样的运算符,情况会是怎样呢?你可以这样做,但首先要限制可被用于参数化类型的类型。读者可以自己先考虑如何实现之。
8. 约束机制及其优点
一个泛型类答应你写自己的类而不必拘泥于任何类型,但答应你的类的使用者以后可以指定要使用的具体类型。通过对可能会用于参数化的类型的类型施加约束,这给你的编程带来很大的灵活性--你可以控制建立你自己的类。让我们分析一个例子:
假定我需要这种类型以支持CompareTo()方法的实现。我能够通过加以约束--为参数化类型指定的类型必须要实现IComparable接口--来指定这一点。例6中的代码就是这样:
你可以指定约束的组合,就象: where T : IComparable, new()。这就是说,用于参数化类型的类型必须实现Icomparable接口并且必须有一个无参构造器。
9. 继续与泛型
10. 泛型和可代替性
当我们使用泛型时,要小心可代替性的情况。假如B继续自A,那么在使用对象A的地方,可能都会用到对象B。假定我们有一篮子水果(a Basket of Fruits (Basket<Fruit>)),而且有继续自Fruit的Apple和Banana(皆为Fruit的种类)。一篮子苹果--Basket of Apples (Basket<apple>)可以继续自Basket of Fruits (Basket<Fruit>)?答案是否定的,假如我们考虑一下可代替性的话。为什么?请考虑一个a Basket of Fruits可以工作的方法:
假如发送一个Basket<Fruit>的实例给这个方法,这个方法将添加一个Apple对象和一个Banana对象。然而,发送一个Basket<Apple>的实例给这个方法时,会是什么情形呢?你看,这里布满技巧。这解释了为什么下列代码:
这在上面的例中在成功的,但也存在非凡情形:有时我们确实想传递一个集合的派生类,此时需要一个集合的基类。例如,考虑一下Animal(如Monkey),它有一个把Basket<Fruit>作参数的方法Eat,如下所示:
假如你有一篮子(a Basket of)Banana-一Basket<Banana>,情况会是如何呢?把一篮子(a Basket of)Banana-一Basket<Banana>发送给Eat方法有意义吗?在这种情形下,会成功吗?真是这样的话,编译器会给出错误信息:
11. 泛型和代理
代理也可以是泛型化的。这样就带来了巨大的灵活性。
假定我们对写一个框架程序很感爱好。我们需要提供一种机制给事件源以使之可以与对该事件感爱好的对象进行通讯。我们的框架可能无法控制事件是什么。你可能在处理某种股票价格变化(double price),而我可能在处理水壶中的温度变化(temperature value),这里Temperature可以是一种具有值、单位、门槛值等信息的对象。那么,怎样为这些事件定义一接口呢?
让我们通过pre-generic代理技术细致地分析一下如何实现这些:
public delegate void NotifyDelegate(Object info);
public interface ISource
{
event NotifyDelegate NotifyActivity;
}
我们让NotifyDelegate接受一个对象。这是我们过去采取的最好措施,因为Object可以用来代表不同类型,如double,Temperature,等等--尽管Object含有因值类型而产生的装箱的开销。ISource是一个各种不同的源都会支持的接口。这里的框架展露了NotifyDelegate代理和ISource接口。
让我们看两个不同的
源码:
public class StockPriceSource : ISource
{
public event NotifyDelegate NotifyActivity;
//…
}
public class BoilerSource : ISource
{
public event NotifyDelegate NotifyActivity;
//…
}
假如我们各有一个上面每个类的对象,我们将为事件注册一个处理器,如下所示:
StockPriceSource stockSource = new StockPriceSource();
stockSource.NotifyActivity
+= new NotifyDelegate(stockSource_NotifyActivity);
//这里不必要出现在同一个程序中
BoilerSource boilerSource = new BoilerSource();
boilerSource.NotifyActivity
+= new NotifyDelegate(boilerSource_NotifyActivity);
在代理处理器方法中,我们要做下面一些事情:
对于股票事件处理器,我们有:
void stockSource_NotifyActivity(object info)
{
double price = (double)info;
//在使用前downcast需要的类型
}
温度事件的处理器看上去会是:
void boilerSource_NotifyActivity(object info)
{
Temperature value = info as Temperature;
//在使用前downcast需要的类型
}
上面的代码并不直观,且因使用downcast而有些凌乱。借助于泛型,代码将变得更易读且更轻易使用。让我们看一下泛型的工作原理:
下面是代理和接口:
public delegate void NotifyDelegate<t>(T info);
public interface ISource<t>
{
event NotifyDelegate<t> NotifyActivity;
}
我们已经参数化了代理和接口。现在的接口的实现中应该能确定这是一种什么类型。
Stock的源代码看上去象这样:
public class StockPriceSource : ISource<double>
{
public event NotifyDelegate<double> NotifyActivity;
//…
}
而Boiler的源代码看上去象这样:
public class BoilerSource : ISource<temperature>
{
public event NotifyDelegate<temperature> NotifyActivity;
//…
}
假如我们各有一个上面每种类的对象,我们将象下面这样来为事件注册一处理器:
StockPriceSource stockSource = new StockPriceSource();
stockSource.NotifyActivity += new NotifyDelegate<double>(stockSource_NotifyActivity);
//这里不必要出现在同一个程序中
BoilerSource boilerSource = new BoilerSource();
boilerSource.NotifyActivity += new NotifyDelegate<temperature>(boilerSource_NotifyActivity);
现在,股票价格的事件处理器会是:
void stockSource_NotifyActivity(double info)
{ //… }
温度的事件处理器是:
void boilerSource_NotifyActivity(Temperature info)
{ //… }
这里的代码没有作downcast并且使用的类型是很清楚的。
12. 泛型与反射
既然泛型是在CLR级上得到支持的,你可以使用反射API来取得关于泛型的信息。假如你是编程的新手,可能有一件事让你迷惑:你必须记住既有你写的泛型类也有在运行时从该泛型类创建的类型。因此,当使用反射API时,你需要另外记住你在使用哪一种类型。我将在例7说明这一点:
例7.在泛型上的反射
public class MyClass<t> { }
class Program
{
static void Main(string[] args)
{
MyClass<int> obj1 = new MyClass<int>();
MyClass<double> obj2 = new MyClass<double>();
Type type1 = obj1.GetType();
Type type2 = obj2.GetType();
Console.WriteLine("obj1’s Type");
Console.WriteLine(type1.FullName);
Console.WriteLine(type1.GetGenericTypeDefinition().FullName);
Console.WriteLine("obj2’s Type");
Console.WriteLine(type2.FullName);
Console.WriteLine(type2.GetGenericTypeDefinition().FullName);
}
}
在本例中,有一个MyClass<int>的实例,程序中要查询该实例的类名。然后我查询这种类型的GenericTypeDefinition()。GenericTypeDefinition()会返回MyClass<T>的类型元数据。你可以调用IsGenericTypeDefinition来查询是否这是一个泛型类型(象MyClass<T>)或者是否已指定它的类型参数(象MyClass<int>)。同样地,我查询MyClass<double>的实例的元数据。上面的程序输出如下:
obj1’s Type
TestApp.MyClass`1
[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1
obj2’s Type
TestApp.MyClass`1
[[System.Double, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1
可以看到,MyClass<int>和MyClass<double>是属于mscorlib配件集的类(动态创建的),而类MyClass<t>属于我自建的配件集。
13. 泛型的局限性
至此,我们已了解了泛型的强大威力。是否其也有不足呢?我发现了一处。我希望微软能够明确指出泛型存在的这一局制性。在表达约束的时候,我们能指定参数类型必须继续自一个类。然而,指定参数必须是某种类的基类型该如何呢?为什么要那样做呢?
在例4中,我展示了一个Copy()方法,它能够把一个源List的内容复制到一个目标list中去。我可以象如下方式使用它:
List<Apple> appleList1 = new List<Apple>();
List<Apple> appleList2 = new List<Apple>();
…
Copy(appleList1, appleList2);
然而,假如我想要把apple对象从一个列表复制到另一个Fruit列表(Apple继续自Fruit),情况会如何呢?当然,一个Fruit列表可以容纳Apple对象。所以我要这样编写代码:
List<Apple> appleList1 = new List<Apple>();
List<Fruit> fruitsList2 = new List<Fruit>();
…
Copy(appleList1, fruitsList2);
这不会成功编译。你将得到一个错误:
Error 1 The type arguments for method
’TestApp.Program.Copy<t>(System.Collections.Generic.List<t>,
System.Collections.Generic.List<t>)’ cannot be inferred from the usage.
编译器基于调用参数并不能决定T应该是什么。其实我想说,Copy方法应该接受一个某种数据类型的List作为第一个参数,一个相同类型的List或者它的基类型的List作为第二个参数。
尽管无法说明一种类型必须是另外一种类型的基类型,但是你可以通过仍然使用约束机制来克服这一限制。下面是这种方法的实现:
public static void Copy<T, E>(List<t> source,
List<e> destination) where T : E
在此,我已指定类型T必须和E属同一种类型或者是E的子类型。我们很幸运。为什么?T和E在这里都定义了!我们能够指定这种约束(然而,C#中并不鼓励当E也被定义的时候使用E来定义对T的约束)。
然而,请考虑下列的代码:
public class MyList<t>
{
public void CopyTo(MyList<t> destination)
{
//…
}
}
我应该能够调用CopyTo:
MyList<apple> appleList = new MyList<apple>();
MyList<apple> appleList2 = new MyList<apple>();
//…
appleList.CopyTo(appleList2);
我也必须这样做:
MyList<apple> appleList = new MyList<apple>();
MyList<fruit> fruitList2 = new MyList<fruit>();
//…
appleList.CopyTo(fruitList2);
这当然不会成功。如何修改呢?我们说,CopyTo()的参数可以是某种类型的MyList或者是这种类型的基类型的MyList。然而,约束机制不答应我们指定一个基类型。下面情况又该如何呢?
public void CopyTo<e>(MyList<e> destination) where T : E
抱歉,这并不工作。它将给出一个编译错误:
Error 1 ’TestApp.MyList<t>.CopyTo<e>()’ does not define type
parameter ’T’
当然,你可以把代码写成接收任意类型的MyList,然后在代码中,校验该类型是可以接收的类型。然而,这把检查工作推到了运行时刻,丢掉了编译时类型安全的优点。
14. 结论
.NET 2.0中的泛型是强有力的,你写的代码不必限定于一特定类型,然而你的代码却能具有类型安全性。泛型的实现目标是既提高程序的性能又不造成代码的臃肿。然而,在它的约束机制存在不足(无法指定一类型必须是另外一种类型的基类型)的同时,该约束机制也给你书写代码带来很大的灵活性,因为你不必拘泥于各种类型的"最小公分母"能力。进入讨论组讨论。