vb.net能夠實現很多c#不能做到的功能,如when語句、optional參數、局部static變量、對象實例訪問靜態方法、handles綁定事件、on error處理異常、object直接后期綁定等等。vb和c#同屬.net的語言,編譯出來的是同樣的cil,但為什么vb支持很多有趣的特性呢。我們一起來探究一下。
(一)局部靜態變量
vb支持用static關鍵字聲明局部變量,這樣在過程結束的時候可以保持變量的數值:
public sub test1()
static i as integer
i += 1 '實現一個過程調用計數器
end sub
我們實現了一個簡單的過程計數器。每調用一次test,計數器的數值就增加1。其實還有很多情況我們希望保持變量的數值。而c#的static是不能用在過程內部的。因此要實現過程計數器,我們必須聲明一個類級別的變量。這樣做明顯不如vb好。因為無法防止其他過程修改計數器變量。這就和對象封裝一個道理,本來應該是一個方法的局部變量,現在我要被迫把它獨立出來,顯然是不好的設計。那么vb是怎么生成局部靜態變量的呢?將上述代碼返匯編,我們可以清楚地看到在vb生成的cil中,i不是作為局部變量,而是作為類的field出現的:
.field private specialname int32 $static$test1$2001$i
也就是說,i被改名作為一個類的字段,但被冠以specialname。在代碼中試圖訪問$static$test1$2001$i是不可能的,因為它不是一個有效的標識符。但是在il中,將這個變量加一的代碼卻與一般的類字段完全一樣,是通過ldfld加載的。我覺得這個方法十分聰明,把靜態變量變成生命周期一樣的類字段,但是又由編譯器來控制訪問的權限,讓它成為一個局部變量。同時也解釋了vb為什么要用兩個不同的關鍵字來聲明靜態變量——static和shared。
由于局部靜態變量的實質是類的字段,所以它和真正的局部變量還是有所不同的。比如在多線程條件下,對局部靜態變量的訪問就和訪問字段相同。
(二)myclass關鍵字
vb.net支持一項很有意思的功能——myclass。大部分人使用myclass可能僅限于調用本類其他構造函數時。其實myclass可以產生一些很獨特的用法。myclass永遠按類的成員為不可重寫的狀態進行調用,即當類的方法被重寫后,用myclass仍能得到自身的版本。下面這個例子和vb幫助中所舉的例子非常相似
public class myclassbase
protected overridable sub greeting()
console.writeline("hello form base")
end sub
public sub useme()
me.greeting()
end sub
public sub usemyclass()
myclass.greeting()
end sub
end class
public class myclasssub
inherits myclassbase
protected overrides sub greeting()
console.writeline("hello form sub")
end sub
end class
我們用一段代碼來測試:
dim o as myclassbase = new myclasssub()
o.useme()
o.usemyclass()
結果是useme執行了子類的版本,而usemyclass還是執行了基類本身的版本,盡管這是一個虛擬方法。觀其il,可以看到其簡單的實現原理:
me用的調用指令是callvirt
il_0001: callvirt instance void app1.myclassbase::greeting()
而myclass調用的是call
il_0001: call instance void app1.myclassbase::greeting()
奇怪的是,如此簡單的一個功能,我竟然無法用c#實現,c#怎樣也不允許我按非虛函數的方式調用一個虛函數。c++可以用類名::方法名的方式訪問自身版本的函數,但c#的類名只能用來訪問靜態的成員。這真是c#一個奇怪的限制。
(三)handles和withevents
vb除了可以用c#那樣的方法來處理事件響應以外,還有從vb5繼承下來的獨特的事件處理方式——withevents。
我喜歡稱這種事件處理方式為靜態的事件處理,書寫響應事件的方法時就已經決定該方法響應的是哪一個事件,而c#則是在代碼中綁定事件的。比如下面這個最簡單的例子:
public class handlerclass
public withevents myobj as eventclass
private sub myobj_myevent(byval sender as object, byval e as system.eventargs) handles myobj.myevent
msgbox("hello")
end sub
public sub new()
myobj = new eventclass
end sub
end class
代碼中用到的eventclass是這樣的:
public class eventclass
public event myevent as eventhandler
protected overridable sub onmyevent(byval e as eventargs)
raiseevent myevent(me, e)
end sub
public sub test()
onmyevent(new eventargs)
end sub
end class
我們來復習一下,這段代碼隱式地給eventclass編寫了兩個方法——add_myevent(eventhandler)和remove_myevent(eventhandler),實際上任何使用事件的上下文都是通過調用這兩個方法來綁定事件和解除綁定的。c#還允許你書寫自己的事件綁定/解除綁定的代碼。
那么withevents是怎么工作的呢?vb.net的編譯器在編譯時自動將
public withevents myobj as eventclass
翻譯成下面這個過程:
private _myobj as eventclass
public property myobj() as eventclass
get
return _myobj
end get
set(byval value as eventclass)
if not (me._myobj is nothing) then
removehandler _myobj.myevent, addressof myobj_myevent
end if
me._myobj = value
if me._myobj is nothing then exit property
addhandler _myobj.myevent, addressof myobj_myevent
end set
end property
由此可見,當對withevents變量賦值的時候,會自動觸發這個屬性以綁定事件。我們所用的大部分事件響應都是1對1的,即一個過程響應一個事件,所以這種withevents靜態方法是非常有用的,它可以顯著增強代碼可讀性,同時也讓vb.net中的事件處理非常方便,不像c#那樣離開了窗體設計器就必須手工綁定事件。
不過在分析這段il的時候,我也發現了vb.net在翻譯時小小的問題,就是ldarg.0出現得過多,這是頻繁使用me或this的表現,所以我們在編碼過程中一定要注意,除了使用到me/this本身引用以外,使用它的成員時不要帶上me/this,比如me.myint = 1就改成myint = 1,這樣的小習慣會為你帶來很大的性能收益。
(四)類型轉換運算符
在visual basic 2005中將加入一個新的運算符——trycast,相當于c#的as運算符。我一直希望vb有這樣一個運算符。vb目前的類型轉換運算符主要有ctype和directcast。他們的用法幾乎一樣。我詳細比較了一下這兩個運算符,得出以下結論:
1、在轉換成引用類型時,兩者沒有什么區別,都是直接調用castclass指令,除非重載了類型轉換運算符ctype。directcast運算符是不能重載的。
2、轉換成值類型時,ctype會調用vb指定的類型轉換函數(如果有的話),比如將string轉換為int32時,就會自動調用visualbasic.compilerservices.integertype.fromstring,而將object轉換為int32則會調用fromobject。其他數值類型轉換為int32時,ctype也會調用類型本身的轉換方法實施轉換。directcast運算符則很簡單,直接將對象拆箱成所需類型。
所以在用于值類型時,ctype沒有directcast快速但可以支持更多的轉換。在c#中,類型轉換則為(type)運算符和as運算符。(type)運算符的工作方式與vb的directcast很相似,也是直接拆箱或castclass的,但是如果遇到支持的類型轉換(如long到int),(type)運算符也會調用相應的轉換方法,但不支持從string到int的轉換。c#另一個運算符as則更加智能,它只要判斷對象的運行實例能否轉成目標類型,然后就可以省略castclass指令,直接按已知類型進行操作,而且編譯器還可以自動對as進行優化,比如節省一個對象引用等。所以在將object轉換成所需的類型時,as是最佳選擇。
由于as有很多優點,visual basic 2005將這一特性吸收了過來,用trycast運算符就可以獲得和as一樣的效果,而且語法與directcast或ctype一樣。
(五)實現接口
vb.net采用的實現接口的語法是vb5發明的implements,這個實現接口的語法在當今主流語言中獨一無二。比如我有兩個接口:
interface interface1
sub test()
end interface
interface interface2
sub test()
end interface
這兩個接口有一個完全一樣的成員test。假設我需要用一個類同時實現兩個接口會怎么樣呢?先想想看,如果是java,jscrip.net這樣的語言就只能用一個test函數實現兩個接口的test成員。假如兩個test只是偶然重名,其內容必須要分別實現怎么辦,于是一些解決接口重名的設計出現了……。在vb中,獨特的implements語句可以讓你想怎么實現接口就怎么實現,比如下面的類implementation用兩個名字根本不一樣的方法實現了兩個接口。
public class implementation
implements interface1, interface2
public sub hello() implements interface1.test
end sub
private sub hi() implements interface2.test
end sub
end class
也就是說,vb允許用任意名字的函數實現接口中的成員,而且訪問器可以是任意的,比如想用public還是private都可以。
c#在處理重名成員上提供了顯式實現(explicit implementation)的語法,其實現上述兩個接口的語法為
public class class1 : interface1, interface2
{
public class1()
{
}
void interface1.test()
{
}
void interface2.test()
{
}
}
注意這里,c#只能用接口名.成員名的名字來命名實現方法,而且訪問器只能是private,不能公開顯式實現的方法。
在考察了il以后,我發現.net支持隱式實現和顯式實現兩種方式。其中隱式實現只要在類里面放一個與接口成員方法名字一樣的方法即可——這一種vb不支持。而顯式實現則在方法的描述信息里加入:
.override testapp.interface1::test
無論是c#的顯式實現還是vb的implements語句都是這樣的原理。也就是說.net提供了換名實現接口成員的功能,但是只有vb將這個自由讓給了用戶,而其他語言還是采用了經典的語法。
(六)默認屬性和屬性參數
在原先的vb6里,有一項奇特的功能——默認屬性。在vb6中,對象的名稱可以直接表示該對象的默認屬性。比如textbox的默認屬性是text,所以下面的代碼
text1.text = "hello"
就可以簡化為
text1 = "hello"
這種簡化給vb帶來了很多麻煩,賦值運算就需要兩個關鍵字——let和set,結果屬性過程也需要let和set兩種。而且這種特征在后期綁定的時候仍能工作。到了vb.net,這項功能被大大限制了,現在只有帶參數的屬性才可以作為默認屬性。如
list1.item(0) = "hello"
可以簡化為
list1(0) = "hello"
這種語法讓有默認屬性的對象看起來像是一個數組。那么vb怎么判斷一個屬性是否是默認屬性呢?看下列代碼
public class proptest
public property p1(byval index as integer) as string
get
end get
set(byval value as string)
end set
end property
default public property p2(byval index as integer) as string
get
end get
set(byval value as string)
end set
end property
end class
p1和p2兩個屬性基本上完全相同,唯一的不同是p2帶有一個default修飾符。反匯編這個類以后,可以發現兩個屬性完全相同,沒有任何差異。但是proptest類卻被增加了一個自定義元屬性system.reflection.defaultmemberattribute。這個元屬性指定的成員是invokemember所使用默認類型,也就是說后期綁定也可以使用默認屬性。可是我試驗將defaultmember元屬性手工添加到類型上卻不能達到讓某屬性成為默認屬性的功能。看來這項功能又是vb的一項“語法甜頭”。但是,vb或c#的編譯器對別人生成的類的默認屬性應該只能通過defaultmemberattribute來判斷,所以我將一個vb類只用defaultmemberattribute指定一個默認方法,不使用default,然后將它編譯以后給c#用,果然,c#將它識別為一個索引器(indexer)!
既然說到了c#的索引器,我們就順便來研究一下vb和c#屬性方面的不同。剛才的實驗結果是vb的默認屬性在c#中就是索引器。但是vb仍然可以用屬性的語法來訪問默認屬性,而c#只能用數組的語法訪問索引器。更特別的是,vb可以創建不是默認屬性,但是帶有參數的屬性,如上面例子里的p1,而c#則不支持帶參數的屬性,如果將vb編寫的,含有帶參數屬性的類給c#用,c#會提示“屬性不受該語言支持,請用get_xxx和set_xxx的語法訪問”。也就是說,帶參數的屬性是clr的一項功能,但不符合cls(通用語言規范),因此就會出現跨語言的障礙。這也更加深了我們對cls的認識——如果你希望讓你的代碼跨語言工作,請一定要注意符合cls。
(七)可選參數和按名傳遞
vb從4.0開始支持“可選參數”這一特性。就是說,函數或子程序的參數有些是可選的,調用的時候可以不輸入。其實vb從1.0開始就有一些函數帶有可選參數,只不過到了4.0才讓用戶自己開發這樣的過程。在vb4里,可選參數可以不帶默認值,而在vb.net里,如果使用可選參數,則必須帶有默認值。如
public sub testoptional(optional i as integer = 1)
end sub
調用的時候,既可以寫成testoptional(2),也可以寫成testoptional(),這種情況參數i自動等于1。如果過程有不止一個可選參數,則vb還提供一種簡化操作的方法——按名傳遞參數。比如過程
public sub testoptional(optional i as int32 = 1, optional j as int32 = 1, optional k as int32 = 1)
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#中經常讓開發者傳參數傳到吐血。
(八)on error語句和when語句
本次討論的是異常處理語句。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對象記錄異常。看來使用err也是語法甜頭,性能苦頭,憑空添加了這兩句(幸好都不太復雜)。
接下來我編寫了一個與此功能類似的函數,用的是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。我查詢了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和v_3是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后程序結構變得異常混亂,這在產生負面作用的時候,是不是能夠變相起到保護我們代碼的作用呢?
(九)實例訪問共享成員
大家都知道靜態成員在vb中叫做共享成員,雖然剛接受起來有點別扭,但“共享成員”的確是名副其實的:
public class class1
public shared i as integer
'other none-shared members
end class
不但像在c#中那樣,可以用class1.i訪問共享成員i,還可以用實例變量來訪問:
dim c1 as new class1
c1.i = 100
就像i是c1的成員一樣!當然只有一個i,任何實例去修改i的值都將導致所有i的值改變(因為其實只有一個)。甚至me和myclass也可以訪問共享成員。
me.i = 100
myclass.i = 100
這在c#中是不可能做到的,一個純正的c#程序員看到這些代碼一定會覺得匪夷所思。為了揭示它的工作原理,我們可以做下列實驗:
dim c1 as class1
c1.i = 100
注意,這里的c1為nothing!,即使是nothing的變量也可以訪問共享成員,而且不會出錯。接下來我們實驗更極端的情況:
dim o as object = new class1
o.i = 100
結果——失敗,不能通過后期綁定訪問共享成員。現在結果已經很明顯,只有在vb明確了解對象類型的情況下,才能使用實例訪問共享成員,vb會自動判斷類型,然后將所有對共享成員訪問的語句改寫成
class1.i = 100
這樣的語法。delphi也支持這一有趣的特征,而且李維在《inside vcl》中將此說成delphi.net相對于.net的擴展之一。
新聞熱點
疑難解答
圖片精選