我們先來看看展波舉的例子:
http://blog.joycode.com/zhanbos/archive/2004/10/26/36605.aspx
在這個例子裡面我們看到,編譯器會檢查scope問題,目的是防止錯誤使用本地變數。但是據我研究,這裡面有“Bug”(注意雙引號),那麼會有什麼有趣的“Bug”呢?我來給大家一個簡單的例子:
public void Test()
{
{
int a;
}
{
int a;
}
}
在這個Test函數裡面有兩對打括弧,標明兩個互不相屬的子範圍。這裡大家也許看的非常不習慣,因為沒有人光禿禿的寫這麼兩對大括弧的。我跟大家說:沒關係,編譯器承認光禿禿的大括弧的,這個也是標準C裡面的規範之一,作用就是把大括弧裡面的所有東西認為是“一句話”,準確點講是邏輯語句,同時內部是一個範圍,約束範圍內的本地變數不會往外傳播。如果大家實在看不習慣了,可以自行加上諸如while(true)之類的首碼,就習慣了。
那麼這段代碼有什麼Bug呢?沒有,確實沒有Bug,編譯順利通過。當然,顯示了兩個Warning,說a沒有被用到,無傷大雅。我們首先來分析一下,編譯器怎麼給把這個給弄通過的呢?我們用Reflector來看一下(當然,因為沒有切實的代碼,所以只能夠看IL,而不能夠看C#):.method public hidebysig instance void Test() cil managed
{
// Code Size: 2 byte(s)
.maxstack 0
.locals (
int32 num1,
int32 num2)
L_0000: nop
L_0001: ret
}
哦!原來編譯器把內部的變數改名字了!或者說編譯器把他們當作完全不同的兩個變數來對待。同時我們在這裡也可以看出來,實際上在IL裡面時不區分範圍的,只有本地變數著一個簡單的概念。無論你在哪個範圍,在什麼時候開始聲明,實際上都是在函數的一開始用一個.locals這樣的偽語句來聲明的。這麼做是簡單省事的辦法,因為如果在使用者原始碼實際聲明的地方才在棧上面開闢空間,那麼最後函數退出的時候就不知道該釋放多少棧空間了。當然這不是不可以解決的,但是那樣的話增加了不必要的複雜度。如果我來設計.NET Framework,我也會通過進階語言的編譯器來約束範圍問題,而不是擺到IL裡面去解決。(畢竟IL裡面沒有這樣的功能不影響我們寫程式)稍微引申一下,我們就知道,一個函數裡面有多少個本地變數,取決於整個函數內部聲明了多少本地變數,而與變數所在範圍無關。在IL這一層裡面暫時我們沒有看到這樣的最佳化工作,我們可以看看這樣的代碼最後被編譯器編譯成什麼了(用Release模式編譯): public int Test()
{
int b;
b = new Random().Next(5);
if (b < 5)
{
int a = new Random().Next(5);
Console.WriteLine(a);
b = a;
}
else
{
int a = new Random().Next(10);
Console.WriteLine(a);
b = a;
}
return b;
}
Reflector 反編譯結果:public int Test()
{
int num1 = new Random().Next(5);
if (num1 < 5)
{
int num2 = new Random().Next(5);
Console.WriteLine(num2);
return num2;
}
int num3 = new Random().Next(10);
Console.WriteLine(num3);
return num3;
}
大家可以看到num1是b,num2和num3則是分別的兩個a。事實上這兩個a互相之間是沒有任何衝突的,也就是說是完全可以重用的,編譯原理裡面也有一個變數重用的最佳化,但是這裡看不到有這樣的最佳化,我覺得比較吃驚。雖然說這也可以算是一種Bug(嚴格說來是也不是),但是我要說的“Bug”不是這個。
分析完上面這些基本知識,我就來勁了: public void Test()
{
{
int a;
}
{
int a;
}
int a;
}
看,編譯出來之後卻出現了錯誤:
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當作另外一個變數。這到底是怎麼回事呢?我們需要作本次探索的最後一個實驗: public void Test()
{
a = 2;
{
int a;
}
{
int a;
}
int a;
}
這下可好,除了剛才那個錯誤之外,還多出來另外一個:
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#的編譯器在這些問題方面確實有相當嚴謹的考慮,不過我還是覺得有一些“悖論”存在,如果能夠更加嚴謹,我認為只會更好。