11. 泛型和代理
代理也可以是泛型化的。这样就带来了巨大的灵活性。
假定我们对写一个框架程序很感兴趣。我们需要提供一种机制给事件源以使之可以与对该事件感兴趣的对象进行通讯。我们的框架可能无法控制事件是什么。你可能在处理某种股票价格变化(double price),而我可能在处理水壶中的温度变化(temperature value),这里Temperature可以是一种具有值、单位、门槛值等信息的对象。那么,怎样为这些事件定义一接口呢?
让我们通过pre-generic代理技术细致地分析一下如何实现这些:
我们让NotifyDelegate接受一个对象。这是我们过去采取的最好措施,因为Object可以用来代表不同类型,如double,Temperature,等等--尽管Object含有因值类型而产生的装箱的开销。ISource是一个各种不同的源都会支持的接口。这里的框架展露了NotifyDelegate代理和ISource接口。
让我们看两个不同的源码:
如果我们各有一个上面每个类的对象,我们将为事件注册一个处理器,如下所示:
温度事件的处理器看上去会是:
上面的代码并不直观,且因使用downcast而有些凌乱。借助于泛型,代码将变得更易读且更容易使用。让我们看一下泛型的工作原理:
下面是代理和接口:
我们已经参数化了代理和接口。现在的接口的实现中应该能确定这是一种什么类型。
Stock的源代码看上去象这样:
而Boiler的源代码看上去象这样:
如果我们各有一个上面每种类的对象,我们将象下面这样来为事件注册一处理器:
现在,股票价格的事件处理器会是:
温度的事件处理器是:
这里的代码没有作downcast并且使用的类型是很清楚的。
12. 泛型与反射
既然泛型是在CLR级上得到支持的,你可以使用反射API来取得关于泛型的信息。如果你是编程的新手,可能有一件事让你疑惑:你必须记住既有你写的泛型类也有在运行时从该泛型类创建的类型。因此,当使用反射API时,你需要另外记住你在使用哪一种类型。我将在例7说明这一点:
例7.在泛型上的反射
在本例中,有一个MyClass<int>的实例,程序中要查询该实例的类名。然后我查询这种类型的GenericTypeDefinition()。GenericTypeDefinition()会返回MyClass<T>的类型元数据。你可以调用IsGenericTypeDefinition来查询是否这是一个泛型类型(象MyClass<T>)或者是否已指定它的类型参数(象MyClass<int>)。同样地,我查询MyClass<double>的实例的元数据。上面的程序输出如下:
可以看到,MyClass<int>和MyClass<double>是属于mscorlib配件集的类(动态创建的),而类MyClass<t>属于我自建的配件集。
13. 泛型的局限性
至此,我们已了解了泛型的强大威力。是否其也有不足呢?我发现了一处。我希望微软能够明确指出泛型存在的这一局制性。在表达约束的时候,我们能指定参数类型必须继承自一个类。然而,指定参数必须是某种类的基类型该如何呢?为什么要那样做呢?
在例4中,我展示了一个Copy()方法,它能够把一个源List的内容复制到一个目标list中去。我可以象如下方式使用它:
然而,如果我想要把apple对象从一个列表复制到另一个Fruit列表(Apple继承自Fruit),情况会如何呢?当然,一个Fruit列表可以容纳Apple对象。所以我要这样编写代码:
这不会成功编译。你将得到一个错误:
编译器基于调用参数并不能决定T应该是什么。其实我想说,Copy方法应该接受一个某种数据类型的List作为第一个参数,一个相同类型的List或者它的基类型的List作为第二个参数。
尽管无法说明一种类型必须是另外一种类型的基类型,但是你可以通过仍旧使用约束机制来克服这一限制。下面是这种方法的实现:
在此,我已指定类型T必须和E属同一种类型或者是E的子类型。我们很幸运。为什么?T和E在这里都定义了!我们能够指定这种约束(然而,C#中并不鼓励当E也被定义的时候使用E来定义对T的约束)。
然而,请考虑下列的代码:
我应该能够调用CopyTo:
我也必须这样做:
这当然不会成功。如何修改呢?我们说,CopyTo()的参数可以是某种类型的MyList或者是这种类型的基类型的MyList。然而,约束机制不允许我们指定一个基类型。下面情况又该如何呢?
抱歉,这并不工作。它将给出一个编译错误:
当然,你可以把代码写成接收任意类型的MyList,然后在代码中,校验该类型是可以接收的类型。然而,这把检查工作推到了运行时刻,丢掉了编译时类型安全的优点。
14. 结论
.NET 2.0中的泛型是强有力的,你写的代码不必限定于一特定类型,然而你的代码却能具有类型安全性。泛型的实现目标是既提高程序的性能又不造成代码的臃肿。然而,在它的约束机制存在不足(无法指定一类型必须是另外一种类型的基类型)的同时,该约束机制也给你书写代码带来很大的灵活性,因为你不必拘泥于各种类型的"最小公分母"能力
代理也可以是泛型化的。这样就带来了巨大的灵活性。
假定我们对写一个框架程序很感兴趣。我们需要提供一种机制给事件源以使之可以与对该事件感兴趣的对象进行通讯。我们的框架可能无法控制事件是什么。你可能在处理某种股票价格变化(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中的泛型是强有力的,你写的代码不必限定于一特定类型,然而你的代码却能具有类型安全性。泛型的实现目标是既提高程序的性能又不造成代码的臃肿。然而,在它的约束机制存在不足(无法指定一类型必须是另外一种类型的基类型)的同时,该约束机制也给你书写代码带来很大的灵活性,因为你不必拘泥于各种类型的"最小公分母"能力