假设某个DLL里有这么一个类:
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也有同样的表现,同样需要注意这一点。