Wu.Country@侠缘

勤学似春起之苗,不见其增,日有所长; 辍学如磨刀之石,不见其损,日所有亏!

深入浅出boxing和unboxing

上次写的一篇关于ref和out关键字的问题,算是自我反省了一次。不过我觉得学习就应该有一种不怕挫折的的精神。继续学习,继续写文章。

这次讨论一下boxing和unboxing问题,这是我今天在一论坛上看到的问题(源文代码):

public interface IStudent 

String Name

get;set;}
 
}
 
public struct Student:IStudent 

public String name; 
public Student(string _name) 
{name=_name;} 
public String Name 

get{return name;} 
set{name=value;} 
}
 
}
 
现在这样调用它: 
Student stu
=new Student("axiang"); 
Object o
=(Object)stu;//boxing 
Student stu1=(Student)o;//unboxing 
stu1.Name="andy"
stu
=(Student)o; 
Console.WriteLine(stu.name);
//what would be written? 
IStudent istu=(IStudent)o;//unboxing 
istu.Name="andychan"
stu
=(Student)o; 
Console.Write(stu.name);
//what would be written? now 

我先不讨论这个结果是什么,显然它这里是用到了boxing与unboxing的问题。我们先从最简单的boxing与unboxing来分析:
        int m_int1        = 1;
        Object m_obj1    
= m_int1;        //boxing
        int m_odb2        = (int)m_obj1;    //unboxing
        m_odb2            = 2;
        Console.WriteLine(
"m_int1:{0}",m_int1);
        Console.WriteLine(
"m_odb2:{0}",m_odb2);
显示结果为
m_int1:1
m_odb2:2
看一下MSDN里的帮助:找到了这样的一张图来说明问题:
untitled.bmp
可以清楚的看到,在boxing与unboxing的时候,分别进行了两次内存COPY(而在Jeffery先生的书上说明只有一次,而在unboxing的时候是有一次内存COPY的)。

好了,这回又来到值类型数据与引用数据类型的问题上来了。上面的例子是用值类型数据为例的,引用类型会是什么结果呢?在MSDN上没有找到相关的帮助,但我们可以试试:
        Class1 m_obj1    = new Class1();
        m_obj1.m_member    
= 1;
        Console.WriteLine(
"m_obj1.m_member:{0}",m_obj1.m_member);
        Object m_obj2    
= (Object)m_obj1;
        m_obj1.m_member    
= 2;
        Console.WriteLine(
"m_obj1.m_member:{0}",m_obj1.m_member);
        Console.WriteLine(
"m_obj2.m_member:{0}",((Class1)m_obj2).m_member);
        Class1 m_obj3    
= (Class1)m_obj2;
        m_obj3.m_member    
= 3;
        Console.WriteLine(
"m_obj1.m_member:{0}",m_obj1.m_member);
        Console.WriteLine(
"m_obj3.m_member:{0}",m_obj3.m_member);
类:
class Class1
{
    
public int m_member    = -1;
}
这回的结果是:
m_obj1.m_member:1
m_obj1.m_member:2
m_obj2.m_member:2
m_obj1.m_member:3
m_obj3.m_member:3
也就是说,boxing与unboxing在引用类型数据上没有起到作用!正如深入剖析引用参数Ref和Out中所讨论到的,这里在堆上的只是引用类型数据的值,而好象是boxing与unboxing的地方,只是做的引用COPY,其实根本上算不上了boxing与unboxing了,都看的出来,只是引用的赋值。然而这里拿它出来讨论是因为可能会有文章一开始提出的那个问题:就是如果一个结构(值类型数据),如果它实现了一个接口,那么在与接口的转化中,会是什么问题呢?
看这样的例子:
        struct1 m_stru        = new struct1();
        m_stru.m_member        
= 1;
        Object m_obj        
= m_stru;
        Iinterface1 m_inter 
= (Iinterface1)m_obj;
        m_inter.m_ID        
= 3;
        struct1 m_stru2        
= (struct1)m_obj;
        Console.WriteLine(
"m_stru.m_member:{0}",m_stru.m_member);
        Console.WriteLine(
"m_stru2.m_member:{0}",m_stru2.m_member);
结构与接口:
interface Iinterface1
{
    
int m_ID{set;get;}
}


struct struct1:Iinterface1
{
    
public int m_member;
    
Iinterface1
}
输出结果:
m_stru.m_member:1
m_stru2.m_member:3
看清楚了吗?即:用接口来修改了值类型(结构)里的数据!

其实这里已经不能说m_obj是值类型数据了,因为经过boxing,它已经成了引用类型。但由于它是用struct通过boxing过去的,所以再以struct身份unboxing回来的时候,都将会产生值类型unboxing的效果,也就是从栈上COPY一份数据到堆上,最终就是所有的数据修改都不会影响boxing后的引用数据。但是,如果用接口(interface)来处理的时候,就不会COPY内存,因为接口也是引用类型,这样在object与interface之间转化的时候,可以不COPY内存。我们就可以像上面的例子那样,用接口来来处理经过boxing的"值类型"数据。

