В данном случае eval — это вовсе не Eval
, который используется в дотнетовском датабиндинге. Здесь речь пойдет о том, как изобразить на .NET что-то, похожее на джаваскриптовый eval
.
А дело вот в чем. Сейчас я занимаюсь одним большим вычислительным проектом. И вот, возникла у меня следующая задача. Пользователи, которым нужно проводить эти вычисления, захотели интерфейс для того, чтобы самим править некоторые используемые формулы. Вследствие этого мне пришлось озаботиться вопросом, а как вообще делать произвольные вычисления в .NET. Чтобы взял строчку типа "2 + 2.3 * x * sin(y)", сказал, что x=3 и y=34, и посчиталось.
Ниже результаты моего ресерча. Может, кому пригодится.
muParser
Библиотека написана на C++. Имеется довольно удобная оболочка для использования данной библиотеки на .NET. Работает и на 32-битной, и на 64-битной архитектуре (правда, те dll-ки, которые шли с примером на .NET-е, были как раз 32-битными, и пришлось их перекомпилировать)
Использование
Parser evaluator = new Parser(); evaluator.SetExpr("x * 4 - sin(y)"); ParserVariable x = new ParserVariable(10.8); ParserVariable y = new ParserVariable(0.8); evaluator.DefineVar("x", x); evaluator.DefineVar("y", y); double result = evaluator.Eval();
Что хорошо
- Код хорошо читаем и очевиден.
- Поддерживается куча математических функций, в том числе и с многими переменными (нахождение максимума, минимума, среднего значения из любого количества переменных)
- Проект вполне жив и развивается. Последняя версия вышла буквально на днях.
Что плохо
Медленно работает! И я даже знаю, почему.
После беглого ознакомления с исходниками ясно, что при каждом вызове Eval
наш парсер заново разбирает выражение. Впрочем, если вычислений немного, то эту библиотеку вполне можно использовать.
Ссылки
- Сайт проекта
- Подробная статья об использовании muParser в .NET. Включает список всех поддерживаемых в выражениях функций.
CodeDOM
CodeDOM — набор инструментов для генерации кода в рантайме. Честно говоря, мне данный аспект .NET был доселе неизвестен и весьма удивил своими возможностями. С помощью средств CodeDOM можно сгенерировать прямо в коде классы, сборки, тут же подгрузить их и использовать.
Правда, из возможных задач для CodeDOM могу себе представить только генерацию классов по структуре БД (если вдруг захочется зачем-то написать самопальный ORM). Либо — моя задача с формулами.
Использование
Суть: сгенерировать сборку, содержащую класс с единственным методом Eval
, куда мы и подпихнем нашу формулу. Потом сборка подгружается, из нее берется класс и юзается.
Генерировать сборку можно несколькими способами. Рассмотрим их.
Генерация сборки из исходников
Сгенерировать сборку из исходников может следующий класс:
class CodeDomEval { CompilerResults compilerResults; /// <summary> /// Исходник, который будем компилировать /// </summary> string SourceFormat = @" using System; namespace Evaluation {{ public class Evaluator {{ public double Evaluate(double[] args) {{ return {0}; }} }} }}"; /// <summary> /// Конструктор /// </summary> /// <param name="expression">Выражение, которое будем вычислять public CodeDomEval(string expression) { Dictionary<string, string> providerOptions = new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } }; CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions); // Компиляция сборки с вычисляющим классом CompilerParameters compilerParams = CreateCompilerParameters(); string src = String.Format(SourceFormat, expression); compilerResults = provider.CompileAssemblyFromSource(compilerParams, src); // Вывод ошибок компиляции foreach (CompilerError error in compilerResults.Errors) { Console.WriteLine("ERROR: {0}", error.ErrorText); } } /// <summary> /// Метод для проведения вычисления /// </summary> /// <param name="args"> /// <returns> public double? Eval(object[] args) { if (compilerResults != null && !compilerResults.Errors.HasErrors && compilerResults.CompiledAssembly != null) { // загружаем сборку Assembly assembly = compilerResults.CompiledAssembly; Type type = assembly.GetType("Evaluation.Evaluator"); // создаем экземпляр сгенерированного класса object instance = assembly.CreateInstance("Evaluation.Evaluator"); // вызываем метод для вычисления нашей функции с заданными параметрами MethodInfo method = type.GetMethod("Evaluate"); double result = (double)method.Invoke(instance, args); // PROFIT return result; } return null; } /// <summary> /// Создание параметров компиляции /// </summary> /// <returns> CompilerParameters CreateCompilerParameters() { CompilerParameters compilerParams = new CompilerParameters() { CompilerOptions = "/target:library /optimize", GenerateExecutable = false, GenerateInMemory = true, IncludeDebugInformation = false }; compilerParams.ReferencedAssemblies.Add("mscorlib.dll"); compilerParams.ReferencedAssemblies.Add("System.dll"); return compilerParams; } }
После этого можно писать как-нибудь так:
CodeDomEval evaluator = new CodeDomEval("args[0] * 4 - Math.Sin(args[1])"); double? result = evaluator.Eval(new object[] {args});
Генерация сборки с помощью объектной модели кода
Звучит по-дурацки, но не знаю еще, как сформулировать. Просто приведу по возможности небольшой пример. Добавим в наш CodeDomEval
такие методы:
CodeNamespace BuildNamespace(string expression) { // Создаем пространство имен CodeNamespace myNamespace = new CodeNamespace("ExpressionEvaluator"); myNamespace.Imports.Add(new CodeNamespaceImport("System")); myNamespace.Imports.Add(new CodeNamespaceImport("System.Windows.Forms")); // Создаем класс CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration(); classDeclaration.IsClass = true; classDeclaration.Name = "Calculator"; classDeclaration.Attributes = MemberAttributes.Public; // Добавляем поле classDeclaration.Members.Add(new CodeMemberField(typeof(double), "result") { Attributes = MemberAttributes.Private }); // Добавляем свойство, связанное с этим полем classDeclaration.Members.Add(this.MakeProperty("Result", "result", typeof(double))); // Добавляем метод для вычисления CodeMemberMethod myMethod = new CodeMemberMethod(); myMethod.Name = "Calculate"; myMethod.ReturnType = new CodeTypeReference(typeof(double)); myMethod.Comments.Add(new CodeCommentStatement("Calculate an expression", true)); myMethod.Attributes = MemberAttributes.Public; myMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(double[]), "args")); // Добавляем в код нашу формулу myMethod.Statements.Add(new CodeAssignStatement(new CodeSnippetExpression("Result"), new CodeSnippetExpression(expression))); myMethod.Statements.Add( new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Result"))); classDeclaration.Members.Add(myMethod); // Добавляем класс в пространство имен myNamespace.Types.Add(classDeclaration); return myNamespace; } CodeMemberProperty MakeProperty(string propertyName, string internalName, Type type) { CodeMemberProperty myProperty = new CodeMemberProperty(); myProperty.Name = propertyName; myProperty.Comments.Add(new CodeCommentStatement(String.Format("The {0} property is the returned result", propertyName))); myProperty.Attributes = MemberAttributes.Public; myProperty.Type = new CodeTypeReference(type); myProperty.HasGet = true; myProperty.GetStatements.Add( new CodeMethodReturnStatement( new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), internalName))); myProperty.HasSet = true; myProperty.SetStatements.Add( new CodeAssignStatement( new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), internalName), new CodePropertySetValueReferenceExpression())); return myProperty; }
"Небольшой" не получился, но как-то так. Как ясно видно из кода, действительно создается объектная модель .NET-кода, из которой потом можно сгенерировать хоть C#-код, хоть VB (не пробовала, но подозреваю, что можно), хоть сборку. Вот так:
CompilerResults CompileAssembly(CodeNamespace ns) { CodeDomProvider codeProvider = new CSharpCodeProvider(); CompilerParameters options = CreateCompilerParameters(); CodeCompileUnit compileUnit = new CodeCompileUnit(); compileUnit.Namespaces.Add(ns); CompilerResults results = codeProvider.CompileAssemblyFromDom(options, compileUnit); if (results.Errors.HasErrors) { foreach (CompilerError error in results.Errors) Console.WriteLine("Compilation error: {0}", error.ErrorText); } return results; }
Генерация сборки из файла
Об этом я просто упомяну, подробно останавливаться не буду, тут все аналогично генерации из исходников и из объектной модели. У класса CodeDomProvider
, наследником которого являются CSharpCodeProvider
(а также CppCodeProvider
, JScriptCodeProvider
и VBCodeProvider
), есть метод CompileAssemblyFromFile
, который и должен все делать.
Что хорошо
Работает быстро. Честно говоря, даже не ожидала такого. Отмечу, что, если запускать вычисления в цикле, то первое отрабатывает за 2-3 мс, а последующие примерно за 0.02 мс.
Что плохо
- Многовато кода.
- Сложно отлаживать.
- Приходится писать формулы со всеми этими
Math.Abs
и т.п. Впрочем, автор второй из указанных статей это вроде победил.
Ссылки
Calculator.NET
На самом деле, сам Calculator.NET — это не библиотека, а проект группы LoreSoft, к которому они писали библиотеку и никак не назвали. Разработано это дело было в 2007 г., но все еще успешно используется некоторыми людьми.
Использование
Тут все не менее просто, чем в muParser.
MathEvaluator eval = new MathEvaluator(); eval.Variables.Add("a", 2); eval.Variables.Add("b", 1); eval.Evaluate("2 * a + b ^ (0,5)");
К сожалению, библиотека плохо работает с отрицательными числами: выдаются ошибки о несоответствии числа параметров. Выражение -2 * (-4)
уже не посчитаешь.
Зато есть фишка, которой нет наверно ни у кого больше. Можно конвертировать единицы измерения. Причем не только единицы длины, но и, скорости. Синтаксис такой:
eval.Evaluate("12[in->m]"); // из дюймов в метры eval.Evaluate("12[knot->kph]"); // из узлов в километры в час
Работают вычисления на удивление быстро. Если вычислять одну функцию в цикле, то в первой итерации она будет вычисляться около 22 мс, зато потом 0.06 и меньше
Что плохо
- Переменные, содержащиеся в выражении, могут содержать только буквы.
- Скупая документация. Список поддерживаемых функций удалось обнаружить только в исходном коде.
- Отрицательные числа ставят библиотеку в тупик
- Десятичным разделителем считается запятая. Или она просто из локали берется. В любом случае, в вычислениях это раздражает.
Что хорошо
- Простой синтаксис
- Быстро работает
- Конвертация единиц измерения.
Ссылки
Остальное
Честно говоря, после беглого знакомства с CodeDOM искать что-то еще стало окончательно лень. Из чувства долга я еще порыла подобные библиотеки, даже покопалась в Calculator.NET, но оказалось, что в основном они залиты не позже 2007 года и с тех пор признаков жизни не подают. Так что на этом я и остановлюсь.
В заключение приведу решение для Silverlight, найденное на StackOverflow, которое особенно меня порадовало:
double result = (double) HtmlPage.Window.Eval("15 + 35");
Сравнение производительности
Собственно, ради этого все и затевалось.
Конфигурация: Intel Core i7 860 (2.80 GHz), 4096 Мб ОЗУ
Тест первый: посчитать значение выражения a1 * 4 - sin(a2) / log10(a3 * max(a4,a5,a6,a7,a8) / (avg(a4,a5,a6,a7,a9) + exp(a10 / 10 + a11/500 + 0.001) ^ a12 ) )
(Calculator.NET не умеет искать максимум, так что у него было произведение)
Библиотека | Тест 1 (5000 испытаний) | Тест 2 | Заключение |
---|---|---|---|
muParser |
| 02:59:24 | Подходит для небольших вычислений. Для тяжелых вычислительных задач слишком медленный. |
CodeDOM |
| 00:04:58 | Подходит для любых вычислений, в том числе больших и злобных. Но требует написать довольно много кода для оболочки. |
Calculator.NET |
| Не удалось запустить из-за проблем библиотеки с отрицательными числами. | Довольно сырая библиотека. Но для ряда несложных задач вполне подойдет. |
Вроде бы в IronPython есть eval, его не пробывали использовать?
ОтветитьУдалитьНе, не пробовала. У меня все вычисления на C#, не хотелось бы в данном случае языки мешать.
ОтветитьУдалитьИ питона я не знаю.
В Java для этого специальный Scripting Framework. Выглядит как Caluclator.NET, только работает =)
ОтветитьУдалитьЯ уверен, в Microsoft.Scripting получится ничуть не хуже:
http://www.secretgeek.net/host_ironpython.asp
python просто для примера. хотя не вижу никакой разницы, на чём "писать" математические формулы. они будут одинаковы на любом языке.
Есть вот такой еще проект:
ОтветитьУдалитьhttp://www.csscript.net/
проект живет, поддерживается, обладает массой возможностей :)
http://ncalc.codeplex.com/
ОтветитьУдалитьПосмотрите Fast Lightweight Expression Evaluator
ОтветитьУдалитьСтраница проекта http://flee.codeplex.com/Wikipage
На ней есть описание и примеры.
Друзья, спасибо за ссылки. Правда, вряд ли я уже соберусь все это перекапывать, ибо моя задача уже решена.
ОтветитьУдалитьЯ бы смотрел в сторону Expression и LinqExpression. Там все это можно изящно достаточно выполнить
ОтветитьУдалитьДарья, а как ваша организация называется и чем занимается, если не секрет?
ОтветитьУдалитьСергей из Владивостока.
Я фрилансер.
ОтветитьУдалитьА данную задачу делала для одной организации, которая занимается строительством гидросооружений.
Что касается калькулятора через сборку, то я делаю так:
ОтветитьУдалить1. система на основе шаблонов генерируются C# исходники в текстовые файлы;
2. дальше компилится сборка под временным файлом;
3. сразу же на лету подгружается;
4. создается экземпляр;
5. вычисляется, если нужно.
PS: при загрузке софта, временные сборки удаляются.
К примеру, у нас шаблон:
double func(double x, double y),
тогда человек просто пишет в тексте, к примеру:
return x*Math.Sin(x*y);
Система сама создает исходит, компилит его, подключает, вычисляет при разных x,y, и выдает результаты. А основной плюс, что можно использовать полноценно C# для вычислений.
2 Анонимный №3:
ОтветитьУдалитьЯ рада, что Вы тоже используете CodeDom.
Microsoft Linq Dynamic можно погуглить. Отлично справляется с подобной задачей без лишних проблем
ОтветитьУдалить