C# 裝箱和拆箱

裝箱和拆箱是一種抽象的概念
裝箱和拆箱是值類型和引用類型之間相互轉換是要執行的操作。
1. 裝箱在值類型向引用類型轉換時發生;
2. 拆箱在引用類型向值類型轉換時發生;

例如:

// 裝箱
object obj = 1;

這行語句將整型常量1賦給object類型的變量obj; 衆所周知常量1是值類型,值類型是要放在棧上的,而object是引用類型,它需要放在堆上;要把值類型放在堆上就需要執行一次裝箱操作。

這行語句的IL代碼如下,請注意註釋部分說明:

.locals init (
  [0] object objValue
)  //以上三行IL表示聲明object類型的名稱爲objValue的局部變量 
IL_0000: nop
IL_0001: ldc.i4.s 9 //表示將整型數9放到棧頂
IL_0003: box [mscorlib]System.Int32 //執行IL box指令,在內存堆中申請System.Int32類型需要的堆空間
IL_0008: stloc.0 //彈出堆棧上的變量,將它存儲到索引爲0的局部變量中

以上就是裝箱所要執行的操作了,執行裝箱操作時不可避免的要在堆上申請內存空間,並將堆棧上的值類型數據複製到申請的堆內存空間上,這肯定是要消耗內存和cpu資源的。

// 拆箱
object objValue = 4;
int value = (int)objValue;

上面的兩行代碼會執行一次裝箱操作將整形數字常量4裝箱成引用類型object變量objValue;然後又執行一次拆箱操作,將存儲到堆上的引用變量objValue存儲到局部整形值類型變量value中。
同樣我們需要看下IL代碼:

.locals init (
  [0] object objValue,
  [1] int32 'value'
) //上面IL聲明兩個局部變量object類型的objValue和int32類型的value變量
IL_0000: nop
IL_0001: ldc.i4.4 //將整型數字4壓入棧
IL_0002: box [mscorlib]System.Int32  //執行IL box指令,在內存堆中申請System.Int32類型需要的堆空間
IL_0007: stloc.0 //彈出堆棧上的變量,將它存儲到索引爲0的局部變量中
IL_0008: ldloc.0//將索引爲0的局部變量(即objValue變量)壓入棧
IL_0009: unbox.any [mscorlib]System.Int32 //執行IL 拆箱指令unbox.any 將引用類型object轉換成System.Int32類型
IL_000e: stloc.1 //將棧上的數據存儲到索引爲1的局部變量即value

拆箱操作的執行過程和裝箱操作過程正好相反,是將存儲在堆上的引用類型值轉換爲值類型並給值類型變量。

1. 爲何需要裝箱?又如何避免裝箱?
(1) 最普通的場景是,調用一個含類型爲Object的參數的方法,該Object可支持任意爲型,以便通用。當你需要將一個值類型(如Int32)傳入時,需要裝箱。
例如:

namespace ConsoleApplicationTest
{
    class Program
    {
        static void Output(Object o)
        {
            Console.WriteLine(o.ToString());
        }

        static void Main(string[] args)
        {
            Int32 a = 10;
            double b = 20.13;
            short c = 100;

            Output(a);
            Output(b);
            Output(c);

            Console.ReadLine();
        }
    }
}

解決方法:可以通過重載函數來避免

namespace ConsoleApplicationTest
{
    class Program
    {
        static void Output(int val)
        {
            Console.WriteLine(val);
        }

        static void Output(double val)
        {
            Console.WriteLine(val);
        }

        static void Output(short val)
        {
            Console.WriteLine(val);
        }

        static void Main(string[] args)
        {
            Int32 a = 10;
            double b = 20.13;
            short c = 100;

            Output(a);
            Output(b);
            Output(c);

            Console.ReadLine();
        }
    }
}

(2) 一個非泛型的容器,同樣是爲了保證通用,而將元素類型定義爲Object。於是,要將值類型數據加入容器時,需要裝箱。
例如:

using System.Collections; // ArrayList

namespace ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var array = new ArrayList();
            array.Add(1);
            array.Add(2);

            foreach (int value in array)
            {
                Console.WriteLine("value is {0}", value);
            }

            Console.ReadLine();
        }
    }
}

在這個過程中會發生兩次裝箱操作和兩次拆箱操作,在向ArrayList中添加int類型元素時會發生裝箱,在使用foreach枚舉ArrayList中的int類型元素時會發生拆箱操作,將object類型轉換成int類型,在執行到Console.WriteLine時,還會執行兩次的裝箱操作;這一段代碼執行了6次的裝箱和拆箱操作;如果ArrayList的元素個數很多,執行裝箱拆箱的操作會更多。

解決方法:可以通過泛型來避免

using System.Collections; // ArrayList

namespace ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<int>();
            list.Add(1);
            list.Add(2);

            foreach (int value in list)
            {
                Console.WriteLine("value is {0}", value);
            }

            Console.ReadLine();
        }
    }
}

