如何通过反射获取C#自动属性的默认值(无需实例化类)
获取C#自动属性初始化器的默认值(无需创建实例)
这个问题问得好!要在不创建MyClass实例的前提下拿到自动属性的默认值,得先搞懂编译器在背后的操作:C#6的自动属性初始化器,其实是让编译器悄悄生成一个私有幕后字段,然后把初始化值的赋值逻辑注入到类的实例构造函数里。所以我们不用创建实例,只要通过反射找到这个幕后字段,再解析构造函数的IL代码,就能把默认值提取出来。
具体操作步骤
- 第一步:拿到目标类型的Type对象:用
typeof(MyClass)就能轻松获取。 - 第二步:定位自动属性的幕后字段:自动属性的幕后字段是编译器生成的,命名格式一般是
<属性名>k__BackingField,比如你的Id属性对应的字段就是<Id>k__BackingField。得用BindingFlags.NonPublic | BindingFlags.Instance作为参数去获取,因为它是私有实例字段。 - 第三步:获取类的无参构造函数:自动属性的初始化逻辑会被注入到所有实例构造函数里,优先选无参的来解析最方便。
- 第四步:解析构造函数的IL代码:找到给幕后字段赋值的
stfld指令,然后往前找加载常量值的IL操作码,就能拿到我们要的默认值了。
完整代码示例
using System; using System.Reflection; using System.Reflection.Emit; public class MyClass { public int Id { get; set; } = 5; } public class PropertyDefaultValueReader { public static object GetAutoPropertyDefaultValue(Type type, string propertyName) { // 先找到目标属性 var property = type.GetProperty(propertyName); if (property == null) throw new ArgumentException($"找不到名为 {propertyName} 的属性"); // 拼接幕后字段的名称,找对应的字段 var backingFieldName = $"<{propertyName}>k__BackingField"; var backingField = type.GetField(backingFieldName, BindingFlags.NonPublic | BindingFlags.Instance); if (backingField == null) throw new InvalidOperationException("没法找到这个自动属性对应的幕后字段"); // 获取无参构造函数 var constructor = type.GetConstructor(Type.EmptyTypes); if (constructor == null) throw new InvalidOperationException("这个类没有无参构造函数,得换个构造函数解析"); // 把构造函数的IL代码转成字节数组,开始解析 var ilBytes = constructor.GetMethodBody().GetILAsByteArray(); int offset = 0; var fieldToken = backingField.MetadataToken; while (offset < ilBytes.Length) { byte opCodeByte = ilBytes[offset]; offset++; // 处理扩展操作码(如果有的话) if (opCodeByte == 0xFE) { opCodeByte = ilBytes[offset]; offset++; } var opCode = OpCodes.GetOpCode(opCodeByte); // 找给目标字段赋值的stfld指令 if (opCode == OpCodes.Stfld) { // 读取字段的元数据Token(占4字节) int token = BitConverter.ToInt32(ilBytes, offset); offset += 4; if (token == fieldToken) { // 回退找到加载常量的指令:Stfld是1字节操作码+4字节Token,总共5字节,所以回退5位 int loadOffset = offset - 5; byte loadOpByte = ilBytes[loadOffset]; var loadOpCode = OpCodes.GetOpCode(loadOpByte); // 处理0-9和-1的整数常量(这些有专门的单字节指令) if (loadOpCode == OpCodes.Ldc_I4_0) return 0; if (loadOpCode == OpCodes.Ldc_I4_1) return 1; if (loadOpCode == OpCodes.Ldc_I4_2) return 2; if (loadOpCode == OpCodes.Ldc_I4_3) return 3; if (loadOpCode == OpCodes.Ldc_I4_4) return 4; if (loadOpCode == OpCodes.Ldc_I4_5) return 5; if (loadOpCode == OpCodes.Ldc_I4_6) return 6; if (loadOpCode == OpCodes.Ldc_I4_7) return 7; if (loadOpCode == OpCodes.Ldc_I4_8) return 8; if (loadOpCode == OpCodes.Ldc_I4_9) return 9; if (loadOpCode == OpCodes.Ldc_I4_M1) return -1; // 处理任意整数常量(用Ldc_I4指令,后面跟着4字节的数值) if (loadOpCode == OpCodes.Ldc_I4) { int value = BitConverter.ToInt32(ilBytes, loadOffset + 1); return value; } // 如果要支持字符串、bool等其他类型,得在这里加对应的解析逻辑,比如Ldstr、Ldc_I4_0(对应false)等 throw new NotSupportedException("目前只支持解析整数类型的默认值,其他类型需要扩展逻辑"); } } else { // 跳过当前指令的操作数,继续往下找 offset += opCode.OperandSize; } } throw new InvalidOperationException("没找到给这个属性赋值默认值的逻辑"); } public static void Main() { var defaultValue = GetAutoPropertyDefaultValue(typeof(MyClass), "Id"); Console.WriteLine($"Id属性的默认值是:{defaultValue}"); // 输出:Id属性的默认值是:5 } }
几个要注意的点
- 幕后字段的命名格式是C#编译器的标准实现,但如果遇到非微软的编译器,可能会有差异,到时候得调整字段名称的匹配方式。
- 上面的代码只处理了整数类型的默认值,如果需要支持字符串、布尔值等其他类型,得扩展IL指令的解析逻辑,比如处理
Ldstr(加载字符串)、Ldc_I4_0(对应false)、Ldc_I4_1(对应true)这些指令。 - 如果你的类没有无参构造函数,那就换其他实例构造函数来解析,逻辑是一样的,因为自动属性的初始化逻辑会被注入到所有实例构造函数的开头。
内容的提问来源于stack exchange,提问作者Bilal Fazlani
相关产品推荐
相关产品推荐

