注意使用装箱和拆箱

在计划列出了这么久之后,终于写出了一篇东西,关于.NET Framework 2.0中的装箱与拆箱(Boxing and Unboxing)。

在计划中已经提到,到现在才写.net 2.0基础技术相关的文章是相当吃力不讨好的工作。不过由于现在公司有一大批没有接触过C#的同事刚刚开始学习,而我在此将他们在学习过程中遇到的问题在这里做一下分析,作为对自己的巩固和对新人的解惑。公司的同事也可以来看看。

(说明:我在文中大量使用了“指向”这个词汇,但这并不代表我正在描述一个指针,也不代表我正在描述一个引用类型,这样的说法只是为了方便理解,因为目前为止我还没有发现一个更好的这么万能的词。)

正文

.NET Framework中将数据类型总体分为值类型和引用类型,所有的值类型分配在栈上,所有的引用类型分配在堆上。而.NET Framework中所有类型的基类都是Object类(object)——包括值类型,而object本身是引用类型。因此可以借由object将值类型转换为引用类型——装箱,然后将object转换回值类型(拆箱)。

装箱和拆箱的概念以及应用在这里我不多做描述,几乎所有的入门书籍上都有详细的介绍。以下的内容主要针对在学习过程中容易遇到的问题和不易理解的地方。

装箱 Boxing

先看下面一段代码:

using System;
using System.Collections.Generic;

class Program
{
    
static void Main(string[] args)
    
{
        
int i = 3;
        
int j = 4;
        swap(
ref (object)i,ref (object)j);
        Console.WriteLine(
"i = {0}, j = {1}", i, j);
        Console.ReadLine();
    }

    
static public void swap(ref object to, ref object from)
    
{
        
object temp;
        temp 
= to;
        to 
= from;
        from 
= temp;
    }

}


/*关于引用类型为什么要加ref的问题我将在下一篇文章中解释。*/


显然,不太高明的错误,但初学者总会犯这样的错误,并对错误提示——"A ref or out argument must be an assignable variable"束手无策。

此处就是一个装箱问题。分析一下:

当把值类型int装箱为object时,会在堆上为其新分配一个空间,且不消除栈上的空间——也就是看似简单的类型转换(object)i并不是简单的将值类型int直接转变为了引用类型object,而是在堆上新建了一个空间,并把int的值赋给其成员,然后返回该对象的引用

那么在调用swap(ref (object)i,ref (object)j)时会发生什么呢?

会将i和j装箱,然后将其引用返回……等等,引用返回给谁了?程序中并没有接受其引用的变量,引用直接被返回给了形参,而形参在函数调用结束时就成为了垃圾。我们可怜的装箱结果刚一出生就成为了垃圾,交换也没有了意义。还好聪明的编译器知道会发生这样的情况:

"A ref or out argument must be an assignable variable"

ref和out参数必须是可赋值的变量。是的,把它首先赋给一个变量不就完了吗?easy!

所以正确的代码是:

using System;
using System.Collections.Generic;

class Program
{
    
static void Main(string[] args)
    
{
        
int i = 3;
        
int j = 4;
        
object oi = (object)i;
        
object oj = (object)j;
        swap(
ref oi,ref oj);
        Console.WriteLine(
"i = {0}, j = {1}", i, j);
        Console.ReadLine();
    }

    
static public void swap(ref object to, ref object from)
    
{
        
object temp;
        temp 
= to;
        to 
= from;
        from 
= temp;
    }

}


OK,编译通过。看看我们的结果吧:

i = 3, j = 4

等等!!交换失败了?

并没有失败,我已经说过了:

当把值类型int装箱为object时,会在堆上为其新分配一个空间,且不消除栈上的空间——也就是看似简单的类型转换(object)i并不是简单的将值类型int直接转变为了引用类型object,而是在堆上新建了一个空间,并把int的值赋给其成员,然后返回该对象的引用。

我们的object对象oi和oj并不是直接指向值类型i和j的,它们指向堆中一个新空间,i和j还一直老老实实的在栈里呆着呢。所以对oi和oj的交换并没有将效果传递到整型i和j。使用如下语句就能看到结果了。

Console.WriteLine("i = {0}, j = {1}", oi, oj);

拆箱 Unboxing

关于拆箱我要提的一点跟装箱一样。就是拆箱之后的结果要使用变量保存,不然是没有意义的。看下面的代码:

using System;
using System.Collections.Generic;

class Program
{
    
static void Main(string[] args)
    
{
        System.Collections.ArrayList list 
= new System.Collections.ArrayList();
        Person p 
= new Person();
        p.Age 
= 10;
        list.Add(p);
        ((Person)list[
0]).Age = 20;
        Console.WriteLine(((Person)list[
0]).Age);
        Console.ReadLine();
    }


    
class Person
    
{
        
private int age;
        
public int Age
        
{
            
get
            
{
                
return age;
            }

            
set
            
{
                age 
= value;
            }

        }

    }

}


这段代码编译和运行都是没有问题的,结果也如预期的是20。现在我们把Person从类改为结构来看看:

struct Person

是的,看上去仍然没有什么问题。现在编译——"Cannot modify the result of an unboxing conversion"。错误出现了,不能修改拆箱变换后的结果。

原理跟装箱一样,拆箱之后在栈上生成了一个新的Person结构,但是没有任何变量指向该结构,也就是说这个跟上面的object一样悲惨,一出生就变成了垃圾。

那么用一个Person结构先来接收这个拆箱的结果,再进行修改怎么样呢?

是可以的。但是这样的修改并不能影响list中原来Person的值,他们存储在不同的空间中。所以你在进行了修改之后不得不先删除list中原来的Person对象,然后再将修改之后的结果重新插入到原来的位置。Sick!!

更好的做法是让Person结构实现一个接口,该接口完成对Age的修改。那么在类型转换(注意我没有使用拆箱这个词)的时候就不会发生拆箱。代码如下:

using System;
using System.Collections.Generic;

class Program
{
    
static void Main(string[] args)
    
{
        System.Collections.ArrayList list 
= new System.Collections.ArrayList();
        Person p 
= new Person();
        p.Age 
= 10;
        list.Add(p);
        ((IPerson)list[
0]).Age = 20;    //类型转换,而非拆箱
        Console.WriteLine(((Person)list[0]).Age);
        Console.ReadLine();
    }

 
    
interface IPerson
    
{
        
int Age
        
{
            
getset;
        }

    }


    
struct Person : IPerson
    
{
        
private int age;
        
public int Age
        
{
            
get
            
{
                
return age;
            }

            
set
            
{
                age 
= value;
            }

        }

    }

}


这段代码便可以在保证Person是结构的情况下完成装箱数据的修改(Effective C# : Item 17: Minimize Boxing and Unboxing)。

至于何时使用引用类型、何时使用值类型可以参考 Effective C# : Item 6: Distinguish Between Value Types and Reference Types。

小结

装箱和拆箱所涉及的内容并不只这么多,他可以算是C#中非常tricky的一部分,以上只对其中最容易疑惑的一点进行了解释,其他问题可以按照这样的思路进行思考,总会得到合理的解释。

全文完。

posted @ 2006-11-02 12:58  redjackwong  阅读(1693)  评论(4编辑  收藏  举报