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(宣告) | |
|---|---|---|
| 寫法 | |
|
| 作用範圍 | 在大括號區塊內 | 到目前區塊(方法、迴圈等)結束 |
| 簡潔度 | 稍微囉唆一點 | 超簡潔,縮排少 |
| 從哪個版本開始 | 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。
GO TO FULL VERSION