Effective C#之17:Minimize Boxing and Unboxing.

探讨了.NET框架中值类型与引用类型之间的转换——装箱与拆箱的过程,并提供了避免其带来的性能损耗及潜在bug的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

rel="File-List" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml"> rel="Edit-Time-Data" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_editdata.mso"> rel="themeData" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_themedata.thmx"> rel="colorSchemeMapping" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_colorschememapping.xml">

Item 17: Minimize Boxing and Unboxing

使装箱拆箱最少

Value types are containers for data. They are not polymorphic types. On the other hand, the .NET Framework was designed with a single reference type, System.Object, at the root of the entire object hierarchy. These two goals are at odds. The .NET Framework uses boxing and unboxing to bridge the gap between these two goals. Boxing places a value type in an untyped reference object to allow the value type to be used where a reference type is expected. Unboxing extracts a copy of that value type from the box. Boxing and unboxing are necessary for you to use value types where the System.Object type is expected. But boxing and unboxing are always performance-robbing operations. Sometimes, when boxing and unboxing also create temporary copies of objects, it can lead to subtle bugs in your programs. Avoid boxing and unboxing when possible.

值类型是数据容器,不是多态类型。另一方面来,.Net框架被设计的时候,只有一个单独的引用类型System.Object在整个对象等级的根部。这2个目标是不一致的。在两个目标之间,.Net框架使用装箱和拆箱建立了一个管道。装箱,将一个值类型置于一个无类型的引用对象里,这样,在需要使用引用类型的地方,该值类型就能被使用了。拆箱则是从箱子里提取该值的一个副本。在要求使用System.Object类型的地方,装箱和拆箱使得值类型也可以使用。但是装箱和拆箱操作简直是掠夺资源。有时,装箱和拆箱也创建对象的临时副本,这会在程序里导致微小的bug。尽可能的避免装箱和拆箱操作。

Boxing converts a value type to a reference type. A new reference object, the box, is allocated on the heap, and a copy of the value type is stored inside that reference object. See Figure 2.3 for an illustration of how the boxed object is stored and accessed. The box contains the copy of the value type object and duplicates the interfaces implemented by the boxed value type. When you need to retrieve anything from the box, a copy of the value type gets created and returned. That's the key concept of boxing and unboxing: A copy of the object goes in the box, and another gets created whenever you access what's in the box.

装箱将一个值类型转换为引用类型。一个新的引用类型,即箱子,是在堆上面进行分配的,在引用类型的内部存储被装箱值类型的副本。图2.3展示了被装箱的对象如何被存储和访问。箱子包含有值类型的一个副本并且复制被装箱的值类型实现的接口。当你需要从一个箱子获取任何东西时,一个值类型的副本被创建并且被返回。这是装箱和拆箱操作的关键概念:对象的一个副本被放进箱子,当你访问箱子里面的东西的时候,另外一个副本又被创建了。

Figure 2.3. Value type in a box. To convert a value type into a System.Object reference, an unnamed reference type is created. The value type is stored inline inside the unnamed reference type. All methods that access the value type are passed through the box to the stored value type.

2.3 位于箱子中的值类型。为了将值类型转换成System.Object的引用,一个未命名的引用类型会被创建。该值类型被内联存储在这个未命名的类型内部。所有对值类型访问的方法,被通过箱子传递给被存储的值类型。


 

The insidious problem with boxing and unboxing is that it happens automatically. The compiler generates the boxing and unboxing statements whenever you use a value type where a reference type, such as System.Object is expected. In addition, the boxing and unboxing operations occur when you use a value type through an interface pointer. You get no warnings boxing just happens. Even a simple statement such as this performs boxing:

在装箱拆箱上,具有欺诈性的问题是:它是自动发生的。无论何时,你在要求使用引用类型(System.Object)的地方,使用值类型的时候,编译器生成装箱和拆箱语句。另外,当你通过一个接口指针使用值类型的时候,装箱和拆箱操作就会发生。装箱发生的时候,你得不到任何警示。甚至像这么一个简单的语句都执行装箱:

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

The referenced overload of Console.WriteLine takes an array of System.Object references. Ints are value types and must be boxed so that they can be passed to this overload of the WriteLine method. The only way to coerce the three integer arguments into System.Object is to box them. In addition, inside WriteLine, code reaches inside the box to call the ToString() method of the object in the box. In a sense, you have generated this construct:

Console.WriteLine的引用重载采用一个System.Object引用的数组。Int是值类型,必须被装箱,那样的话才能被传给WriteLine方法的重载版本。唯一将3个整型参数强制转变成System.Object的方法就是对它们进行装箱。还有,在WriteLine里面,代码会深入到箱子内部调用箱子里的对象的ToString()方法。在某种意义上,你已经生成了下面的结构:

  1.             Int32 i =25;
  2.             Object o = i; // box
  3.             Console.WriteLine(o.ToString());

Inside WriteLine, the following code executes:

WriteLine里面,会执行下面代码:

  1.             Object o;
  2.             Int32  i = ( Int32 )o; // unbox
  3.             String output = i.ToString( );

You would never write this code yourself. However, by letting the compiler automatically convert from a specific value type to System.Object, you did let it happen. The compiler was just trying to help you. It wants you to succeed. It happily generates the boxing and unboxing statements necessary to convert any value type into an instance of System.Object. To avoid this particular penalty, you should convert your types to string instances yourself before you send them to WriteLine:

你可能从来没有自己写下这些代码。然而,通过利用编译器自动将一个特定的值类型转换成System.Object,确实是你让这些发生的(产生了这些代码)。编译器仅仅是试图帮助你,它希望你成功。它很高兴的生成必要的装箱和拆箱语句来将任何值类型转换成System.Object的实例。为了避免这个特殊的处罚,在将自己的类型发送给WriteLine前,应该自己将其转换成string实例。

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

This code uses the known type of integer, and value types (integers) are never implicitly converted to System.Object. This common example illustrates the first rule to avoid boxing: Watch for implicit conversions to System.Object. Value types should not be substituted for System. Object if you can avoid it.

这个代码使用了已知的整数类型,值类型(整数)从不会隐式的被转换成System.Object.这个常见的例子展示了避免装箱的第一个规则:注意向System.Object的隐式转换。如果你能避免的话,值类型不应该被替换成System.Object

Another common case in which you might inadvertently substitute a value type for System.Object is when you place value types in .NET 1.x collections. This incarnation of the .NET Framework collections store references to System.Object instances. Anytime you add a value type to a collection, it goes in a box. Anytime you remove an object from a collection, it gets copied from the box. Taking an object out of the box always makes a copy. That introduces some subtle bugs in your application. The compiler does not help you find these bugs. It's all because of boxing. Start with a simple structure that lets you modify one of its fields, and put some of those objects in a collection:

你可能不经意的将值类型转换成System.Object的另外一个常见的例子是:将值类型置于.Net1.x的集合中。这个具体化的.Net框架集合存储对System.Object实例的引用。任何时候,将值类型加入集合时,它都是被装箱的。任何时候,从集合里移除一个对象时,都是从箱子里面传出一个副本。从箱子里拿出一个对象总是会产生一副本。这会给你的应用程序引入一些微小的bug。编译器在寻找这些bug方面帮不了你,这全是因为装箱。有一个简单的结构体,可以允许你修改它的一个字段,将一些这样的对象放入集合中:

 

  1.    public struct Person
  2.     {
  3.         private String name;
  4.         public String Name
  5.         {
  6.             get
  7.             {
  8.                 return name;
  9.             }
  10.             set
  11.             {
  12.                 name = value;
  13.             }
  14.         }
  15.         public Person(String name)
  16.         {
  17.             this.name = name;
  18.         }
  19.         public override String ToString()
  20.         {
  21.             return name;
  22.         }
  23.   }
  24.      // Using the Person in a collection:
  25.     ArrayList attendees = new ArrayList();
  26.     Person p = new Person("Old Name");
  27.     attendees.Add(p);
  28.     // Try to change the name:
  29.     // Would work if Person was a reference type.
  30.     Person p2 = ((Person)attendees[0]);
  31.     p2.Name = "New Name";
  32.      // Writes "Old Name":
  33.     Console.WriteLine(attendees[0].ToString());

Person is a value type; it gets placed in a box before being stored in the ArrayList. That makes a copy. Then another copy gets made when you remove the Person object to access the Name property to change. All you did was change the copy. In fact, a third copy was made to call the ToString() function through the attendees[0] object.