代碼和1中的代碼的差別在於集合的類型使用了泛型的List,而非ArrayList;我們同樣可以通過查看IL代碼查看裝箱拆箱的情況,上述代碼只會在Console.WriteLine()方法時執行2次裝箱操作,不需要拆箱操作。
可以看出泛型可以避免裝箱拆箱帶來的不必要的性能消耗;當然泛型的好處不止於此,泛型還可以增加程序的可讀性,使程序更容易被複用等等。

**當然,凡事並不能絕對,假設你想改造的代碼爲第三方程序集,你無法更改,那你只能是裝箱了。
對於裝箱/拆箱代碼的優化,由於C#中對裝箱和拆箱都是隱式的,所以,根本的方法是對代碼進行分析,而分析最直接的方式是瞭解原理結何查看反編譯的IL代碼。比如:在循環體中可能存在多餘的裝箱,你可以簡單採用提前裝箱方式進行優化。**

2. 裝箱/拆箱的內部操作
將值類型轉換爲引用類型,需要進行裝箱操作(boxing)
(1) 首先從託管堆中爲新生成的引用對象分配內存。
(2) 然後將值類型的數據拷貝到剛剛分配的內存中。
(3) 返回託管堆中新分配對象的地址。
可以看出,進行一次裝箱要進行分配內存和拷貝數據這兩項比較影響性能的操作。

將引用內型轉換爲值內型,需要進行拆箱操作(unboxing)
(1) 首先獲取託管堆中屬於值類型那部分字段的地址,這一步是嚴格意義上的拆箱。
(2) 將引用對象中的值拷貝到位於線程堆棧上的值類型實例中。
經過這2步,可以認爲是同boxing是互反操作。嚴格意義上的拆箱,並不影響性能,但伴隨這之後的拷貝數據的操作就會同boxing操作中一樣影響性能。

3. 對裝箱/拆箱更進一步的瞭解
裝箱/拆箱並不如上面所講那麼簡單明瞭,比如:裝箱時,變爲引用對象,會多出一個方法表指針,這會有何用處呢?
我們可以通過示例來進一步探討。
舉個例子:

namespace ConsoleApplicationTest
{
    struct A : ICloneable
    {
        public Int32 x;

        public override String ToString() {
            return String.Format("{0}",x);
        }

        public object Clone() {
            return MemberwiseClone();
        }
    } 

    class Program
    {
        static void Main(string[] args)
        {
            A a;
            a.x = 100;

            // 編譯器發現A重寫了ToString方法,會直接調用ToString的指令。
            // 因爲A是值類型,編譯器不會出現多態行爲。因此,直接調用,不裝箱。
            Console.WriteLine(a.ToString());

            // GetType是繼承於System.ValueType的方法,要調用它,需要一個方法表指針。
            // 於是a將被裝箱,從而生成方法表指針,調用基類的System.ValueType。
            Console.WriteLine(a.GetType());

            // 因爲A實現了Clone方法,所以無需裝箱。
            A a2 = (A)a.Clone();

            // 當a2爲轉爲接口類型時,必須裝箱,因爲接口是一種引用類型。
            ICloneable c = a2;

            // 無需裝箱,在託管堆中對上一步已裝箱的對象進行調用。 
            Object o = c.Clone(); 

            Console.ReadLine();
        }
    }
}

如何更改已裝箱的對象呢?
對於已裝箱的對象,因爲無法直接調用其指定方法,所以必須先拆箱,再調用方法,但再次拆箱,會生成新的棧實例,而無法修改裝箱對象。

namespace ConsoleApplicationTest
{
    struct A : ICloneable
    {
        public Int32 x;

        public override String ToString() {
            return String.Format("{0}",x);
        }

        public object Clone() {
            return MemberwiseClone();
        }

        public void Change(Int32 x)
        {
            this.x = x;
        } 
    } 

    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.x = 100;
            Object o = a; // 裝箱成o,下面,想改變o的值。
            ((A)o).Change(200);

            Console.WriteLine(o.ToString()); // 輸出還是爲100,沒改掉
            Console.ReadLine();
        }
    }
}

正確修改方法:

namespace ConsoleApplicationTest
{
    interface IChange // 添加一個接口
    {
        void Change(Int32 x);
    } 

    struct A : ICloneable, IChange
    {
        public Int32 x;

        public override String ToString() {
            return String.Format("{0}",x);
        }

        public object Clone() {
            return MemberwiseClone();
        }

        public void Change(Int32 x)
        {
            this.x = x;
        } 
    } 

    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.x = 100;
            Object o = a; // 裝箱成o,下面,想改變o的值。
            ((IChange)o).Change(200); // 改爲IChange

            Console.WriteLine(o.ToString()); // 輸出爲200,已改掉
            // 在將o轉型爲IChange時,這裏不會進行再次裝箱,當然更不會拆箱,因爲o已經是引用類型,
            // 再因爲它是IChange類型,所以可以直接調用Change,於是,更改的也就是已裝箱對象中的字段了
            Console.ReadLine();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章