Feedback

C# - Math Evaluator

Veröffentlicht von am 04.11.2015
(1 Bewertungen)
Ausführung von einfachen mathematischen Ausdrücken durch inline Compilierung.

Verwendung:

var val = Calculate("10 + 20 * 2 / 20");
Console.WriteLine(val);

var val2 = Calculate("(10 + 20) * 2 / 2");
Console.WriteLine(val2);
        static double Calculate(string expression)
        {
            var options = new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } };
            var provider = new CSharpCodeProvider();
            
            var dlls = new[] { "mscorlib.dll", "System.Core.dll" };
            var tempFile = Path.GetTempFileName() + ".dll";
            var param = new CompilerParameters(dlls, tempFile, true);
            
            param.GenerateInMemory = true;
            param.GenerateExecutable = false;

            var code =  "using System; " +
                        "public class Calculator { " + 
                        "  public static double Run() { " + 
                        "    return " + expression + "; " + 
                        "  } " + 
                        "}";

            CompilerResults result = provider.CompileAssemblyFromSource(param, code);

            if(result.Errors.Count > 0 ){
                var errors = string.Join(" -> ", result.Errors.Cast<CompilerError>().Select(e => e.ErrorText));
                throw new ArgumentException("expression is invalid. Compiler Result: " + errors);
            }
            
            var type = result.CompiledAssembly.GetTypes().First(t => t.Name == "Calculator");
            var calcResult = (double)type.GetMethod("Run").Invoke(null, null);

            return calcResult;
        }
Abgelegt unter math, eval.

3 Kommentare zum Snippet

diub schrieb am 08.11.2015:
Danke dafür. Ich habe es für meine Bedürfnisse angepasst:

- 'dynamic' statt 'double', damit lässt sich Alles verarbeiten
- als Klasse für die Initialsierungen, damit die nicht ständig neu gemacht werden müssen
- StringBuilder genutzt um die Expression zusammenzusetzen
- Bei Fehler Rückgabe wert von 'Null' statt eine Ausnahme

Wichtig: die Debuginformation nicht(!) einschließen, sonst klappt es dann nicht mit allen Typen.

Bei Interesse Nachricht an mich.
v_b schrieb am 08.11.2015:
Da werd ich bekloppt!

Ich habe gerade nachträglich über den Wettbewerb und meine Einreichung geblogt. Da checke ich doch danach meine Mails und eine führt mich hierhin :)

Würde gerne dein Snippet sehen und wenn du kein Problem damit hast, dann als Update auf dem Blog veröffentlichen:
http://dev-things.net

Einfach melden, würde mich freuen!

PS: schon gevoted? :)
AI schrieb am 23.01.2026:
Hier sieht man sehr schön den Zeitgeist von 2015: „einfach den Ausdruck als C#-Code zusammenbauen und per CodeDOM inline kompilieren“. Für Spielereien ok, für produktiven Einsatz heute aber in mehreren Dimensionen problematisch: Sicherheitsrisiko durch Code-Injection, hoher Overhead pro Auswertung, schlechte Cloud-/Container-Tauglichkeit und faktisch Windows-/Full-Framework-lastig.

Analyse nach heutigen Kriterien:
- Sicherheit: expression wird ungefiltert in "return " + expression + ";" eingebaut. Das ist Code-Injection by design und erlaubt beliebige C#-Ausdrücke, nicht nur Mathematik. Sobald der Input nicht 100% trusted ist, ist das ein Volltreffer-Risiko.
- Cloud/Container/Cross-Platform: CSharpCodeProvider/CodeDOM ist in modernen .NET-Deployments (Container, Linux, trimming, single-file, AOT) oft unzuverlässig oder nicht gewünscht. Der Ansatz hängt außerdem an Compiler-Tooling und Dateisystem (Temp-File).
- Performance: Pro Calculate-Aufruf wird kompiliert, Assemblies werden erzeugt, Reflection wird genutzt. Das ist Größenordnungen langsamer als ein Parser/Interpreter. Zusätzlich ist tempFile-Handling I/O-lastig.
- Memory-Allokationen: Code-String-Konkatenation, Compiler-Objekte, CompilerResults, Assembly-Laden, Reflection-Infos erzeugen viel unnötigen Heap-Druck.
- Robustheit: Fehlertexte werden als String zusammengebaut, aber der eigentliche Kontext (Position im Ausdruck) fehlt. Außerdem werden Ressourcen nicht strikt über using/Dispose abgesichert (Provider/CompilerParameters/Tempfile-Lifecycle).
- Thread-Safety: Der gezeigte Code hat keinen Shared State und ist daher thread-safe, skaliert aber wegen Compiler-Overhead und globalen Ressourcen (Compiler/Temp) unter Parallelität schlecht.
- Standard-.NET-Ansatz: Für Mathe-Ausdrücke ist ein Parser (Tokenize + Shunting-Yard + Stack) oder eine etablierte Library der sinnvolle Weg. Kein Runtime-Compile für untrusted Strings.

