1. 程式人生 > >C#--三行程式碼帶你理解神祕的拆箱和裝箱

C#--三行程式碼帶你理解神祕的拆箱和裝箱

一、在說拆箱和裝箱之前的準備知識

首先,我們需要知道C#中有兩種型別:值型別和引用型別

名稱 值型別 引用型別
表示型別 基本型別 類,陣列,介面 ,C#特有的委託.
儲存內容 值的引用
儲存位置 堆疊 託管堆

二、拆箱和裝箱的概念

上面為什麼要講C#的兩種型別呢,因為拆箱和裝箱實質上就是兩個型別之間的轉換.
拆箱: 引用型別 —>值型別
裝箱: 值型別—–>引用型別

三、拆箱和裝箱例項

我們看下面一篇程式碼,非常簡單,就三句程式碼,就完成了裝箱和拆箱的一個過程。

namespace
DR_HelloWorld {
class Program { static void Main(string[] args) { int num = 12; object numObj = num; int num2 = (int)numObj; } } }

1、分析:

首先我們這三句程式碼中,出現了三個變數,兩個型別:

int型別 : num,num2
object型別:numObj

很明顯我們知道int型別是值型別,object型別是引用型別,但是為什麼int型別是值型別,object型別是引用型別呢?我們可以跳入兩者的定義程式碼中一探究竟:

int: 本質上是一個結構體(struct),所以int是一個值型別

這裡寫圖片描述

object: 本質上是一個類(class),類屬於引用型別

這裡寫圖片描述

那我們就可以很清楚的知道:

1、Int32—>Object 裝箱 :

object numObj = num;

2、Object —>Int32 拆箱:

int num2 = (int)numObj;

2、通過C#程式碼的IL語言,檢視裝箱和拆箱:

這裡我們要通過檢視c#原始碼的IL語言來檢視程式碼的執行過程.

IL,全稱是 Intermediate
Language,是微軟平臺上的一門中間語言,我們平常開發寫的c#語言,在編譯器中都會自動轉換為IL,然後由即時編譯器(JIT
Compiler)轉換為二進位制機器碼,最後被CPU執行.

如果小夥伴們不知道該怎麼看IL程式碼的話,可以移步這裡C# IL DASM 使用 PS:我感覺講的特別好,我就是從那裡學會的.

以下是我們上面例項程式碼的IL程式碼:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 程式碼大小       19 (0x13)
  .maxstack  1
  .locals init ([0] int32 num,
           [1] object numObj,
           [2] int32 num2)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12   //載入一個值12放在堆疊中
  IL_0003:  stloc.0         //從堆疊頂彈出12並將其儲存num中。
  IL_0004:  ldloc.0         //將num的值,載入到堆疊中。
  IL_0005:  box        [mscorlib]System.Int32  //裝箱---將值類Int32轉換為物件引用
  IL_000a:  stloc.1         //棧頂彈出當前的值 ,儲存到numObj 中
  IL_000b:  ldloc.1         //將numObj 的值,載入到棧中
  IL_000c:  unbox.any  [mscorlib]System.Int32  //拆箱--將已裝箱Int32的轉換成未裝箱形式。
  IL_0011:  stloc.2         //棧頂彈出當前的值 ,儲存到num2中
  IL_0012:  ret             
} // end of method Program::Main

四、拆箱和裝箱對於程式碼效率的影響:

裝箱和拆箱因為多了個執行過程,肯定會對程式碼的執行速度產生影響,我們通過以下的程式碼,列印他們的執行時間,可以得出的我們的結論:

 Stopwatch watch = new Stopwatch();
 watch.Start();

 string s = "測試資料";
 for (int i = 0; i < 100000; i++)
 {
     s = s + 1;
 }
 watch.Stop();
 Console.WriteLine("----直接新增---會執行裝箱過程---" + watch.ElapsedMilliseconds);

 watch.Restart();
 string s1 = "測試資料";
 for (int i = 0; i < 100000; i++)
 {
     s1 = s1 + 1.ToString();
 }
 watch.Stop();
 Console.WriteLine("----ToString()新增---不會執行裝箱---" + watch.ElapsedMilliseconds);

 Console.ReadLine();

執行結果:

這裡寫圖片描述

很明顯在迴圈100000次之後,因為裝箱的操作,導致程式碼的執行速度變慢了.

五、如何優化拆箱和裝箱:

1、警惕隱式型別轉換–使用合理的方式進行型別轉換

為什麼要說使用合理的方式?
因為我們很容易就忽略了一些值型別隱式轉換為System.Object的操作
例如:

 string s = "測試資料";
 s = s + 1;

1是值型別,s是string型別,為引用型別,這個”s +1”操作,雖然沒有顯式的型別轉換,但是確實發生了裝箱的操作
這個程式碼的IL語言如下:

  .locals init ([0] string s)
  IL_0000:  nop
  IL_0001:  ldstr      bytearray (4B 6D D5 8B 70 65 6E 63 )                         // Km..penc
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.1
  IL_0009:  box        [mscorlib]System.Int32  //裝箱操作
  IL_000e:  call       string [mscorlib]System.String::Concat(object,
                                                              object)
  IL_0013:  stloc.0
  IL_0014:  ret

