《CLR Via C# 第3版》笔记之(四) - 类中字段的默认赋值
在C#中,除了可以在类的构造函数中初始化私有字段的值,还可以在私有字段定义的地方进行初始化(即默认赋值)。下面讨论默认赋值和在构造函数中赋值的区别,以便更好的在代码中使用这两种赋值。
主要内容:
- 对代码生成的影响
- 对代码执行的影响
1. 对代码生成的影响
首先构造两个Class,其中ClassA使用默认赋值的方式,ClassB使用构造函数赋值的方式。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class ClassA { private Int32 a = 123; private String b = "abc" ; private Object c = new object (); public ClassA() { } public ClassA( int aa) { a = aa; } } public class ClassB { private Int32 a; private String b; private Object c; public ClassB() { a = 123; b = "abc" ; c = new object (); } public ClassB( int aa) { a = aa; } } |
编译成dll后,再用ILSpy查看其IL代码,发现ClassA生成的代码比较多。即每个构造函数开始执行处,都会将字段的默认赋值生成IL代码插入其中。
ClassA IL代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | .class public auto ansi beforefieldinit ClassA extends object { // Fields .field private int32 a .field private string b .field private object c // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2073 // Code size 40 (0x28) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 123 IL_0003: stfld int32 class cnblog_bowen.ClassA::a IL_0008: ldarg.0 IL_0009: ldstr "abc" IL_000e: stfld string class cnblog_bowen.ClassA::b IL_0013: ldarg.0 IL_0014: newobj instance void object::.ctor() IL_0019: stfld object class cnblog_bowen.ClassA::c IL_001e: ldarg.0 IL_001f: call instance void object::.ctor() IL_0024: nop IL_0025: nop IL_0026: nop IL_0027: ret } // End of method ClassA..ctor .method public hidebysig specialname rtspecialname instance void .ctor ( int32 aa ) cil managed { // Method begins at RVA 0x209c // Code size 47 (0x2f) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 123 IL_0003: stfld int32 class cnblog_bowen.ClassA::a IL_0008: ldarg.0 IL_0009: ldstr "abc" IL_000e: stfld string class cnblog_bowen.ClassA::b IL_0013: ldarg.0 IL_0014: newobj instance void object::.ctor() IL_0019: stfld object class cnblog_bowen.ClassA::c IL_001e: ldarg.0 IL_001f: call instance void object::.ctor() IL_0024: nop IL_0025: nop IL_0026: ldarg.0 IL_0027: ldarg.1 IL_0028: stfld int32 class cnblog_bowen.ClassA::a IL_002d: nop IL_002e: ret } // End of method ClassA..ctor } // End of class cnblog_bowen.ClassA |
ClassB IL代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | .class public auto ansi beforefieldinit ClassB extends object { // Fields .field private int32 a .field private string b .field private object c // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20cc // Code size 40 (0x28) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void object::.ctor() IL_0006: nop IL_0007: nop IL_0008: ldarg.0 IL_0009: ldc.i4.s 123 IL_000b: stfld int32 class cnblog_bowen.ClassB::a IL_0010: ldarg.0 IL_0011: ldstr "abc" IL_0016: stfld string class cnblog_bowen.ClassB::b IL_001b: ldarg.0 IL_001c: newobj instance void object::.ctor() IL_0021: stfld object class cnblog_bowen.ClassB::c IL_0026: nop IL_0027: ret } // End of method ClassB..ctor .method public hidebysig specialname rtspecialname instance void .ctor ( int32 aa ) cil managed { // Method begins at RVA 0x20f5 // Code size 17 (0x11) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void object::.ctor() IL_0006: nop IL_0007: nop IL_0008: ldarg.0 IL_0009: ldarg.1 IL_000a: stfld int32 class cnblog_bowen.ClassB::a IL_000f: nop IL_0010: ret } // End of method ClassB..ctor } // End of class cnblog_bowen.ClassB |
由此看出,虽然默认赋值的方法比较直观和方便,但是从生成的代码来看,默认赋值的方法会导致代码膨胀,所以不应在以下场合使用:
1)字段比较多的Class
2)构造函数有多个重载版本
2. 对代码执行的影响
通过上面的IL代码,我们发现默认赋值除了会导致代码膨胀,赋值的时机也和在构造函数中对字段的赋值不一样。
我们知道,类的构造函数在执行之前,都会调用其基类的构造函数,由于所以类都默认继承System.Object,所以上面的ClassA和ClassB虽然没有指定基类,
但都继承于System.Object,所以都会调用System.Object的构造函数。
调用System.Object的构造函数的IL代码即为:call instance void object::.ctor()从上面的IL代码中,我们发现:
1)默认赋值方式是在调用System.Object的构造函数前给字段赋值的
2)构造函数中赋值方式是在调用System.Object的构造函数后给字段赋值的 这里的差别虽然很小,但是有时却会导致代码产生不同的结果,从而带来潜在的bug。
这两种赋值方式在什么情况下会导致执行结果不同呢?
根据其赋值时机的不同,我们可以推断在如下情况下,两种赋值方式的执行结果不同。
基类中调用虚方法并且如果子类覆盖(override)了此虚方法,那么此虚方法中的字段就有可能已经初始化或者未初始化。
第一种情况 (默认赋值的方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | class Test { static void Main() { // 第一步:调用SubClass的构造函数 SubClass sub = new SubClass(); Console.ReadKey( true ); } } public class BaseClass { // 第四步:调用基类构造函数,其中虚方法Print已经被子类覆盖 public BaseClass() { Print(); } public virtual void Print() { Console.WriteLine( "Base class initilized!" ); } } public class SubClass : BaseClass { // 第三步:对sub_a,sub_b,obj进行赋值,然后再调用基类构造函数 private Int32 sub_a = 123; private String sub_b = "abc" ; private Object obj = new object (); // 第二步:由于是默认赋值的方式,所以先将sub_a,sub_b,obj赋值后再调用基类构造函数 public SubClass() { } // 第五步:调用被覆盖的Print方法,由于obj已被赋值,所以进入else分支去执行 public override void Print() { if ( null == obj) Console.WriteLine( "Sub class is uninitilize!" ); else { Console.WriteLine( "a= " + sub_a); Console.WriteLine( "b= " + sub_b); Console.WriteLine( "Sub class was initilized!" ); } } } |
执行结果如下,执行过程可以参见上面代码中的注释

第二种情况(构造函数中对字段赋值的方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class Test { static void Main() { // 第一步:调用SubClass的构造函数 SubClass sub = new SubClass(); Console.ReadKey( true ); } } public class BaseClass { // 第三步:调用基类构造函数,其中虚方法Print已经被子类覆盖 public BaseClass() { Print(); } public virtual void Print() { Console.WriteLine( "Base class initilized!" ); } } public class SubClass : BaseClass { private Int32 sub_a; private String sub_b; private Object obj; // 第二步:由于是在构造函数对字段辅助的方式,所以先默认调用基类构造函数 public SubClass() { // 第五步:基类构造函数执行完后,进入下面的赋值 sub_a = 123; sub_b = "abc" ; obj = new object (); } // 第四步:调用被覆盖的Print方法,由于obj还未被赋值,所以进入if分支去执行 public override void Print() { if ( null == obj) Console.WriteLine( "Sub class is uninitilize!" ); else { Console.WriteLine( "a= " + sub_a); Console.WriteLine( "b= " + sub_b); Console.WriteLine( "Sub class was initilized!" ); } } } |
执行结果如下,执行过程可以参见上面代码中的注释

小小的赋值,也会导致意外的bug。所以我们在使用时默认赋值时一定要对其赋值的时机做到心中有数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
2010-06-17 Openlayers 源码分析(版本2.9.1)—Openlayers