news 2026/9/9 5:26:20

C#对象动态添加属性:ExpandoObject、DynamicObject与Emit实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#对象动态添加属性:ExpandoObject、DynamicObject与Emit实战

如果你写过上位机或者数据采集类的系统,一定遇到过这种让人抓狂的情况:业务对象早就定义好了,结果对接的设备或者外部服务今天要多传一个温度,明天要多带一个通道名称,后天又要加一个检测结果。前端接口已经定死,数据模型又不能总是为了某个临时场景去改类、加属性、重新编译发布。于是很多人会问一句话:能不能在运行时给现有对象动态添加属性?

这个问题在 C# 里不像在 JavaScript 里那么直接。JS 的对象本来就是哈希表的形态,你随时可以obj.extra = 1。但 C# 是强类型语言,对象的结构在编译期基本就锁死了。最近我在一个 .NET 9 项目里专门折腾了这整件事,把 ExpandoObject、DynamicObject、反射 Emit、序列化扩展这些方案都试了一遍,也踩了几个比较隐蔽的坑。这篇就把不同做法的适用场景、完整代码、以及实测下来的注意点整理出来。如果你正准备在 C# 上位机、机器视觉、或者报表字段动态拼接这类项目里加动态属性,这篇应该能帮你少绕很多弯路。

1. 先把需求拆明白:到底要“动态”到哪一层

很多朋友一上来就问“怎么给对象动态加属性”,但经过我仔细观察,他们真正想要的其实是完全不同层面的东西。这步没理清楚,后面所有方案都会选错。

1.1 三个不同层级的动态化需求

第一层:我希望原对象的实例本身长出新的属性。比如我已经有一个DeviceData对象,在内存里跑着,我希望直接对实例执行类似device.Temperature = 26.5的操作。注意,这句话逻辑上有个硬伤:类的结构在编译期已经固定,类型系统不允许它平白无故多出一个Temperature属性。想让实例长出属性,本质上是需要让编译器在运行时看到一个“新类型”,这条路只能走反射生成子类型或者动态代理,代价比较高。

第二层:我能接受新建一个对象,这个对象包含原有对象的所有字段,同时还能附加新字段。这种场景最常见。比如从设备采集到基础数据后,我要输出一个带检测结果的完整报文;或者在 WinForm 表格里临时拼几个展示列。原始对象可以保持不变,我只需要一个“看起来像原来对象、但又多了字段”的新对象。这种做法工程量小,使用也灵活。

第三层:其实我只需要输出 JSON 时多几个字段。说白了,系统对接是 JSON 报文层面的事,内部我根本不关心那个类是不是真的有这些属性,我只要序列化之后键值对齐全就行。这种情况最简单,甚至可以完全不动业务对象,只做序列化拼装。

我在实际项目里遇到最多的其实是第二层和第三层,真正需要第一层的反而是极少数。比如 C# 上位机开发中,通讯报文结构经常跟着设备型号走,但同一套界面控件、同一个处理流程还想复用,动态给对象挂字段就是常用技巧。

1.2 .NET 9 和 C# 13 到底带来了什么新底牌

先给个结论:.NET 9 并没有提供一个类似 Python 那种“运行时随便给对象加属性”的原生 API。C# 语言本身从 C# 4 开始有dynamic关键字,配合ExpandoObjectDynamicObject可以做到动态成员访问,但底层依然是字典或者自定义动态分发。

不过 .NET 9 时代的几个趋势对这个问题有实际影响:

  • C# 12 / .NET 8 之后,主构造函数、集合表达式等语法让写这类辅助代码省了不少样板。
  • .NET 9 进一步强化了 Native AOT 支持,而Reflection.Emit不能在 Native AOT 环境下使用,所以单纯用老反射方案的同学会越来越受限。
  • C# 13 引入了“扩展成员(Extension Members)”语言预览特性,它能在编译期给已有类型追加属性和方法,算是部分回应了“想让现有类多点东西”的诉求。但它是静态扩展,和“运行时动态添加”有本质区别。

