詳細介紹C#編譯器
本文講述C#編譯器的一些問題,目的是防止錯誤使用本地變量。但是據我研究,這里面有“Bug”(注意雙引號),那么會有什么有趣的“Bug”呢?首先大家看下一個簡單的例子:
- publicvoidTest()
- {
- {
- inta;
- }
- {
- inta;
- }
- }
在這個Test函數里面有兩對打括號,標明兩個互不相屬的子范圍。這里大家也許看的非常不習慣,因為沒有人光禿禿的寫這么兩對大括號的。我跟大家說:沒關系,編譯器承認光禿禿的大括號的,這個也是標準C里面的規范之一,作用就是把大括號里面的所有東西認為是“一句話”,準確點講是邏輯語句,同時內部是一個范圍,約束范圍內的本地變量不會往外傳播。如果大家實在看不習慣了,可以自行加上諸如while(true)之類的前綴,就習慣了。
那么這段代碼有什么Bug呢?沒有,確實沒有Bug,編譯順利通過。當然,顯示了兩個Warning,說a沒有被用到,無傷大雅。我們首先來分析一下,編譯器怎么給把這個給弄通過的呢?我們用Reflector來看一下(當然,因為沒有切實的代碼,所以只能夠看IL,而不能夠看C#):
- publichidebysiginstancevoidTest()cilmanaged
- {
- //CodeSize:2byte(s)
- .maxstack0
- .locals(
- int32num1,
- int32num2)
- L_0000:nop
- L_0001:ret
- }
哦!原來編譯器把內部的變量改名字了!或者說編譯器把他們當作完全不同的兩個變量來對待。同時我們在這里也可以看出來,實際上在IL里面時不區分范圍的,只有本地變量著一個簡單的概念。無論你在哪個范圍,在什么時候開始聲明,實際上都是在函數的一開始用一個.locals這樣的偽語句來聲明的。這么做是簡單省事的辦法,因為如果在用戶源代碼實際聲明的地方才在棧上面開辟空間,那么最后函數退出的時候就不知道該釋放多少棧空間了。當然這不是不可以解決的,但是那樣的話增加了不必要的復雜度。如果我來設計.NET Framework,我也會通過高級語言的編譯器來約束范圍問題,而不是擺到IL里面去解決。(畢竟IL里面沒有這樣的功能不影響我們寫程序)稍微引申一下,我們就知道,一個函數里面有多少個本地變量,取決于整個函數內部聲明了多少本地變量,而與變量所在范圍無關。在IL這一層里面暫時我們沒有看到這樣的優化工作,我們可以看看這樣的代碼最后被編譯器編譯成什么了(用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 反編譯結果:
- 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則是分別的兩個a。事實上這兩個a互相之間是沒有任何沖突的,也就是說是完全可以重用的,編譯原理里面也有一個變量重用的優化,但是這里看不到有這樣的優化,我覺得比較吃驚。雖然說這也可以算是一種Bug(嚴格說來是也不是),但是我要說的“Bug”不是這個。
分析完上面這些基本知識,我就來勁了:
- publicvoidTest()
- {
- {
- inta;
- }
- {
- inta;
- }
- inta;
- }
看,編譯出來之后卻出現了錯誤:
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
哦,原來這個跟聲明的順序還沒有關系,只要子范圍里面有a了,那就不能夠再定義這個變量了。這個難道跟IL里面所有變量都在函數開始部分聲明有關系?看起來好像是這么一回事,但是實際上不是,因為C#編譯器完全可以像前面那樣,把最后一個a當作另外一個變量。這到底是怎么回事呢?我們需要作本次探索的最后一個實驗:
- publicvoidTest()
- {
- a=2;
- {
- inta;
- }
- {
- inta;
- }
- inta;
- }
這下可好,除了剛才那個錯誤之外,還多出來另外一個:
error CS0103: The name 'a' does not exist in the class or namespace 'ConsoleApplication1.Class2'
也就是說,編譯器根本就沒有把后面那個a當作從函數一開始的地方定義來看待。但是這兩個錯誤合起來反而容易讓我們產生這樣的錯覺和悖論:
因為前面兩個a在范圍外面就應該消失其影響力,那就不應該跟后面的a產生沖突。但現在既然你說了,第三個a的定義根前面那兩個a的其中某一個定義相沖突了,那我就只能夠認為后面這個a實際上在前兩個a被定義出來之前就已經存在了,因為后面這個a處于外層范圍,它不會在內層范圍失去作用之前失效,這樣還能夠解釋得通。可是這么解釋我只能夠認為外層的a應該在函數一開始的地方就生效了(老式的C編譯器有一段時間確實是這樣的),可是偏偏還來一個CS0103錯誤!解釋不通,有“Bug”!
最后我來修正這個我一開始提出的說法,其實并沒有Bug。得出有Bug的結論,那是從純粹的語法角度看這個問題的,我也覺得應該容許在第三個a的定義出現,頂多只給出一個Warning。但是微軟卻給出了一個錯誤,我想這是從避免不必要的Bug的角度考慮,盡量保護開發人員避免不必要的煩惱。開發人員確實很有可能在定義了第三個a的時候忘記第一二個a已經失效了,同時也忘記了自己定義過第三個a,還以為自己用的是第一個或者第二個a里面的數據。不過對于這種解釋,我還是有意見的:既然約束已經縮窄到這個地步了,那為什么要允許第二個a的定義呢?如果開發人員會忘記自己定義過第三個a,有什么理由認為不會把第二個a的定義給忘記了,以為自己在用第一個a呢?
本來上面所寫的那些統統都是垃圾代碼,我認為,在一個函數內部根本就不應該有相同的變量來迷惑自己。C#編譯器在這些問題方面確實有相當嚴謹的考慮,不過我還是覺得有一些“悖論”存在,如果能夠更加嚴謹,我認為只會更好。