用Roslyn玩轉代碼之一: 解析與執行字符串表達式

??最近框架中的可視化界面設計需要使用到表達式引擎(解析代碼字符串并動態執行),之前舊框架的實現是將表達式字符串解析為語法樹后解釋執行該表達式,本文介紹如何使用Roslyn解析表達式字符串,并直接轉換為Linq的表達式后編譯執行。
一、語法(Syntax)與語義(Semantic)
??C#的代碼通過Roslyn解析為相應的語法樹,并且利用語義分析可以獲取語法節點所對應的符號及類型信息,這樣利用這些信息可以正確的轉換為Linq的表達式。這里作者就不展開了,可以參考Roslyn文檔。
- 語法分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-analysis - 語義分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/semantic-analysis - 語法轉換
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-transformation
二、實現表達式解析器(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;
/// <summary>
/// 解析表達式字符串轉換為Linq的表達式
/// </summary>
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<MethodDeclarationSyntax>().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) },
};
/// <summary>
/// 根據類型字符串獲取運行時類型
/// </summary>
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>("DateTime.Today.AddDays(1 + 1)");
[Test]
public void MethodCallTest2() => Run<DateTime>("DateTime.Today.AddDays(DateTime.Today.Year)");
[Test]
public void MethodCallTest3() => Run<DateTime>("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>("DateTime.Today.AddDays(-1)");
[Test]
public void NewTest() => Assert.True(Run<DateTime>("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/
??另上述代碼僅示例,比如表達式輸入參數等未實現,小伙伴們可以繼續自行完善。

浙公網安備 33010602011771號