《CSharp In Depth》读书笔记(3)——Value types and reference types

基本概念

这个概念在CSharp中应该说怎么强调都不为过。 我们在CSharp中使用的所有的变量或者是值类型的(value type),或者是引用类型的(reference types)。

最基本的区别

有C/C++的指针概念的读者大概非常容易理解这个概念——引用类型变量其实全部都是“指针”。 知道了这个基本区别,下面的事实边很容易推导出来。

这个区别导致的直接后果是——对于引用类型,其默认的拷贝操作永远是浅拷贝。 即新定义的“对象”其实只是一个引用,这个引用和旧对象的引用指向相同的值。

了解了这个基本区别,我们再来看看CSharp中一些基本类型都属于哪一种:

内存使用上的区别

类型转换上的区别

存在这种区别的原因是值类型变量本身没有任何“附加信息”对当前类型进行说明。 值类型变量所存储的所有类型即为值本身。

而引用类型变量(指向的实例)总是需要存储一些“附加信息”用于对当前对象的类型信息进行说明。 我们在对引用类型变量进行类型转换的时候,实际上没有改变引用指向的对象(指向的实例)。 编译器只是新建了一个引用,检查这个新的类型的引用是否与所指向的对象的类型兼容,如此而已。

关于值类型与引用类型的各种误解

市面上有着各种关于值类型与引用类型的“传说”。 包括博主本人之前也对相关概念有一些误解。 书中介绍了几个比较经典的例子。 澄清这些误解有助于我们在开发中避免一些似是而非的玩法。

struct比class轻便好用

首先需要明确一个概念——struct是值类型,class是引用类型。

如果在防止内存碎片和减轻gc压力的层面来说,使用struct确实比class更好。 因为在函数体内部定义的值类型的struct,会在函数执行完毕后被自动回收。 而引用类型的class则需要等到引用计数为0才可以被gc释放。

当时这不等价于struct比class更快更好用。 值型变量的特点在于其赋值和参数传递的过程中都是深拷贝。 所以一般适用于小且频繁使用的数据结构。

比较大的class改成struct的直接后果就是每次赋值和参数传递(如果没有用ref的话)都会造成一次深拷贝。 在gc上获得的性能提升常常不足以“支付”如此惨重的代价。

引用类型占用堆内存,值类型占用栈内存。

这个说法前一半是对的。后一半则不很准确。 准确的说法是值类型变量占用的哪里的内存与其被定义的地方有关——如果是在函数内定义的则是栈内存。 作为类的成员变量定义的则是堆内存。 实际上函数内定义的变量也不都使用栈内存,比如匿名函数。

CSharp中默认的参数传递方法是引用传递(passed by reference)

这个实际上是把引用型变量和传址调用搞混了。 CSharp实际上默认的行为是对于passed by value。 只不过对于引用类型变量,复制的对象是一个引用,指向的对象是同一个。 有时候会给人一种传址调用的错觉。

装箱与拆箱(boxing and unboxing)

明确了上边的概念,再来理解装箱与拆箱(boxing and unboxing)就相对容易得多。

int i = 5;
object o = i;
int j = (int)o;

上例中o是一个引用类型变量,值是一个指向另一个新对象的引用。这个对象的值是(int 5)。 所以改变o的值不会对i有任何影响。 在拆箱的时候,编译器首先会判断o中对象的类型与目标类型是否相容。 如果不相容会先报错。拆箱的过程同样是将o指向的对象的值拷贝到j中。 因此针对j的赋值同样不会对o有影响。

装箱操作的潜在性能问题

从上边的例子我们可以看到一个装箱操作实际上在堆上创建了一个新的引用型对象。 如果你的代码中存在大量的装箱操作,那么你最好关心一下gc的性能有没有收到影响。

小结

参考资料

[1] Parameter passing in C#