C# 裝箱與拆箱機制詳解,你不知道的基礎知識都在這兒

本文參考 《CLR via C#》、搜索引擎

值類型比引用類型要 “輕” 那麼一點,值類型使用的時候也非常的方便,它們 不作爲對象在託管推中分配,沒有被當作垃圾回收掉,也不能通過指針進行引用。但許多的時候都需要對值類型進行實例的引用,這就是我們所常說的 “裝箱”,當然,有裝箱就有拆箱,下面就讓我們一起來了解一下,值類型與引用類型之間的那些事兒吧 . . .
.
裝箱是一個非常浪費性能的操作,在學習過程中,我們儘量避免這種操作,養成好的習慣 . . .


在這裏插入圖片描述

文章目錄


.

裝箱與拆箱

—— 裝箱

值類型 轉換成 引用類型 就要使用到 裝箱 機制,裝箱的時候會發生如下幾種情況:

  • 在託管堆中分配內存
  • 值類型的字段複製到新分配的堆內存
  • 返回對象地址。 該地址是對象引用,值類型 --> 引用類型

下面這個圖演示了 裝箱 的過程:

在這裏插入圖片描述

C# 編譯器會自動生成對值類型實例進行裝箱所需的 IL 代碼,下面我會進行演示 . . .

.

—— 拆箱

有裝箱就有拆箱,那麼怎麼樣才能完成拆箱呢? 完成拆箱主要有兩步:

  • 獲取已裝箱的 值類型在堆中的各個字段的地址(拆箱
  • 將字段包含的值從堆中複製到基於棧的值類型實例

在這裏插入圖片描述

.

介紹完裝箱與拆箱的概念原理之後,我們下面就來研究一下他們的代碼實現吧

例如我們對一個 Int32 型 的數據進行裝箱與拆箱:
在這裏插入圖片描述

進行裝箱的時候,我們感覺就像直接複製了一樣,實則 像上面那個裝箱圖一樣做了許多事情,在進行拆箱的時候,感覺就像強制類型轉換一樣,但也不然 . . .

短短的兩行代碼,卻直接演示了 裝箱與拆箱. . .

這裏需要注意的是,我們進行拆箱的時候,如果引用的對象不是所需值類型的已裝箱實例,就會拋出異常,比如下面這種情況:
在這裏插入圖片描述
如果我們一定要使用 Int16 來進行拆箱,那麼我們可以使用下面的這種寫法:
在這裏插入圖片描述
對對象進行拆箱時,只能轉型爲最初未裝箱的值類型 Int32,之後再進行強制轉換爲 Int16 . . .

上面我們提到過,進行拆箱時會進行一次字段複製,那麼我們輸出 newValue的值 應該是 42,事實也如此 . . .

.

下面我們看一個例子,其中進行了幾個裝箱呢?
在這裏插入圖片描述

它的結果是:123,5
因爲 o 引用的是已經裝箱的 v,不管我們如何的修改未裝箱的 v,都不會影響到它 . . .

那麼它到底進行了幾次裝箱呢? 答案是三次,是不是有點小意外 ^ _ ^,我們來看一看這個程序生成的 IL 代碼,我們可以通過 ILDASM 工具進行查看:

  • 直接將.exe 文件插入其中即可:
    在這裏插入圖片描述
  • 生成的 IL 代碼:
    在這裏插入圖片描述

注意我用紅色框起來的部分,我們使用 Console.WriteLine 進行輸出時,它把三個參數都當成 String型數據連接起來,然後輸出 . . .

那麼三次裝箱是哪三次呢? 下面就是正確的答案,你知道嗎:

- Object o = v; 				// 第一次裝箱
- Console.WriteLine 中的 v  			// 第二次裝箱
- Console.WriteLine 中的 (Int32)o   		// 拆箱後又進行裝箱

之所以三個參數都是 String型,是因爲 Console.WriteLine沒有重載 Console.WriteLine(Int32,
String, Int32) 這個方法,所以Console.WriteLine 直接把它們都當成引用類型(String) 連接起來了 . .
.
大家可以細細的品 . . . ^ _ ^

.

裝箱與拆箱的基本概念與代碼介紹到此,下面我們來實踐一下一個小程序,看看其中有多少的裝箱拆箱,此外,我再次提醒,裝箱很廢性能,儘量避免這樣的操作,但有的時候,我們又不得不去進行裝箱,比如上面的那個有三次裝箱的 Console.WriteLine,我們可以將它改成如下的樣子:

在這裏插入圖片描述

這樣子,這個例子就進行了 一次裝箱, v.ToString 和 ", " 和 o 是一樣的引用類型 . . .

. . .


.

實例講解程序中的裝箱與拆箱機制

當我們把值類型轉化爲接口類型也需要裝箱操作的,下面這個例子就完美的體現出,如果看懂了,我們就真正的理解裝箱與拆箱機制了,每一行 Console.WriteLine 代碼我都加以註釋 . . .

using System;

namespace BoxDemo
{ 
    class Program
    {
        static void Main(string[] args)
        {
            // 值類型,在棧上創建兩個 Point實例
            Point p1 = new Point(10, 10);
            Point p2 = new Point(20, 20);

	    // 調用 ToString(虛方法) 不裝箱 p1
            Console.WriteLine(p1.ToString());

	    // 調用 GetType(非虛方法)時,要對 p1 進行裝箱
            Console.WriteLine(p1.GetType());                //Object

  	    // 調用 CompareTo 不裝箱 p1
            // 調用的是重載過的 CompareTo
            Console.WriteLine(p1.CompareTo(p2));

 	    // 裝箱 放到 c中
            IComparable c = p1;
            Console.WriteLine(c.GetType());

	    // 調用 CompareTo 不裝箱 p1
            // 調用的是IComparable 實現的接口 CompareTo
            Console.WriteLine(p1.CompareTo(c));

	    // c 不裝箱(引用 p1)    p2 裝箱
            Console.WriteLine(c.CompareTo(p2));

	    // 對 c 拆箱,字段複製到 p2中
            p2 = (Point)c;
            Console.WriteLine(p2.ToString());
        }
    }

    internal struct Point : IComparable
    {
        private Int32 m_x, m_y;

	public Point(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }

	public override String ToString()
        {
            // 返回 Point,避免 ToString 裝箱
            return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());
        }

	public Int32 CompareTo(Point other)
        {
            // 計算 Point的 哪個點 離 (0,0) 更遠
            return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) -
                Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
        }

	// 實現接口中的 CompareTo
        public Int32 CompareTo(object obj)
        {
            if(this.GetType() != obj.GetType())
            {
                throw new ArgumentException("o is not a Point");    
            }
            
            // 調用類型安全的 CompareTo方法
            return CompareTo((Point)obj);   
        }
    }
}

