首页 > 其他分享 >.NET实现解析字符串表达式

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

时间:2023-04-21 15:24:25浏览次数:59  
标签:private 字符串 new NET 解析 Expression 表达式

一、引子·功能需求

我们创建了一个 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方法动态创建数据类。

功能介绍

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

  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<Type> 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;

    /// <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;
    }

}

标签:private,字符串,new,NET,解析,Expression,表达式
From: https://www.cnblogs.com/Z7TS/p/17339894.html

相关文章

  • CentOS网卡无法启动返回'Failed to start LSB:Bring up/down networking.'
    装了一台虚机,配置docker服务的时候发现忘了开CPU虚拟化,关机开启后再登录,发现网卡down了,重启网卡报错。1.journalctl-ex  #查看日志,发现返回错误'FailedtostartLSB:Bringup/downnetworking.';2.vi/var/long/messages  #再查看系统日志,发现有关于NetworkManager的信......
  • 微软的ADO.NET帮助类SqlHelper.cs
    微软的ADO.NET帮助类,SqlHelper.cs 1//===============================================================================2//MicrosoftDataAccessApplicationBlockfor.NET3//http://msdn.microsoft.com/library/en-us/dnbda/html/daab-rm.asp4//......
  • 正则表达式获取字符串中电话号码的方式
    我一开始是想找数字1然后切出11位数字这样但是newbing给了一个简单了当的方法正则表达式直接切11位数字分享出来以供参考/***@Description:正则表达式寻找字符串中的电话号码*@paramstring有11位电话存在的字段*@author:@NewBing**/......
  • iOS:常用的正则表达式
    转载自Swift正则表达式-简书(jianshu.com)数字:^[0-9]*$n位的数字:^\d{n}$至少n位的数字:^\d{n,}$m-n位的数字:^\d{m,n}$零和非零开头的数字:^(0|[1-9][0-9]*)$非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?......
  • [2core]条形码+ZXing.NET+SkiaSharp
    在将验证码功能代码从.netframework迁移到.net6后,马上就想到了条形码(1D和2D)的迁移事项,主要它们两者都涉及到图片处理方面的知识和技术。由于之前使用了类库ZXing.NET,因此在.net6中也使用了它。但是如果想要跨平台使用,就需要和图片处理的类库建立绑定关系,所以就选择了其支持的Skia......
  • js 实现字符串反转
    1.情景展示在JavaScript当中,如何实现字符串倒转(倒置、反转)?2.具体分析数组Array实现元素倒转,有专门的函数reserve(),我们直接调用即可。为了使用这个功能,我们可以把字符串先拆分成数组,然后,再调用反转函数,最后再拼成字符串。3.解决方案以字符串:Marydon的博客园为例进行说明。......
  • ASP.NET点击按钮回车提交web页面回车提交点击回车按钮提交
    ASP.NET回车提交事件其实说到底并不是ASP.NET的编程问题,却是关于htmlform中的submit按钮就是如何规划的具体讨论。也可归于ASP.NET编程的一部分,那么ASP.NET回车提交事件的具体实现是怎么样的呢?下面我们具体的看下:ASP.NET回车提交事件实现1、当你的光标焦点进入某个表单元素......
  • [转]MySQL怎么将字符串转为datetime类型
    原文地址:https://zhuanlan.zhihu.com/p/553928079以前只知道第一种方法:方法1:使用str_to_date()函数str_to_date()是专门的字符串转日期函数,可以将字符串转换为日期时间值。str_to_date(str,format)str:必须项。要格式化为日期的字符串format:必须项。要使用的格式。例......
  • 如何在 .NET Core WebApi 中处理 MultipartFormDataContent 中的文件
    在上一篇文章(如何在.NETCoreWebApi中处理MultipartFormDataContent)中,我们有描述过如何以最简单的方式在.NETCoreWebApi中处理MultipartFormDataContent。基于框架层面的封装,我们可以快速的从Request.Form中分别拿到文件内容和文本内容,但是这些默认的解析方式都是建......
  • net core 6 部署到ubuntu
    一、安装dotnetSDK 1.更新源sudoapt-getupdate;2.启用MicrosoftPPAwgethttps://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.debsudodpkg-ipackages-microsoft-prod.deb3.安装.NETCoreSDKsudoaptinstallapt-transport-ht......