CLR via C# 引用类型和值类型
CLR支持两种类型:引用类型和值类型。虽然FCL的大多数类型都是引用类型,但程序员用得最多的还是值类型。
引用类型总是从托管堆分配,C#的new操作符返回对象内存地址---即指向对象数据的内存地址。
//--
使用引用类型必须留意性能问题。首先要认清楚以下四个事实:
1.内存必须从托管堆分配。
2.堆上分配的每个对象都有一些额外成员,这些成员必须初始化。
3.对象中的其他字节(为字段而设)总是设为零。
4.从托管堆分配对象时,可能强制执行一次垃圾回收。
//--
如果所有类型都是引用类型,应用程序的性能将显著下降。
为了提升简单和常用的类型的性能,CLR提供了名为“值类型”的轻量级类型。
值类型的实例一般在线程栈上分配(虽然可以作为字段嵌入引用类型的对象中)。
在代表值类型实例的变量中不包含指向实例的指针。相反,变量中包含了实例本身的字段。由于变量已包含了实例的字段,所以操作实例中的字段不需要提领指针。
值类型的实例不受垃圾回收器的控制。因此,值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。
//--
任何称为“类”的类型都是引用类型。相反,所有值类型都称为结构或枚举。
所有结构都是抽象类型System.ValueType的直接派生类。System.ValueType本身又直接从System.Object派生。
根据定义,所有值类型都必须从System.ValueType派生。所有枚举都从System.Enum抽象类型派生,后者又从System.ValueType派生。
//--
虽然不能在定义值类型时为它选择基类型,但是,值类型可以实现一个或多个接口。
除此之外,所有值类型都隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。
//--
在托管代码中,要由定义类型的开发人员决定在什么地方分配类型的实例,使用类型的人对此并没有控制权。
//--
上面代码有这样一行:
因为这行代码的写法,似乎是要在托管堆上分配一个SomeVal实例。但是C#编译器知道SomeVal是值类型,所以会生成正确的 IL 代码,在线程栈上分配一个SomeVal实例。C#还会确保值类型中的所有字段都初始化为零。
上述代码还可以这样写:
这一行生成的 IL 代码也会在线程栈上分配实例,并将字段初始化为零。唯一的区别在于,如果使用new操作符,C#会认为实例已初始化。一下代码更清楚地进行了说明:
//--
设计自己的类型时,要仔细考虑类型是否应该定义成值类型而不是引用类型。值类型有时能提供更好的性能。
具体地说,除非满足一下全部条件,否则不应该将类型声明为值类型:
1.类型具有基元类型的行为。也就是说,是十分简单的类型,没有成员会修改类型的任何实例字段。如果类型没有提供会更改其字段的成员,就说该类型是不可变(immutable)类型。事实上,对于许多值类型,我们建议将全部字段标记为readonly。
2.类型不需要从其他任何类型继承。
3.类型也不派生出其他任何类型。
类型实例大小也应在考虑之列,因为实参默认以传值方式传递,造成对值类型实例中的字段进行复制,对性能造成损害。同样的,被定义为返回一个值类型的方法在返回时,实例中的字段会复制到调用者分配的内存中,对性能造成损害。所以,要将类型声明为值类型,除了要满足以上全部条件,还必须满足一下任意条件:
1.类型的实例较小(16字节或更小)。
2.类型的实例较大(大于16字节),弹不作为方法实参传递,也不从方法返回。
//--
值类型的优势是不作为对象在托管堆上分配。当然,与引用类型相比,值类型也存在自身的一些局限。
下面列出了值类型和引用类型的一些区别:
1.值类型对象有两种表示形式:未装箱和已装箱。相反,引用类型总是处于已装箱形式。
2.值类型从System.ValueType派生。该类型提供了与System.Object相同的方法。但是System.ValueType重写了Equals方法,能在两个对象的字段值完全匹配的前提下返回true。此外,System.ValueType重写了GetHashCode方法。生成哈希码时,这个重写方法所用的算法会将对象的实例字段中的值考虑在内。由于这个默认实现存在性能问题,所以定义自己的值类型时应重写Equals和GetHashCode方法,并提供它们的显式实现。
3.由于不能将值类型作为基类型来定义新的值类型或者新的引用类型,所以不应在值类型中引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封(不可重写)。
4.引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null,表明当前不指向有效对象。相反,值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0。
5.将值类型变量赋给另一个值类型变量,会执行逐字段的复制。将引用类型的变量赋给另一个引用类型的变量只复制内存地址。
6.基于上一条,两个或多个引用类型变量能引用堆中同一个对象,所以一个变量执行的操作可能影响到另一个变量引用的对象。相反,值类型变量自成一体,对值类型变量执行的操作不可能影响另一个值类型变量。
7.由于未装箱的值类型不再堆上分配,一旦定义了该类型的一个实例的方法不再活动,为它们分配的存储就会被释放,而不是等着进行垃圾回收。