[C#.NET] using區段是否等於try/finally
續上篇[.NET] 使用 using 或 try/finally 清理資源,我們都知道using可以釋放實作IDisposable介面的類別,主要是釋放非托管的物件,例如:在操作檔案的時候就必須要釋放才不會造成檔案鎖定,為了確保資源能正確的被釋放,我們可以用using或是try/finally在finally區段裡釋放資源以及該做的事,思考一下using內部幫我們做了什麼呢?
使用
try/finally的寫法如下
public string ReaderLine(string FileName)
{
if (!File.Exists(FileName))
return null;
Stream stream = null;
StreamReader reader = null;
string line = "";
StringBuilder sb = new StringBuilder();
try
{
stream = File.Open(FileName, FileMode.Open);
reader = new StreamReader(stream);
while ((line = reader.ReadLine()) != null)
{
sb.AppendLine(line);
}
return sb.ToString();
}
finally
{
if (stream != null)
stream.Close();
if (reader != null)
reader.Close();
}
}
使用using的寫法如下
public string ReaderLine(string FileName)
{
if (!File.Exists(FileName))
return null;
string line = "";
StringBuilder sb = new StringBuilder();
using (Stream stream = File.Open(FileName, FileMode.Open))
{
using (StreamReader reader = new StreamReader(stream))
{
while ((line = reader.ReadLine()) != null)
{
sb.AppendLine(line);
}
return sb.ToString();
}
}
}
我用using或try/finally的寫法例外發生時會被吃掉嗎?
我相信很多人對上述的寫法會有疑問,我們來做個小實驗,我在try區塊以及using區塊裡面故意寫轉型失敗的程式碼,我們可以發現VS會拋出例外訊息。
所以不管是using還是try/finally都不會吃掉例外訊息,並且能確保資源被釋放,釋放演練請看[.NET] 使用 using 或 try/finally 清理資源
using和try/finally有什麼不同? using幫我們做了什麼?
基本上它們兩個的功能是一樣的,主要都是用來釋放資源用,先來用Reflector看看using所產生的IL,我們可以觀察到using區段裡面是用try/finlly寫的
.method public hidebysig instance string ReaderLine(string FileName) cil managed
{
.maxstack 2
.locals init (
[0] string line,
[1] class [mscorlib]System.Text.StringBuilder sb,
[2] class [mscorlib]System.IO.Stream stream,
[3] class [mscorlib]System.IO.StreamReader reader,
[4] string a,
[5] uint8 b,
[6] string CS$1$0000,
[7] bool CS$4$0001)
L_0000: nop
L_0001: ldarg.1
L_0002: call bool [mscorlib]System.IO.File::Exists(string)
L_0007: stloc.s CS$4$0001
L_0009: ldloc.s CS$4$0001
L_000b: brtrue.s L_0012
L_000d: ldnull
L_000e: stloc.s CS$1$0000
L_0010: br.s L_008d
L_0012: ldstr ""
L_0017: stloc.0
L_0018: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
L_001d: stloc.1
L_001e: ldarg.1
L_001f: ldc.i4.3
L_0020: call class [mscorlib]System.IO.FileStream [mscorlib]System.IO.File::Open(string, valuetype [mscorlib]System.IO.FileMode)
L_0025: stloc.2
L_0026: nop
L_0027: ldloc.2
L_0028: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
L_002d: stloc.3
L_002e: nop
L_002f: br.s L_003b
L_0031: nop
L_0032: ldloc.1
L_0033: ldloc.0
L_0034: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine(string)
L_0039: pop
L_003a: nop
L_003b: ldloc.3
L_003c: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
L_0041: dup
L_0042: stloc.0
L_0043: ldnull
L_0044: ceq
L_0046: ldc.i4.0
L_0047: ceq
L_0049: stloc.s CS$4$0001
L_004b: ldloc.s CS$4$0001
L_004d: brtrue.s L_0031
L_004f: ldstr "999"
L_0054: stloc.s a
L_0056: ldloc.s a
L_0058: call uint8 [mscorlib]System.Byte::Parse(string)
L_005d: stloc.s b
L_005f: ldloc.1
L_0060: callvirt instance string [mscorlib]System.Object::ToString()
L_0065: stloc.s CS$1$0000
L_0067: leave.s L_008d
L_0069: ldloc.3
L_006a: ldnull
L_006b: ceq
L_006d: stloc.s CS$4$0001
L_006f: ldloc.s CS$4$0001
L_0071: brtrue.s L_007a
L_0073: ldloc.3
L_0074: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0079: nop
L_007a: endfinally
L_007b: ldloc.2
L_007c: ldnull
L_007d: ceq
L_007f: stloc.s CS$4$0001
L_0081: ldloc.s CS$4$0001
L_0083: brtrue.s L_008c
L_0085: ldloc.2
L_0086: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_008b: nop
L_008c: endfinally
L_008d: nop
L_008e: ldloc.s CS$1$0000
L_0090: ret
.try L_002e to L_0069 finally handler L_0069 to L_007b
.try L_0026 to L_007b finally handler L_007b to L_008d
}
再看看Reflector不優化IL的結果,選C# → None,Reflector會幫我們直接翻譯IL成C#,當然你還可以翻成其它.NET語言
public string ReaderLine(string FileName)
{
string line;
StringBuilder sb;
Stream stream;
StreamReader reader;
string a;
byte b;
string CS$1$0000;
bool CS$4$0001;
if (File.Exists(FileName) != null)
{
goto Label_0012;
}
CS$1$0000 = null;
goto Label_008D;
Label_0012:
line = "";
sb = new StringBuilder();
stream = File.Open(FileName, 3);
Label_0026:
try
{
reader = new StreamReader(stream);
Label_002E:
try
{
goto Label_003B;
Label_0031:
sb.AppendLine(line);
Label_003B:
if ((((line = reader.ReadLine()) == null) == 0) != null)
{
goto Label_0031;
}
a = "999";
b = byte.Parse(a);
CS$1$0000 = sb.ToString();
goto Label_008D;
}
finally
{
Label_0069:
if ((reader == null) != null)
{
goto Label_007A;
}
reader.Dispose();
Label_007A:;
}
}
finally
{
Label_007B:
if ((stream == null) != null)
{
goto Label_008C;
}
stream.Dispose();
Label_008C:;
}
Label_008D:
return CS$1$0000;
}
用Reflector幫我們翻譯IL的結果,using區段變成了try/finally
寫try一定要寫catch嗎?可以不寫catch嗎?
台灣很多的書籍都會教try/catch或是try/catch/finally的寫法,回顧一下它的用法,把程式邏輯擺在try區段,把例外處理擺在catch區段,一定要做的事則擺放在finally區段,以下try/catch/finally的用法,我相信很多人都看過以下的寫法也會這樣寫,但是眼尖的朋友一定會發現catch區段的寫法有很大的問題,以前發過的文章在這裡就不在多敘述,[.NET] 使用 try/catch 攔截例外應該注意事項,本篇重點是using內部的實作方式。
public string ReaderLine(string FileName)
{
if (!File.Exists(FileName))
return null;
Stream stream = null;
StreamReader reader = null;
string line = "";
StringBuilder sb = new StringBuilder();
try
{
stream = File.Open(FileName, FileMode.Open);
reader = new StreamReader(stream);
while ((line = reader.ReadLine()) != null)
{
sb.AppendLine(line);
}
string a = "999";
byte b = byte.Parse(a);
return sb.ToString();
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (stream != null)
stream.Close();
if (reader != null)
reader.Close();
}
}
在底層,catch沒寫好,直接拋Exception類別,會造成Stack的困擾,在UI層反而得到不完整的錯誤訊息,若你在開發底層原件,還在直接拋Exception類別,請把這壞習慣改掉,直接改拋throw,除非你能確保拋出正確的Exception類別;若你在開發UI層,可能會有記錄例外狀況的時候,可以用StackTrace記錄錯誤內容,請參考[.NET] 追蹤類別–StackTrace
try
{
//TODO:要處理的事,程式邏輯
}
catch
{
throw;
}
finally
{
//DOTO:一定要做的事
}
或是連catch都不寫
try
{
//TODO:要處理的事,程式邏輯
}
finally
{
//DOTO:一定要做的事
}
try/finally這樣的寫法在CLR裡也是有跡可循,只針對已知的例外使用try/catch。
using不是比較好用嗎?為什麼要用try/finally?
如果瞭解using好處的人,會推崇使用using,因為它幫我們處理了變數的生命週期,並且呼叫Dispose(),這是好事,因為人都是會粗心大意的,但using的用意是要確保釋放資源;但並非所有的物件都有using可以用,若沒有using的物件要怎麼辦?比如執行緒鎖定,在這裡用Monitor類當範例,當呼叫Monitor.Enter鎖定時,就要在finally區段呼叫Monitor.Exit,已確保物件解鎖。
object _thisLock = new object();
Monitor.Enter(_thisLock);
try
{
//TODO
}
finally
{
Monitor.Exit(_thisLock);
}
當類別沒有實作IDisposable介面,表示沒有辦法用using,我們可以在自己用try/finally或try/catch/finally處理,在finally區段裡一定要確定資源會被釋放或是該做的事有做。
當程式碼有一定要做的事,千萬不忘記做
後記:
由上面的演示我們可以知道
1.using跟try/finally,都不會吃掉例外訊息,因為沒有寫catch區段。
2.using是try/finally的簡易寫法,using裡面已經包含呼叫Dispose方法。
3.若要自己寫try/finally區段得自己呼叫Dispose方法。
PS.純個人看法,兩者的寫法結果都一樣,瞭解兩者之間的優點之後,就看自己喜歡什麼樣子的寫法,using的寫法看起來比較短,也不用擔心忘了寫釋放,若只有一層我會使用,但如果有多層using(兩層以上),我就會覺得大括號太多閱讀上有很大的不便,using之間可能有很多行程式碼,我就不會採用這樣的寫法,當然,若你的VS有裝外掛,可以解決閱讀上的困擾,下圖是VS採用telerik的結果。
我比較喜歡用try/finally同時處理多個實作IDisposable的物件
相關的文章參考:
- using適用於只有少數物件需要釋放的情況,try/finally語法則適用於多個物件需要釋放的情況。這兩種語法是等價的,在編譯時編譯器會將using改為try/finally的寫法,故這兩種寫法皆可確保資源能有效的被釋放。[C#]Effective C# 條款十五:利用using和try/finally語句來清理資源.
- finally 區塊對於清除 try 區塊中配置的任何資源,以及執行即使有例外狀況也必須執行的任何程式碼十分有用。不論 try 區塊如何結束,程式控制權最後都轉移到 finally 區塊。http://msdn.microsoft.com/zh-tw/library/zwc8s4fz.aspx
- 由於會隱含執行Dispose,所以不需要再手動處理,如果自己在手動執行Close()、Dispose(),只會讓CLR再度浪費資源進而影響程式效能。[C#][Tips]再探using陳述式
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET