Fork me on GitHub

.NET值类型和引用类型101

1.1.1 摘要

      什么是值类型?什么是引用类型?这问题大家很久以前就讨论和研究过了,而且我相信很多人对值类型和引用类型的区别和用法都娴熟于心。这里我给出自己的总结,而且也提供大家一个复习值类型和引用类型的机会。

      熟悉C/C++的程序员都知道你可以为任何类型的对象建立指针来引用它们。这也不是java,任何类型都自动声明为引用类型,然后C#中的类型提供了值类型和引用类型。

1.1.2 正文

type0

图1值类型和引用类型大纲

      值类型是用来存放对象的值,那么引用类型就是存放对象的引用值,真的是这样吗?我看到很多人都说:“值类型就是用来存放值,而且不能或不应该。存放方法和行为”在我看来值类型不仅仅是存放值的,还是有存放方法和行为的,例如:DateTime和Decimal毫无疑问是值类型,而且.NET为该类型提供了许多方法和行为,以致DateTime和Decimal的功能强大。

      值类型的值存放在栈中,引用类型的对象存放在堆中,真的是这样吗?我觉得这要视乎语言而定,就像我们前面提到C/C++类型都被定义为值类型,而Java中的类型都被定义为引用类型(前置条件一),而且我们不应该认为存储区域是一成不变的,要看具体的语言,说不定猴年马月C#会把一些引用类型放在栈中。

      还有引用类型对象存放在堆中是没有问题的,但是值类型的值就存放区域要根据值类型的定义来确定,例如我们把一个值类型分别定义为一个方法中局部变量和一个类中的字段,存放区域就不一样了,前着存放在栈中,而后者存放在堆中(前置条件二)。而且我相信在上学的时候老师讲的最多是值类型和引用类型存放区域—堆栈,以后要分别加上连个前置条件。

 

  • 根据具体的语言
  • 根据值类型具体定义

type1

图2值类型和引用类型存储

      上面简单地描述了值类型和引用类型在内存中存储的方式,值类型数据存储在栈中,引用类型的引用值存放在栈,而对象和数据值存储在堆中,大家都清楚.NET中提供的预定义类型中除了object和string是引用类型外其他类型都是值类型。让我们通过具体的例子来讲讲值类型和引用类型。

      OK首先我们定义一个Custom类,然后添加两个字段_index和_description,为了简洁起见我们没有使用属性,具体例子如下:

/// <summary>
/// A custom clase.
/// </summary>
public class CustomType
{
    /// <summary>
    /// _index is value type.
    /// _description is reference.
    /// </summary>
    private int _index;
    private string _description;

}

    假设我们实例化该类型的一个对象,然后让我们分析一下该对象在内存中是如何分配的,好现是画画的时间了。

  • 引用类型的对象和数据总是存储在堆中
  • 值类型和引用类型的引用值可以存储在栈或堆视乎语言环境而定

 

type2

图3引用类型存储方式

      通过上图我们可以发现引用类型的对象是存放在堆中这是毫无疑问的,而我们在CustomType中定义的值类型_index也是存放在堆中,这充分说明一点就是值类型的存储区域要根据具体定义确定。

      接下来我们介绍一个值类型--Struct,我们定义名为CustomType的结构体,然后添加两个字段和之前类中的一样,现在又是画画的时间了。

 

type3

图4值类型存储方式

     现在我们实例化一个CustomType对象,由于我们清楚地知道CustomType是一个值类型,所以它将被存储在栈中,而且_index的值和_description的引用值都被存储在栈中,_description具体的值或对象被存储在堆中。

     通过前面的引用类型和值类型对象在内存中存储的例子,我们知道不能笼统地说:“值类型存储在栈中,引用类型存储在堆中”,而是要加上前置的条件(语言类型,定义类型)。

     让我们通过一段简单的代码向大家展示值类型和引用类型之间的使用上的区别,这里我们使用Struct和Class为例。

public class MyType
{
    /// <summary>
    /// This class has two fields.
    /// </summary>
    private CustomType type1;
    private CustomType type2;

    /// <summary>
    /// Initializes a new instance of the <see cref="MyType"/> class.
    /// </summary>
    public MyType()
    {
        this.type1 = new CustomType();
        this.type2 = new CustomType();
    }
}
//// Instantiates a object of MyType.
MyType myType = new MyType();

     现在让大家分析一下当类型CustomType分别是结构体和类时内存分配空间的大小和分配次数(假设在32位CLR下)。

首先当CustomType是结构体时只需要一次内存分配,大小为CustomType类型大小的两倍,由于CustomType大小为8Byte,则MyType分配空间大小为16Byte。如果CustomType是类时我们需要三次内存分配,一次是MyType对象的堆中分配,接着两次分别是CustomType对象的堆中分配。

    再举一个简单的例子:

