Как скомпилировать и выполнить пользовательскую формулу динамически (во время выполнения) в C#?
Я хочу написать кусок кода на C#, который способен компилировать и выполнять пользовательские формулы, введенные в виде строки (действительный код C#) в форме окна. Есть ли простой и элегантный способ сделать это?
Как пример, см. Метод ниже:
public double UserDefinedFormula(double x, double y, string str)
{
double z;
// BEGIN: Formula code provided by user as string
if (str == "sum")
z = x + y;
else
z = Math.Sqrt(x + y + 5);
// END: Formula code provided by user as string
return z;
}
Строка формулы, введенная пользователем, может содержать все, что угодно, при условии, что это допустимый код C#. В приведенном выше примере пользователь вводит следующую строку в текстовую область формы параметра:
if (str == "sum")
z = x + y;
else
z = Math.Sqrt(x + y + 5);
3 ответа
Вы можете использовать этот фрагмент кода:
[Obfuscation(Exclude = true, ApplyToMembers = true)]
public interface IXyFunction<TInput, TOutput>
{
TOutput Run(TInput x, TInput y);
}
public static class CodeProvider
{
public static string LastError { get; private set; }
private static int counter;
public static IXyFunction<TInput, TOutput> Generate<TInput, TOutput>(string code)
{
return Generate<TInput, TOutput>(code, null);
}
public static IXyFunction<TInput, TOutput> Generate<TInput, TOutput>(string code, string[] assemblies)
{
if (String.IsNullOrEmpty(code))
throw new ArgumentNullException("code");
const string ERROR = "Error(s) while compiling";
string className = "_generated_" + counter++;
string typeInName = typeof(TInput).FullName;
string typeOutName = typeof(TOutput).FullName;
string namespaceName = typeof(CodeProvider).Namespace;
string fullClassName = namespaceName + "." + className;
LastError = String.Empty;
CSharpCodeProvider codeCompiler = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v3.5" } });
CompilerParameters parameters = new CompilerParameters(assemblies)
{
GenerateExecutable = false,
GenerateInMemory = true,
CompilerOptions = "/optimize"
};
string path = Assembly.GetExecutingAssembly().Location;
parameters.ReferencedAssemblies.Add(path);
if (typeof(CodeProvider).Assembly.Location != path)
parameters.ReferencedAssemblies.Add(typeof(CodeProvider).Assembly.Location);
string executerName = typeof(IXyFunction<TInput, TOutput>).FullName;
executerName = executerName.Substring(0, executerName.IndexOf('`'));
code = @" using System;
namespace " + namespaceName + @"
{
public class " + className + @" : " + executerName + "<" + typeInName + ", " + typeOutName + @">
{
public " + typeOutName + @" Run(" + typeInName + @" x, " + typeInName + @" y)
{"
+ code + @"
}
}
}";
CompilerResults results = codeCompiler.CompileAssemblyFromSource(parameters, code);
if (results.Errors.HasErrors)
{
System.Text.StringBuilder err = new System.Text.StringBuilder(512);
foreach (CompilerError error in results.Errors)
err.Append(string.Format("Line: {0:d}, Error: {1}\r\n", error.Line, error.ErrorText));
Console.WriteLine(err);
LastError = err.ToString();
return null;
}
object objMacro = results.CompiledAssembly.CreateInstance(fullClassName);
if (objMacro == null)
throw new ApplicationException(ERROR + " class " + className);
return (IXyFunction<TInput, TOutput>)objMacro;
}
}
Использование:
IXyFunction<int, int> dynMethod = CodeProvider.Generate<int, int>("return x + y;");
Console.WriteLine(dynMethod.Run(5, 10));
Я использую этот фрагмент в решении.NET 3.5, если вы используете другую версию, настройте codeCompiler
переменная.
Ты можешь использовать CodeDOM
скомпилировать код для вас, а затем запустить его с помощью отражения.
// create compiler
CodeDomProvider provider = CSharpCodeProvider.CreateProvider("C#");
CompilerParameters options = new CompilerParameters();
// add more references if needed
options.ReferencedAssemblies.Add("system.dll");
options.GenerateExecutable = false;
options.GenerateInMemory = true;
// compile the code
string source = ""; // put here source
CompilerResults result = provider.CompileAssemblyFromSource(options, source);
if (!result.Errors.HasErrors)
{
Assembly assembly = result.CompiledAssembly;
// instance can be saved and then reused whenever you need to run the code
var instance = assembly.CreateInstance("Bla.Blabla");
// running some method
MethodInfo method = instance.GetType().GetMethod("Test"));
var result = (bool)method.Invoke(null, new object[] {});
// untested, but may works too
// dynamic instance = assembly.CreateInstance("Bla.Blabla");
// var result = instance.Test();
}
Дело в том, чтобы создать source
должным образом. Это может быть так просто, как
using System;
namespace Bla
{
public class Blabla
{
public static int Test()
{
return 1 + 2;
}
}
}
Который является строкой в одну строку
using System;namespace Bla {открытый класс Blabla { public static int Test() { return 1+2; }}}
Вы можете иметь параметры для предварительно скомпилированных функций или иметь ряд таких функций
public static double Test(double x, double y)
{
return x + y;
}
и вызвать это так
var result = (double)method.Invoke(null, new object[] {x, y});
Вдохновленный ответами, я решил настроить Eval Function для моего случая, чтобы получить следующее решение:
// user defined function: public double UserFunc(double x, double y, string str)
public static object UserDefinedFunc(string UserCode, object[] Parameters)
{
CSharpCodeProvider c = new CSharpCodeProvider();
ICodeCompiler icc = c.CreateCompiler();
CompilerParameters cp = new CompilerParameters();
cp.ReferencedAssemblies.Add("system.dll");
cp.CompilerOptions = "/t:library";
cp.GenerateInMemory = true;
StringBuilder sb = new StringBuilder("");
sb.Append("using System;\n");
sb.Append("namespace CSCodeEvaler{ \n");
sb.Append("public class CSCodeEvaler{ \n");
// start function envelope
sb.Append("public double UserFunc(double x, double y, string str){ \n");
sb.Append("double z; \n");
// enveloped user code
sb.Append(UserCode + "\n");
// close function envelope
sb.Append("return z; \n");
sb.Append("} \n");
sb.Append("} \n");
sb.Append("}\n");
CompilerResults cr = icc.CompileAssemblyFromSource(cp, sb.ToString());
if (cr.Errors.Count > 0)
{
MessageBox.Show("ERROR: " + cr.Errors[0].ErrorText,
"Error evaluating cs code", MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
System.Reflection.Assembly a = cr.CompiledAssembly;
object o = a.CreateInstance("CSCodeEvaler.CSCodeEvaler");
Type t = o.GetType();
MethodInfo mi = t.GetMethod("UserFunc");
object s = mi.Invoke(o, Parameters);
return s;
}
Вышеуказанный метод оценки может быть вызван следующим образом:
// user defined function entered as text
string code_user_func = @"if (str == ""sum"")
z = x + y;
else
z = Math.Sqrt(x + y + 5);";
// Parameter values
object [] Parameters = new object[] { 5.2, 6.5, "sum" };
// call evaluation method
double r = (double) UserDefinedFunc(code_user_func, Parameters);
// show result
MessageBox.Show("Result with UserDefinedFunc: " + r);