.NET实现解析字符串表达式
一、引子·功能需求
我们创建了一个 School 对象,其中包含了教师列表和学生列表。现在,我们需要计算教师平均年龄和学生平均年龄。
//创建对象
School school = new School()
{
Name = "小菜学园",
Teachers = new List<Teacher>()
{
new Teacher() {Name="波老师",Age=26},
new Teacher() {Name="仓老师",Age=28},
new Teacher() {Name="悠老师",Age=30},
},
Students= new List<Student>()
{
new Student() {Name="小赵",Age=22},
new Student() {Name="小钱",Age=23},
new Student() {Name="小孙",Age=24},
},
//这两个值如何计算?
TeachersAvgAge = "",
StudentsAvgAge = "",
};
如果我们将计算教师平均年龄的公式交给用户定义,那么用户可能会定义一个字符串来表示:
Teachers.Sum(Age)/Teachers.Count
或者可以通过lambda来表示:
teachers.Average(teacher => teacher.Age)
此时我们就获得了字符串类型的表达式,如何进行解析呢?
二、构建字符串表达式
手动构造
这种方式是使用 Expression 类手动构建表达式,虽然不符合我们的实际需求,但是它是Dynamic.Core底层实现的方式。Expression 类的文档地址为::https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expression?view=net-6.0
// 创建参数表达式
var teachersParam = Expression.Parameter(typeof(Teacher[]), "teachers");
// 创建变量表达式
var teacherVar = Expression.Variable(typeof(Teacher), "teacher");
// 创建 lambda 表达式
var lambdaExpr = Expression.Lambda<Func<Teacher[], double>>(
Expression.Block(
new[] { teacherVar }, // 定义变量
Expression.Call(
typeof(Enumerable),
"Average",
new[] { typeof(Teacher) },
teachersParam,
Expression.Lambda(
Expression.Property(
teacherVar, // 使用变量
nameof(Teacher.Age)
),
teacherVar // 使用变量
)
)
),
teachersParam
);
// 编译表达式树为委托
var func = lambdaExpr.Compile();
var avgAge = func(teachers);
使用System.Linq.Dynamic.Core
System.Linq.Dynamic.Core 是一个开源库,它提供了在运行时构建和解析 Lambda 表达式树的功能。它的原理是使用 C# 语言本身的语法和类型系统来表示表达式,并通过解析和编译代码字符串来生成表达式树。
// 构造 lambda 表达式的字符串形式
string exprString = "teachers.Average(teacher => teacher.Age)";
// 解析 lambda 表达式字符串,生成表达式树
var parameter = Expression.Parameter(typeof(Teacher[]), "teachers");
var lambdaExpr = DynamicExpressionParser.ParseLambda(new[] { parameter }, typeof(double), exprString);
// 编译表达式树为委托
var func = (Func<Teacher[], double>)lambdaExpr.Compile();
// 计算教师平均年龄
var avgAge = func(teachers);
三、介绍System.Linq.Dynamic.Core
使用此动态 LINQ 库,我们可以执行以下操作:
- 通过 LINQ 提供程序进行的基于字符串的动态查询。
- 动态分析字符串以生成表达式树,例如ParseLambda和Parse方法。
- 使用CreateType方法动态创建数据类。
功能介绍
普通的功能此处不赘述,如果感兴趣,可以从下文提供文档地址去寻找使用案例。
- 添加自定义方法类
可以通过在静态帮助程序/实用工具类中定义一些其他逻辑来扩展动态 LINQ 的分析功能。为了能够做到这一点,有几个要求:
- 该类必须是公共静态类
- 此类中的方法也需要是公共的和静态的
- 类本身需要使用属性进行注释[DynamicLinqType]
[DynamicLinqType]
public static class Utils
{
public static int ParseAsInt(string value)
{
if (value == null)
{
return 0;
}
return int.Parse(value);
}
public static int IncrementMe(this int values)
{
return values + 1;
}
}
此类有两个简单的方法:
当输入字符串为 null 时返回整数值 0,否则将字符串解析为整数
使用扩展方法递增整数值
用法:
var query = new [] { new { Value = (string) null }, new { Value = "100" } }.AsQueryable();
var result = query.Select("Utils.ParseAsInt(Value)");
除了以上添加[DynamicLinqType]属性这样的方法,我们还可以在配置中添加。
public class MyCustomTypeProvider : DefaultDynamicLinqCustomTypeProvider
{
public override HashSet<Type> GetCustomTypes() =>
new[] { typeof(Utils)}.ToHashSet();
}
文档地址
使用项目
- 规则引擎RulesEngine中解析表达式的实现:https://github.com/microsoft/RulesEngine/wiki
- 自己封装了低代码中公式编辑器中公式的解析功能
四、浅析System.Linq.Dynamic.Core
System.Linq.Dynamic.Core中 DynamicExpressionParser 和 ExpressionParser 都是用于解析字符串表达式并生成 Lambda 表达式树的类,但它们之间有一些不同之处。
ExpressionParser 类支持解析任何合法的 C# 表达式,并生成对应的表达式树。这意味着您可以在表达式中使用各种运算符、方法调用、属性访问等特性。
DynamicExpressionParser 类则更加灵活和通用。它支持解析任何语言的表达式,包括动态语言和自定义 DSL(领域特定语言)
我们先看ExpressionParser这个类,它用于解析字符串表达式并生成 Lambda 表达式树。
我只抽取重要的和自己感兴趣的属性和方法。
- TextParser 类,实现算法有点类似于有限状态自动机(FSM):https://leetcode.cn/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/solutions/372095/biao-shi-shu-zhi-de-zi-fu-chuan-by-leetcode-soluti/
- MethodFinder,使用了反射机制,通过调用 GetMethods() 方法获取指定类型中定义的所有方法,并根据参数数量和类型等条件检查参数是否符合特定的条件。如果参数满足了条件,则将该方法添加到结果列表中。
public class ExpressionParser
{
//字符串解析器的配置,比如区分大小写、是否自动解析类型、自定义类型解析器等
private readonly ParsingConfig _parsingConfig;
//查找指定类型中的方法信息,通过反射获取MethodInfo
private readonly MethodFinder _methodFinder;
//用于帮助解析器识别关键字、操作符和常量值
private readonly IKeywordsHelper _keywordsHelper;
//解析字符串表达式中的文本,用于从字符串中读取字符、单词、数字等
private readonly TextParser _textParser;
//解析字符串表达式中的数字,用于将字符串转换为各种数字类型
private readonly NumberParser _numberParser;
//用于帮助生成和操作表达式树
private readonly IExpressionHelper _expressionHelper;
//用于查找指定名称的类型信息
private readonly ITypeFinder _typeFinder;
//用于创建类型转换器
private readonly ITypeConverterFactory _typeConverterFactory;
//用于存储解析器内部使用的变量和选项。这些变量和选项不应该由外部代码访问或修改
private readonly Dictionary<string, object> _internals = new();
//用于存储字符串表达式中使用的符号和值。例如,如果表达式包含 @0 占位符,则可以使用 _symbols["@0"] 访问其值。
private readonly Dictionary<string, object?> _symbols;
//表示外部传入的参数和变量。如果表达式需要引用外部的参数或变量,则应该将它们添加到 _externals 中。
private IDictionary<string, object>? _externals;
/// <summary>
/// 使用TextParser将字符串解析为指定的结果类型.
/// </summary>
/// <param name="resultType"></param>
/// <param name="createParameterCtor">是否创建带有相同名称的构造函数</param>
/// <returns>Expression</returns>
public Expression Parse(Type? resultType, bool createParameterCtor = true)
{
_resultType = resultType;
_createParameterCtor = createParameterCtor;
int exprPos = _textParser.CurrentToken.Pos;
//解析条件运算符表达式
Expression? expr = ParseConditionalOperator();
//将返回的表达式提升为指定类型
if (resultType != null)
{
if ((expr = _parsingConfig.ExpressionPromoter.Promote(expr, resultType, true, false)) == null)
{
throw ParseError(exprPos, Res.ExpressionTypeMismatch, TypeHelper.GetTypeName(resultType));
}
}
//验证最后一个标记是否为 TokenId.End,否则抛出语法错误异常
_textParser.ValidateToken(TokenId.End, Res.SyntaxError);
return expr;
}
// ?: operator
private Expression ParseConditionalOperator()
{
int errorPos = _textParser.CurrentToken.Pos;
Expression expr = ParseNullCoalescingOperator();
if (_textParser.CurrentToken.Id == TokenId.Question)
{
......
}
return expr;
}
// ?? (null-coalescing) operator
private Expression ParseNullCoalescingOperator()
{
Expression expr = ParseLambdaOperator();
......
return expr;
}
// => operator - Added Support for projection operator
private Expression ParseLambdaOperator()
{
Expression expr = ParseOrOperator();
......
return expr;
}
}
.NET实现解析字符串表达式的更多相关文章
- 在C#开发中使用第三方组件LambdaParser、DynamicExpresso、Z.Expressions,实现动态解析/求值字符串表达式
在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser.DynamicExpresso.Z.Expressions,它们各自功能 ...
- 解决JSP 不解析EL表达式
解决JSP 不解析EL表达式,jsp在使用EL表达式的时候发现它不被解析,而是直接以字符串的形式显示了出来,经过查阅资料和实践,终于得知了原因并找到了解决方案 原因是:在默认情况下,Servlet 2 ...
- 解析数学表达式 代码解析AST语法树
2019年2月20日09:18:22 AST语法树自己写代码解析的话就比较麻烦,有现成的库可以解析PHP,就像webpack就是自己解析js的语法代码,编译成各种版本的可用代码 github http ...
- PHP 实现字符串表达式计算
什么是字符串表达式?即,将我们常见的表达式文本写到了字符串中,如:"$age >= 20",$age 的值是动态的整型变量. 什么是字符串表达式计算?即,我们需要一段程序来执 ...
- NVelocity解析字符串
之前都是先从模板文件里面读取html字符串,现在要求将模板存入数据库或缓存了,怎么办呢?在网上找了下资料,终于找到解决办法. 如下: public class NVelocityHelper { // ...
- 字符串表达式String Expressions
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [SAP ABAP开发技术总结]字符串表达式String Expressions
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- sql 解析字符串添加到临时表中 sql存储过程in 参数输入
sql 解析字符串添加到临时表中 sql存储过程in 参数输入 解决方法 把字符串解析 添加到 临时表中 SELECT * into #临时表 FROM dbo.Func_SplitOneCol ...
- 使用堆栈结构进行字符串表达式("7*2-5*3-3+6/3")的计算
问题: 给定字符串String str = "7*2-5*3-3+6/3", 求出字符串里面表达式的结果? 像javascript有自带的eval()方法,可以直接计算.但java ...
- CF552E 字符串 表达式求值
http://codeforces.com/contest/552/problem/E E. Vanya and Brackets time limit per test 1 second memor ...
随机推荐
- LINUX下的VSCODE-C/C++配置
LINUX下的VSCODE-C/C++配置 1.生成默认的任务文件 2.lunch.json,调整"configurations"里的成员,如下 ①添加 "preLaun ...
- ggplot2: display every nth value on discrete axis
every_nth = function(n) { return(function(x) {x[c(TRUE, rep(FALSE, n - 1))]}) } ggplot(mpg, aes(x = ...
- 一道测试Java值传递的题目
请给出下列代码的执行结果: public class T3 { public static void main(String[] args) { T3 t3 = new T3(); t3.first( ...
- docker&docker-compose安装
一.docker安装 1.通过 uname -r 命令查看当前的内核版本,Docker 要求 CentOS 系统的内核版本高于 3.10 uname -r 2.查看系统是否安装过docker yum ...
- 测试环境docker化实践
测试环境对于任何一个软件公司来讲,都是核心基础组件之一.测试环境伴随着发展也从单一的几套环境发展成现在的任意的docker动态环境+docker稳定环境环境体系.期间环境系统不断的演进,去适应集群扩张 ...
- 嵌入式linux系统新人学习回顾
(1).开发环境搭建 1.虚拟机ubuntu 2.远程登录/远程传输/串口三合一软件MobaXterm 3.FTP传输工具FileZilla 4.TFTP服务器软件tftpd.exe (2)开发板硬件 ...
- Flask CURD(增删改查)
1.创建flask项目 2.修改配置文件: ''' config.py 保存项目配置 ''' 导入Flask模块 from flask import Flask 额外安装: 数据库操作模块 from ...
- LaravelORM 中的 withSum , withAvg, withMax,withMin 的实现
Orm::withCount(['relation as relation_sum' =>function($query){ $query->select(DB::raw("su ...
- 如何利用Apifox通过签名计算及数据加解密进行用户认证接口测试?
用户注册场景:输入签名数据signature,appId,13位时间戳timestamp,6位随机数nonce,merchantId(非必填,本次不填)的请求参数发送给服务器,服务器返回响应数值后,校 ...
- Linux & 标准C语言学习 <DAY8_1>
一.进制转换 1.为什么要使用二进制.八进制.十六进制 因为目前的CPU只能识别高低两种电平,只能对二进制数据进行计算 二进制虽然能够直接被计算机识别,但是不方 ...