.

平常注意的兩個操作點

一、重複數據應避免多次裝箱

例如下面,我們需要輸出三個相同數據的值類型,但 Console.WriteLine 對他進行了三次裝箱:

int v = 11;

Console.WriteLine("{0}, {1}, {2}", v, v, v);

解決辦法:手動進行一次裝箱,只進行一次裝箱:

int v = 11;

Object o = v;
Console.WriteLine("{0}, {1}, {2}", o, o, o);

如果不知道這裏的情況,請看上面這個相關的例子:
在這裏插入圖片描述

.

二、使用接口更改已經裝箱值類型中的字段

首先,我們來測試一下沒有使用接口的情況:

定義一個測試類:

在這裏插入圖片描述

測試代碼:

在這裏插入圖片描述

測試的結果是:

在這裏插入圖片描述

這裏,我們可能會想不到爲什麼最後的輸出也是 (2,2)?

原因解析: 因爲對引用類型進行拆箱時,它將已裝箱 Point 中的字段複製到 線程棧上的一個 Point上面!但是已經裝箱的 Point不會受這個 Change調用的影響,所以這就需要我們藉助接口的使用了

.

接口的使用,改變已經裝箱的值類型:

定義一個接口,並定義一個實現接口的類:

在這裏插入圖片描述

測試代碼:

在這裏插入圖片描述

輸出的結果爲:

在這裏插入圖片描述

倒數第二個輸出,造成這樣的原因類似於:
在這裏插入圖片描述

最後一個輸出,o 引用的已裝箱 Point 轉型爲一個 IChangeBoxedPoint。這裏不需要裝箱,因爲 o本來就是引用類型,它直接調用 Change 修改對應的數據 . . . 接口方法 Change 使我們能夠完成我們想要的操作 . . .


5.20 快樂

文中可能有許多不對的地方,歡迎前輩糾正,謝謝大家 ^ _ ^

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