有人说这是语言游戏,其实不然。类型代表着约束,你只有清楚这些约束,才能在.Net的规则下得心应手。下面深入谈谈:
(1)托管堆中的值类型
这里需要反驳一个流传甚广的谬论:.Net中值类型在栈上,引用类型在托管堆上。
如:
int[] data = new int[100000000];
上例中数组是引用类型,数组里的1亿个int是值类型,你说这1亿个值类型是在哪里?肯定是在托管堆上,栈里也塞不下啊。所以,我的看法是:
“作为引用类型数据成员的值类型是内联在托管堆上引用类型数据内部的。”
别小看这种说法,下面看两段代码:
int[] data0 = new int[100000000];
for (int i = 0; i < data0.Length; i++)
{
data0[i] = i;
}
// b
int[] data1 = new int[100000000];
int length = data1.Length;
for (int i = 0; i < length; i++)
{
data0[i] = i;
}
这两段代码实现的功能都一样。b段代码那句 int length = data1.Length; 看则是多余的,实质上不是。在a中的循环体内,是将在栈中的i和在堆中的data0.Length内联的那个字段做比较,而在b中的循环体中,是将在栈中的i和同在栈中的length做比较。在某些情况下,会产生数量级的性能差异,详情见我之前的一篇博客《也谈谈性能:局部性与性能的实验观察》。
(2)值类型的内存管理
.Net中值类型是很值得探讨的,它是编写高性能.Net程序所必需了解的,也是使用纯.Net代码绕开GC,精细控制内存的唯一方式。
.Net的内存大体上有三块:栈:由编译器自动管理;托管堆:由GC管理;非托管堆:手动管理,手动分配和释放。只有值类型的数据是可以分配在栈上,可以分配在非托管堆上,也可以分配在非托管堆上的,而引用类型的数据(如果有的话),一定是托管堆上的。
值类型和指针类型是完全绕开GC的。这就为我们打开了两扇门:
(a)实时编程。GC是实时编程的最大障碍。大量的使用值类型可以绕开GC,进行实时编程。要知道,对于Java那样的语言,进行实时编程是非常困难的,甚至需要设计专门的实时Java虚拟机,.Net则完全不需要。这一点对有实时要求的控制系统非常重要。
(b)高性能编程。使用值类型和指针可以非常精细的操作内存,编写高效的代码:
· 你可以直接用指针去操作值类型;
· 你可以将值类型内联在class中,委托类类型用GC去管理值类型的生命周期;
· 你可以将值类型分配在非托管堆中,自己管理它的生命周期;
· 你可以将值类型分配在栈中,或者用stackalloc在栈中分配值类型数组。
(3)如果对.Net的类型系统不了解,可能会导致程序开发中的问题。
比如,我以前不知道将值类型转换为接口类型会进行装箱操作,在图像处理库中正准备为每一个像素规定几个接口进行抽象。一张图片有几百万像素几千万像素是很正常的,如果通过接口来操作这些像素会发生大量的装箱操作,导致程序性能急剧下降。