所以现在的选型不能只看“能不能实现”,还要看你的部署环境允不允许运行时生成 IL。这块我在后面方案对比里会重点说。

2. 入门最常用方案:ExpandoObject 与 dynamic 组合

如果你的动态字段不要求绑定到某个具体业务类型,或者你的场景本来就是“整个对象都是动态的”,那 ExpandoObject 绝对是第一个要试的方案。

2.1 ExpandoObject 三分钟上手

提到 C# 动态属性,很多人第一个想到的就是它。使用方式是这样的:

using System.Dynamic; dynamic device = new ExpandoObject(); device.Name = "一号采集站"; device.ChannelCount = 8; device.Status = "Online";

赋值之后,你可以直接通过device.Name读取。但这里有一个太多新手踩过的坑:如果你不是用dynamic声明变量,而是写成下面这样,编译都会不过:

var device = new ExpandoObject(); device.Name = "一号采集站"; // 编译错误:ExpandoObject 没有 Name 属性

因为var推断出来的类型是ExpandoObject,而 ExpandoObject 编译期并没有Name属性。它之所以能用点号访问,靠的是dynamic在运行时动态绑定。一旦声明成具体类型,编译器就不会走这套逻辑。

ExpandoObject 本质上就是IDictionary<string, object?>的一层外壳。这一点非常有用,很多时候你需要遍历它所有的动态属性,直接把它转成字典再处理:

dynamic device = new ExpandoObject(); device.Name = "一号采集站"; device.ChannelCount = 8; var map = (IDictionary<string, object?>)device; foreach (var kv in map) { Console.WriteLine($"{kv.Key} = {kv.Value}"); }

输出结果很直观,所有动态加进去的字段都会出现在字典里。

2.2 把现有对象的属性“搬”进动态对象

ExpandoObject 还有一个很常见的用法:把一个已经存在的普通对象的属性,全部复制到 ExpandoObject 上,然后再追加新字段。相当于做了个“扩展投影”。比如我现在有个温度设备实体:

public class TemperatureDevice { public string DeviceId { get; set; } = ""; public double Temperature { get; set; } }

我需要生成一份包含设备基础信息,外加人工判定结果的报文,就可以这样写:

public static ExpandoObject ToExpando(object source) { var expando = new ExpandoObject(); var dict = (IDictionary<string, object?>)expando; foreach (var property in source.GetType().GetProperties()) { if (property.GetIndexParameters().Length > 0) { continue; } dict[property.Name] = property.GetValue(source); } return expando; }

调用起来效果相当漂亮:

var device = new TemperatureDevice { DeviceId = "DEV-001", Temperature = 36.8 }; dynamic payload = ToExpando(device); payload.JudgeResult = "OK"; payload.AlarmLevel = 1; var payloadDict = (IDictionary<string, object?>)payload; Console.WriteLine(payloadDict["DeviceId"]); // DEV-001 Console.WriteLine(payloadDict["JudgeResult"]); // OK

这种“反射读取属性 + ExpandoObject 承载额外字段”的组合,非常适合先把原对象丢给第三方组件,第三方再往上面追加自定义字段的场景。注意这里面有个概念必须想清楚:原来的device对象本身并没有真的多出属性,我们只是创建了一个新的动态对象,把旧对象的值复制进去并追加了字段。原实例的类型自始至终都没有改变。

ExpandoObject 的方案优缺点也非常明显。轻量、代码好写、遍历方便是它的最大优势。但它如果放在数据量很大的场景下,性能就一般了,因为每次动态访问都要经过字典查找。而且如果你做了属性名拼写错误,编译器不会告诉你,直到运行时才会抛出RuntimeBinderException。做上位机长期维护时,这种“运行到某行才炸”的写法要格外小心。

3. 想“包装”现有对象又保留原始字段的自定义 DynamicObject 方案

