VB.NET是怎樣做到的(八)——On Error語句和When語句
2024-07-10 13:01:31
供稿:網友
感覺vb.net特有的功能快要被我研究完了,那么這個系列也快要終止了,不知道能不能湊足10篇。
本次討論的是異常處理語句。vb.net推薦使用try...end try塊來進行結構化的異常處理,但是為了確保兼容性,它也從以前版本的basic中借鑒了on error語句。其實on error并不能算是vb的優點,因為使用它會破壞程序的結構,讓帶有異常處理的程序難以看懂和調試。但是我一直很驚嘆于vb的工程師是怎樣實現它的,因為on error可以讓異常的跳轉變得很靈活,不像try那樣受到限制。首先看看try是怎樣實現的:
public function f1() as integer
try
dim n as integer = 2 / n
catch ex as exception
msgbox(ex.message)
end try
end function
這是最簡單的異常處理程序,通過reflector反匯編(如果用ildasm,不要選擇“展開try-catch”),可以發現整個過程被翻譯成19條指令。留意這一句:
.try l_0000 to l_0006 catch exception l_0006 to l_0022
這就是典型的try塊,在catch處直接指定要捕獲的異常,然后指定catch區的位置,非常清晰。還要留意這兩句:
l_0007: call projectdata.setprojecterror
l_001b: call projectdata.clearprojecterror
可以看出,這兩句是在catch塊的開頭和末尾。深入這兩個過程我發現它是在為err對象記錄異常??磥硎褂胑rr也是語法甜頭,性能苦頭,憑空添加了這兩句(幸好都不太復雜)。
接下來我編寫了一個與此功能類似的函數,用的是on語句處理異常:
public function f2() as integer
on error goto catchblock
dim n as integer = 2 / n
exit function
catchblock:
msgbox(err.description)
end function
這不比上一個過程復雜,但是反匯編以后,它的il代碼竟然有47條指令,剛才才19條啊!最主要的改變是try部分,現在它是這樣:
.try l_0000 to l_0022 filter l_0022 l_0036 to l_0060
注意,catch不見了,而出現了filter。我從沒在c#生成的il中見過filter。由于try和filter不屬于il,而是屬于元數據,所以我查詢了meta data一節的文檔,filter大概能夠進行一些過濾,滿足一定條件才進入處理異常的塊中,本例來說,l_0022指令開始就是過濾器,它是:
l_0022: isinst exception
l_0027: brfalse.s l_0033
l_0029: ldloc.s v_4
l_002b: brfalse.s l_0033
l_002d: ldloc.3
l_002e: brtrue.s l_0033
l_0030: ldc.i4.1
l_0031: br.s l_0034
l_0033: ldc.i4.0
l_0034: endfilter
endfilter就是異常處理部分代碼的開始。而l0030之前的代碼是過濾器的判斷部分,v_4是vb自己加入保存錯誤代碼的變量。在整個反匯編中,我發現設計成處理異常部分的代碼在il里其實也是在try塊中,也就是說程序的結構已經不是規整的try...catch塊,產生異常的語句和處理異常的語句在一起,而真正處理異常的指令是一大堆繁冗拖沓的跳轉語句。
下面看看我編寫的第三個例子:
public function f3() as integer
on error resume next
dim n as integer = 2 / n
end function
這個值有2行的過程動用了vb強大的語法殺手——on error resume next,它將忽略所有異常,讓代碼緊接產生異常的語句繼續執行下去,猜猜這個功能產生了多少il指令?答案是50條!比普通的on error還要長。其實現我就不多說了,和前面的on語句差不多。不過50這個數字似乎提醒了大家,不要在程序里偷懶使用on error處理異常,這樣產生的代價是不可接受的。
最后一個例子是vb.net的when語句,它可以實現對catch部分的過濾:
public function f1() as integer
dim n as integer = 0
try
dim m as integer = 2 / n
catch ex as exception when n = 0
msgbox(ex.message)
end try
end function
里面的when語句進行了對變量n的判斷,僅當n = 0的時候才進入處理部分。聽到“過濾”兩個字,我們已經猜出,它是用try...filter來實現的。沒錯。這里的filter主要是進行ex是否是exception型,n是否等于零等,當過濾成功,就會轉移到異常處理段進行處理。這次vb生成的代碼要比on error語句規則得多,結構相當清晰。
本次我們還借助on error語句和when語句了解到try filter結構,它是c#不能生成的,因此,我發現它不能被常見的反編譯器反編譯(因為反編譯器的編寫者只知道c#,呵呵)。而且用了on error后程序結構變得異常混亂,這在產生負面作用的時候,是不是能夠變相起到保護我們代碼的作用呢?
end sub
如果只想指定k,讓i和j使用默認值,就可以使用按名傳遞,如下
testoptional(k := 2)
而且這種方式不受參數表順序的限制
testoptional(k := 2, i := 3, j := 5)
這些的確是相當方便的功能,c#就不支持上述兩個特性。我們看看它是怎樣在il級別實現的。上述第一個方法在il中的定義為
.method public instance void testoptional([opt] int32 i) cil managed
{
.param [1] = int32(0x00000001)
.maxstack 8
可見,參數被加上了[opt]修飾符,而且.param指定了參數的默認值。這是只有vb能識別的內容,c#會跳過他們。在調用的時候,vb若發現參數被省略,則自動讀取.param部分的默認值,并顯式傳遞給過程。這一部分完全由編譯器處理,而且沒有任何性能損失,和手工傳遞所有參數是完全一樣的。至于按名傳遞,vb會自動調整參數的順序,其結果與傳統方式的傳遞也沒有任何的不同。這說明我們可以放心地使用這項便利。而且帶有可選參數的過程拿到c#中,頂多變成不可選參數,也不會造成什么其他的麻煩。
ps.很多com組件都使用了默認參數,而且有些過程的參數列表非常長,在vb里可以輕松地處理它們,而在c#中經常讓開發者傳參數傳到吐血。