装箱和拆箱的问题(NET1.1+)

【问】假设有这样一段代码

[C#]

namespace MyTest

{

public interface I

{

void Change(int x, int y);

}

public struct Point:I

{

public int X { get; set; }

public int Y { get; set; }

public override string ToString()

{

return X+""+Y;

}

public void Change(int x, int y)

{

X=x;

Y=y;

}

}

public class Program

{

static void Main(string[] args)

{

Point p = new Point();

p.X = 1;

p.Y = 2;

object o = p;

((I)p).Change(2, 2);

Console.WriteLine(p);

((Point)o).Change(2, 2);

Console.WriteLine(p);

}

}

}

[VB.net]

Namespace MyTest

Public Interface I

Sub Change(x As Integer, y As Integer)

End Interface

Public Structure Point

Implements I

Public Property X() As Integer

Get

Return m_X

End Get

Set(value As Integer)

m_X = value

End Set

End Property

Private m_X As Integer

Public Property Y() As Integer

Get

Return m_Y

End Get

Set(value As Integer)

m_Y = value

End Set

End Property

Private m_Y As Integer

Public Overrides Function ToString() As String

Return X & "" & Y

End Function

Public Sub Change(x__1 As Integer, y__2 As Integer) Implements I.Change

X = x__1

Y = y__2

End Sub

End Structure

Public Class Program

Public Shared Sub Main()

Dim p As New Point()

p.X = 1

p.Y = 2

Dim o As Object = p

DirectCast(p, I).Change(2, 2)

Console.WriteLine(p)

CType(o, Point).Change(2, 2)

Console.WriteLine(p)

End Sub

End Class

End Namespace

问上面程序运行之后,输出是什么?

【错误回答】

输出两次“2,2”。因为第一次强制把值类型转化成引用类型,根据引用类型的特点(如果:A=B,那么A改变了B也一定随着A改变;反之亦然);第二次在取出已经改变的Point,然后人为改变成2,2.

【正解】要回答这个问题,首先我们必须要彻底弄清楚“拆箱”和“装箱”在.NET中的内部运行机制。在.NET程序中总共有2中大类型——值类型和引用类型。“值类型”泛指struct类型(实际上,int,byte本质也是值类型,因为都可以找到他们的原型——Int32和Byte等)。其余都可以算作是引用类型了。这样一来,赋值时候总共有三种关系:

1) A=B(A,B都是值类型,则系统把右边的值拷贝给左边,A,B互不影响)。

2) A=B(A,B都是引用类型,且B继承于A,那么B受A的影响而影响)。

3) A=B(A是引用类型,B是值类型,且A转换成B。例如B实现了一个A接口,或者是把B直接赋值给object。通常,我们把“值类型”到“引用类型”成为“装箱”

4) B=(B)A (A是引用类型,B是值类型,且在经过3之后进行强制转换。通常,我们把“引用类型”强制转换到“值类型”成为“拆箱”)。

其中1和2不再叙述,专门讨论3和4——对于3,大家要有一个概念——那就是当把任意的值类型赋值给引用类型的时候,引用类型其实引用的并不是“值类型”自身,而是值类型的一个拷贝(副本,相当于3中A引用B,其实A引用的是B的一个拷贝)。因此和B毫无关系。所以上述回答错误之处在于看到什么东西转化成”引用类型“就武断地认为“级联改变”,没有考虑到“级联改变”是“引用类型<=>引用类型”(是2的情况),而忽略了“值类型=引用类型”(两个类型不等的情况)。

根据这个道理,我们就能分析出题目运行结果:

1) 因为o是引用类型引用了p,且p是值类型。所以p相当于拷贝了一份自身被o引用。同样地,(I)p的时候,因为I是接口,也是引用类型,引用了p了的一个副本,和p无关。因此输出的p还是1,2.

2) 因为o得到的是p的副本,强制转化成Point之后也是对副本又进行了一次拷贝(是对副本的副本)做了改动,并非p自身,输出的也是1,2。因此我们说,把引用类型强制转化成值类型也一定发生了拷贝行为,相当于可以理解成“值类型=值类型“的情形。因此修改拷贝后的值类型也一定不会影响先前的引用类型中的那个“值类型”的值。同样地,第二次错误的回答也是基于“‘强制转换’总是把前一次的值取出来,所以改变还是原来的值”——总而言之,都是“以偏概全”,把“引用类型<=>引用类型”当成是万能公式去套用显然不行的。

总结:

1) 因为无论拆箱和装箱,都会引发数据拷贝的情况。因此我们尽量使用泛型或者重载函数等取代频繁的拆箱和装箱。

2)  拆箱不能独立于装箱存在。对一个没有装箱的引用对象进行拆箱自然引发空引用异常。

【拓展】

“无论装箱和拆箱”总是要引发数据拷贝,这句话我们现在可以通过程序运行证得。不过我们似乎还不甘心——难道真是这样吗?为什么“装箱”或者“拆箱”要发生数据拷贝呢?为了解释这个问题,我们需要对.NET运行时候内存情况进行剖析——

在程序运行时,其实基于.NET的内存区域已经被自动划分成两大块——“堆”(heap)和“栈”(stack)。“栈”(stack)只用于存放值类型的数据(包含int,double等一切struct基本类型,以及自定义的struct类型)。当一个值类型和一个值类型通过赋值符号“=”发生关系时候,实际上进行了拷贝。比如我们有一个int a=1, 现在我们又声明了int b,同时令b=a。其实内存先给a分配了栈的区域,赋值1;然后又给b分配了栈的区域,把a的值拷贝给了b。因此b还是1.内存的图可以形象这样表述:

 

 

当为一个类(接口等)声明一个引用类型对象时,其实先在栈中存放了一个指向该对象的指针(尽管C#不建议使用指针,这是考虑对托管的堆释放造成不可靠的情况产生,但是类或者接口——进一步说,任意一个引用变量名,其本质是一个指针,或者说一个引用变量名),然后在“托管堆”中真正分配了这个类的实体对象,自然地,栈中的“类指针”指向托管堆的“实体”,比如当object obj = new object();时候,情况如下图:

 

 

现在情况是,因为“结构类型”总是在栈上创建,怎么也不肯到“堆”上去;但是“类指针”要引用“类实体”,该实体就必须在“堆”上创建。怎么办?唯一的办法就是当引用(指针)指向一个“值类型”时候,值类型自动复制一份到“堆”上去。所以从下图中可以看出,引用类型指向的“结构实体”和原来的实体毫不相干了。又如现在object obj = a;情况变成这个样子(实际上obj指向是a的副本a',不是a)

 

 

同样地,引用类型强制转化成值类型(因为值类型)只能放到“栈”上,于是只能再次自身拷贝一次,比如int d = (int)obj;

posted @ 2011-10-25 13:02  Serviceboy  阅读(677)  评论(0编辑  收藏  举报