Effective C# Item17: Minimize Boxing and Unboxing

      在.Net中有值和引用两种类型,它们是不一致的。.Net Framework使用装箱和拆箱来做为二者之间的桥梁。通过装箱可以将一个值类型封装到一个无类型的引用型对象中。拆箱则是将引用类型从包装中释放出来。当我们需要将值类型当作引用类型一样来处理时,装箱和拆箱是必要的。但是这样的操作会浪费额外的资源,一般来说装箱和拆箱时也会创建临时的对象拷贝,这可能会为我们的程序造成难以查觉的隐患。我们应当尽量减少装箱和拆箱操作。

      装箱可以将一个值类型转变为引用类型,是分配在堆上的。在这个box中包含了被装箱的值类型的拷贝和其实现的一些接口。当我们需要再将其复原的时候,就创建一个对应的值类型,并赋以箱中的拷贝。

      装箱和拆箱是自动发生的,这正是它们危险的地方。当我们将一个值类型当作引用类型来使用的时候,编译器就会自动进行这些转换。另外,当我们通过接口操作值类型的时候也会引发装箱和拆箱。它发生时没有任何警告。下面这个例子中就发生了装箱:

Console.WriteLine("A few numbers: {0}, {1}, {2}"253250);

      重载的Console.WriteLine的参数是一组引用,对于int类型来说,必须首先进行装箱才能被WriteLine方法使用。另外WriteLine会调用装箱对象的ToString()方法。这样我们的代码其实是比较类似于这样:

      首先是装箱:

int i = 25;
object o = i;
Console.WriteLine(o.ToString());

      在拆箱时:

object o;
int i = (int)o;
string output = i.ToString();

      我们不应当让编译器自动做这样的转换。编译器的本意是好的,它自动生成了必要的装箱和拆箱代码来帮助我们通过编译。为了避免发生这种情况,我们可以在WriteLine之前先将我们的类型转变为string:

Console.WriteLine("A few numbers: {0}, {1}, {2}"25.ToString(), 32.ToString(), 50.ToString());

      这样做就可以避免转换的发生。这样一个简单的例子告诉我们应当时刻留意这种转换。如果可以的话,我们应当尽量避免自动转换的发生。

      另一个我们可能会遇到的将值类型自动转换为引用类型的地方是.Net 1.x中的集合。在.Net 1.x中集合存储的是System.Object实例。当我们向其中添加值类型的时候,boixng就会发生。当我们使用这些存储的实例时,返回的是被装箱对象中的值类型的拷贝,而非是值类型本身。这容易造成一些潜在的bug:

        public struct Person
        
{
            
private string name;
            
public string Name
            
{
                
get
                
{
                    
return name;
                }

                
set
                
{
                    name 
= value;
                }

                

            }

            
public override string ToString()
            
{
                
return name;
            }

        }

                        
//尝试修改
            ArrayList list = new ArrayList();
            Person p 
= new Person();
            p.Name 
= "a";
            list.Add(p);

            Person p2 
= (Person)list[0];
            p2.Name 
= "b";

            Console.WriteLine(list[
0].ToString());

      修改是不会成功的。Person是一个值类型,在储存在ArrayList之前发生了装箱操作。当我们后来在使用它时,返回的是一个拷贝。我们不能通过修改它来达到修改集合中对象的目的。

      从这一点也可以看出我们创建值类型时应当将其声明为不可变的。如果我们确实需要可变的值类型,那么我们可以使用System.Array来达到目的。

      如果数组不是一个合适的集合,那么我们可以使用接口的方法达到目的。通过接口,我们可以修改box中的值类型的值:

        public interface IPersonName
        
{
            
string Name
            
{
                
get;
                
set;
            }

        }


        
public struct Person:IPersonName
        
{
            
private string name;
            
public string Name
            
{
                
get
                
{
                    
return name;
                }

                
set
                
{
                    name 
= value;
                }

                

            }

            
public override string ToString()
            
{
                
return name;
            }

        }

      通过接口我们访问到了原始的对象,而不是原对象的拷贝。这样我们就可以修改集合中的值类型的值。

ArrayList list = new ArrayList();
Person p 
= new Person();
p.Name 
= "a";
list.Add(p);
((IPersonName)list[
0]).Name = "b";
Console.WriteLine(list[
0].ToString());

      在C# 2.0中上面的许多限制都得到了改进,但是尽管如果次我们还是要尽量避免的。频繁的装箱和拆箱不仅浪费系统资源,降低程序的执行效率,还可能造成意料之外的bug。在可能的情况下尽量不要使用。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

      PS:在MSDN上有一篇关于装箱和拆箱不错的文章,介绍了使用装箱类来达到减少这种转换的目的

      http://www.microsoft.com/china/msdn/Archives/voices/csharp03152001.asp

      回到目录

posted on 2006-10-24 09:10  aiya  阅读(1654)  评论(0编辑  收藏  举报