其实这一问题在Jeffery的书中已经讨论过,这就是一种他所说的内存“欺骗”。这样的方法听说在CV++中经常用,而在C#里,本来是不用的,但这样的结构在编译时“骗过”了编译器,而且取得了在程序设计上的灵活性。然而Jeffery先生并不赞成这样的使用方法,因为虽然取得了一定的灵活性,同时也程序变得复杂了。

至于本文一开始的那段代码的结果,应该可以分析出来了,就是用struct来转时候,是一次内存的COPY,也就是unboxing,而用interface来转的时候,就只是一次引用的COPY,所以通过interface可以修改到boxing后的struct的数据。

posted on 2006-03-02 16:32 Wu.Country@侠缘 阅读(1549) 评论(13)  编辑 收藏

评论

#1楼  2006-03-02 16:53 A.Z      

我还以为深入浅出MFC呢,请看http://a-z.cnblogs.com/archive/2006/02/08/327218.html
  回复  引用  查看    

#2楼 [楼主] 2006-03-02 17:06 Wu.Country@侠缘      

哦。。。。。。。可能程序员对深入浅出MFC都很了解吧。。。呵呵,偷个关键字。
你的那篇文章在你POST的时候我就看了一遍的,但我没学过汇编,所以有一些看不明白。有时间我还会看一下的。
我还仔细看了一下关于boxing与unboxig之间的问题,看作者说的比我理解的清楚多了,谢谢了。   回复  引用  查看    

#3楼  2006-03-02 23:07 东海风      

如果说关于ref和out关键字的问题应该反醒的话,我觉得这里好像是犯了一个同样的错误。我认为对于boxing与unboxing,值类型和引用类型是一样的,楼主之所以在第二个测试中得出“boxing与unboxing在引用类型数据上没有起到作用”的结果,是因为将“更改变量”与“更改变量的成员”未正确区分造成的。看如下代码:

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
TempClass modelTmp1 = new TempClass();
modelTmp1.Name = "Model1";
TempClass modelTmp2 = new TempClass();
modelTmp2.Name = "Model2";

TempClass tmp1 = modelTmp1;

object obj1 = (object)tmp1;

TempClass tmp2 = (TempClass)obj1;
tmp2 = modelTmp2;

Console.WriteLine("tmp1.Name:{0}",tmp1.Name);
Console.WriteLine("obj1.Name:{0}", ((TempClass)obj1).Name);
Console.WriteLine("tmp2.Name:{0}", tmp2.Name);

Console.ReadLine();
}
}

class TempClass
{
private string name;

public string Name
{
get { return name; }
set { name = value; }
}
}

}
运行的结果将是:

tmp1.Name:Model1
obj1.Name:Model1
tmp2.Name:Model2

显然,对于引用类型,boxing+unboxing的结果同样不是原来的引用。   回复  引用  查看    

#4楼 [楼主] 2006-03-03 08:58 Wu.Country@侠缘      

如果你看了我前面的那篇文章,可能就不会有这个问题了,我画了张图来解释你的代码:
classRelation.bmp
 
像“装配脑袋”一样,一句一句的给你解释一下:
 TempClass modelTmp1 = new TempClass();
modelTmp1.Name = "Model1";
TempClass modelTmp2 = new TempClass();
modelTmp2.Name = "Model2";
//实例,所以modelTmp1指向实例1,modelTmp2指向实例2

TempClass tmp1 = modelTmp1;
object obj1 = (object)tmp1;
//tmp1与modeTmp1一样,都指向modelTmp1,
 //obj1也和tmp1一样,指向modelTmp1,只是以不同的“身份”出现。
 
TempClass tmp2 = (TempClass)obj1;
tmp2 = modelTmp2;
//tmp2指向obj1,些时以TempClass的类型出现。也就是tmp2指向instance1
//修改tmp2指向,以TmpClass类型指向instance2
 
 好了,最后的指向结构就是我的那张图了。至于输出结果当然就是你给出的了。
 这就是“装配脑袋”上次给我解释的过程(当然我是这样理解的)。
  回复  引用  查看    

#5楼  2006-03-03 09:29 装配脑袋      

这回引用关系分析得不错,呵呵。引用类型根本没有装箱的概念。   回复  引用  查看    

#6楼 [楼主] 2006-03-03 10:33 Wu.Country@侠缘      

谢谢,呵呵,多谢你上次的耐心解释呀。。。。。。   回复  引用  查看    

#7楼  2006-03-03 11:17 东海风      

装配脑袋说的对:引用类型是没有装箱概念的。引用《C#高级编程》里的一句话:封箱和拆箱可以把值类型转换为引用类型,或把引用类型转换为值类型。
在侠缘的这篇文章里,我们可以暂且把装箱理解为“从子类到父类的转换”,把拆箱理解为“从父类到子类的转换”。我上一篇回复的意思是:将一个值类型变量装箱,再拆箱后赋给另一个变量,这两个变量是相互独立的,只是他们存储的值相同而已,这完全没有问题,修改其中任一个变量的值自然不会引起另一个变量的值发生变化。将一个引用型变量“装箱”,再“拆装”后赋给另一个变量,这两个引用类型的变量同样是独立的,只是由于引用类型变量的特性,他们不是具有相同的“值”,而是“指向同一个实例的引用”,修改其中任何一个变量(当然还是修改引用),同样不会引起另外一个变量发生变化。总之,这是值类型与引用类型的区别,与boxing或unboxing是无关的。说白了,这和下面这段代码的意思是一样的:
int v1=1;
int v2=v1;
v2=2;

