1 // Lib.dll 2 public class Lib 3 { 4 public const string VERSION = "1.0"; 5 public static void PrintVersion(string version = "1.0") 6 { 7 Console.WriteLine(version); 8 } 9 }
然後有這麼個調用方:
// Program.exe class Program { static void Main() { Console.WriteLine(Lib.VERSION); Lib.PrintVersion(); Console.Read(); } }
Program.exe的運行結果是顯而易見的: 1.0 1.0 過了一段時間,Lib更新版本了:
// Lib.dll public class Lib { public const string VERSION = "2.0"; public static void PrintVersion(string version = "2.0") { Console.WriteLine(version); } }
把Lib.dll重新編譯確保Program.exe引用了最新的DLL,然後再運行Program.exe,結果: 1.0 1.0 重新編譯Program.exe以後再次運行: 2.0 2.0 發現問題了吧,調用方必須重新編譯才能確保可選參數和常量的值是最新的。 原因是這樣的:
1 .method private hidebysig static 2 void Main () cil managed 3 { 4 // Method begins at RVA 0x2050 5 // Code size 30 (0x1e) 6 .maxstack 8 7 .entrypoint 8 9 IL_0000: ldstr "1.0" 10 IL_0005: call void [mscorlib]System.Console::WriteLine(string) 11 IL_000a: ldstr "1.0" 12 IL_000f: call void CsConsole.Program/Lib::PrintVersion(string) 13 IL_0014: call int32 [mscorlib]System.Console::Read() 14 IL_0019: pop 15 IL_001a: ret 16 }
這是第一次編譯之後Program.Main方法的IL,Lib.VERSION完全被編譯成了字面量"1.0"(第10行),而第11行的"1.0"是來自(第一次編譯時的)Lib.dll的元數據。 顯然,不管怎麼更新Lib的代碼,只要不重新編譯Program,這裡的兩個值就沒辦法得到更新,而實際的生產環境中經常沒法保證調用方會被重新編譯。 至於為什麼要這樣編譯,我是這麼理解的。 常量的值是在常量池裡待著的,通過類的成員去取值顯然是不如直接從常量池取來得方便快捷。 而可選參數的實現方式,則是在編譯時提前進行判斷與賦值,節省了運行時的時間。 解決方法: 對於可選參數,《CLR via C#》建議的方法是這樣的:
1 public static void PrintVersion(string version = null) 2 { 3 if (version == null) 4 { 5 version = "1.0"; 6 } 7 Console.WriteLine(version); 8 }
很顯然這段代碼的行為與之前相比是有變化的,但是大多數情況下確實可以解決值更新的問題,但是也帶來了運行時效率的問題。 對於常量,從我使用的示例就可以看出來,版本號這類的值不應該定義為常量,使用readonly可以達到目的。 而對於真正的常量,則不應該輕易地變更它的值。 最後嘛,Java雖然沒有const,但是static final也有同樣的表現,同樣需要注意這一點。