《CSharp In Depth》读书笔记(3)——Value types and reference types
基本概念
这个概念在CSharp中应该说怎么强调都不为过。 我们在CSharp中使用的所有的变量或者是值类型的(value type),或者是引用类型的(reference types)。
最基本的区别
- 值类型的“变量”存储的是值本身。
- 引用类型的“变量”存储的是指向值的引用。
有C/C++的指针概念的读者大概非常容易理解这个概念——引用类型变量其实全部都是“指针”。 知道了这个基本区别,下面的事实边很容易推导出来。
- 值类型变量在执行拷贝操作时会复制整个值。
- 引类用型变量在执行拷贝操作时会复制当前的引用。
这个区别导致的直接后果是——对于引用类型,其默认的拷贝操作永远是浅拷贝。 即新定义的“对象”其实只是一个引用,这个引用和旧对象的引用指向相同的值。
了解了这个基本区别,我们再来看看CSharp中一些基本类型都属于哪一种:
- 数组是引用类型。即使数组元素可以是int这样的值类型变量。 因此作为函数参数传递的时候实际上只是复制了数组的引用。
- 枚举类型是值类型。
- 代理(delegate)是引用类型。
- 接口(interface)是引用类型。但是可以用值型变量实现。
内存使用上的区别
- 值类型变量存储在其被定义的地方。 这意味着如果是在函数中被定义的则使用栈内存。 反之则使用对内存。
- 引用类型变量(即引用所指向的实例)永远存储在堆上。
类型转换上的区别
- 值类形变量不能够被继承。
- 引用类型变量可以被继承。
存在这种区别的原因是值类型变量本身没有任何“附加信息”对当前类型进行说明。 值类型变量所存储的所有类型即为值本身。
而引用类型变量(指向的实例)总是需要存储一些“附加信息”用于对当前对象的类型信息进行说明。 我们在对引用类型变量进行类型转换的时候,实际上没有改变引用指向的对象(指向的实例)。 编译器只是新建了一个引用,检查这个新的类型的引用是否与所指向的对象的类型兼容,如此而已。
关于值类型与引用类型的各种误解
市面上有着各种关于值类型与引用类型的“传说”。 包括博主本人之前也对相关概念有一些误解。 书中介绍了几个比较经典的例子。 澄清这些误解有助于我们在开发中避免一些似是而非的玩法。
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)就相对容易得多。
上例中o是一个引用类型变量,值是一个指向另一个新对象的引用。这个对象的值是(int 5)。 所以改变o的值不会对i有任何影响。 在拆箱的时候,编译器首先会判断o中对象的类型与目标类型是否相容。 如果不相容会先报错。拆箱的过程同样是将o指向的对象的值拷贝到j中。 因此针对j的赋值同样不会对o有影响。
装箱操作的潜在性能问题
从上边的例子我们可以看到一个装箱操作实际上在堆上创建了一个新的引用型对象。 如果你的代码中存在大量的装箱操作,那么你最好关心一下gc的性能有没有收到影响。
小结
- 引用类型变量的值是一个引用,不是变量本身。
- 值类型变量的值是变量的值本身。
- 引用类型和值类型哪个更加高效不能一概而论,需要根据应用场景具体分析。
- 引用类型的内存占用总是发生在堆上。而值类型的内存占用可能发生在堆上也可能在栈上。
- 函数调用时传递了一个引用类型参数,那么实际上参数是值拷贝(copy by value),只不过这个值是一个引用。
- 值类型变量装箱之后则变成了引用类型变量,拆箱后则恢复为值类型变量。