• 用Roslyn玩转代码之一: 解析与执行字符串表达式


      最近框架中的可视化界面设计需要使用到表达式引擎(解析代码字符串并动态执行),之前旧框架的实现是将表达式字符串解析为语法树后解释执行该表达式,本文介绍如何使用Roslyn解析表达式字符串,并直接转换为Linq的表达式后编译执行。

    一、语法(Syntax)与语义(Semantic)

      C#的代码通过Roslyn解析为相应的语法树,并且利用语义分析可以获取语法节点所对应的符号及类型信息,这样利用这些信息可以正确的转换为Linq的表达式。这里作者就不展开了,可以参考Roslyn文档。

    二、实现表达式解析器(ExpressionParser)

    1. 解析字符串方法

      下面开始创建一个类库工程,引用包Microsoft.CodeAnalysis.CSharp.Features,然后参照以下代码创建ExpressionParser类, 静态ParseCode()方法是解析字符串表达式的入口:

    using System.Linq.Expressions;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    using Microsoft.CodeAnalysis.CSharp.Syntax;
    
    namespace ExpEngine;
    
    public sealed class ExpressionParser : CSharpSyntaxVisitor<Expression>
    {
        private ExpressionParser(SemanticModel semanticModel)
        {
            _semanticModel = semanticModel;
        }
    
        private readonly SemanticModel _semanticModel;
    
        /// 
        /// 解析表达式字符串转换为Linq的表达式
        /// 
        public static Expression ParseCode(string code)
        {
            var parseOptions = new CSharpParseOptions().WithLanguageVersion(LanguageVersion.CSharp11);
            var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
                .WithNullableContextOptions(NullableContextOptions.Enable);
    
            var tree = CSharpSyntaxTree.ParseText(code, parseOptions);
            var root = tree.GetCompilationUnitRoot();
            var compilation = CSharpCompilation.Create("Expression", options: compilationOptions)
                .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
                .AddSyntaxTrees(tree);
            var semanticModel = compilation.GetSemanticModel(tree);
            //检查是否存在语义错误
            var diagnostics = semanticModel.GetDiagnostics();
            var errors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
            if (errors > 0)
                throw new Exception("表达式存在语义错误");
    
            var methodDecl = root.DescendantNodes().OfType().First();
            if (methodDecl.Body != null && methodDecl.Body.Statements.Count > 1)
                throw new NotImplementedException("Parse block body");
    
            if (methodDecl.ExpressionBody != null)
                throw new NotImplementedException("Parse expression body");
    
            var firstStatement = methodDecl.Body!.Statements.FirstOrDefault();
            if (firstStatement is not ReturnStatementSyntax returnNode)
                throw new Exception("表达式方法不是单行返回语句");
    
            var parser = new ExpressionParser(semanticModel);
            return parser.Visit(returnNode.Expression)!;
        }
    }
    

    2. 解析运行时类型的方法

      因为转换过程中需要将Roslyn解析出来的类型信息转换为对应的C#运行时的类型,所以需要实现类型转换的方法:

        private readonly Dictionary<string, Type> _knownTypes = new()
        {
            { "bool", typeof(bool) },
            { "byte", typeof(byte) },
            { "sbyte", typeof(sbyte) },
            { "short", typeof(short) },
            { "ushort", typeof(ushort) },
            { "int", typeof(int) },
            { "uint", typeof(uint) },
            { "long", typeof(long) },
            { "ulong", typeof(ulong) },
            { "float", typeof(float) },
            { "double", typeof(double) },
            { "char", typeof(char) },
            { "string", typeof(string) },
            { "object", typeof(object) },
        };
    
        /// 
        /// 根据类型字符串获取运行时类型
        /// 
        private Type ResolveType(string typeName)
        {
            if (_knownTypes.TryGetValue(typeName, out var sysType))
                return sysType;
    
            //通过反射获取类型
            var type = Type.GetType(typeName);
            if (type == null)
                throw new Exception($"Can't find type: {typeName} ");
    
            return type;
        }
    

    3. 解析各类语法节点转换为对应的Linq表达式

      这里举一个简单的LiteralExpression转换的例子,其他请参考源码。需要注意的是Linq的表达式严格匹配类型签名,比如方法调用object.Equals(object a, object b), 如果参数a是int类型,需要使用Expression.Convert(int, typeof(object))转换为相应的类型。

        private Type? GetConvertedType(SyntaxNode node)
        {
            var typeInfo = _semanticModel.GetTypeInfo(node);
            Type? convertedType = null;
            if (!SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType))
                convertedType = ResolveType((INamedTypeSymbol)typeInfo.ConvertedType!);
    
            return convertedType;
        }
    
        public override Expression? VisitLiteralExpression(LiteralExpressionSyntax node)
        {
            var convertedType = GetConvertedType(node);
            var res = Expression.Constant(node.Token.Value);
            return convertedType == null ? res : Expression.Convert(res, convertedType);
        }
    

    三、测试解析与执行表达式

      现在可以创建一个单元测试项目验证一下解析字符串表达式并执行了,当然实际应用过程中应缓存解析并编译的表达式委托:

    namespace UnitTests;
    
    using static ExpEngine.ExpressionParser;
    
    public class Tests
    {
        [Test]
        public void StaticPropertyTest() => Assert.True(Run<object>("DateTime.Today") is DateTime);
    
        [Test]
        public void InstancePropertyTest() => Run<int>("DateTime.Today.Year");
    
        [Test]
        public void MethodCallTest1() => Run("DateTime.Today.AddDays(1 + 1)");
    
        [Test]
        public void MethodCallTest2() => Run("DateTime.Today.AddDays(DateTime.Today.Year)");
    
        [Test]
        public void MethodCallTest3() => Run("DateTime.Today.AddDays(int.Parse(\"1\"))");
    
        [Test]
        public void MethodCallTest4() => Assert.True(Run<bool>("Equals(new DateTime(1977,3,1), new DateTime(1977,3,1))"));
    
        [Test]
        public void PrefixUnaryTest() => Run("DateTime.Today.AddDays(-1)");
    
        [Test]
        public void NewTest() => Assert.True(Run("new DateTime(1977,3,16)") == new DateTime(1977, 3, 16));
    
        [Test]
        public void BinaryTest1() => Assert.True(Run<float>("3 + 2.6f") == 3 + 2.6f);
    
        [Test]
        public void BinaryTest2() => Assert.True(Run<bool>("3 >= 2.6f"));
    }
    

    四、 一些限制与TODO

      Linq的表达式本身存在一些限制,请参考文档:
    https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/

      另上述代码仅示例,比如表达式输入参数等未实现,小伙伴们可以继续自行完善。

  • 相关阅读:
    Java学习笔记3.4.3 static关键字 - 静态代码块
    华为诺亚ViG架构媲美CNN、Transformer,图神经网络也能用作CV骨干模型
    算法通过村第十七关-贪心|白银笔记|贪心高频问题
    常德市2022年成人高考疫情防控的重要提醒
    day61
    Web3.0带来天翻地覆的变化?全面科普!所谓的Web3.0到底是什么?
    树莓派系统压缩备份实操
    HotSpot垃圾算法实现之枚举根节点和安全点安全区域
    【数据结构阶级】链表面试题(万字详解带你手撕链表)
    《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(9)-Fiddler如何设置捕获Https会话
  • 原文地址:https://www.cnblogs.com/BaiCai/p/17952057