那我們應該如下操作:

 string s = "測試資料";
 s = s + 1.ToString();

這個呼叫了Int32的ToString()方法,就變為兩個string型別的資料新增,就不存在裝箱操作了
這個程式碼的IL語言如下:

  .locals init ([0] string s,
           [1] int32 CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldstr      bytearray (4B 6D D5 8B 70 65 6E 63 )                         // Km..penc
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.1
  IL_0009:  stloc.1
  IL_000a:  ldloca.s   CS$0$0000
  IL_000c:  call       instance string [mscorlib]System.Int32::ToString()   //這裡不再裝箱,而是呼叫了Int32的ToString()方法
  IL_0011:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0016:  stloc.0
  IL_0017:  ret

2、使用泛型—執行時繫結資料型別,減少裝箱與拆箱

首先我們需要知道什麼是泛型?

泛型,簡單的來說,是一種可以接收很多種型別的型別,具體是接收多少種,你可以自己去約束,預設是全部型別,一般是用”T”表示.

來個例子感受一下:

簡單示例(一)—-泛型引數的使用

1、指定引數型別的方法:

/// <summary>
/// 指定引數型別
/// </summary>
/// <param name="s"></param>
public static void TestMethod(int s)
{
    s.GetType();
}

因為GetType()是Object的方法,但是引數傳的是Int32型別,肯定會有裝箱操作.
這個程式碼的IL語言如下:

.method public hidebysig static void  TestMethod(int32 s) cil managed
{
  // 程式碼大小       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  box        [mscorlib]System.Int32     //裝箱
  IL_0007:  call       instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
  IL_000c:  pop
  IL_000d:  ret
} // end of method Program::TestMethod

2、不指定引數型別,使用泛型接收的方法

/// <summary>
/// 不指定引數型別,使用泛型接收
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
public static void TestMethod2<T>(T t)
{
     t.GetType();
}

這個方法,我們的引數定義的是泛型的引數,所以,雖然GetType()是Object的方法,但是不會進行裝箱操作.
這個程式碼的IL語言如下:

.method public hidebysig static void  TestMethod2<T>(!!T t) cil managed
{
  // 程式碼大小       16 (0x10)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarga.s   t
  IL_0003:  constrained. !!T  //約束要對其進行虛方法呼叫的型別
  IL_0009:  callvirt   instance class [mscorlib]System.Type    [mscorlib]System.Object::GetType() //對物件呼叫後期繫結方法,並且將返回值推送到計算堆疊上。
  IL_000e:  pop
  IL_000f:  ret
} // end of method Program::TestMethod2

很明顯使用泛型的並沒有裝箱操作,但是執行了constrained指令,那到底是box指令執行快,還是constrained指令執行快,我們需要做個測試.
仍然是100000次迴圈:

 Stopwatch watch = new Stopwatch();
 watch.Start();

 string s = "測試資料";
 for (int i = 0; i < 10000000; i++)
 {
      TestMethod(1);
 }
 watch.Stop();
 Console.WriteLine("---TestMethod--指定型別引數--會執行裝箱過程---" + watch.ElapsedMilliseconds);

 watch.Restart();
 string s1 = "測試資料";
 for (int i = 0; i < 10000000; i++)
 {
     TestMethod2(1);
 }
 watch.Stop();
 Console.WriteLine("----TestMethod2--泛型引數--不會執行裝箱---" + watch.ElapsedMilliseconds);

 Console.ReadLine();

執行結果:

這裡寫圖片描述

很明顯,還是泛型方法執行的速度快一點.

簡單示例(二)—ArrayList和List

上面那個例子是我自己想的,很簡單,很容易理解,但是我們要知道在正常的使用過程中,最常涉及到拆箱/裝箱 和泛型操作的就是列表操作,例如ArrayList和List:

//非泛型集合
ArrayList array = new ArrayList();
array.Add(1);

//泛型集合
List<int> list = new List<int>();
list.Add(1);

其中,
ArrayList是來自System.Collections,是一個非泛型的集合—-存在裝箱

這裡寫圖片描述

List來源於System.Collections.Generic,是一個泛型集合—–不存在裝箱

這裡寫圖片描述

這個程式碼的IL語言如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 程式碼大小       35 (0x23)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Collections.ArrayList 'array',
           [1] class [mscorlib]System.Collections.Generic.List`1<int32> list)
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.1
  IL_0009:  box        [mscorlib]System.Int32
  IL_000e:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
  IL_0013:  pop
  IL_0014:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0019:  stloc.1
  IL_001a:  ldloc.1
  IL_001b:  ldc.i4.1
  IL_001c:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
  IL_0021:  nop
  IL_0022:  ret
} // end of method Program::Main

五、總結:

1、拆箱和裝箱的存在,讓值型別和引用型別之間的轉換變得方便
2、但是在大量的資料操作中,頻繁的裝箱和拆箱操作會大大消耗CPU的資源,降低程式碼的執行速率
3、為了解決這個問題,我們要合理的使用型別轉換和泛型類與泛型方法來防止隱式的裝箱和拆箱操作

參考資料


歡迎關注博主的微信公眾號,快快加入哦,期待與你一起成長!