ExpandoObject 有一个现实问题:它复制字段时是“快照式”的。如果原对象的属性值变了,ExpandoObject 里拷贝出来的那份不会同步更新。在部分上位机界面绑定、实时数据监控场景里这会导致界面数据和实际数据对不上。

如果你希望在不破坏原类型的情况下,给“现有对象实例”接入动态字段,同时保留原对象属性的实时读取能力,可以考虑自定义一个DynamicObject包装器。

3.1 自定义动态包装器实现

思路不复杂:创建一个泛型包装类,内部持有原对象引用,同时维护一个字典专门存放动态追加的属性。当动态访问发生时,先查字典,查不到就反射去读原对象的真实属性。这个包装器的代码我贴出来,可以直接抄:

using System.Dynamic; using System.Reflection; public class ExtensionProxy<T> : DynamicObject where T : class { private readonly T _target; private readonly Dictionary<string, object?> _extras = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, PropertyInfo?> _propertyCache; public ExtensionProxy(T target) { _target = target ?? throw new ArgumentNullException(nameof(target)); _propertyCache = new Dictionary<string, PropertyInfo?>(StringComparer.OrdinalIgnoreCase); foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { _propertyCache[prop.Name] = prop; } } public T Target => _target; public override bool TryGetMember(GetMemberBinder binder, out object? result) { if (_propertyCache.TryGetValue(binder.Name, out var realProp) && realProp is not null && realProp.CanRead) { result = realProp.GetValue(_target); return true; } if (_extras.TryGetValue(binder.Name, out result)) { return true; } result = null; return false; } public override bool TrySetMember(SetMemberBinder binder, object? value) { if (_propertyCache.TryGetValue(binder.Name, out var realProp) && realProp is not null && realProp.CanWrite) { realProp.SetValue(_target, value); return true; } _extras[binder.Name] = value; return true; } public override IEnumerable<string> GetDynamicMemberNames() { foreach (var name in _propertyCache.Keys) { yield return name; } foreach (var name in _extras.Keys) { yield return name; } } }

调用的时候,需要用dynamic来接收包装对象:

public class ConnectionConfig { public string Host { get; set; } = "127.0.0.1"; public int Port { get; set; } = 502; } var config = new ConnectionConfig { Host = "192.168.1.10", Port = 502 }; dynamic proxy = new ExtensionProxy<ConnectionConfig>(config); // 读取原始属性:走反射 Console.WriteLine(proxy.Host); // 添加动态属性:走 extras 字典 proxy.ChannelName = "流水线A"; proxy.TimeoutMs = 3000; // 动态属性可以正常读取 Console.WriteLine(proxy.ChannelName); // 原始对象属性变化,包装器依旧读得到最新值 config.Port = 503; Console.WriteLine(proxy.Port); // 503

注意代码中我对原有的可写属性做了特殊处理:如果访问名能对应到原对象的真实属性,并且属性可写,就优先写入原属性,保持数据双向一致;如果原属性是只读的,或者根本没有这个属性,才存进_extras动态字典。这个设计在多半业务场景下最好用。

3.2 理解 DynamicObject 的绑定机制

DynamicObject可以理解为:C# 编译器遇到dynamic对象上的成员访问时,不直接生成普通的成员调用指令,而是生成一段“调用动态绑定逻辑”的代码。绑定逻辑会调用你重写的方法,比如TryGetMemberTrySetMemberTryInvokeMember

这个方法设计得非常有意思,它让你可以把“动态属性”完全掌控在自己手里。比如你可以让属性名忽略大小写,或者根据权限决定一个属性到底能不能被赋值。我就曾经在上位机项目里用这个方式做了设备参数的“只读/可写”控制:某些参数只允许上位机读,不允许现场手动改,在TrySetMember里直接拒绝即可。

还需要注意一个坑:DynamicObject的子类只有在被当作dynamic使用时才会走动态绑定。如果你把它改成具体类型,比如var proxy = new ExtensionProxy<ConnectionConfig>(config); proxy.Host;,编译器会告诉你ExtensionProxy<ConnectionConfig>上面没有Host属性。这跟 ExpandoObject 的情况一模一样,动态分派只发生在动态调用点上。

