Albert Einstein writingПонякога ми идват странни идеи. Странни може би не е точната дума. В английския има термин определящ начина на мислене на даден човек, когато получи задача, за която знае, че всеки ще я реши по един единствен начин. Точно в такива моменти при някои хора се задейства един определен начин на мислене, който се опитва да намери различно от стандартното решение. Наричат го “thinking outside the box”. Преди няколко месеца ми се наложи да пиша програма, която да приема като вход някакъв аритметичен израз под формата на обикновен текст и после да връща резултата от него. Повечето прохождащи програмисти биха се насочили директно към Google, търсейки начин да се справят с проблема (или още по-лошо – готово решение). Добрите програмисти биха се сетили за алгоритми като shunting-yard и обратен полски запис (RPN). А аз като един много посредствен програмист реших да си реша задачата по моя си посредствен начин. Компилирах си израза run-time и оставих гениалния expression evaluator на CLR-а на .NET да ми реши проблема. Imba, а?

CSharpCodeProvider

I love C#В .NET има един много готин клас. Казва се CSharpCodeProvider и предоставя функционалност за компилиране на C# код до междинния език на CLR-а, който се казва MSIL. След това през reflection имате възможност да достъпвате елементите му (класове, интерфейси, методи и  т.н.) и съответно да си правите техни инстанции (обекти). Именно това седи в основата на моето решение на задачата. Създавам си собствен код, вмъквам в него израза, който искам да изчисля, казвам на компилатора да ми го компилира и след това просто го изпълнявам. Няма стекове, няма приоритети, няма обратен полски запис, няма автомати няма сложни дървета с операции и числа. Простичко компилиране на код от любимия ни C# ;)

Реализация

Lamp ideaЕто сега ще видите и кода, който прави магията, описана по-горе. Първо си дефинирам едно изключение, което да ме информира, че израза не е коректен. Можете да си предефинирате конструктора, свойствата Message, InnerException и т.н. Аз съм го оставил празен, защото за целите на демото не ми трябва конкретната грешка, а само факта, че се е случила.

using System;

namespace ExpressionEvaluation
{
    public class IncorrectExpressionException :
        Exception
    {

    }
}

След това идва ред на класа, който върши основната работа. Това което прави е да получи израза под формата на string, да го обработи, да го пусне през компилатора и после с рефлекшън да го изпълни и да вземе резултата, който всъщност ни трябва.

using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;

namespace ExpressionEvaluation
{
    public class ExpressionEvaluator
    {
        private const string codeFormat = @"
        using System;
        namespace ExpressionEvaluation
        {
            public class EvaluatorHelper
            {
                public double Evaluate()
                {
                    return {0};
                }
            }
        }";
        private CSharpCodeProvider cSharpCodeProvider;
        private CompilerParameters cp;

        private string PrepareExpression(string expression)
        {
            StringBuilder expressionBuilder =
                new StringBuilder(expression);
            expressionBuilder.Replace("sqrt", "Math.Sqrt");
            expressionBuilder.Replace("ln", "Math.Log");
            expressionBuilder.Replace("pow", "Math.Pow");
            //return string.Format(codeFormat, expressionBuilder);
            return codeFormat.Replace("{0}",
                expressionBuilder.ToString());
        }

        public ExpressionEvaluator()
        {
            cSharpCodeProvider = new CSharpCodeProvider();
            cp = new CompilerParameters();
            cp.ReferencedAssemblies.Add("system.dll");
            cp.GenerateInMemory = true;
        }

        public bool TryEvaluate(string expression,
            out double result)
        {
            try
            {
                result = this.Evaluate(expression);
                return true;
            }
            catch (IncorrectExpressionException)
            {
                result = 0;
                return false;
            }
        }

        public double Evaluate(string expression)
        {
            try
            {
                expression = this.PrepareExpression(expression);
                CompilerResults compilerResults =
                    cSharpCodeProvider.
                    CompileAssemblyFromSource(cp, expression);
                Assembly assembly =
                    compilerResults.CompiledAssembly;
                object instance = assembly.CreateInstance(
                    "ExpressionEvaluation.EvaluatorHelper");
                object invokeResult = instance.GetType().
                    GetMethod("Evaluate").Invoke(instance, null);
                double result = 0;
                double.TryParse(
                    invokeResult.ToString(), out result);
                return result;
            }
            catch
            {
                throw new IncorrectExpressionException();
            }
        }
    }
}

И накрая едно конзолно приложение, което показва как се използва класа. То чете израз от конзолата и изписва отново на конзолата получения резултат.

using System;

namespace ExpressionEvaluation
{
    class ExpressionEvaluatorDemo
    {
        static void Main()
        {
            string expression = Console.ReadLine();
            ExpressionEvaluator expressionEvaluator =
                new ExpressionEvaluator();
            double result = 0;
            if (expressionEvaluator.TryEvaluate(expression,
                out result))
            {
                Console.WriteLine(result);
            }
            else
            {
                Console.WriteLine("Incorrect expression!");
            }
        }
    }
}

Заключение

Monkey thinkingКаква е целта на този пост? Целта е да ви покажа, че когато ви дадат наглед проста задача с ясно детерминирано решение, винаги можете да излезете от рамките на това, което другите ще направят. Да излезете рамките на елементарното решение и да си измислите нещо различно, нещо собствено, нещо гениално. Моето решение на тази задача не е нито най-бързото, нито най-правилното, но със сигурност е най-краткото и което е по-важно най-различното. Няма как да измислите нещо готино, ако правите нещата като останалите, нали? Винаги се стремете да си решавате задачите по своя си уникален начин и ще видите как с всеки изминал ден ще ви идват все по-странни и все по-интересни идеи. А това е важно за вас, ако искате да станете новия Mark Zuckerberg ;)