dotnet C# 使用 using 關鍵字釋放 IDisposable 的結構體是否會裝箱

lindexi發表於2024-06-18

在 C# 裡面的 using 關鍵字可以非常方便呼叫 IDisposable 介面的 Dispose 方法,進行一些資源的釋放或實現有趣的邏輯的執行

配合 using 關鍵字使用的型別需要繼承 IDisposable 介面,根據基礎的 C# 知識,大家都知道 using 關鍵字其實會自動在 IL 層拆開為在 finally 裡面呼叫 Dispose 方法。如以下的簡單程式碼

IDisposable disposable = xxx;
using (disposable)
{
   ... // 執行一些程式碼
}

將會被轉換為大概如下的程式碼

IDisposable disposable = xxx;

try
{
   ... // 執行一些程式碼
}
finally
{
    disposable.Dispose();
}

再根據另一個 C# 基礎知識,如果一個結構體被當成介面使用,即使用介面承接結構體,那這個過程將會進行裝箱。結構體裝箱將意味著需要更高的開銷,將會導致這個過程建立一個物件,頻繁使用可能存在一點 GC 壓力

一般情況下會在這裡使用結構體的業務,都是期望 GC 沒有壓力的。如果 using 會導致結構體轉換為介面,從而導致裝箱,無疑這個過程是有傷的

額外提一下為什麼結構體轉換為介面將需要裝箱的過程,這是因為結構體將會在介面裡丟失結構體資訊,由於結構體在區域性變數作用範圍時是存放在棧上的,如作為方法引數傳遞時,也都是在棧範圍的。再使用方法呼叫引數傳遞作為例子,結構體在棧上這就意味著需要執行時知道壓棧空間的大小。結構體是明確知道其佔用空間的,但是介面則不然,這部分將導致無法進行編譯時處理,如果依然讓介面使用結構體形式在記憶體中存放,將會由其佔用空間不可知導致方法呼叫無法正常工作。那執行時能夠知道一個介面是由結構體組成的,那為什麼執行時不做呢?其實執行時也只有在將結構體傳遞給介面變數那一刻之後,後續就不可知了,因為執行時也沒有為此分配更多的記憶體空間來進行記錄,一旦分配更多的記憶體空間來記錄一個介面是否實際為結構體,那這個分配成本就和裝箱差不多了。除了方法呼叫裝箱之外,還有陣列集合等一系列問題。陣列問題可以稍微提一下就是如果一個介面的陣列裡面既然存放有幾層此介面的結構體和型別,那這個介面陣列要怎麼辦?陣列本身需要明確的分配空間大小,如果開發者期望這麼玩,那就不好玩了,究竟一個陣列裡面的元素應該佔用多大的空間才合適,這是在陣列建立的時候不知道的,只有物件放入到陣列裡面時,陣列才能知道。如此大家也能看到結構體給介面時,進行裝箱能完全將結構體放入到物件裡面,解決了非常多的問題,這也就是為什麼如此設計的原因

那本文提出的問題呢?答案是不會裝箱的。畢竟 using 只是一個語法而已,聰明的構建器自然不會做出先將結構體裝箱給到介面再呼叫介面方法的事情

如以下程式碼定義了一個結構體繼承 IDisposable 介面

internal struct DisposableStruct : IDisposable
{
    public void Dispose()
    {

    }
}

使用如下程式碼時,不會出現裝箱問題

using var disposableStruct = new DisposableStruct();
Console.WriteLine("Hello, World!");

從 IL 層面看,以上程式碼的邏輯如下

    IL_0000: ldloca.s     V_0
    IL_0002: initobj      KiheekawyalawGechurwagocal.DisposableStruct
    .try
    {
      IL_0008: ldstr        "Hello, World!"
      IL_000d: call         void [System.Console]System.Console::WriteLine(string)
      IL_0012: leave.s      IL_0022
    } // end of .try
    finally
    {
      IL_0014: ldloca.s     V_0
      IL_0016: constrained. KiheekawyalawGechurwagocal.DisposableStruct
      IL_001c: callvirt     instance void [System.Runtime]System.IDisposable::Dispose()
      IL_0021: endfinally
    } // end of finally
    IL_0022: ret

以上的 IL 重新轉換為 C# 程式碼如下

    DisposableStruct disposableStruct = new DisposableStruct();

    try
    {
        Console.WriteLine("Hello, World!");
    }
    finally
    {
        disposableStruct.Dispose();
    }

從 IL 上沒有看到任何裝箱程式碼,從轉換回的 C# 程式碼也可以看到沒有任何的將結構體給到介面的程式碼

透過以上的說明,大家可以放心給繼承 IDisposable 的結構體使用 using 語法,這是一個非常高效能的做法

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 61400df7abb7994de43efaeae1187abf34e16524

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 61400df7abb7994de43efaeae1187abf34e16524

獲取程式碼之後,進入 Workbench/KiheekawyalawGechurwagocal 資料夾,即可獲取到原始碼

相關文章