另外,GetDynamicMemberNames建议尽量重写。很多调试工具、数据序列化组件、以及某些 UI 绑定框架,都会通过这个方法来了解对象有哪些动态成员。不重写的话,外部工具可能只能看到类上真正的静态成员,完全不知道你挂了额外的动态字段。

4. 不碰对象本身:序列化层的“动态添加属性”

在实际做系统对接的时候,有很大一部分场景根本不需要让内存里的对象真正多一个属性,你只是希望最终吐给别人的 JSON 里多几个字段。这种情况下,最不容易出问题的方案其实是直接在序列化层做。

4.1 用字典合并动态字段

思路非常简单:把原对象序列化或者反射成一个字典结构,然后把动态附加字段放进去。我比较推荐的做法是写一个通用扩展方法:

public static class ObjectExtensions { public static Dictionary<string, object?> AttachExtra( this object source, params (string Name, object? Value)[] extras) { var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase); foreach (var property in source.GetType() .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) { if (property.GetIndexParameters().Length == 0) { result[property.Name] = property.GetValue(source); } } foreach (var extra in extras) { result[extra.Name] = extra.Value; } return result; } }

调用起来很舒服:

var device = new TemperatureDevice { DeviceId = "DEV-002", Temperature = 42.1 }; var payload = device.AttachExtra( ("JudgeResult", "OverTemp"), ("Score", 0.98) ); Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload));

这里完全没有修改device对象,也没有新建什么动态类型,只是在最终输出的载荷层做了一次合并。如果团队约定好对外报文统一用字典生成,那么这个方案最干净,既不会污染业务模型,也不影响内部强类型的使用。唯一要留意的是属性名如果和附加字段重名,会把原来的值覆盖掉,所以在附加字段的命名上要养成带前缀的好习惯,比如加ext_或者按业务模块命名。

4.2 直接操作 JsonObject 再序列化

如果你的项目已经基于 System.Text.Json,还可以用JsonObject直接对序列化得到的节点做追加字段。它的好处是不用关心原对象具体有哪些属性,框架自动把属性变成了 JSON 节点,你再往里塞新字段就行:

using System.Text.Json; using System.Text.Json.Nodes; var device = new TemperatureDevice { DeviceId = "DEV-003", Temperature = 39.5 }; JsonObject json = JsonSerializer.SerializeToNode(device)!.AsObject(); json["DetectionTime"] = JsonValue.Create(DateTime.Now); json["JudgeResult"] = JsonValue.Create("Fever"); Console.WriteLine(json.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));

这种方式常用于网关服务或者协议转换层。比如你做机器视觉系统的时候,海康相机或者 VisionMaster 跑完检测会产生结果数据,你不想把视觉结果字段写死在设备实体类里,就可以先把实体序列化成JsonObject,再把识别标签、置信度、检测框这些动态信息追加进去。

JsonObject方式也有边界:如果一个 JSON 值本身是嵌套对象,你要用JsonObject去组装,比直接反射字典要繁琐。而且JsonValue.Createnull值的处理不是那么无脑,字段值经常为空时,直接用字典方案会更省心。

4.3 为什么我不建议无脑上全局转换器

可能有人已经想到了:能不能写一个自定义JsonConverterFactory,让所有对象在序列化的时候都自动带上外部字典里的附加字段?理论上当然可以,但我在实际项目中试过之后发现,这种“全局横切”的做法维护成本很高。因为你很难确定哪个类型需要扩展、哪个类型不需要扩展;你也很容易和已有的自定义转换器互相打架。而且一旦约定混乱,序列化出来的字段顺序、命名策略都会变成一个小型灾难。

我的建议是把序列化层扩展限定在“出入口”:要么在入站反序列化时把未知字段收集到JsonExtensionData字典,要么在出站时显式调用上面的AttachExtraJsonObject追加。让代码的读者一眼就能看出“这个对象被附加了什么”,比在框架层做全局黑魔法要靠谱得多。

