DynamicExpresso 表达式


DynamicExpresso 表达式实战笔记:工业场景数据处理与状态管理

适用场景:IoT 数据采集、SCADA 系统、设备变量清洗
核心引擎DynamicExpresso(C# 动态表达式解析器)
整理时间:2026-08-10


0. 什么是 DynamicExpresso?

DynamicExpresso 是一个轻量级的 C# 表达式解析库,可在运行时动态编译和执行字符串形式的表达式。它支持:

  • 大部分 C# 语法(算术、逻辑、三元运算、方法调用等)
  • 变量注入(SetVariable
  • 常用 .NET 类型和方法(如 MathstringDateTime
  • 无外部依赖,解析速度快,适合高频率调用的工业场景

在工业数据处理中,我们利用它把设备原始值通过可配置的表达式转换为业务值(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)
  • vnull"" 时返回 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 不支持多语句、变量声明、循环等,避免嵌套过深
防御性编程 始终考虑 null0、类型转换异常;善用三元运算和 ??
语义清晰 优先使用 .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) 明确指定类型,避免隐式转换问题

六、架构设计建议

graph LR A[设备原始值] --> B(GwDeviceVariable) B --> C{表达式引擎} C -->|注入 lastCooked 等| D[DynamicExpresso] D --> E[计算结果 CookedValue] E -->|更新 LastCookedValue| B E --> F[上报/存储/控制]

核心思想

  • 表达式层:纯函数,无状态,只负责数学转换与逻辑判断。
  • 业务层:维护状态(历史值、有效值、累计量、报警标记等)。
  • 解耦方式:通过 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 会抛出 ParseExceptionEvaluationException,应捕获并记录原始表达式和变量值,便于排查配置错误。

try
{
    ret.CookedValue = interpreter.Eval(expression);
}
catch (Exception ex)
{
    _logger.LogError(ex, "表达式计算失败: {Expr}", expression);
    ret.CookedValue = 0; // 可设默认值
}

结合 DynamicExpresso 在工业遥测系统中的实战经验,梳理表达式的陷阱与最佳实践。遵循“状态外置、表达式纯计算”的原则,能够极大提升数据处理层的灵活性和可靠性。

posted @ 2026-08-10 10:03  古月秋筠  阅读(8)  评论(0)    收藏  举报