Effective C# 原則17:裝箱和拆箱的最小化(譯)

來源:互聯網
上載者:User

Effective C# 原則17:裝箱和拆箱的最小化
Item 17: Minimize Boxing and Unboxing

實值型別是資料的容器,它們不具備多太性。另一方面就是說,.Net架構被設計成單一繼承的參考型別,System.Object,在整個繼承關係中做為根對象存在。設計這兩種類型的目的是截然不同的,.Net架構使用了裝箱與拆箱來連結兩種不同類型的資料。裝箱是把一個實值型別資料放置在一個無類型的引用對象上,從而使一個實值型別在須要時可以當成參考型別來使用。拆箱則是額外的從“箱”上拷貝一份實值型別資料。裝箱和拆箱可以讓你在須要使用System.Object對象的地方使用實值型別資料。但裝箱與拆箱操作卻是效能的強盜,在些時候裝箱與拆箱會產生一些臨時對象,它會導致程式存在一些隱藏的BUG。應該儘可能的避免使用裝箱與拆箱。

裝箱可以把一個實值型別資料轉化也一個參考型別,一個新的引用對象在堆上建立,它就是這個“箱子”,實值型別的資料就在這個參考型別中儲存了一份拷貝。參見圖2.3,示範了裝箱的對象是如何訪問和儲存的。箱子中包含一份這個實值型別對象的拷貝,並且複製實現了已經裝箱對象的介面。當你想從這個箱子中取回任何內容時,一個實值型別資料的拷貝會被建立並返回。這就是裝箱與拆箱的關鍵性概念:對象的一個拷貝存放到箱子中,而不管何時你再訪問這個箱子時,另一個拷貝又會被建立。


圖2.3,實值型別資料在箱子中。把一個實值型別資料轉化成一個System.Object的引用,一個無名的參考型別會被建立。實值型別的資料就儲存在這個無名的引用對象中,所有的存取方法都要通過這個箱子才能到達實值型別資料存放區的地方。

最陰險的地方是這個裝箱與拆箱很多時候是自動完成的!當你在任何一個期望類型是System.Object的地方使用實值型別資料時,編譯器會產生裝箱與拆箱的語句。另外,當你通過一個介面指標來訪問實值型別資料時,裝箱與拆箱也會發生。當你裝箱時不會得到任何警告,即使是最簡單的語句也一樣。例如下面這個:
Console.WriteLine("A few numbers:{0}, {1}, {2}",
  25, 32, 50);

使用重載的Console.WriteLine函數須要一個System.Object類型的數組引用,整型是實值型別,所以必須裝箱後才能傳給重載的WriteLine方法。唯一可以強制這三個整數成為System.Object對象的方法就是把它們裝箱。另外,在WriteLine內部,通過調用箱子物件上的ToString()方法來到達箱子內部。某種意義上講,你產生了這樣的結構:

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

在WriteLine內部,下面的執行了下面的代碼:

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

你可能自己從來不會寫這樣的代碼,但是,卻讓編譯器自動從一個指定的類型轉化為System.Object,這確實是你做的。編譯器只是想試著協助你,它想讓你成功(調用函數),它也很樂意在必要時候為你產生裝箱和拆箱語句,從而把一個實值型別資料轉化成System.Object的執行個體。為了避免這麼挑剔的懲罰,在使用它們來調用WriteLine之前,你自己應該把你的類型轉化成字串的執行個體。

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

(譯註:注意,在自己調用ToString方法時,還是會在堆上建立一個引用執行個體,但它的好處是不用拆箱,因為對象已經是一個參考型別了。)

這段代碼使用已知的整數類型,而且實值型別再也不會隱式的轉化為System.Object類型。這個常見的例子展示了避免裝箱的第一個規則:注意隱式的轉化為System.Object,如果可以避免,實值型別不應該被System.Object代替。

另一個常見情況就是,在使用.Net 1.x的集合時,你可能無意的把一個實值型別轉化成System.Object類型。任何時候,當你添加一個實值型別資料到集合時中,你就建立了一個箱子。任何時候從集合中移出一個對象時,你得到的是箱子裡的一個拷貝。從箱子裡取一個對象時,你總是要建立一個拷貝。這會在應用程式中產生一些隱藏的BUG。編譯器是不會幫你尋找這些BUG的。這都是裝箱惹的禍。讓我們開始建立一個簡單的結構,可以修改其中一個欄位,並且把它的一些執行個體對象放到一個集合中:

public struct Person
{
  private string _Name;

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

  public override string ToString( )
  {
    Return _Name;
  }
}

// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p );

// Try to change the name:
// Would work if Person was a reference type.
Person p2 = (( Person )attendees[ 0 ] );
p2.Name = "New Name";

// Writes "Old Name":
Console.WriteLine(
  attendees[ 0 ].ToString( ));

Person是一個實值型別資料,在儲存到ArrayList之前它被裝箱。這會產生一個拷貝。而在移出的Persone對象上通過訪問屬性做一些修改時,另一個拷貝被建立。而你所做的修改只是針對的拷貝,而實際上還有第三個拷貝通過ToString()方法來訪問attendees[0]中的對象。

正因為這以及其它一些原因,你應該建立一些恒定的實值型別(參見原則7)。如果你非要在集合中使用可變的實值型別,那就使用System.Array類,它是型別安全的。

如果一個數組不是一個合理的集合,以C#1.x中你可以通過使用介面來修正這個錯誤。盡量選擇一些介面而不是公用的方法,來訪問箱子的內部去修改資料:

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

struct Person : IPersonName
{
  private string _Name;

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

  public override string ToString( )
  {
    return _Name;
  }
}

// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p ); // box

// Try to change the name:
// Use the interface, not the type.
// No Unbox needed
(( IPersonName )attendees[ 0 ] ).Name = "New Name";

// Writes "New Name":
Console.WriteLine(
  attendees[ 0 ].ToString( )); // unbox

裝箱後的參考型別會實現原資料類型上所有已經實現的介面。這就是說,不用做拷貝,你可以通過調用箱子上的IPersonaName.Name方法來直接存取請求到箱子內部的實值型別資料。在實值型別上建立的介面可以讓你訪問集合裡的箱子的內部,從而直接修改它的值。在實值型別上實現的介面並沒有讓實值型別成為多態的,這又會引入裝箱的懲罰(參見原則20)。

在C#2.0中對泛型簡介中,很多限制已經做了修改(參見原則49)。泛型介面和泛型集合會時同處理好集合與介面的困境。在那之前,我們還是要避免裝箱。是的,實值型別可以轉化為System.Object或者其它任何的介面引用。這些轉化是隱式的,使得發現它們成為繁雜的工作。這些也就是環境和語言的規則,裝箱與拆箱操作會在你不經意時做一些對象的拷貝,這會產生一些BUG。同樣,把實值型別多樣化處理會對效能有所損失。時刻注意那些把實值型別轉化成System.Object或者介面類型的地方:把實值型別放到集合裡,調用定義參數為System.Object類型的方法,或者強制轉化為System.Object。能夠避免就盡量避免!
===============================

   

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.

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.


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.

The insidious problem with boxingand 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 warningsboxing just happens. Even a simple statement such as this performs boxing:

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:

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

 

Inside WriteLine, the following code executes:

object o;
int i = ( int )o; // unbox
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 convertyour types tostring instances yourself before you send them to WriteLine:

Console.WriteLine("A few numbers:{0}, {1}, {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.

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 acollection, 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:

public struct Person
{
  private string _Name;

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

  public override string ToString( )
  {
    Return _Name;
  }
}

// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p );

// Try to change the name:
// Would work if Person was a reference type.
Person p2 = (( Person )attendees[ 0 ] );
p2.Name = "New Name";

// Writes "Old Name":
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.

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.

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:

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

struct Person : IPersonName
{
  private string _Name;

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

  public override string ToString( )
  {
    return _Name;
  }
}

// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p ); // box

// Try to change the name:
// Use the interface, not the type.
// No Unbox needed
(( IPersonName )attendees[ 0 ] ).Name = "New Name";

// Writes "New Name":
Console.WriteLine(
  attendees[ 0 ].ToString( )); // unbox

 

The box reference typeimplements 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).

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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.