C#中的值类型和引用类型

值类型和引用类型

 本篇笔记结合了《CLR Via C#》和《C# in Depth》两本书中讲述的值类型和引用类型的区别和特性、值类型的装箱和拆箱这两部分内容。

但我根据装箱部分的理解所整理出来的配图可能会有错误和遗漏,希望能有人来指正。


现实世界中的值和引用

报纸与值类型

先假设你正在读的是一份真正的报纸。为了给朋友一份,需要影印报纸的全部内容并交给他。届时,他将获得属于他自己的一份完整的报纸。

在这种情况下,我们处理的是值类型的行为。所有信息都在你的手上,不需要从任何其他地方获得。制作了副本之后,你的这份信息和朋友的那份是各自独立的。可以在自己的报纸上添加一些注解,他的报纸根本不会改变。

网址与值类型

再假设你正在读的是一个网页。你需要给朋友的就是网页的URL。这是引用类型的行为,URL代替引用。为了真正读到文档,必须在浏览器中输入URL,并要求它加载网页来导航引用。

另一方面,假如网页由于某种原因发生了变化,你和你的朋友下次载入页面时,都会看到那个改变。

在C#和.NET中,值类型和引用类型的差异与现实世界中的差别类似。

谁是值类型或引用类型

.NET中的大多数类型都是引用类型,除了以下总结的特殊情况,类是引用类型,而结构是值类型。特殊情况包括如下方面:

①数组类型是引用类型,即使元素类型是值类型(所以即便 int 是值类型, int[] 仍是引用类型);

②枚举(使用 enum 来声明)是值类型;

③委托类型(使用 delegate 来声明)是引用类型;

④接口类型(使用 interface 来声明)是引用类型,但可由值类型实现。


值类型和引用类型的基础知识

值类型

大多数表达式都有与其相关的静态类型。对于值类型的表达式,它的值就是表达式的值。

值类型都隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。

虽然不能在定义值类型时为他选择基类型,但如果愿意,值类型可实现一个或多个接口。

引用类型

对于引用类型的表达式,它的值是一个引用,是new操作符返回对象内存地址——即指向对象数据的内存地址。

使用引用类型的四个事实

使用引用类型必须留意性能问题。首先要认清楚以下四个事实。

1.内存必须从托管堆分配。

2.堆上分配的每个对象都有一些额外成员,这些成员必须初始化。

3.对象中的其他字节(为字段而设)总是设为零。

4.从托管堆分配对象时,可能强制执行一次垃圾回收。


二者在内存中的区别

Point 类型可以实现为结构或类。

Point p1 = new Point(10, 20);
Point p2 = p1;

左部分指出当 Point 是引用类型时所涉及的值,右部分展示了当Point 是一个值类型时的情形。

①在 Point 是引用类型的情况下,那个值是引用: p1 和 p2 都引用同一个对象。

②在 Point 是值类型的情况下, p1 的值是一个完整的数据,也就是 x 和 y 值。将 p1 的值赋给 p2 ,会复制 p1 的所有数据。


声明值类型的条件

除非满足以下全部条件,否则不应将类型声明为值类型

①类型没有提供会更改其字段的成员,也就是说该类型是不可变类型,建议将绝大多数的值类型的字段都编辑为readonly。

②类型不需要从其他任何类型继承,类型也不派生出其他任何类型。

③类型的实例较小(16字节或更小);或者类型的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回。

 


值类型和引用类型的区别

值类型的主要优势是不作为对象在托管堆上分配。与引用类型相比,值类型也存在自身的一些局限。下面列出了二者的一些区别。

①值类型对象有两种表示形式:未装箱和已装箱。引用类型则总是处于已装箱形式。

②定义自己的值类型时应重写Equals和GetHashCode,并提供他们的显式实现。

③不应在值类型中引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封。

④引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null。值类型的变量总是包含其基础类型的一个值,所有成员都初始化为0。

⑥值类型变量赋值给另一个值类型变量,逐字段地复制。引用类型赋值,只复制地址。

⑦未装箱的值类型不在堆上分配。一旦定义了该类型的一个实例的方法不再活动,
为他们分配的存储就会被释放,而不是等着进行垃圾回收。


值类型的装箱和拆箱

值类型比引用类型轻,是他们不作为对象在托管堆中分配,不被垃圾回收,也不通过指针进行引用。但许多时候都需要获取对值类型实例的引用。

举个例子

例如假定要创建ArrayList对象来容纳一组Point结构。

struct Point{
    public Int32 x,y;
}
//测试类
ArrayList list = new ArrayList();
Point p;//分配一个Point,不再堆中分配
for(Int32 i = 0;i<10;i++){
    p.x = p.y = i;//初始化值类型中的成员
    list.Add(p);//对值类型装箱,将引用添加到ArrayList中
}
//本例的Add方法原型
public virtual Int32 Add(Object value);

可以看出Add获取的是一个Object参数,也就是说Add获取对托管堆上的一个对象的引用来作为参数。

但代码传递的是Point是值类型。为了使代码正确工作,Point值类型必须转换成真正的、在堆中托管的对象,而且必须获取对该对象的引用。

装箱机制

将值类型转换成引用类型要使用装箱机制。下面总结了对值类型的实例进行装箱时发生的事情。

①在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆所有对象都有的两个额外成员所需的内存量。

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

③返回对象地址。现在该地址是对象引用,值类型成了引用类型。

在运行时,当前存在于Point值类型实例p中的字段复制到新分配的Point对象中。已装箱Point对象的地址返回并传给Add方法。

Point对象一直存在于堆中,直到被垃圾回收。Point值类型变量可被重用,因为ArrayList不知道关于他的任何事情。

在这种情况,已装箱值类型的生存期超过了未装箱值类型的生存期。

拆箱

假定要用以下代码获取ArrayList的第一个元素。

Point p=(Point) a[0];

获取ArrayList的元素0包含的引用或指针,试图将其放到Point值类型的实例p中。

为此已装箱Point对象中的所有字段都必须赋值到值类型变量p中,后者在线程栈上。CLR分两步完成复制。

①获取已装箱Point对象中的各个Point字段的地址。这个过程称为拆箱。

②第二步将字段包含的值从堆复制到基于栈的值类型实例中。

拆箱不是直接将装箱过程倒过来,其代价比装箱低得多。拆箱就是获取指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。

指针指向的是已装箱实例中的未装箱部分。所以和装箱不同,拆箱不要求在内存中复制任何字节。往往紧接着拆箱发生一次字段复制。

已装箱值类型实例在拆箱时,内部发生这些事。

①如果包含“对已装箱值类型实例的引用”的变量变为null,抛出NullReferenceException异常。

②如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。

第二条意味着在对象进行拆箱时,只能转型为最初未装箱的值类型。

//错误
Int32 x=5;
Object o=x;//对x装箱,o引用已装箱对象
Int16 y=(Int16)o;//抛出InvalidCastExcrption异常
//正确
Int32 x=5;
Object o=x;//对x装箱,o引用已装箱对象
Int16 y=(Int16)(Int32)o;//先拆箱为争取类型,再转型
原文地址:https://www.cnblogs.com/errornull/p/10022846.html