MyType[] var = new MyType[888];

     如果MyType是一个值类型,则只需要一次分配,大小为 MyType对象大小的888倍。但如果MyType是一个引用类型,刚开始需要一次分配,分配后数组的各元素值为 null。如果再初始化数组中的每个元素,我们总共将需要执行889次分配——889 次分配要比 1次分配耗费更多的时间。分配许多引用类型对象将在堆空间上造成很多碎片,从而降低系统的速度。

      很多人都说:“结构体是一个轻量级类”,他们这样认为是根据结构体的效率比类要好,但我觉得这样理解不够全面,通过前面的两个例子我们的确看到结构体的确效率优于类,因为值类型不需要垃圾回收(除装箱的值类型之外),而且没有类型识别开销。但如果我们要进行的是大量的数据copy时候,值类型要复制要初始化每个变量的值,而引用类型只需复制引用值就OK了。

MyType T1 = new MyType();
MyType T2 = T1;
type4 type5

图5值类型引用类型赋值

      通过上面的例子我们发现当MyTpye是值类型时我们需要复制N次而引用类型只需复制一次,如果复制数据量多时值类型效率比引用类型效率要低。因为引用类型只需复制引用值就OK了。

值类型和引用类型应用场合区别:

  • 该类型的主要职责是否用于数据存储?
  • 该类型的公有接口是否完全由一些数据成员存取属性定义?
  • 是否确信该类型永远不可能有子类?
  • 是否确信该类型永远不可能具有多态行为?

装箱和拆箱

     Boxing是.NET中提供一种机制使得根据值类型产生相应的对象,Unboxing就是把对象中的值取出来在放到相应的值类型中。然我们通过一段简单的例子来说明。

int i = 5;

object o = i; //boxing

int j = (int)o; //unboxing

    我们定义了一个值类型i和一个引用类型o,当由值类型i赋值给o时候发生了boxing,而由对象o赋值到j时候发生unboxing。但我们要注意的是如果我们boxing是long,而unboxing到int时候会抛出一个InvalidCastException异常。我们可以通过以下方式处理转换异常问题,但这样给代码带来了不必要的冗余和精度缺失,所以我们在进行unboxing时应该和boxing变量类型一致。

long i = 5;
object o = i;  //boxing

int j = (int)(long)o; //unboxing

     通过前面的例子我们发现boxing和unboxing的转换并没有什么标识,我们只可以根据值类型和引用类型之间转换判断boxing和unboxing发生。为了帮助大家理解这一点,考虑下面代码boxing和unboxing操作的次数:

ArrayList list = new ArrayList();
list.Add(22);
list.Add(23);

list.Add((int)list[0] + (int)list[1]);

foreach (int i in list)
{
    Console.WriteLine("Number is: {0}.\n", i);
}

     首先我们往list数组中放入两个值类型,由于Add()方法接受的参数是object类型,所以需要进行两次boxing操作。

     接着我们把list中的两个对象取处理,然后转换为值类型进行相加操作这需要两次unboxing操作,最后把结果指存放到list中要进行boxing操作。

     在遍历list时候我们要把list中的值保存到i中发生unboxing操作,接着我们调用Console. WriteLine (string format, object arg)要对i进行boxing操作。

     所以发生了4次boxing和3次unboxing操作。值得庆幸的是boxing和unboxing操作不多,对我们的程序效率暂时还没有大的影响,但如果频繁进行boxing和unboxing操作就会对我们程序产生不可估量的影响,这也是.NET提出要使用泛型的一个原因。

    上面的例子我觉得发生比较隐秘是在WriteLine()方法,因为平时使用太多反而没有注意该方法传递的参数类型。

接下来我们通过一些更加隐秘的例子说明什么时候会发生boxing操作。

int i = 23;
i.ToString();     //A
i.GetHashCode();  //B
i.GetType();      //C
i.GetTypeCode();  //D

      请大家分析一下那个方法进行了boxing操作,激动人心的时刻又到了来让我们公布答案--C。

原因很简单Object.GetType()方法不可以重写,所以值类型类Integer没有实现自己的GetType方法,只能通过boxing操作调用Object的GetType()方法。

type6

图6 MSIL代码

 

1.1.3 总结

本文主要介绍什么是值类型和引用类型,和它们在内存中的分配我们从中看到了值类型和引用类型的区别:

  • 引用类型存放对象的引用值,而不是对象本身
  • 值类型存放是具体的数据本身
  • 很多时候值类型比引用类型高效,但也有例外情况
  • 引用类型的对象存储在堆中,而值类型数据可以存放在栈或堆中,要根据具体的定义
  • 值类型值可以通过boxing转换为引用类型,通过unboxing转换为值类型
posted @ 2011-05-29 20:26  JK_Rush  阅读(4106)  评论(8编辑  收藏  举报