這是我們某個組員在程式設計過程中提出的疑問。因為這個編譯錯誤很容易避免,所以我一直也沒有仔細想過這個問題,直到看過他的程式碼後才意識到,這個問題並不是那麼簡單的。
先看看這段程式碼:
程式碼
class Program
{
static void Main(string[] args)
{
byte[] buf = new byte[1024];
T t = new T();
string str = "1234";
int n = 1234;
int? nn = 1234;
DateTime dt = DateTime.Now;
object o = 1234;
Console.WriteLine("finish");
}
}
class T { }
你覺得這段程式碼裡有幾個變數沒有使用過呢?
如果從程式設計師的角度來看,答案應該是所有變數都沒有使用過。但編譯器給的結果卻有點違反直覺:
變數「str」已賦值,但其值從未使用過變數「n」已賦值,但其值從未使用過變數「nn」已賦值,但其值從未使用過
奇怪的地方在於,雖然所有變數都是用同樣的方式聲明,但編譯器只認為其中一部分沒有使用過。這是怎麼回事呢?
我們一個一個來分析。首先看看數組,如果使用預設值的話,編譯器給的資訊就不同了:
byte[] buf1 = null; // 有警告
byte[] buf2 = new byte[1024]; // 沒有警告
這個結果似乎表明,如果參數賦值為null,那麼編譯器並不會真的執行賦值,變數會當作沒有使用過。用IL檢查的結果也可以證明此說法:對第一行,編譯器沒有產生任何對應的語句;對第二條則使用了newattr指令來建立陣列。
對於自訂的類別:
T t1 = null; // 有警告
T t2 = new T(); // 沒有警告
這個結果應當是可以理解的(儘管可以理解,但我認為並不好,理由見後)。雖然我們並沒有呼叫該類的任何方法,但是類的構造函數仍然可能執行某些操作,所以只要創建了一個類,編譯器就會把它當作已經使用過的。
對於基本值類型,其表現和引用類型又有所不同,編譯器並不把初始賦值當作變數的使用:
int n1 = 0; // 有警告
int n2 = 1234; // 有警告
int? n3 = null; // 有警告
int? n4 = 0; // 有警告
int? n5 = 1234; // 有警告
string從實作上來說應當算是引用型,但表現上卻更類似值型,警告資訊也和值型別相同。
對於稍微複雜的值類型,結果有點微妙:
DateTime dt1; // 有警告
DateTime dt2 = new DateTime(); // 有警告
DateTime dt3 = new DateTime(2009,1,1); // 沒有警告
DateTime dt4 = DateTime.Now; // 沒有警告
這個結果有一點是需要注意的。儘管DateTime的預設建構子和帶參構造函數從使用者角度看同樣是建構函數,但在編譯器的角度來看卻是不一樣的。用IL反編譯也可以看出,如果呼叫預設建構函式的話,那麼編譯器呼叫的是initobj指令,而對帶參構造函式呼叫的則是call ctor指令。此外,儘管從程式設計師的角度來看賦值程式碼的格式是完全相同的,但編譯器卻會根據所賦的值不同而採取不同的構造策略,這也是比較違反直覺的。
最後的結論比較遺憾,那就是C#的編譯警告並不足以給予程式設計師足夠的保護,特別是對於陣列:
byte[] buf = new byte[1024];
如果僅構造這樣一個陣列而沒有使用的話,那麼編譯器並不會給予程式設計師任何警告訊息。
另外一個問題也是值得考慮的,聲明一個類別而不使用任何方法,例如僅僅
T t = new T()
這是合理的行為嗎?編譯器應該為此發出警告嗎?
我個人的看法是,從使用的角度來說,這是不合理的,應當盡量避免,編譯器發現此用法的話應該提出警告。如果確實有需要的話,可以透過編譯指令或Attribute的方法來特別聲明來避免警告訊息。然而C#編譯器的行為卻是不發出警告,這一點我是不認同的。當然,我也希望大家提出自己的想法。