1. 类型对象
首先看如下代码:
class Program { static void Main(string[] args) { List<object> objs = new List<string>(); Console.ReadKey(); } }
- CLR会为每一个指定了泛型实参的封闭类型创建一个类型对象(同一个类型实参只会有一个类型对象)
- 所有为封闭类型创建的类型对象的父类型对象均为其泛型开放对象的父类型的类型对象。
- 所有为封闭类型创建的类型对象中均保存了在其开放类型中定义的静态成员变量。
- 每一个封闭类型的类型对象在创建时均会调用在其开放类型中定义的静态构造函数来初始化静态成员。
因此上述代码其实CLR为创建两个新的类型:List<Object>,List<string>(真实的类型名称会有所差别)。这两个类型均继承于List<T>的父类:Object.
故List<String>与List<Object>之前没有继承关系,并且它们之前也不会共享在List<T>中所定义的静态成员。(由上图可知)。
还是看一下IL确认一下:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 20 (0x14) .maxstack 1 .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<object> objs, //生成的真实的类名为List~1<object>,~1表示该泛型为一元的 [1] class [mscorlib]System.Collections.Generic.List`1<string> strs) IL_0000: nop IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<object>::.ctor() IL_0006: stloc.0 IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor() IL_000c: stloc.1 IL_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0012: pop IL_0013: ret } // end of method Program::Main
2. 泛型方法的内部机制
普通的方法调用过程是如下进行的:
-
CLR首先会在调用对象的类型对象的方法表中查找是否已保存MethodA的本地代码地址
若没有保存地址,则说明方法的IL代码还未编译成本地代码,此时JIT(即时编译器)会编译方法的IL代码,将后将本地代码保存于此。
- 调用本地代码
但是对于泛型方法,此处就不一样了(对于每一种特定的类型实参,IL需要为其生成特定的代码)。
因此当CLR查找待调用方法是否已编译成本地代码时,需要以方法名+类型实参为Id来查找。此处的类型对象则需要保存一个列表来保存各种不同
类型实参所对应的本地代码地址。
注意:此处CLR作了一些优化:对于一个泛型方法,所有类型实参指定为引用类型的方法调用,都会调用同一个地址的本地代码。
简单点说,就是JIT为所有以引用类型为类型实参的泛型方法调用生成同一份本地代码。
3. 逆变与协变
在.NET没有提供逆变与协变的功能之前,泛型类型在指定类型实参后便不能再更改。像开头的例子:List<String>对象便不能赋给List〈Object>的变量。
其实在某些情况下,限制这种类型实参的改变是不太合理的。比如:IEnumberable〈T〉这个泛型接口只提供了枚举器的功能,即调用者只能通过这个接口来从对象中获取数据。
在这种情况下IEnumerable<Object>变量应该能引用List<String>对象才对,因为在运行时不会出错。但是这种赋值操作在之前是被编译器禁止的。
在.NET 4中微软修补了这一功能的缺失,提供了协变与逆变的功能。
协变:泛型的类型实参可以被其基类代替,在定义泛型时需使用out关键字声明(类型参数在类的实现中只能用作输出参数)
例如: IEnumerable<out T>
这样的声明表示该泛型是一个协变量。因此IEnumerable<Object> obje = new List<String>()这个表达式不会报错。( List<T>是实现了IEnumerable<T>接口的 )
逆变: 泛型的类型实参可以被其派生类代替,在定义泛型时需使用in关键字声明(类型参数在类的实现中只能用作输入参数)
例如:
public interface ICustomList<in T> { void Add(T item); } public class CustomList<T> : ICustomList<T> { public void Add(T item) { return; } }
static void Main(string[] args)
{
ICustomList<String>strs = new CustomList<Object>();//此处是不是看上去有点怪,但是他确实是正确的代码。T在ICustomList接口中只会被用作输入参数。 }
回到最初的问题:List<Object> objs = new List<String>();
这样做是错误的,因为在运行时,List<Object>与List<String>是两个不同的类,他们之前没有继承关系。因此上述代码错误。而协变与逆变解决的是接口之间或接口与类之前转换的问题。
(但是泛型委托的逆变与协变貌似是类与类之前的转换)
注意:
- 只有类型之前存在一个引用的转换,才能使用该协变与逆变。(因此,上述的ICustomList不适用值类型)
-
关键字in与out是不能继承的
例如:
public interface ICustomList<in T> { void Add(T item); } public interface ICustomCollection<T> : ICustomList<T> { } public class CustomList<T> : ICustomCollection<T> { public void Add(T item) { return; } } static void Main(string[] args) { ICustomCollection<string> strs = new CustomList<Object>(); //此处会编译错误,因为ICustomCollection并没有继承定义在其父接口ICustomList上的in关键字。 }
评论