TempClass r1=new TempClass();
TempClass r2=r1;
r2=new TempClass();
我是这样理解的,可能有不当之处,欢迎大家共同讨论。做技术嘛,就需要这样,是吗?   回复  引用  查看    

#8楼 [楼主] 2006-03-03 11:37 Wu.Country@侠缘      

我明白你的意思了,
你的意思是说:上面例子里,修改unboxing之后的tmp1或者tmp2与修改unboxing之后的值类型是一样的,只不过一个修改的是引用的地址值,另一个是修改的实际数据值。而我的文章例子里没有修改引用地址值,而是修改的引用的实例的成员值。所以你提出了你的问题。对吧!?
其实明白是什么意思及概念就行了。
我的文章是想说明,本来通过boxing后,对于值类型,再unboxing回来,是不能修改boxing后的值的。但如果利用了接口,是可以用接口来修改boxing后的值类型数据的。谢谢你提出的讨论。希望对阅读的朋友们都有帮助,不至于像我那样犯错误了。呵呵。。。   回复  引用  查看    

#9楼  2006-03-03 18:14 东海风      

OK!   回复  引用  查看    

#10楼  2006-03-03 18:48 Sheva      

其实了解boxing和unboxing对提高你的程序的性能是有很大帮助的。
比如:
Int32 x = 12345;
System.Console.WriteLine(x);

Int32 x = 12345;
System.Console.WriteLine(x.ToString());
这两行代码虽然结果一样,但是第一个例子会产生boxing操作,而第二个例子则不会。

Sheva   回复  引用  查看    

#11楼 [楼主] 2006-03-06 09:20 Wu.Country@侠缘      

是呀,应该多明白一些隐式的操作。而且新装箱与拆箱在本地CPU上也很浪费的,在A.Z的给出的文章里说的很清楚,大家可以看一下关于装箱与拆箱方面的内容。   回复  引用  查看    

#12楼  2006-06-06 10:45 一唯      

差点被楼住搞晕了。再吧jeffrey的书翻了一下,又清晰了。我的总结就是box和unbox操作是对于值型和引用型的相互转换,所以引用型与引用型是不需要box和unbox的。对于这一点发现侠缘的一个错误:
Student stu=new Student("axiang");
Object o=(Object)stu;//boxing
Student stu1=(Student)o;//unboxing
stu1.Name="andy";
stu=(Student)o;
Console.WriteLine(stu.name);//what would be written?
IStudent istu=(IStudent)o;//unboxing ★
istu.Name="andychan";
stu=(Student)o;
Console.Write(stu.name);//what would be written? now
★这里并没有unbox。因为接口是引用型。可以看汇编代码:
.locals init ([0] valuetype Student stu,
[1] object o,
[2] valuetype Student stu1,
[3] class IStudent istu)
......//省略
IL_0043: ldloc.1 //这里开始执行★处指令:拿到o
IL_0044: castclass IStudent //注意没有unbox,仅仅是一个映射,istu和o指向同一个地方
IL_0049: stloc.3 //保存istu,操作完成
IL_004a: ldloc.3
IL_004b: ldstr "andychan"
IL_0050: callvirt instance void IStudent::set_Name(string)
....//省略
IL_006d: ret

以前比较清楚,很久没看就模糊了,这下游比较清楚了。谢谢侠缘的文章。   回复  引用  查看    

#13楼 [楼主] 2006-06-06 14:18 Wu.Country@侠缘      

谢谢“一唯”的说明。
然而一开始我就没说
IStudent istu=(IStudent)o;是unboxing操作.
[在此不得不承认代码里的注释错误//unboxing?。。向读者表示报谦了,然而全文从上到下都没有说上面是unboxing操作,仔细阅读的应该可以看明白我的本意。再次感谢“一唯”的说明。]

"至于本文一开始的那段代码的结果,应该可以分析出来了,就是用struct来转时候,是一次内存的COPY,也就是unboxing,而用interface来转的时候,就只是一次引用的COPY,所以通过interface可以修改到boxing后的struct的数据。 "

注意:而用interface来转化的时候,就。。。。。。

下面这一段也明确说明了。

“其实这里已经不能说m_obj是值类型数据了,因为经过boxing,它已经成了引用类型。但由于它是用struct通过boxing过去的,所以再以struct身份unboxing回来的时候,都将会产生值类型unboxing的效果,也就是从栈上COPY一份数据到堆上,最终就是所有的数据修改都不会影响boxing后的引用数据。但是,如果用接口(interface)来处理的时候,就不会COPY内存,因为接口也是引用类型,这样在object与interface之间转化的时候,可以不COPY内存。我们就可以像上面的例子那样,用接口来来处理经过boxing的"值类型"数据。”   回复  引用  查看    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      


相关链接: