zoukankan      html  css  js  c++  java
  • 装箱和拆箱的问题(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;

  • 相关阅读:
    使用java Graphics 绘图工具生成顺丰快递电子面单
    NPM使用命令总结
    MYSQL主从库同步配置过程
    Redis的事务功能详解
    MapReduce 原理与 Python 实践
    Django权限机制的实现
    Python调用外部程序——os.system()和subprocess.call
    oracle11g安装教程(注意事项及图文教程)
    顶级的JavaScript框架、库、工具及其使用
    经典CSS坑:如何完美实现垂直水平居中?
  • 原文地址:https://www.cnblogs.com/ServiceboyNew/p/2223602.html
Copyright © 2011-2022 走看看