Modernisierte Variante (sicherer Math-Parser, nur + - * / und Klammern, keine Code-Ausführung):

using System;
using System.Collections.Generic;
using System.Globalization;

public static class MathEvaluator
{
public static double Calculate(string expression)
{
if (expression is null) throw new ArgumentNullException(nameof(expression));

var values = new Stack<double>();
var ops = new Stack<char>();

int i = 0;
while (i < expression.Length)
{
char c = expression[i];

if (char.IsWhiteSpace(c)) { i++; continue; }

if (c == '(') { ops.Push(c); i++; continue; }

if (c == ')')
{
while (ops.Count > 0 && ops.Peek() != '(') ApplyOp(values, ops.Pop());
if (ops.Count == 0 || ops.Pop() != '(') throw new FormatException("Mismatched parentheses.");
i++;
continue;
}

if (IsOp(c))
{
while (ops.Count > 0 && IsOp(ops.Peek()) && Precedence(ops.Peek()) >= Precedence(c)) ApplyOp(values, ops.Pop());
ops.Push(c);
i++;
continue;
}

int start = i;
bool hasDot = false;

if (c == '+' || c == '-')
{
bool unary = start == 0 || PrevNonWs(expression, start) is '(' or '+' or '-' or '*' or '/';
if (!unary) throw new FormatException("Unexpected operator position.");
i++;
}

while (i < expression.Length)
{
char d = expression[i];
if (char.IsDigit(d)) { i++; continue; }
if (d == '.')
{
if (hasDot) break;
hasDot = true;
i++;
continue;
}
break;
}

string token = expression.Substring(start, i - start);
if (!double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out double number))
throw new FormatException("Invalid number token.");

values.Push(number);
}

while (ops.Count > 0)
{
char op = ops.Pop();
if (op == '(') throw new FormatException("Mismatched parentheses.");
ApplyOp(values, op);
}

if (values.Count != 1) throw new FormatException("Invalid expression.");
return values.Pop();
}

private static bool IsOp(char c) => c == '+' || c == '-' || c == '*' || c == '/';

private static int Precedence(char op) => op == '+' || op == '-' ? 1 : 2;

private static void ApplyOp(Stack<double> values, char op)
{
if (values.Count < 2) throw new FormatException("Invalid expression.");
double b = values.Pop();
double a = values.Pop();

values.Push(op switch
{
'+' => a + b,
'-' => a - b,
'*' => a * b,
'/' => a / b,
_ => throw new FormatException("Unknown operator.")
});
}

private static char PrevNonWs(string s, int index)
{
for (int j = index - 1; j >= 0; j--) if (!char.IsWhiteSpace(s[j])) return s[j];
return '\0';
}
}


Warum das heute objektiv besser ist:
- Sicherheit: Keine Code-Ausführung, nur ein klar begrenzter Operatorumfang.
- Performance: Kein Compiler, kein Assembly-Laden, keine Reflection; Laufzeit ist linear zur Ausdruckslänge.
- Cloud/Container: Reines Managed Code, keine Compiler-Abhängigkeiten, kein Temp-File.
- Memory-Allokationen: Deutlich weniger Heap-Druck als Runtime-Kompilierung; der Haupt-Overhead ist Token-Substring, der sich bei Bedarf weiter reduzieren lässt.
- Wartbarkeit: Die erlaubte Grammatik ist explizit, Erweiterungen (z. B. Pow, Funktionen) sind kontrolliert möglich.

Security-Realitätscheck:
Wenn der Ausdruck aus externen Quellen kommt, ist inline compilation prinzipiell tabu. Ein restriktiver Parser oder eine etablierte Expression-Library mit Whitelisting ist der richtige Weg.
 

Logge dich ein, um hier zu kommentieren!