• .NET实现解析字符串表达式


    一、引子·功能需求

    我们创建了一个 School 对象,其中包含了教师列表和学生列表。现在,我们需要计算教师平均年龄和学生平均年龄。

    //创建对象
    School school = new School()
    {
        Name = "小菜学园",
        Teachers = new List()
        {
            new Teacher() {Name="波老师",Age=26},
            new Teacher() {Name="仓老师",Age=28},
            new Teacher() {Name="悠老师",Age=30},
        },
        Students=  new List()
        {
            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.Lambdadouble>>(
        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)lambdaExpr.Compile();
    
    // 计算教师平均年龄
    var avgAge = func(teachers);

    三、介绍System.Linq.Dynamic.Core

    使用此动态 LINQ 库,我们可以执行以下操作:

    • 通过 LINQ 提供程序进行的基于字符串的动态查询。
    • 动态分析字符串以生成表达式树,例如ParseLambda和Parse方法。
    • 使用CreateType方法动态创建数据类。

    功能介绍

    普通的功能此处不赘述,如果感兴趣,可以从下文提供文档地址去寻找使用案例。

    1. 添加自定义方法类

    可以通过在静态帮助程序/实用工具类中定义一些其他逻辑来扩展动态 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 GetCustomTypes() =>
            new[] { typeof(Utils)}.ToHashSet();
    }

    文档地址

    使用项目

    四、浅析System.Linq.Dynamic.Core

    System.Linq.Dynamic.Core中 DynamicExpressionParser 和 ExpressionParser 都是用于解析字符串表达式并生成 Lambda 表达式树的类,但它们之间有一些不同之处。

    ExpressionParser 类支持解析任何合法的 C# 表达式,并生成对应的表达式树。这意味着您可以在表达式中使用各种运算符、方法调用、属性访问等特性。

    DynamicExpressionParser 类则更加灵活和通用。它支持解析任何语言的表达式,包括动态语言和自定义 DSL(领域特定语言)

    我们先看ExpressionParser这个类,它用于解析字符串表达式并生成 Lambda 表达式树。

    我只抽取重要的和自己感兴趣的属性和方法。

    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;
    
        /// 
        /// 使用TextParser将字符串解析为指定的结果类型.
        /// 
        /// 
        /// 是否创建带有相同名称的构造函数
        /// Expression
        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;
        }
    
    }

    __EOF__

  • 本文作者: Broder
  • 本文链接: https://www.cnblogs.com/Z7TS/p/17339894.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    Vue3+vite 添加router后的2个坑
    物联网时代的等保测评:保障万物互联的安全
    “蔚来杯“2022牛客暑期多校训练营10 EF题解
    AI推介-大语言模型LLMs论文速览(arXiv方向):2024.03.01-2024.03.05
    华为OD机试真题 Java 实现【数组二叉树】【2023 B卷 200分】,附详细解题思路
    C语言之预处理,头文件
    Academic accumulation|社会创业研究:过去的成就和未来的承诺
    郑州直销系统开发为什么需要不断地过滤人?
    Github 2024-07-11 开源项目日报 Top10
    【洛谷算法题】P1421-小玉买文具【入门1顺序结构】
  • 原文地址:https://www.cnblogs.com/Z7TS/p/17339894.html