Effective C# Item17:儘量減少裝箱和拆箱

    裝箱和拆箱存在的意義:值類型是數據的容器,它存儲在堆棧上,不具備多態性,而.NET框架在整個對象層次的設計中,使用System.Object作爲所有類型的基類,但是Obejct是引用類型,而作爲值類型的基類System.ValueType,是從System.Object派生出來的,這就產生了矛盾,裝箱和拆箱就是爲了解決這兩種類型之間的差異。

    裝箱會將一個值類型放入一個未具名類型(untyped)的引用對象中,從而允許該值類型應用於那些只能使用引用類型的場合。拆箱則會從前面的裝箱對象中提取出一個值類型的副本。裝箱和拆箱都是比較耗時的操作。

    裝箱操作會將值類型轉換爲一個引用類型,這個過程中會創建一個新的引用獨享,然後將其分配到堆上,同時值類型的副本會被存儲在該引用對象內部。當我們需要從裝箱對象中獲取任何信息時,會創建值類型的一個副本,然後將其返回。其中的關鍵是:當我們需要引用類型時,會創建一個新的引用類型的對象並將其放入到堆中;當我們需要訪問已經裝箱的對象信息時,就會創建對應值類型的一個副本,並將其返回。

    裝箱和拆箱最大的問題是它們會自動發生。當我們使用的是值類型,而期望的是引用類型,那麼編譯器就會自動產生裝箱和拆箱語句。

   我們來看下面的語句,居然也發生了裝箱和拆箱操作。

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


    上述代碼之所以發生了裝箱,是因爲WriteLine方法需要的參數類型是System.Object,而25是一個int類型,屬於值類型,因此需要裝箱,而在WriteLine方法內部實現時,需要調用方法參數的ToString()方法,爲了調用裝箱對象的方法,就會發生拆箱的操作。

    爲了避免裝箱和拆箱,可以將上述代碼進行如下修改。

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


    另外,由於裝箱和拆箱都會產生新的實例,那麼有時會產生一些詭異的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時,會進行裝箱操作,這時會有一次複製操作,當我們需要獲得ArrayList內Person對象的信息時,需要一次拆箱,又會有一次複製操作,因此,當我們並沒有對ArrayList內的對象進行修改,而是針對副本進行修改。

    我們可以通過以下的方式來修改上述代碼存在的問題。

代碼
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


    裝箱後的引用類型實現了原來值類型對象上所有的接口,這意味着不會再發生複製,但是當我們調用IPersonName.Name屬性時,它會將調用請求轉發給“箱子”內部的值類型,在值類型上實現接口使我們可以訪問”箱子“的內部,從而允許直接改變ArrayList中的信息。 

   總之,我們應該對任何將值類型轉換爲System.Object或者接口類型的構造保持密切的關注,例如將值類型放入集合中,在值類型上調用System.Object定義的方法等,這些操作都會將值類型轉換爲System.Object,只要有可能,我們都應該避免這種轉換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章