C# 8.0本质论
上QQ阅读APP看书,第一时间看更新

3.1 类型的划分

一个类型要么是值类型,要么是引用类型。区别在于拷贝方式:值类型的数据总是拷贝值;而引用类型的数据总是拷贝引用。

3.1.1 值类型

除了string,本书目前讲到的所有预定义类型都是值类型。值类型直接包含值。换言之,变量引用的位置就是内存中实际存储值的位置。因此,将一个值赋给变量1,再将变量1赋给变量2,会在变量2的位置创建值的拷贝,而不是引用变量1的位置。这进一步造成更改变量1的值不会影响变量2的值。图3.1对此进行了演示。number1引用内存中的特定位置,该位置包含值42。将number1的值赋给number2之后,两个变量都包含值42。但修改其中任何一个值都不会影响另一个值。

图3.1 值类型的实例直接包含数据

类似地,将值类型的实例传给Console.WriteLine()这样的方法也会生成内存拷贝。在方法内部对参数值进行的任何修改都不会影响调用函数中的原始值。由于值类型需要创建内存拷贝,因此定义时不要让它们占用太多内存(通常应该小于16字节)。

3.1.2 引用类型

相反,引用类型的变量存储对数据存储位置的引用,而不是直接存储数据。要去那个位置才能找到真正的数据。所以为了访问数据,“运行时”要先从变量中读取内存位置,再“跳转”到包含数据的内存位置。“运行时”的这种操作称为“解引用”。为引用类型的变量分配实际数据的内存区域称为(heap),如图3.2所示。

引用类型不像值类型那样要求创建数据的内存拷贝,所以拷贝引用类型的实例比拷贝大的值类型实例更高效。将引用类型的变量赋给另一个引用类型的变量,只会拷贝引用而不需要拷贝所引用的数据。事实上,每个引用总是处理器的“原生大小”:32位处理器拷贝32位引用,64位处理器拷贝64位引用,以此类推。显然,拷贝对一个大数据块的引用要比拷贝整个数据块快得多。

由于引用类型只拷贝对数据的引用,所以两个不同的变量可引用相同的数据。如果两个变量引用同一个对象,则当通过一个变量更改了对象的内部数据时,可以通过另一个变量看到对象内部数据的变化。无论赋值还是方法调用都会如此。因此,如果在方法内部更改引用类型的数据,控制返回调用者之后,将看到更改后的结果。有鉴于此,如果对象在逻辑上是固定大小、不可变的值,就考虑定义成值类型。如果逻辑上是可引用、可变的东西,就考虑定义成引用类型。

除了string和自定义类(如Program),本书目前讲到的所有类型都是值类型。但大多数类型都是引用类型。虽然偶尔需要自定义的值类型,但更多的还是自定义的引用类型。

图3.2 引用类型的实例指向堆