詳細(xì)介紹C#編譯器
本文講述C#編譯器的一些問(wèn)題,目的是防止錯(cuò)誤使用本地變量。但是據(jù)我研究,這里面有“Bug”(注意雙引號(hào)),那么會(huì)有什么有趣的“Bug”呢?首先大家看下一個(gè)簡(jiǎn)單的例子:
- publicvoidTest()
- {
- {
- inta;
- }
- {
- inta;
- }
- }
在這個(gè)Test函數(shù)里面有兩對(duì)打括號(hào),標(biāo)明兩個(gè)互不相屬的子范圍。這里大家也許看的非常不習(xí)慣,因?yàn)闆](méi)有人光禿禿的寫(xiě)這么兩對(duì)大括號(hào)的。我跟大家說(shuō):沒(méi)關(guān)系,編譯器承認(rèn)光禿禿的大括號(hào)的,這個(gè)也是標(biāo)準(zhǔn)C里面的規(guī)范之一,作用就是把大括號(hào)里面的所有東西認(rèn)為是“一句話”,準(zhǔn)確點(diǎn)講是邏輯語(yǔ)句,同時(shí)內(nèi)部是一個(gè)范圍,約束范圍內(nèi)的本地變量不會(huì)往外傳播。如果大家實(shí)在看不習(xí)慣了,可以自行加上諸如while(true)之類(lèi)的前綴,就習(xí)慣了。
那么這段代碼有什么Bug呢?沒(méi)有,確實(shí)沒(méi)有Bug,編譯順利通過(guò)。當(dāng)然,顯示了兩個(gè)Warning,說(shuō)a沒(méi)有被用到,無(wú)傷大雅。我們首先來(lái)分析一下,編譯器怎么給把這個(gè)給弄通過(guò)的呢?我們用Reflector來(lái)看一下(當(dāng)然,因?yàn)闆](méi)有切實(shí)的代碼,所以只能夠看IL,而不能夠看C#):
- publichidebysiginstancevoidTest()cilmanaged
- {
- //CodeSize:2byte(s)
- .maxstack0
- .locals(
- int32num1,
- int32num2)
- L_0000:nop
- L_0001:ret
- }
哦!原來(lái)編譯器把內(nèi)部的變量改名字了!或者說(shuō)編譯器把他們當(dāng)作完全不同的兩個(gè)變量來(lái)對(duì)待。同時(shí)我們?cè)谶@里也可以看出來(lái),實(shí)際上在IL里面時(shí)不區(qū)分范圍的,只有本地變量著一個(gè)簡(jiǎn)單的概念。無(wú)論你在哪個(gè)范圍,在什么時(shí)候開(kāi)始聲明,實(shí)際上都是在函數(shù)的一開(kāi)始用一個(gè).locals這樣的偽語(yǔ)句來(lái)聲明的。這么做是簡(jiǎn)單省事的辦法,因?yàn)槿绻谟脩?hù)源代碼實(shí)際聲明的地方才在棧上面開(kāi)辟空間,那么最后函數(shù)退出的時(shí)候就不知道該釋放多少??臻g了。當(dāng)然這不是不可以解決的,但是那樣的話增加了不必要的復(fù)雜度。如果我來(lái)設(shè)計(jì).NET Framework,我也會(huì)通過(guò)高級(jí)語(yǔ)言的編譯器來(lái)約束范圍問(wèn)題,而不是擺到IL里面去解決。(畢竟IL里面沒(méi)有這樣的功能不影響我們寫(xiě)程序)稍微引申一下,我們就知道,一個(gè)函數(shù)里面有多少個(gè)本地變量,取決于整個(gè)函數(shù)內(nèi)部聲明了多少本地變量,而與變量所在范圍無(wú)關(guān)。在IL這一層里面暫時(shí)我們沒(méi)有看到這樣的優(yōu)化工作,我們可以看看這樣的代碼最后被編譯器編譯成什么了(用Release模式編譯):
- publicintTest()
- {
- intb;
- b=newRandom().Next(5);
- if(b<5)
- {
- inta=newRandom().Next(5);
- Console.WriteLine(a);
- b=a;
- }
- else
- {
- inta=newRandom().Next(10);
- Console.WriteLine(a);
- b=a;
- }
- returnb;
- }
Reflector 反編譯結(jié)果:
- publicintTest()
- {
- intnum1=newRandom().Next(5);
- if(num1<5)
- {
- intnum2=newRandom().Next(5);
- Console.WriteLine(num2);
- returnnum2;
- }
- intnum3=newRandom().Next(10);
- Console.WriteLine(num3);
- returnnum3;
- }
大家可以看到num1是b,num2和num3則是分別的兩個(gè)a。事實(shí)上這兩個(gè)a互相之間是沒(méi)有任何沖突的,也就是說(shuō)是完全可以重用的,編譯原理里面也有一個(gè)變量重用的優(yōu)化,但是這里看不到有這樣的優(yōu)化,我覺(jué)得比較吃驚。雖然說(shuō)這也可以算是一種Bug(嚴(yán)格說(shuō)來(lái)是也不是),但是我要說(shuō)的“Bug”不是這個(gè)。
分析完上面這些基本知識(shí),我就來(lái)勁了:
- publicvoidTest()
- {
- {
- inta;
- }
- {
- inta;
- }
- inta;
- }
看,編譯出來(lái)之后卻出現(xiàn)了錯(cuò)誤:
error CS0136: A local variable named 'a' cannot be declared in this scope because it would give a different meaning to 'a', which is already used in a 'child' scope to denote something else
哦,原來(lái)這個(gè)跟聲明的順序還沒(méi)有關(guān)系,只要子范圍里面有a了,那就不能夠再定義這個(gè)變量了。這個(gè)難道跟IL里面所有變量都在函數(shù)開(kāi)始部分聲明有關(guān)系?看起來(lái)好像是這么一回事,但是實(shí)際上不是,因?yàn)镃#編譯器完全可以像前面那樣,把最后一個(gè)a當(dāng)作另外一個(gè)變量。這到底是怎么回事呢?我們需要作本次探索的最后一個(gè)實(shí)驗(yàn):
- publicvoidTest()
- {
- a=2;
- {
- inta;
- }
- {
- inta;
- }
- inta;
- }
這下可好,除了剛才那個(gè)錯(cuò)誤之外,還多出來(lái)另外一個(gè):
error CS0103: The name 'a' does not exist in the class or namespace 'ConsoleApplication1.Class2'
也就是說(shuō),編譯器根本就沒(méi)有把后面那個(gè)a當(dāng)作從函數(shù)一開(kāi)始的地方定義來(lái)看待。但是這兩個(gè)錯(cuò)誤合起來(lái)反而容易讓我們產(chǎn)生這樣的錯(cuò)覺(jué)和悖論:
因?yàn)榍懊鎯蓚€(gè)a在范圍外面就應(yīng)該消失其影響力,那就不應(yīng)該跟后面的a產(chǎn)生沖突。但現(xiàn)在既然你說(shuō)了,第三個(gè)a的定義根前面那兩個(gè)a的其中某一個(gè)定義相沖突了,那我就只能夠認(rèn)為后面這個(gè)a實(shí)際上在前兩個(gè)a被定義出來(lái)之前就已經(jīng)存在了,因?yàn)楹竺孢@個(gè)a處于外層范圍,它不會(huì)在內(nèi)層范圍失去作用之前失效,這樣還能夠解釋得通??墒沁@么解釋我只能夠認(rèn)為外層的a應(yīng)該在函數(shù)一開(kāi)始的地方就生效了(老式的C編譯器有一段時(shí)間確實(shí)是這樣的),可是偏偏還來(lái)一個(gè)CS0103錯(cuò)誤!解釋不通,有“Bug”!
最后我來(lái)修正這個(gè)我一開(kāi)始提出的說(shuō)法,其實(shí)并沒(méi)有Bug。得出有Bug的結(jié)論,那是從純粹的語(yǔ)法角度看這個(gè)問(wèn)題的,我也覺(jué)得應(yīng)該容許在第三個(gè)a的定義出現(xiàn),頂多只給出一個(gè)Warning。但是微軟卻給出了一個(gè)錯(cuò)誤,我想這是從避免不必要的Bug的角度考慮,盡量保護(hù)開(kāi)發(fā)人員避免不必要的煩惱。開(kāi)發(fā)人員確實(shí)很有可能在定義了第三個(gè)a的時(shí)候忘記第一二個(gè)a已經(jīng)失效了,同時(shí)也忘記了自己定義過(guò)第三個(gè)a,還以為自己用的是第一個(gè)或者第二個(gè)a里面的數(shù)據(jù)。不過(guò)對(duì)于這種解釋?zhuān)疫€是有意見(jiàn)的:既然約束已經(jīng)縮窄到這個(gè)地步了,那為什么要允許第二個(gè)a的定義呢?如果開(kāi)發(fā)人員會(huì)忘記自己定義過(guò)第三個(gè)a,有什么理由認(rèn)為不會(huì)把第二個(gè)a的定義給忘記了,以為自己在用第一個(gè)a呢?
本來(lái)上面所寫(xiě)的那些統(tǒng)統(tǒng)都是垃圾代碼,我認(rèn)為,在一個(gè)函數(shù)內(nèi)部根本就不應(yīng)該有相同的變量來(lái)迷惑自己。C#編譯器在這些問(wèn)題方面確實(shí)有相當(dāng)嚴(yán)謹(jǐn)?shù)目紤],不過(guò)我還是覺(jué)得有一些“悖論”存在,如果能夠更加嚴(yán)謹(jǐn),我認(rèn)為只會(huì)更好。