5. 反射 + Reflection.Emit:运行时生成真正带新属性的类型

如果你确实需要让一个对象实例在运行时拿出一个“新类型”,也就是第一层那类需求,可以考虑Reflection.Emit。它能动态创建一个继承自原类型的子类,给子类增加新属性,然后把原对象的数据复制到新对象上。最终你在外面拿到的对象类型已经不是原来的TemperatureDevice,而是TemperatureDevice_Ext,但它可以赋给TemperatureDevice变量,并且通过反射能拿到新增的JudgeResult属性。

5.1 思路拆解:为什么是子类而不是改原类

CLR 不允许运行时修改一个已经加载的类型的成员列表。想给现有类型加属性,唯一可靠的方向是创建一个子类型,把原类型已有的字段和方法都继承下来,再在子类型上声明新的属性。因为子类自然继承原类的公共成员,所以把子类实例赋给基类引用在类型系统上是完全合法的。

Reflection.Emit的好处是能做真正有PropertyInfo的属性,System.Text.Json、数据表格绑定、表达式树等看得到 CLR 元数据的组件都能识别它。坏处是代码复杂度高,而且只有在未被 Native AOT 裁剪的运行时环境才可用。

5.2 核心代码:动态生成子类型并添加属性

下面这段代码我做了精简,留下最核心的动态类型生成逻辑。它能把一个新属性挂到某个类型的动态子类上:

using System.Reflection; using System.Reflection.Emit; public static class DynamicTypeBuilder { public static Type ExtendWithProperty( Type baseType, string propertyName, Type propertyType) { AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run); ModuleBuilder module = assembly.DefineDynamicModule("MainModule"); TypeBuilder typeBuilder = module.DefineType( baseType.Name + "_Ext", TypeAttributes.Public | TypeAttributes.Class, baseType); string fieldName = "_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1); FieldBuilder field = typeBuilder.DefineField( fieldName, propertyType, FieldAttributes.Private); PropertyBuilder property = typeBuilder.DefineProperty( propertyName, PropertyAttributes.None, propertyType, Type.EmptyTypes); MethodBuilder getter = typeBuilder.DefineMethod( "get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes); ILGenerator getIl = getter.GetILGenerator(); getIl.Emit(OpCodes.Ldarg_0); getIl.Emit(OpCodes.Ldfld, field); getIl.Emit(OpCodes.Ret); property.SetGetMethod(getter); MethodBuilder setter = typeBuilder.DefineMethod( "set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(void), new[] { propertyType }); ILGenerator setIl = setter.GetILGenerator(); setIl.Emit(OpCodes.Ldarg_0); setIl.Emit(OpCodes.Ldarg_1); setIl.Emit(OpCodes.Stfld, field); setIl.Emit(OpCodes.Ret); property.SetSetMethod(setter); return typeBuilder.CreateTypeInfo().AsType(); } }

然后写一个通用的“复制并添加”方法,把原对象的公共属性值拷贝到新对象上:

public static TBase AddPropertyToInstance<TBase>( TBase source, string propertyName, object? propertyValue) where TBase : class { ArgumentNullException.ThrowIfNull(source); Type dynamicType = DynamicTypeBuilder.ExtendWithProperty( source.GetType(), propertyName, propertyValue?.GetType() ?? typeof(object)); TBase instance = (TBase)Activator.CreateInstance(dynamicType)!; foreach (PropertyInfo prop in source.GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance)) { if (prop.CanRead && prop.CanWrite && prop.GetIndexParameters().Length == 0) { prop.SetValue(instance, prop.GetValue(source)); } } dynamicType.GetProperty(propertyName)!.SetValue(instance, propertyValue); return instance; }

使用示例:

