泛型早在.net2.0就出来了,大家已经对它的使用很熟悉了,在工作中也大量的使用过它,但是大家对其工作原理,以及MS针对泛型对CLR做了那些工作是否了解呢。本文就是对泛型的本质进行讲解,希望能对大家有所帮助。
1 引入泛型
.Net2.0出来的时候,大家很轰动,其中.NET2.0做的一个很大的改变就是增加了泛型。在1.1的版本,大家还在使用一些如:ArrayList等集合。就算现在是.net3.5的时代,还是有很多程序员在继续使用1.1版本的集合,并没有采用范型集合,毕竟一个新技术的使用是要一段时间的,也许大家看了本文后会在适当的时候使用范型。
1
ArrayList arr
=
new
ArrayList();
2
List
<
int
>
arr1
=
new
List
<
int
>
();
3
for
(
int
i
=
1
; i
<
10
; i
++
)
4
{
5
arr.Add(i);
6
arr1.Add(i);
7
}
这是一个很基础的泛型应用,可能大家大部分使用泛型都是类似上面的方式。我们就从这个简单的代码讲起。表面上看好像1.1版本和2.0版本的集合使用上没什么其别,一样的方便。但是实质上MS在底层做了很多复杂的工作。我们看看它的IL代码:
1
IL_0012:
ldloc.0
2
IL_0013:
ldloc.2
3
IL_0014:
box
[mscorlib]System.Int32
4
IL_0019:
callvirt
instance
int32
[mscorlib]System.Collections.ArrayList::
Add
(
object
)
5
IL_001e:
pop
6
IL_001f:
ldloc.1
7
IL_0020:
ldloc.2
8
IL_0021:
callvirt
instance
void
class
[mscorlib]System.Collections.Generic.List`
1
<
int32
>::
Add
(!
0
)
9
在这里我们看看它第三行,熟悉IL的人都知道。这行box是在执行装箱操作。我们可以看看的Add方法原型:
Code
1 ArrayList:
2 public virtual int Add(Object value) {
3 if (_size == _items.Length) EnsureCapacity(_size + 1);
4 _items[_size] = value;
5 _version++;
6 return _size++;
7 }
8 List:
9 public void Add(T item) {
10 if (_size == _items.Length) EnsureCapacity(_size + 1);
11 _items[_size++] = item;
12 _version++;
13 }
我们可以看到ArrayList的Add方法的参数是object类型,我们都知道int --> object是要经过装箱操作,装箱操作又是个很费时间的事情,这就影响了性能。这里我们就引出泛型的第一个好处:性能的好处,避免了频繁的装箱拆箱操作。泛型的第二个好处:保证了类型的绝对安全,这点就不多讲了。上面的IL里多了一个类型:System.Collections.Generic.List`1这是.net CLR为泛型生成一个带“'”的类型,后面的数字表示<x,x>里参数x的个数。还有一个就是!0,这个是什么呢,请看下面的分析。
2 解析泛型
还是先看看一段代码:
public
void
intrGeneric
<
T
>
()
{
Console.WriteLine(
typeof
(T));
}
intrGeneric
<
int
>
();
intrGeneric
<
object
>
();
intrGeneric
<
string
>
();
那么当我们不知道intrGeneric<T>的T的类型的时候,IL是在怎么处理的呢。看看产生的IL:
1
.method
public
hidebysig
instance
void
intrGeneric<T>()
cil
managed
noinlining
2
{
3
//
Code size 18 (0x12)
4
.maxstack
8
5
IL_0000:
nop
6
IL_0001:
ldtoken
!!T
7
IL_0006:
call
class
[mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
8
IL_000b:
call
void
[mscorlib]System.Console::WriteLine(
object
)
9
IL_0010:
nop
10
IL_0011:
ret
11
}
//
end of method Generics_CSharp::intrGeneric
在这里我们可以看到编译器产生的IL是并没有为T指定一个特定的类型,但是输出的结果为System.Int32,System.Object,System.String。我们知道typeof()输出的Runtime Type。我们很容易的得出泛型的另外一个特性:运行时本质。由CLR运行时支持,真正的泛型实例化是发生在JIT编译时,生成不同的本地代码。我们再来看看上面的IL代码:ldtoken !!T,ldtoken指令:将元数据标记转换为其运行时表示形式,并将其推送到计算堆栈上。!!T时编译器生成的一个占位符,工作形式时这样的:第一次编译的时候,首先生成IL代码以及元数据,T只是一个隐藏的符号,这是并没有对泛型类型进行实例化,当JIT编译的时候,将以实际类型替换IL代码和元数据的T符号,并将其转换为本地代码。
说到这里我们大概对泛型有个大概的了解。Java编译器在编译泛型的时候,会将所有的泛型参数替换为Object,这实际上还是存在装箱拆箱的过程。还有C++的实现也存在一个很大的问题,那就是代码爆炸,C++会为每种类型都要生成自己的一份代码。C#与JAVA比较解决了性能问题,与C++比较,解决了代码爆炸的问题。那么C#是怎么解决代码爆炸的问题,做到代码共享的呢?这也是C#泛型的一个特性。
我们知道一个程序域有个loader heap,里面有个method table,每种类型都有自己的一个一个方法表,而且这个方法表列出所有的方法,方法表中包含了一个槽表,指向各个方法的描述(MethodDesc),提供了类型的行为能力,用于不同的交互操作实现的调用,在第一次调用时,会调用JIT编译程序,对其进行扫描,发现有引用别的类型,就会导向元数据,让后再根据元数据导向到所要查找的类型的具体位置,当范型参数是引用类型的时候,引用类型变量都是指向托管堆的指针,(在32位的操作系统上指针都是32位的)而对于指针完全是可以用相同的方式操作的。这再很大程度上避免了代码爆炸。如果为值类型的时候,由于值类型都是直接操作数据本身,JIT不会为一个不知道大小的参数去产生同样的代码。下次JIT编译的时候,先去查找是否有相同的类型代码,如果有的话,就不会再次编译。因此C#泛型做到了时间和空间的双重效应。看看下面windbg+sos的调试,我们看看它经过JIT编译过后的本地代码,我们更加肯定的验证了以上的我们所论述的。
1
0
:
010
> !u 00dc1128
2
Normal JIT generated code
3
Collections.Program.Main(System.String[])
4
Begin 00dc1128, size
90
35 00dc117f 8bdf mov ebx,edi
36 00dc1181 8bce mov ecx,esi
37 00dc1183 3909 cmp dword ptr [ecx],ecx
38 00dc1185 e886b4cbff call 00a7c610 (Collections.Generics_CSharp.intrGeneric[[System.Int32, mscorlib]](), mdToken: 06000003)
39 00dc118a 90 nop
40 00dc118b 8bce mov ecx,esi
41 00dc118d ba2892a700 mov edx,0A79228h (MD: Collections.Generics_CSharp.intrGeneric[[System.Object, mscorlib]]())
42 00dc1192 3909 cmp dword ptr [ecx],ecx
43 00dc1194 e897b4cbff call 00a7c630 (Collections.Generics_CSharp.intrGeneric[[System.__Canon, mscorlib]](), mdToken: 06000003)
44 00dc1199 90 nop
45 00dc119a 8bce mov ecx,esi
46 00dc119c ba6892a700 mov edx,0A79268h (MD: Collections.Generics_CSharp.intrGeneric[[System.String, mscorlib]]())
47 00dc11a1 3909 cmp dword ptr [ecx],ecx
48 00dc11a3 e888b4cbff call 00a7c630 (Collections.Generics_CSharp.intrGeneric[[System.__Canon, mscorlib]](), mdToken: 06000003)
49
00dc11a8
90
nop
50
00dc11a9 e81e886278
call
mscorlib_ni+
0x3299cc
(793e99cc) (System.Console.Read(),
mdToken:
060007b6)
51
00dc11ae
90
nop
52
00dc11af
90
nop
53
00dc11b0 8d65f4 lea esp,[ebp-0Ch]
54
00dc11b3 5b
pop
ebx
55
00dc11b4 5e
pop
esi
56
00dc11b5 5f
pop
edi
57
00dc11b6 5d
pop
ebp
58
00dc11b7 c3
ret
59
这里调试的正式调用泛型方法的函数,看看红色的地方,看看这三个CALL,大家可以看到第一个(int)的地址是:00a7c610 ,后面两个(object和string)的地址都是:00a7c630 。这可以说明值类型是不同的类型生成不同的代码,而引用类型是共用一个代码。大家还可以看到mdToken: 06000003。这也说明无论是值类型还是引用类型都是共用一个占位符,引用的是同一个类型下的方法和同一个MethodDesc,进而验证泛型是基于JIT的。
由此我们可以看出为了是泛型能够工作,MS必须做一下的工作:
创建新的IL指令,使之能够识别类型参数。
修改现有的元数据表的格式。
修改各种编程语言使他们能支持泛型。
修改编译器,能生成新的IL和元数据。
修改JIT编译器。
以上是本人对C#泛型的一点理解。。关于泛型的特性和语法不是本文的重点,园子里有很多文章可以参考。有不同意见欢迎大家指出来。希望大家看了本文后对泛型有个更加深入的理解。
欢迎大家对CLR系列提出意见以及希望讲解的知识点。
待续。。。