1 finally與return
try-catch-finally是很常用的語法結構,用來控制可能發生異常時的程序流程,其中catch和finally至少要有一個。初學try語法時可能會要問一個問題:如果在try塊中return,那麼finally還會執行嗎?答案是肯定的。這個非常容易驗證,就不舉例子了。這樣帶來一些很好的特性,例如我們可以在try塊中嘗試打開數據庫,然後讀取數據,然後直接把得到的數據return出去,關閉數據連接的工作就交給finally來做——finally中先判斷數據庫是否正常打開了,打開了就關閉。這樣代碼寫起來很清晰,每個部分各做各的事。這樣我們也可以非常肯定的說,無論發生什麼情況(只要不是進程被強行殺掉),finally中的內容一定是要執行的。
那麼是不是可以再問一個問題——如果在finally塊中也寫了return,那麼會怎麼樣呢?試驗一下就很容易知道,finally塊中是不允許寫return的,如果一定要寫,就會得到一個編譯期錯誤:
error CS0157: Control cannot leave the body of a finally clause
2 先return?先finally?
既然finally一定是要執行的,即使try塊中有return,那麼這兩者的執行順便是怎麼樣的呢?簡單的做一個實驗(下面要說明,這個實驗看上去的結果並不這麼直觀的表現出它的內在):
using System;
public class TestClass1
...{
public static void Main()
...{
Console.WriteLine("{0}", Func1());
}
public static int Func1()
...{
int a = 1;
try
...{
return a;
}
finally
...{
a++;
}
}
}
運行這個程序,很容易得到結果為“1”。那麼看上去是執行return在先,而finally在後了。真的是這樣嗎?
例子中我要return的a是一個值類型,那麼如果是引用類型,結果又會如何呢?
using System;
public class TestClass2
...{
public int value = 1;
}
public class TestClass1
...{
public static void Main()
...{
Console.WriteLine("{0}", Func2().value);
}
public static TestClass2 Func2()
...{
TestClass2 t = new TestClass2();
try
...{
return t;
}
finally
...{
t.value++;
}
}
}
這一次運行的結果並不是1,而是2。顯然,運行Func2()返回的結果並不直接是return後面寫的t,而是經過finally塊執行後值發生變化的t。如何來解釋這種區別呢?
3 CLR的棧
要解釋這種區別,就需要看看其IL是什麼,從調用函數、參數棧的角度來理解。CLR在執行中也有棧,但這個棧的用途與傳統的本地代碼中的棧並不完全相同。本地代碼中棧的用處非常大,不但可以用來臨時保存寄存器的值,還用來保存局部變量,此外還用來保存部分或全部傳給函數的參數,而函數的返回值一般是通過EAX寄存器來傳遞的,而不是用棧。但在CLR中,局部變量並非顯式的用棧來保存,棧只是用來調用函數時傳遞參數,此外,函數的返回值也是用棧來保存的。當調用一個函數時,將函數所需要的參數依次壓棧,函數裡面直接取用這些參數,在函數返回時將返回值壓棧,函數返回後,棧頂即是返回值。如果調用者並不關心返回值,那麼需要執行一下pop語句,把返回值彈出,這樣保證函數在調用前後棧頂的位置是相同的。
當通過壓棧傳遞參數時,參數的類型不同,壓棧的內容也不同。如果是值類型,壓棧的就是經過復制的參數值,如果是引用類型,那麼進棧的只是一個引用,這也就是我們所熟悉的,傳遞值類型時,函數內修改參數值不會影響函數外,而引用類型的話則會影響。
代碼中當我們執行new時,對應的IL是newobj,其結果是創建一個TestClass2類型的對像並返回一個引用放置於棧上,之後的stloc就將這個引用保存為局部變量,於是棧上沒有了其他內容。Try塊並沒有執行太多操作,只是把剛保存的引用再放到棧上,再保存為另一個局部變量,這個局部變量就是稍後要返回的引用,此時我們擁有兩個局部變量,但它們是指向同一個對象的兩個引用。Finally塊先拿出開始時保存的引用放到棧上,dup語句使得棧頂再增加一個完全一樣的引用,之後ldfld語句是從棧頂對象取一個成員放到棧上,所取的成員是value,之後再往棧上壓一個1,再執行add,就實現了1+1=2的過程,add從棧上彈出兩個值,再向棧壓回一個值。此時再調用stfld就把剛剛壓棧的2設置給棧上2之下的那個引用所指對象的value屬性上。而在finally之後的部分才是真正的return,它試圖取出我們所保存的第二個局部變量壓棧,將它作為返回值。但對於引用類型來說,它與先前所操作的引用所指的是同一對象,因此finally塊中的操作會影響到返回值,也就非常好理解了。
4 改編
知道了finally與return的實現原理,也就不難做出進一步的推廣。例如把程序改成這樣(返回時由直接返回t變為在t上調用一個做一些操作後返回自己的函數),其執行結果也不難猜出來吧:
using System;
public class TestClass2
...{
public int value = 1;
public TestClass2 Double()
...{
value *= 2;
return this;
}
}
public class TestClass1
...{
public static void Main()
...{
Console.WriteLine("{0}", Func2().value);
}
public static TestClass2 Func2()
...{
TestClass2 t = new TestClass2();
try
...{
&n