var source = new TemperatureDevice { DeviceId = "DEV-004", Temperature = 38.2 }; TemperatureDevice extended = AddPropertyToInstance( source, "JudgeResult", "OverTemp"); Type t = extended.GetType(); Console.WriteLine(t.FullName); // TemperatureDevice_Ext Console.WriteLine(t.GetProperty("JudgeResult")?.GetValue(extended)); // OverTemp

这段代码最核心的价值在于:extended变量的静态类型仍然是TemperatureDevice,你原有代码里所有接收基类的方法都可以继续用;但通过反射你在运行时拿到了一个真正的新属性。

5.3 使用 Emit 的几个重要提醒

首先,Activator.CreateInstance(dynamicType)要求原类型必须具备可访问的无参构造函数。如果原类型只有带参构造函数,你就需要用ConstructorBuilder显式定义构造函数,或者通过FormatterServices.GetUninitializedObject跳过构造函数创建对象,后者危险性高,不那么建议轻易使用。

其次,复制属性时如果原类型里有只读属性或者没有 setter 的属性,字段值不会自动复制过去。比如某些模型类通过构造函数初始化属性而没有 setter,你复制出来的子类实例很可能属性值为默认空值。这种问题很难在运行前发现,要在封装层做防御性检查和日志。

最后是 AOT 兼容性问题。如果你的 .NET 9 项目开启了 Native AOT 发布,运行时不能再通过Reflection.Emit动态创建类型。这是硬限制。所以在工业上位机场景里,如果你打算做离线部署又追求启动快,我建议少用 Emit,尽量往 ExpandoObject、DynamicObject 或序列化扩展方案上靠。

6. 认识 C# 13 的扩展成员:它和“动态”是两回事

既然标题里挂着 .NET 9,就不得不提 C# 13 带来的一个语言级新方向:扩展成员。这个特性虽然还在预览阶段,但它确实能给你一种“给既有类型加成员”的能力。

6.1 扩展成员的意图与限制

C# 从很早就有扩展方法,但它只能加方法,不能加属性。C# 13 预览版把扩展能力推进了一大步:你可以为已有类型声明扩展属性、扩展静态成员,甚至允许扩展字段的占位接口。语法上更接近为类型追加一段“成员块”,让你在没有继承、没有修改源码的情况下,让某个类型从调用方视角看起来拥有新成员。

不过要特别强调:扩展成员是编译期绑定,不是运行时动态加属性。所谓“扩展出来的属性”,本质上是编译器在后台帮你生成一段访问逻辑,属性集合在编译完成后就固定了。它不会因为外部输入不同而在进程运行期间改变成员集合。你无法一边跑着程序,一边往扩展类里面塞一个运行时才决定名字的属性。

那它有什么用?如果你是自己在维护一套基础库,经常需要给第三方模型补充特定领域的计算字段,又不希望业务代码到处用字符串字典,那扩展成员是很好的“静态层面的补丁”。

使用的时候需要在工程文件里开启预览语言版本:

<PropertyGroup> <LangVersion>preview</LangVersion> </PropertyGroup>

由于是预览特性,API 和写法在后续版本里还有调整的可能,生产项目如果追求稳定,建议先做小规模验证再决定是否铺开。

6.2 和 DynamicObject 的分工

可以把 C# 13 扩展成员理解成“编译期锦上添花”,DynamicObject / ExpandoObject 则是“运行时动态应变”。两者不是竞争关系,而是服务于不同节奏的需求。

如果你的字段是从配置、数据库或者上位机采集报文里读出来的,字段数量甚至字段名都要看现场情况,那扩展成员救不了你,因为没人能在编译期把这些未知字段写进去;这时候还是动态对象或者字典承载比较合适。

如果字段是固定的,只是你不想污染实体类、不想为了几个展示字段大动干戈,那扩展成员将来很可能比 DynamicObject 更值得依赖——它有编译器检查,重构时不容易把字符串写错。

但我在这篇里没有把扩展成员作为主推方案,原因很简单:发布稳定版本的项目里,少用还在预览期的语言特性永远是安全第一的原则。你在 GitHub 上玩样例可以,拿到产线上跑,还是先把 ExpandoObject 或 JsonObject 跑通再说。

6.3 选择方案前的优先级判断

我自己在项目里判断优先级是从“最小副作用”入手。

第一优先级永远是序列化层拼字段。如果只是报文输出或者接口返回,那毫不客气地说,给内存对象搞动态属性属于过度设计,用AttachExtraJsonObject就够了。

第二优先级才是 ExpandoObject 和 DynamicObject。它们适合业务逻辑里确实需要以属性方式访问动态字段的场景,比如 UI 绑定、动态条件处理、协议解析后的统一访问。

第三优先级才轮到 Reflection.Emit。它适合强类型组件要求你提供一个真正带新属性的 Type 的场景,例如某些表格控件的高级绑定、某些表达式树计算,以及旧框架对PropertyDescriptor有依赖的场景。你要为它付出更多的调试成本,同时必须放弃 Native AOT 发布这条路。

7. 选型速查与动态属性排错经验

整篇谈了很多方案,最后用一张表帮你迅速定位。表格不是万能的,但至少能把大体方向固定下来,避免在错误方案里越走越深。

场景推荐方案理由主要代价
接口或 JSON 输出要额外字段字典合并 / JsonObject不动业务模型,代码直观需要约定字段命名
原对象字段要动态展示且字段随时变ExpandoObject最简单,遍历方便无法和原对象属性保持双向同步
要包装现有对象并保持原属性实时一致DynamicObject能同时管理真实属性和动态
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 5:26:16

齿轮视觉测量系统如何落地?从硬件选型到数据接口的完整流程解析

复杂齿轮也能“一放一测”&#xff01;手把手拆解齿轮视觉测量系统的落地流程齿轮测量这件事&#xff0c;过去想到的就是齿轮测量中心、三坐标、接触式扫描&#xff0c;测一个复杂齿轮可能要几分钟甚至更久&#xff0c;还要看操作人员装夹水平。现在有一种方案正在大量进入精密…

作者头像 李华
网站建设 2026/9/9 5:26:09

Selenium安装配置全攻略:浏览器驱动匹配与自动化脚本实战

Selenium装不上、跑不通、报一堆错&#xff0c;这个问题我从入行到现在见了至少几百次。上周同事还抱着电脑过来&#xff0c;说昨天能跑的脚本今天早上突然就挂了&#xff0c;启动浏览器那一步直接抛SessionNotCreatedException。我打开chrome://version看了一眼&#xff0c;又…

作者头像 李华
网站建设 2026/9/9 5:23:52

HJ165 小红的优惠券:贪心与连续区间覆盖的算法解析

第一次看到“HJ165 小红的优惠券”这个标题&#xff0c;很多人的第一反应是“这不就是一道模拟题吗&#xff0c;把优惠券按价格排序然后算一算”。真上手以后才会发现&#xff0c;这道题的精髓根本不是模拟&#xff0c;而是隐藏在“优惠券”这个生活场景后面的连续区间覆盖和贪…

作者头像 李华
网站建设 2026/9/9 5:21:30

Open3D体素质心下采样:无人机点云预处理与可视化实战

点云数据一多起来&#xff0c;最先想到的不是什么高深算法&#xff0c;而是怎么“减负”。我在处理无人机航测点云时&#xff0c;一个架次下来往往几千万个点&#xff0c;直接扔进算法里跑&#xff0c;内存先崩为敬。这种场景下&#xff0c;Open3D 里最常见的降采样手段就是体素…

作者头像 李华
网站建设 2026/9/9 5:21:28

串口通信全双工与半双工:从RS232到交换机25GE口配置

打开串口调试助手&#xff0c;点了“发送”按钮&#xff0c;下面的接收区却半天没反应&#xff1b;或者RX计数跳得飞快&#xff0c;TX计数却纹丝不动。这时候很多人第一反应是“板子坏了”。但我在实际调试中见过太多类似情况&#xff0c;最后查出来根本不是硬件问题&#xff0…

作者头像 李华