Person是一个值类型,在它被存储在ArrayList之前被进行了装箱,这产生了一个副本。当你移除这个Person对象,访问它的Name属性并进行修改时,又产生了另外一个副本。所有你做得工作,都是改变了它的副本。事实上,通过attendees[0]对象调用ToString()方法时,产生了第三个副本。

For this and many other reasons, you should create immutable value types (see Item 7). If you must have a mutable value type in a collection, use the System.Array class, which is type safe.

出于这个和很多其它原因,你应该创建具有不可变性的类型(Item7)。如果你必须在一个集合里面使用具有可变性的值类型,那么使用System.Array类型,它是类型安全的。

If an array is not the proper collection, you can fix this error in C# 1.x by using interfaces. By coding to interfaces rather than the type's public methods, you can reach inside the box to make the change to the values:

如果array不是合适的集合,在C#1.x里面,你可以使用接口来修复这个问题。通过对接口进行编码而不是对类型的公共方法编码,你可以深入到箱子内部,对它的值进行修改。

 

  1.    public interface IPersonName
  2.     {
  3.         string Name
  4.         {
  5.             get;
  6.             set;
  7.         }
  8.    }
  9.     struct Person : IPersonName
  10.     {
  11.         private string name;
  12.         public string Name
  13.         {
  14.             get
  15.             {
  16.                 return name;
  17.             }
  18.             set
  19.             {
  20.                 name = value;
  21.             }
  22.         }
  23.         public Person(String name)
  24.         {
  25.             this.name = name;
  26.         }
  27.         public override string ToString()
  28.         {
  29.             return name;
  30.         }
  31.    }
  32.  
  33.     // Using the Person in a collection:
  34.     ArrayList attendees = new ArrayList();
  35.     Person p = new Person("Old Name");
  36.     attendees.Add(p); // box
  37.      // Try to change the name:
  38.     // Use the interface, not the type.
  39.     // No Unbox needed
  40.     ((IPersonName)attendees[0]).Name = "New Name";
  41.     // Writes "New Name":
  42.     Console.WriteLine(attendees[0].ToString()); // unbox
  43.  

The box reference type implements all the interfaces implemented by the original object. That means no copy is made, but you call the IPersonName.Name method on the box, which forwards the request to the boxed value type. Creating interfaces on your value types enables you to reach inside the box to change the value stored in the collection. Implementing an interface is not really treating a value type polymorphically, which reintroduces the boxing penalty (see Item 20).

“箱子”引用类型实现了所有原来对象实现的接口。这意味着,没有副本会被产生,但是你调用了箱子上的IPersonName.Name方法,它会将该请求推进给被装箱的值类型。在值类型上面创建接口,使你能够到达箱子的内部,修改存储在集合里面的值。实现接口并不是将值类型看成是多态性的,那样的话还是会重新引入装箱的带来的惩罚。(Item 20)

Many of these limitations change with the introduction of generics in C# 2.0 (see Item 49). Generic interfaces and generic collections will address the both the collection and the interface situations. Until then, though, avoid boxing. Yes, value types can be converted to System.Object or any interface reference. That conversion happens implicitly, complicating the task of finding them. Those are the rules of the environment and the language. The boxing and unboxing operations make copies where you might not expect. That causes bugs. There is also a performance cost to treating value types polymorphically. Be on the lookout for any constructs that convert value types to either System.Object or interface types: placing values in collections, calling methods defined in System.Object, and casts to System.Object. Avoid these whenever you can.

很多这些限制,在C#2.0引入泛型后,都有了改变。(Item 49)。泛型接口和泛型集合将会同时适应集合和接口的情况。即使到了那时,也要避免装箱。是的,值类型能够被转换成System.Object或者任何接口引用。这些转换都是隐式发生的,找到它们较复杂。这些是环境和语言的规则。装箱和拆箱操作会在你意想不到的地方产生副本。那会引起bug。像多态一样去对待值类型也会带来性能上的损失。对任何将值类型转换成System.Object或者接口的结构,要提高警惕:将值类型放入集合,调用在System.Object上定义的方法,强制转换成System.Object。任何时候,只要你能,就要避免。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值