前言
很多.NET的初学者对const和readonly的使用很模糊,本文就const和readonly做一下深度分析,包括:
1. const数据类型的优势
2. const数据类型的劣势
3. readonly类型的优势
4. readonly类型的劣势
5. 编译器对const数据类型如何做处理
6. 编译器对readonly数据类型如何做处理
const常量
编译器在编译const常量的时候会直接把常量值嵌入到IL代码中,这是const常量的优势,即在程序运行过程中不需要通过类型对象或者实例对象检索字段的值,这样多多少少能提高性能。为了更直观的看一下const常量在IL代码下的存在状态,我们举一个例子。
注释:IL代码是编译器编译完成生成的代码,我们所见到的托管dll文件和托管exe文件内部其实都是这种代码。
const常量应用实例
1. 在VS中新建一个解决方案,命名为ConstantTest,在解决方案中添加两个项目,分别为ClassTest(类库项目)和ConstantTest(控制台项目)
2. 在ClassTest项目中添加类ClassTemp,ClassTemp的具体代码如下:
using System; namespace ClassTest { public class ClassTemp { public const Int32 num_X = 20; public const Int32 num_Y = 20; public const String rightInfo = "Calculate right!"; public const String errorInfo = "Calculate error!"; } }
3. 在项目ConstantTest项目中引用ClassTest.dll,在Program.cs中添加如下代码:
using System; using ClassTest; namespace ConstantTest { class Program { static void Main(string[] args) { try { Int32 x = ClassTemp.num_X; Int32 y = ClassTemp.num_Y; Int32 result = x * y; Console.WriteLine(ClassTemp.rightInfo + " result: " + result); } catch (Exception) { Console.WriteLine(ClassTemp.errorInfo); } finally { Console.ReadKey(); } } } }
4. 然后编译项目ConstantTest,用ildasm查看生成的ConstantTest.exe文件,结果如下:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 67 (0x43) .maxstack 2 .locals init ([0] int32 x, [1] int32 y, [2] int32 result) IL_0000: nop .try { .try { IL_0001: nop IL_0002: ldc.i4.s 10 IL_0004: stloc.0 IL_0005: ldc.i4.s 20 IL_0007: stloc.1 IL_0008: ldloc.0 IL_0009: ldloc.1 IL_000a: mul IL_000b: stloc.2 IL_000c: ldstr"Calculate right!result: "
IL_0011: ldloc.2 IL_0012: box [mscorlib]System.Int32 IL_0017: call string [mscorlib]System.String::Concat(object, object) IL_001c: call void [mscorlib]System.Console::WriteLine(string) IL_0021: nop IL_0022: nop IL_0023: leave.s IL_0035 } // end .try catch [mscorlib]System.Exception { IL_0025: pop IL_0026: nop IL_0027: ldstr"Calculate error!"
IL_002c: call void [mscorlib]System.Console::WriteLine(string) IL_0031: nop IL_0032: nop IL_0033: leave.s IL_0035 } // end handler IL_0035: nop IL_0036: leave.s IL_0041 } // end .try finally { IL_0038: nop IL_0039: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_003e: pop IL_003f: nop IL_0040: endfinally } // end handler IL_0041: nop IL_0042: ret } // end of method Program::Main
不用去理解上面IL代码各行的意思,我们只需要确定我们在代码中使用的常量值在IL代码中确实是以
10 20 Calculate right! Calculate error!
出现的,这也说明我们开始做的结论是正确的。
const常量的问题
但是const常量的这种性质也带来了缺陷,就是针对于版本控制出现的问题,就拿上面的列子来说,如果上面的两个项目分别由两个不同的人甚至公司来完成的(公司A和B),现在公司A要对ClassTemp.dll进行升级,比如把里面的num_X改成30,如果我们把升级后的ClassTemp.dll放到ConstantTest的项目中,ConstantTest的执行结果是不会变的(还是400),因为ConstantTest.exe的IL代码之前已经确定了,除非我们重现编译ConstantTest项目,如果对于项目特多的情况这样显然不方便。
因为常量的值是由编译器来识别的,所以常量的类型只能是基元类型,这是const常量的另一个劣势
readonly字段的优势
相比const常量来说,readonly字段在版本控制方面就好了很多,我们还是拿上面的列子来说,不过相应的数据得改一下,直接把修改后的代码贴出来:
ClassTemp.cs
using System; namespace ClassTest { public class ClassTemp { public static readonly Int32 num_XR = 20; public static readonly Int32 num_YR = 20; public static readonly String rightInfoR = "Calculate right!"; public static readonly String errorInfoR = "Calculate error!"; } }
ConstantTest—>Program.cs
using System; using ClassTest; namespace ConstantTest { class Program { static void Main(string[] args) { try { Int32 x = ClassTemp.num_XR; Int32 y = ClassTemp.num_YR; Int32 result = x * y; Console.WriteLine(ClassTemp.rightInfoR + " result: " + result); } catch (Exception) { Console.WriteLine(ClassTemp.errorInfoR); } finally { Console.ReadKey(); } } } }
先看一下结果:
把num_XR改成30,重新编译ClassTemp项目,运行程序结果如下:
结果确实发生了改变,我们下面说一下readonly这种字段是怎么加载的,在说明之前,贴出IL代码
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 78 (0x4e) .maxstack 3 .locals init ([0] int32 x, [1] int32 y, [2] int32 result) IL_0000: nop .try { .try { IL_0001: nop IL_0002: ldsfld int32 [ClassTest]ClassTest.ClassTemp::num_XR IL_0007: stloc.0 IL_0008: ldsfld int32 [ClassTest]ClassTest.ClassTemp::num_YR IL_000d: stloc.1 IL_000e: ldloc.0 IL_000f: ldloc.1 IL_0010: mul IL_0011: stloc.2 IL_0012: ldsfld string [ClassTest]ClassTest.ClassTemp::rightInfoR IL_0017: ldstr " result: " IL_001c: ldloc.2 IL_001d: box [mscorlib]System.Int32 IL_0022: call string [mscorlib]System.String::Concat(object, object, object) IL_0027: call void [mscorlib]System.Console::WriteLine(string) IL_002c: nop IL_002d: nop IL_002e: leave.s IL_0040 } // end .try catch [mscorlib]System.Exception { IL_0030: pop IL_0031: nop IL_0032: ldsfld string [ClassTest]ClassTest.ClassTemp::errorInfoR IL_0037: call void [mscorlib]System.Console::WriteLine(string) IL_003c: nop IL_003d: nop IL_003e: leave.s IL_0040 } // end handler IL_0040: nop IL_0041: leave.s IL_004c } // end .try finally { IL_0043: nop IL_0044: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0049: pop IL_004a: nop IL_004b: endfinally } // end handler IL_004c: nop IL_004d: ret } // end of method Program::Main
CLR加载字段
CLR支持静态字段和非静态字段,我们上面使用的静态字段,静态字段是在类型对象中加载的,类型对象是在类型加载到一个AppDomain时创建的(AppDomain可以暂时理解为程序运行的内存空间),而类型第一次有方法被调用的时候(包含构造方法)会加载到AppDomain中,我们画一个图可能看的更清晰一些:
这样每次启动程序的时候ClassTemp类中的字段值都会重新赋值,所以即使不重新编译ConstantTest项目,依然能得到正确结果。
另外,readonly字段可以是任何类型而不拘泥于基元类型。
注意:readonly字段只能在一个构造方法中写入,另外,利用反射页可以修改字段的值。
如果您有什么意见或者文章对您有任何的帮助,请在评论区告诉我!!!