「C#:逆ポーランド記法を利用した数式の計算(6)」では、逆ポーランド記法を利用して、数式の計算を行うコードを示しましたが、こんどはRoslyn使ってコードをコンパイルし、それを実行することで、数式を計算するクラスを作成してみました。
準備
準備として、Microsoft.CodeAnalysis.CSharp をNuGetからインストールします。
.csprojファイルには、以下のような記述が追加されます。
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
</ItemGroup>
ちなみに、.NET Core2.0で動作を確認しています。たぶん、そのまま .NET Framework4.6 とかの環境でも動くと思います。
数式を計算するExpressionクラス
早速、数式を計算するExpressionクラスのコードを示します。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
...
public class Expression {
private const string _sourceCode = @"
using System;
namespace Gushwell {{
public class ExpressionCalculator {{
public object Calculate() {{
return {0};
}}
}}
}}";
public static object Calculate(string exp) {
var sourceCode = CreateSourceCode(exp);
var compilation = CreateCompilation(sourceCode);
return RunCode(compilation);
}
private static object RunCode(CSharpCompilation compilation) {
using (var ms = new MemoryStream()) {
EmitResult result = compilation.Emit(ms);
if (result.Success) {
ms.Seek(0, SeekOrigin.Begin);
var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
var instance = assembly.CreateInstance("Gushwell.ExpressionCalculator");
var type = assembly.GetType("Gushwell.ExpressionCalculator");
var method = type.GetMember("Calculate").First() as MethodInfo;
var ans = method.Invoke(instance, null);
return ans;
} else {
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
string message = "";
foreach (var diagnostic in failures) {
message += $"\t{diagnostic.Id}: {diagnostic.GetMessage()}";
}
throw new InvalidOperationException(message);
}
}
}
private static CSharpCompilation CreateCompilation(string sourceCode) {
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
string assemblyName = Path.GetRandomFileName();
var references = new MetadataReference[] {
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)
};
var compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
return compilation;
}
private static string CreateSourceCode(string exp) {
return string.Format(_sourceCode, exp);
}
}
簡単にやっていることを書くと、こんな感じ。
- C#のクラス定義のテンプレートコード(文字列)を用意
- 引数で与えられた式をこのテンプレートコードに埋め込む
- このコードをCSharpSyntaxTree.ParseTextで、パースする
- 3.でパースした結果を Emit APIで、アセンブリにコンパイルする(メモリストリームに生成)
- コンパイルした結果できたアセンブリをロード
- リフレクション使って、ロードしたクラスのメソッドを呼び出す
これで、数式を計算させています。
型はobjectにしていますので、必要ならば、呼び出した側で適切な型にキャストする必要があります。boxing発生してしまいますが、特定の型に固定してしまうのもどうなんだろう? ということで、objectとしました。
なお、このコードは、Compiling C# Code Into Memory and Executing It with Roslynを参考にしています。っていうか、ロジックは、ほぼそのまんまです。
これを、数式計算用として再利用しやすいようにクラスとしてきちんとコードにしたのが、このExpressionクラスです。
Expression.Calculateメソッドを呼び出してみる
Expression.Calculateメソッドを呼び出すテストコードを書いてみます。
public class Program {
public static void Main(string[] args) {
var x1 = Expression.Calculate("((1 + 2) * (3 + 4)) / 3");
Console.WriteLine(x1);
var x2 = Expression.Calculate("(1+2+3+4+5+6+7+8+9+10)/10.0");
Console.WriteLine(x2);
}
}
実行結果
7
5.5
と意図通りの結果が出ました。
独り言
ところで、これを作成している途中、
dotnet ExpSample.dll
って、実行すると以下のエラーが出てたんですが、いつのまにか出なくなってました。
CS0103: The name 'Console' does not exist in the current context
なぜ、このエラーがでたのか、どうしてエラーが消えたのか原因がわかりません。
確実にわかっているのは、動的にコンパイルするコードの中(ここでは、_sourceCodeの中)に、Console.WriteLine()
呼び出すコードがあると、.NET Coreでは、このエラーが出るということです。
でも、Console.WriteLine()
削ってもエラーが出ていたような気がするんだよな。
もしかしたら、それは気のせいかもしれません。今考えると、単にちゃんとリビルドされていなかっただけかな、という気もしています。