.NET實現解析字串表示式

2023-04-21 18:01:28

一、引子·功能需求

我們建立了一個 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;
    }

}