11
14

More than 5 years have passed since last update.

C#:Roslynを使って数式を計算するクラスを作成してみた

Posted at

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

簡単にやっていることを書くと、こんな感じ。

  1. C#のクラス定義のテンプレートコード(文字列)を用意
  2. 引数で与えられた式をこのテンプレートコードに埋め込む
  3. このコードをCSharpSyntaxTree.ParseTextで、パースする
  4. 3.でパースした結果を Emit APIで、アセンブリにコンパイルする(メモリストリームに生成)
  5. コンパイルした結果できたアセンブリをロード
  6. リフレクション使って、ロードしたクラスのメソッドを呼び出す

これで、数式を計算させています。

型は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()削ってもエラーが出ていたような気がするんだよな。
もしかしたら、それは気のせいかもしれません。今考えると、単にちゃんとリビルドされていなかっただけかな、という気もしています。

11
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14