CodeGym /課程 /C# SELF /關閉串流與釋放資源 ( using)

關閉串流與釋放資源 ( using)

C# SELF
等級 36 , 課堂 0
開放

1. 在 C# 裡怎麼正確釋放資源?

想像一下作業系統就像一個很嚴格的圖書館員。你借了一本書(開了一個檔案/串流),在看,然後……忘記還了!圖書館員就會生氣:「怎麼還沒還書?!」串流也是這樣。開著的串流會佔用資源:檔案描述器、一塊記憶體,還會把檔案鎖住讓其他應用程式不能用。

如果你沒關串流,結果可能從「有東西怪怪的」到「全炸掉,誰都不能寫這個檔案」。如果程式裡有一堆沒關的串流——系統可能會開始「資源外洩」然後直接掛掉。

危險在哪?

  • 檔案沒關,指令沒寫進硬碟(像寫入時資料可能還卡在 buffer 裡)。
  • 檔案被其他程式鎖住——同事和其他程式都會暴怒。
  • 描述器上限:在 Windows/Linux 裡每個 process 開檔案/串流都有上限。

IDisposable 介面

任何操作 unmanaged 資源(串流、檔案、資料庫、socket)的 class,都必須實作 IDisposable 介面。


public interface IDisposable
{
    void Dispose();
}

Dispose() 方法裡通常會把所有可憐的資源都釋放掉:檔案終於關了、連線斷了、記憶體也釋放了。

串流就是會抓著各種重要資源的物件:檔案、連線、記憶體。如果你沒關它,檔案可能會一直被鎖住(你的 Word 會說 "檔案被其他應用程式開啟!"),系統也會沒記憶體。所以用完一定要把串流釋放掉。

在 .NET 裡串流都會實作 IDisposable。意思就是:你要記得關它,呼叫 Dispose()(或直接包在 using 區塊裡)。

2. 關閉串流的方式:從危險到穩妥

方式 1. 「手動」關閉:不要這樣做!

這是很舊的寫法。雖然我以前也有拿來當範例,但現在沒人這樣寫啦 :P


var stream = new FileStream("file.txt", FileMode.Open);
// 操作串流
stream.Close(); // 或 stream.Dispose()

問題:如果開和關之間出錯/丟例外,檔案就會一直開著被鎖住。就像你聞到披薩味直接衝出圖書館,書還在你手上……

方式 2. 用 try...finally


FileStream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.Open);
    // 操作串流
}
finally
{
    if (stream != null)
        stream.Dispose();
}

這樣就很穩:finally 就算出錯也一定會執行。但老實說,這樣寫很懶。

方式 3. 漂亮又安全:using 運算子

經典語法 (using ( ... ) { ... })


using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // 操作串流
}
// 這裡 stream.Dispose() 會自動被呼叫!

重點就是——using 區塊裡的東西都能用串流,區塊結束時檔案一定會關掉(就算有例外也一樣)。

方式 4. 現代語法

現代語法 (using var)


using var stream = new FileStream("file.txt", FileMode.Open);
// 操作串流
// ... Dispose 會自動在變數離開作用域時呼叫

超棒!不用多寫大括號和縮排。

底層怎麼運作?

using 其實會被編譯器展開成 try...finally,只是你不用自己寫。開玩笑說:「using 幫你寫乾淨的 code——搞不好以後還會幫你泡咖啡、滑 Stack Overflow?」

經典 using 跟現代 using 的差別

經典 using 區塊 using var(宣告)
寫法
using (var x = ...) { ... }
using var x = ...; ...
作用範圍 在大括號區塊內 到目前區塊(方法、迴圈等)結束
簡潔度 稍微囉唆一點 超簡潔,縮排少
從哪個版本開始 C# 1.0 C# 8.0 以上

3. 什麼是 using 宣告?

5 年前世界上出現了一種新的、超簡潔的 IDisposable 物件用法。

using 宣告——就是你不用寫區塊,直接用 using 宣告變數,這個變數會在目前區塊(像方法或迴圈)結束時自動釋放,而不是在大括號結束時。


using var stream = new FileStream("file.txt", FileMode.Open);
// 操作串流
Console.WriteLine(stream.Length);

