值类型与引用类型

摘要:
  • 定义与概述
  • 内存的分配
  • 判等(Equals,ReferenceEquals,==/!=)
  • 装箱与拆箱[互相转换]

1、定义与概述

从上一节中图2-2中通用类型系统的基本结构。我们知道CTS包含了值类型和引用类型。

  • 引用类型[银行卡]:在.NET Framework中, 任何称为“类(class)”的所有类型。引用类型是指变量仅存储地址,对应的数据可以在该地址中找到引。用类型存储到Heap内部,并且可以通过地址寻址到值。就像c++中指针的理解相类似。这么看来有点象我们的银行卡(引用类型)可以寻到ATM(寻址)取现金(值)。虽然例子有点牵强,但是能够说明主要的区别。
  • 值类型[现金]:而其中的结构类型(struct),枚举类型(enum)统称为值类型。值类型是指变量直接存储其数据,值类型一般存储在堆栈(Stack)中。

      两者之间的内存分配情况下文会进行实例说明。值得一提的是,我们在平常研发的过程中,用的比较多的是预定义值类型。而这些预定义类型并没有内置于C#语言中,而是内置于NET  Framework中。例如 C#中声明一个int类型数据,声明的实际上是Net 上的System.Int32的一个实例。预定义类型了解下。接下来,将会用例子说明值类型与引用类型之间的区别以及内存分配。

2、内存的分配

  对于值类型和引用类型的对比的例子,值类型用struct类型,引用类型用class类型进行演示。具体代码如下:

 using System;

public struct SomeVal
{
     public int x;
}

public class  someRef
{
     public int x;
}

public class App
{
     public static void Main(string[] args)
     {

          var v1=new SomeVal();//在栈上分配
          var r1=new someRef();//在堆上分配

          v1.x=5;//在栈上修改
          r1.x=5;//提领指针

          Console.WriteLine("v1.x is {0}",v1.x);//输出 5
          Console.WriteLine("r1.x is {0}",r1.x);//输出 5

          var v2=v1;//在栈上分配并复制成员
          var r2=r1;//只复制引用(指针)

          v2.x=8;//v2.x会更改,而v1.x不会更改
          r2.x=9;//r1和r2都会修改

          Console.WriteLine("v1.x is {0}",v1.x);//输出 5
          Console.WriteLine("v2.x is {0}",v2.x);//输出 8
          Console.WriteLine("r1.x is {0}",r1.x);//输出 9
          Console.WriteLine("r2.x is {0}",r2.x);//输出 9
     }
}

 上面例子相对应的内存分配图如下:

  说明:分别声明与实例化v1和r1一个结构体和类,从图1-1中可以看出,v1将值直接存储在栈上,而r1只是在栈上存储地址,通过该地址指向其内部值;在图1-2中,重新声明v2和r2,并将v1和r1分别赋值给v2和r2;其内部内存发生了情况图1-2可以明确知道,v2创建了一个新的副本,而r2只是将地址指向到与r1相同的地址;那么现在如果我改变v2的x的值,v1的x的值是不会一起改变的,因为v2只是v1的一个副本,而如果改变r2的x的值,v1的x值会一起改变,这是因为r1和r2所指向的相同的地址。如图1-3。

  由此例子衍生总结出值类型与引用类型的区别如下:

  • 初始化差异:引用类型的变量包含的是堆上的一个对象的地址。默认情况下,在创建一个引用类型的变量时,它被初始化为null,如果直接使用将会抛出NullReferenceException异常。而值类型的默认初始化所有成员为0;
  • 变量赋值差异:将一个值类型的变量赋给另外一个值变量,将会创建一个副本(逐个字段的复制),而引用类型是的变量赋给另外一个引用类型变量时,复制了内存地址(引用)。
  • 已装箱和装箱的差异:引用类型的对象总是处于已装箱形式;而值类型的对象有两种形式未装箱和已装箱的形式,对于装箱与拆箱的
  • 继承性差异:不能将一个值类型作为基类型来定义一个新的值类型或者一个新的引用类型。值类型一般都是sealed关键字修饰;值类型是从System.ValueType派生的,该类重写了Equals 方法,能在两个对象的字段完全匹配的前提下返回True;而引用类型可以支持多态,继承,并且都是从System.Object 派生而来。

