老生常谈
所有的类型可以划分为两类:值类型和引用类型。他们的区别在于复制策略的差异,后者又造成每种类型在内存中的存储位置不同。
值类型
值类型直接包含值。换句话说,变量引用的位置就是值在内存中实际存储的位置。因此,将一个变量的值赋值给另一个变量时,会在新变量的位置创建原始变量的值的一个内存副本。所以,更改第一个变量的值是不会影响第二个变量的值。将值类型作为参数传递给方法时,也会生成一个内存副本。
值类型所需要的内存量会在编译时固定下来,且不会在运行时改变。
引用类型
引用类型存储的是对一个内存地址的引用,要去该位置才能找到真正的数据。因此,要访问数据,“运行时”要从变量中读取内存的位置,然后跳转到包含数据的位置。
引用类型的访问需要一次额外的跳转,所以速度似乎会慢一些。然而,将一个引用类型的变量赋值给另一个引用类型的变量,只会多出地址的一个内存副本(32位处理器只需要一个32位的地址,64位处理器值需要一个64位的地址),它在内存的利用率上更好一些。在数据较大的情况下,由于不需要复制实际数据,所以引用类型比值类型更有效。
除了string和object,C#基本类型都是值类型。
一、结构
定义一个自定义的值类型。
internal struct Angle { public Angle(int hours, int minutes, int seconds) { _hours = hours; _minutes = minutes; _seconds = seconds; } public int Hours { get { return _hours;} } private readonly int _hours; public int Minutes { get { return _minutes; } } private readonly int _minutes; public int Seconds { get { return _seconds; } } private readonly int _seconds; public Angle Move(int hours, int minutes, int seconds) { return new Angle(Hours + hours, Minutes + minutes, Seconds + seconds); } }
虽然语言本身未做要求,但作为一个良好的习惯,我们应该确保值类型是不可变。换句话说,一段实例了一个值类型,那么这个实例就不能修改。如果要修改,应创建一个新的实例(如上面代码Move方法)。
二、装箱
值类型转换成引用类型的过程叫装箱(boxing),相反的过程称为拆箱(unboxing)。装箱过程:
1.在堆中分配好内存。将用于存放值类型的数据及少许额外开销(一个SyncBlock-Index以及方法表指针)。
2.接着发生一次内存复制动作,栈上值类型数据复制到堆上分配好的位置。
3.最后,引用类型对象引用得到更新,指向堆上的位置。
不允许在locak()语句中使用值类型。
lock语句:用于同步代码。实际会编译为System.Threading.Monitor的Enter()和Exit()方法。这两个方法必须成对调用。
Enter()记录由其唯一的引用型参数传递的一个lock,这样,使用相同的引用调用Exit()的时候,就可以释放该lock。使用值类型的问题在于装箱,每次Enter()和Exit()时,都会在堆上创建一个新值。将一个副本的引用和另一个不同副本的引用进行比较,总会返回false。所以无法将Enter()与对应的Exit()钩到一起。
避免拆箱
拆箱指令不包括将数据复制回栈的动作。有些语言可以直接访问堆上的值类型,但在C#中,只有当值类型做为引用类型的一个字段访问时才可能这样。
由于接口是引用类型,所以通过接口访问已装箱的值时,拆箱和复制是可避免的。
int number=12;
object o;
//boxing
o=number;
//避免拆箱
string text=((IFormattable)o).ToString();
当调用值类型的接口方法时,实例必须是一个变量,因为方法可能会改变值。由于拆箱会生成一个托管地址,“运行时”就会有一个存储位置和一个变量。结果,运行时值传递接口的托管地址,并不需要拆箱操作。
除此之外,调用一个struct 的ToString()方法(它重写object的ToString()方法),也不需要执行unboxing。在编译时,显然会调用struct重写的ToString()方法,因为所有值类型都是密封的。
三、枚举
枚举的基础类型不能是char.
枚举之间的类型兼容性
C#不支持两个不同枚举数组的直接转型,办法是先转型为一个数组,在转型为第二个枚举。