// 這裡檔案還是開著!

// ... 方法結束
// stream.Dispose() 會在這裡自動呼叫

跟經典 using 的主要差異:

  • 不用大括號,不會多一層巢狀區塊。
  • 變數可以用到目前整個區塊結束(通常是方法,有時是迴圈、class,如果宣告在 class 層級)。
  • 資源釋放只會在區塊結束時發生。

為什麼這麼酷?

  • 巢狀層數少——code 變超短超好讀。
  • 多資源也好處理——可以連續宣告好幾個 using 變數,等方法結束時一起釋放。
  • 不容易出錯——不會忘記在哪裡該呼叫 Dispose()

4. 比較一下:經典 vs. 現代 using

來看一下 code 比較。

經典寫法


using (var reader = new StreamReader("input.txt"))
{
    using (var writer = new StreamWriter("output.txt"))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            writer.WriteLine(line.ToUpper());
        }
    }
} // 這裡兩個檔案都會被關掉

現代寫法(C# 8+)


using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");

string line;
while ((line = reader.ReadLine()) != null)
{
    writer.WriteLine(line.ToUpper());
}
// 兩個檔案都會在這裡(方法結束時)關掉

是不是簡單多了? 如果巢狀再多一點,現代寫法真的超省事。

5. Dispose() 什麼時候會被呼叫?

這裡很多學生會搞錯:以為 Dispose 會在用完那行馬上呼叫——其實不是!

來看這個例子:


void MyMethod()
{
    using var fileStream = new FileStream("data.bin", FileMode.Open);
    // ... 一堆 code,可能還有迴圈和巢狀呼叫
    // fileStream 還是開著!

    // 這裡還能用 fileStream
}
// 這裡(方法結尾的 }),fileStream.Dispose() 才會被呼叫

重點:如果你在迴圈裡宣告 using 變數,Dispose 會在每次迴圈結束時呼叫。


foreach (var path in filePaths)
{
    using var reader = new StreamReader(path);
    // 用 reader 做事
} // 每次迴圈結束都會呼叫 reader.Dispose()(檔案會被關掉)

6. 搬舊 code 時的錯誤

有時你會搬舊 code 或複製經典 using 範例,但變數需要比大括號更長的「壽命」。這時經典寫法就不適合,用 using 宣告就很完美。

但還是有細節。像在迴圈裡有兩個資源,但其中一個要「活」比另一個久——就要照順序宣告:


using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
    using var resource2 = ...;
    // resource2 活一輪
    // resource1 活整個 function
}

7. 實作練習

繼續我們的學習小專案——一個小小的咖啡訂單模擬器,你前幾天已經有改過。現在讓它能把訂單歷史寫進文字檔,啟動時也能讀出來。

步驟 1:把訂單寫進檔案


using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("咖啡: 拿鐵; 牛奶: 燕麥; 尺寸: 大杯");

// StreamWriter 的第二個參數 append: true 代表我們要加在檔案後面,不是覆蓋。

步驟 2:讀訂單歷史


using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine($"訂單: {line}");
}

等這個方法跑完,檔案就會自動關掉。

8. using 宣告的最佳實踐

1. 只要是實作 IDisposable 的物件都要用 using
在 .NET 裡大部分操作檔案、串流、資源的 class 都有這個介面。這就是訊號:要用 using 來釋放我!

2. 注意作用範圍:不要在不需要的地方宣告 using-var
如果變數只用幾行,就只在那裡宣告,不要提早。

3. 不要忘記釋放順序
如果你連續宣告好幾個 using 變數——Dispose 會反過來呼叫:


using var first = new Resource("First");
using var second = new Resource("Second");
// ... 做事
// 會先 Dispose second,再 Dispose first

有時很重要,像一個資源要依賴另一個(例如寫入串流要先釋放,檔案才能釋放)。

4. 不要在方法外用 using 宣告
using 宣告不能用在 class 層級(像欄位)。只能在方法、建構子等裡面用。

5. 搭配錯誤處理
就算有 using,有些例外還是要自己處理——如果你要控管讀寫錯誤,記得加 try-catch

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION