DynamicExpresso 表达式
DynamicExpresso 表达式实战笔记:工业场景数据处理与状态管理
适用场景:IoT 数据采集、SCADA 系统、设备变量清洗
核心引擎:DynamicExpresso(C# 动态表达式解析器)
整理时间:2026-08-10
0. 什么是 DynamicExpresso?
DynamicExpresso 是一个轻量级的 C# 表达式解析库,可在运行时动态编译和执行字符串形式的表达式。它支持:
- 大部分 C# 语法(算术、逻辑、三元运算、方法调用等)
- 变量注入(
SetVariable) - 常用 .NET 类型和方法(如
Math、string、DateTime) - 无外部依赖,解析速度快,适合高频率调用的工业场景
在工业数据处理中,我们利用它把设备原始值通过可配置的表达式转换为业务值(CookedValue),实现零代码修改即可调整数据清洗逻辑。
一、零值处理:当前值为 0 时使用上一个有效值
1.1 基础需求
当设备通信中断或传感器异常时,采集值可能返回 0。为避免数据断崖(例如温度曲线突降),需要保持上一次有效值。
1.2 表达式写法(原始方案)
v == 0 ? pv : v
v:当前原始值(item.Values[0])pv:上一次原始值(item.Values[1])
1.3 ⚠️ 关键陷阱:pv 是原始值,不是计算值
该方案在连续多个周期原始值均为 0 时失效:
| 时序 | 当前原始值 v | 上一次原始值 pv | 表达式结果 | 问题 |
|---|---|---|---|---|
| T1 | 0 | 0(初始) | 0 | 正常 |
| T2 | 100 | 0 | 100 | 正常 |
| T3 | 0 | 100 | 100 | ✅ 正确回退 |
| T4 | 0 | 0 | 0 | ❌ 错误!T3 的原始值其实是 0,导致回退链断裂 |
根因:pv 仅存储上一次的原始采集值,而非上一次表达式计算后的有效值。当原始值连续为 0 时,pv 也变为 0,表达式无法获取更早的有效值。
1.4 ✅ 正确方案:在 C# 层维护 LastCookedValue
让表达式引擎只做计算,状态交由 C# 业务对象管理。
步骤 1:扩展变量模型
public class GwDeviceVariable
{
public object LastCookedValue { get; set; } = 0; // 新增字段
// ... 其他属性
}
步骤 2:计算后更新状态
// 伪代码:在读取变量并计算时
var interpreter = new Interpreter();
interpreter.SetVariable("v", rawValue);
interpreter.SetVariable("lastCooked", item.LastCookedValue);
ret.CookedValue = interpreter.Eval(expression);
item.LastCookedValue = ret.CookedValue; // 保存本次计算结果,供下次使用
步骤 3:修正后的表达式
v == 0 ? lastCooked : v
修正后效果:
| 时序 | v | lastCooked | 结果 | 说明 |
|---|---|---|---|---|
| T3 | 0 | 100 | 100 | ✅ 使用计算值 |
| T4 | 0 | 100 | 100 | ✅ 持续保持有效值 |
这种模式同样适用于“保持最后非空字符串”、“持续输出最后一次正常值”等场景。
二、强制返回常量 0
2.1 最简写法
0
DynamicExpresso 解析后就是一个数值常量 0,无需任何条件判断。
2.2 常见误区
| 错误写法 | 问题 |
|---|---|
"0" |
返回字符串 "0",后续数值运算会抛异常 |
0 == 0 |
返回布尔值 true |
v * 0 |
冗余计算,且 v = null 时可能抛 NullReferenceException |
💡 原则:当目标就是数值 0 时,直接写 0,不要绕弯。
三、字符串空值判断
3.1 推荐表达式
!string.IsNullOrEmpty(v)
- 当
v为null或""时返回false - 有内容(包括空格
" "、"0")返回true
3.2 为什么不用其他写法?
| 写法 | 风险 |
|---|---|
v == "" |
v = null 时抛出 NullReferenceException |
v.Length == 0 |
同上 |
v != null && v != "" |
正确但冗长,且易漏掉条件 |
3.3 DynamicExpresso 对 string 的支持
DynamicExpresso 默认已引用 string 类型,可直接调用静态方法:
string.IsNullOrEmpty(v)string.IsNullOrWhiteSpace(v)
无需额外 Reference(typeof(string)),也无需注册自定义函数。
3.4 扩展:空格也视为无效
!string.IsNullOrWhiteSpace(v)
适用于表单验证、标识符清洗等对空白字符敏感的场合。工业数据中通常用 IsNullOrEmpty 即可,除非有特殊要求。
四、复合表达式最佳实践
4.1 完整示例:防零 + 单位换算 + 精度控制
Math.Round(((v == 0 ? lastCooked : v) / 100000.0) * Math.PI * 1.96, 2)
该表达式同时完成:
- 防零处理(用
lastCooked) - 缩放(除以 100000.0)
- 数学运算(乘 π 和系数)
- 四舍五入保留两位小数
4.2 设计原则
| 原则 | 说明 |
|---|---|
| 状态外置 | 历史值、累计值等状态由 C# 对象维护,表达式只做纯计算 |
| 单表达式 | DynamicExpresso 不支持多语句、变量声明、循环等,避免嵌套过深 |
| 防御性编程 | 始终考虑 null、0、类型转换异常;善用三元运算和 ?? 等 |
| 语义清晰 | 优先使用 .NET 标准方法(如 IsNullOrEmpty),而非手写逻辑 |
| 可测试性 | 表达式本身可脱离数据库和设备独立进行单元测试 |
五、常用表达式速查表
| 需求 | 表达式 | 备注 |
|---|---|---|
| 当前为 0 时使用上次计算值 | v == 0 ? lastCooked : v |
需 C# 维护 LastCookedValue |
| 当前为 0 时使用上次原始值 | v == 0 ? pv : v |
⚠️ 连续 0 时会失效 |
| 强制返回数值 0 | 0 |
不要写成 "0" 或 false |
| 字符串非空判断 | !string.IsNullOrEmpty(v) |
安全处理 null |
| 字符串非空白判断 | !string.IsNullOrWhiteSpace(v) |
空格也视为无效 |
| 安全除法(防除零) | v == 0 ? 0 : a / v |
避免 DivideByZeroException |
| 空值合并 | v ?? 0 |
将 null 替换为 0 |
| 布尔取反 | !flag |
适用于开关量取反 |
| 多条件复合 | (x > 0 && y < 100) ? x + y : 0 |
支持短路逻辑 |
| 类型转换 | Convert.ToDouble(v) |
明确指定类型,避免隐式转换问题 |
六、架构设计建议
核心思想:
- 表达式层:纯函数,无状态,只负责数学转换与逻辑判断。
- 业务层:维护状态(历史值、有效值、累计量、报警标记等)。
- 解耦方式:通过
SetVariable注入运行时状态,计算后回写状态字段。
这种分层设计的优势:
- 表达式可配置、可热更新,无需重启服务。
- 表达式的单元测试完全脱离外部依赖。
- 状态管理集中在 C# 代码中,避免了在动态表达式中实现复杂状态机(如累加、计时)带来的维护灾难。
七、扩展阅读与实践提示
7.1 DynamicExpresso 常用 API
var interpreter = new Interpreter();
// 注入变量
interpreter.SetVariable("v", 123.4);
interpreter.SetVariable("name", "Sensor01");
// 执行表达式
object result = interpreter.Eval("v * 2");
// 或指定类型
double typedResult = interpreter.Eval<double>("v * 2 + 1");
// 注册自定义函数
interpreter.SetFunction("Square", (Func<double, double>)(x => x * x));
7.2 性能注意事项
- 解析过程(
Interpreter.Parse)有开销,建议缓存Lambda表达式。对于高频调用(如每秒数千次),使用Parse后得到的委托直接调用。var func = interpreter.ParseAsExpression<Func<double, double, double>>("v + offset"); double result = func(100, 5); - 避免在循环中重复创建
Interpreter实例,可复用并仅修改变量值。
7.3 异常处理
DynamicExpresso 会抛出 ParseException 或 EvaluationException,应捕获并记录原始表达式和变量值,便于排查配置错误。
try
{
ret.CookedValue = interpreter.Eval(expression);
}
catch (Exception ex)
{
_logger.LogError(ex, "表达式计算失败: {Expr}", expression);
ret.CookedValue = 0; // 可设默认值
}
结合 DynamicExpresso 在工业遥测系统中的实战经验,梳理表达式的陷阱与最佳实践。遵循“状态外置、表达式纯计算”的原则,能够极大提升数据处理层的灵活性和可靠性。

浙公网安备 33010602011771号