天天看点

C# 引用类型、值类型

在fcl中,所有称为“结构”(struct)的类型都是值类型,所有称为“类”(class)的类型都是引用类型。所有的struct都直接派生自抽象类system.valuetype,而system.valuetype直接从system.object派生。所有的枚举都直接从system.enum派生,而后者又派生自system.valuetype,所以枚举也是值类型。由于clr的单继承规则,所以我们在定义值类型时,不能指定基类型,但可以实现接口。同时从下图生成的il也可以看出,值类型是隐式密封的(sealed),也就是说也不能从值类型派生。

C# 引用类型、值类型

虽然引用类型与值类型实质只是内存分配上的差异,但这种差异会导致两种类型在行为表现上有着明显不同,比如下面的例子:

首先我们定义一个一值类型与一个引用类型,内部都只有一个字段。用new操作符分配内存时,值类型v1的内存分配在了线程栈上,引用类型r1的内存分配在了托管堆上,在程序运行到第一次writeline输出时,看到的结果是一致的。但接下来声明两个新的对象并执行赋值时,这里的发生的事明显不同:虽然赋值操作都是拷贝线程栈上变量的内容,但由于值类型变量v1的栈内容就是valtype类型实例本身,而引用类型r1的栈内容是reftype对象实例在堆上的地址。所以赋值后的结果就是,v1和v2各保存了一份valtype类型实例,而r1和r2保存了同一块堆内存的地址。所以改变r2对象导致了r1对象的随同改变。下面是内存示意图:

C# 引用类型、值类型

图1

C# 引用类型、值类型

图2 

虽然值类型实例不需要垃圾回收,但由于值类型在传递时,传递的是内容本身,所以并不适合将所一些实例较大的类型定义为值类型。实现上除非满足以下所有条件,否则不应该将一个类型声明为值类型。

没有更改其字段的成员,即该类型是不可变的。(建议所有字段为readonly)

类型不需要从其他任何类型继承。(值类型不能选择基类)

类型也不会派生出其他任何类型。(所有的值类型都是隐式密封sealed的)

实例较小(约<=16byte)或较大但不作为方法实参传递,也不从方法返回。

将值类型转换成一个引用类型的过程叫装箱,整个过程看起来是这样的:

在托管堆中分配好内存,分配的内存量=值类型的各个字段所需的内存量+所有堆上对象都有的两个额外成员(类型对象指针和同步块索引)所需的内存量。

值类型的字段复制到新分配的内存。

返回对象的地址。

拆箱仅是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。虽然拆箱比装箱代价低,但实际在拆箱之后往往紧接着就是赋值操作(内存复制)。显然装箱和拆箱/复制会对应用程序的速度与内存消耗上产生不利影响,所以应该了解到这一点,并尽量避免装箱和拆箱操作。那么什么时候会发生装箱和拆箱,最直观的方法就是看生成的il代码(il对应指令是分别是box与unbox),比如下面的例子:

C# 引用类型、值类型

示例中arraylist的add方法参数是object类型,也就是说一个引用类型(在堆上分配的内存),当我们传递int类型时,这里便会将int实例装箱,以返回一个堆上的地址。在将array[0]强制转型为int时,由于值类型int的对象是在线程栈上分配的,所以这里拆箱并紧接着发生赋值(内存复制)操作。同时为了对比,我加了引用类型的reference,可以看出引用类型是不会发生装箱与拆箱的。

那么如何避免(或减少)装箱与拆箱:

尽量使用泛型集合。

尽量将装箱与拆箱操作移到循环体之外。

定义一个方法如果可接收引用类型或值类型时,尽量不要将参数定义为object,可以考虑通过重载定义多个版本或定义泛型方法。

<a target="_blank" href="http://www.cnblogs.com/hecool/p/3149833.html">原文地址</a>