const与readonly深度分析(.NET)

时间:2022-08-13 18:42:45

前言

很多.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 = ;
public const Int32 num_Y = ;
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
.locals init ([] int32 x,
[] int32 y,
[] int32 result)
IL_0000: nop
.try
{
.try
{
IL_0001: nop
IL_0002: ldc.i4.s 10
IL_0004: stloc.
IL_0005: ldc.i4.s 20
IL_0007: stloc.
IL_0008: ldloc.
IL_0009: ldloc.
IL_000a: mul
IL_000b: stloc.
IL_000c: ldstr

"Calculate right!result: "

      IL_0011:  ldloc.
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 = ;
public static readonly Int32 num_YR = ;
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();
}
}
}
}

先看一下结果:

const与readonly深度分析(.NET)

把num_XR改成30,重新编译ClassTemp项目,运行程序结果如下:

const与readonly深度分析(.NET)

结果确实发生了改变,我们下面说一下readonly这种字段是怎么加载的,在说明之前,贴出IL代码

.method private hidebysig static void  Main(string[] args) cil managed
{
.entrypoint
// 代码大小 78 (0x4e)
.maxstack
.locals init ([] int32 x,
[] int32 y,
[] int32 result)
IL_0000: nop
.try
{
.try
{
IL_0001: nop
IL_0002: ldsfld int32 [ClassTest]ClassTest.ClassTemp::num_XR
IL_0007: stloc.
IL_0008: ldsfld int32 [ClassTest]ClassTest.ClassTemp::num_YR
IL_000d: stloc.
IL_000e: ldloc.
IL_000f: ldloc.
IL_0010: mul
IL_0011: stloc.
IL_0012: ldsfld string [ClassTest]ClassTest.ClassTemp::rightInfoR
IL_0017: ldstr " result: "
IL_001c: ldloc.
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中,我们画一个图可能看的更清晰一些:

const与readonly深度分析(.NET)

这样每次启动程序的时候ClassTemp类中的字段值都会重新赋值,所以即使不重新编译ConstantTest项目,依然能得到正确结果。

另外,readonly字段可以是任何类型而不拘泥于基元类型。

注意:readonly字段只能在一个构造方法中写入,另外,利用反射页可以修改字段的值。

如果您有什么意见或者文章对您有任何的帮助,请在评论区告诉我!!!