3、判等(Equals,ReferenceEquals,==/!=)

  明确了值类型与引用类型的区别之后,那么现在如何判定两个对象之间是否相等。在对象的比较中通常用Equals(),ReferenceEquals()和操作符==/!=三种常见的方法。

  •   Equals()判断的是“同一性”而非“相等性”,在NET Framework中很多类型实现了该方法的覆写[后期会介绍]。其中值类型的System.ValueType就覆写该方法,对实例数据的判等也就是相等性。当然值得注意一点,重写Equals()还必须重写GetHashCode方法,不然会编译出错。
  •   ReferenceEquals()静态方法主要用于判断“同一性”
  •   操作符==/!= 其内部是调用安全的Equals方法,所以不能运用到判断同一性上。

  ps:同一性:指向同一个对象引用;相等性:两个对象包含相同的值。

而对于值类型和引用类型的在三个方面判等上的区别如下:

  • 值类型判等
    • Equals()方法:System.ValueType重载了System.Object的Equals方法,用于实现对实例数据的判等
    • ReferenceEquals(),对值类型应用ReferenceEquals将永远返回false;
    • ==,为重载的==的值类型,将比较两个值是否“按位”相等
  • 引用类型的判等
    • Equals,主要有两个方法,如下
      public virtual bool Equals(object obj);
      public static bool Equals(object objA,object objB);
      一种是虚方法,默认为引用地址比较;而静态方法,如果objA是与objB相同的实例,或者如果两者均为空引用,或者如果objA.Equals(objB)返回true,则为true;否则为false因此判等的返回值要根据具体的覆写情况而定
    • ReferenceEquals,静态方法,只能用于引用各类型,用于比较两个实例对象是否只想同一个引用地址。
    • ==,默认为引用地址比较,通常进行是吸纳了==的重载,未重载==的引用各类型将比较两个对象是否引用地址,等同于引用各类型的Equals方法。关于重载和覆写的学习后面会介绍。

4、装箱与拆箱[互相转换]

  对于值类型与引用类型的互换,这就涉及到装箱(boxing)和拆箱(unbox)的知识点。这两者之间的定义如下所示。

装箱 (boxing)
在程序设计中,值类型实例到对象的转换,它暗示在运行时实例将携带完整的类型信息,并在堆中分配。Microsoft 中间语言 (MSIL) 指令集的 box 指令,通过复制值类型,并将它嵌入到新分配的对象中,将值类型转换为引用类型。
拆箱(unboxing)
是将引用类型转换为值类型利用装箱和拆箱功能,可通过允许值类型的任何值与Object 类型的值相互转换,将值类型与引用类型链接起来

  这里对boxing和unboxing内部发生的事情进行简单的说明如下

  • 装箱(boxing)操作内部发生的事情
    • 在托管堆中分配好内存。分配的内存量是值类型的个字段需要的内存量加上托管堆的所有对象都有的两个额外成员(类型对象指针和同步块索引)需要的内存量
    • 值类型的字段赋值到新分配的堆内存
    • 返回对象的地址。
  • 拆箱(unboxing)操作内部发生的事情
    • 获取已装箱的对象的地址
    • 将字段从堆中赋值到基于栈的值实例中。

  第二篇,不容易的说。若有什么错误请指正,倘若觉得不错请点击下推荐,谢谢。

 参考文档:

  CLR VIA C#

  你必须知道的.Net

posted @ 2014-01-25 16:00  卤鸽  阅读(417)  评论(0编辑  收藏  举报