Pull to refresh

Вычисляем динамические выражения на C# 4.0 с помощью dynamic

Reading time12 min
Views2.8K
Здравствуйте.
Вчера я опубликовал на Хабре перевод статьи об одной из новых возможностей четвертой версии C# 4.0 – ключевого слова dynamic. В комментариях развернулась бурная дискуссия, основными мотивами которой было две вещи: производительность динамиков и область их применения. В этой статье я не буду затрагивать первый вопрос, а попытаюсь привести пример того, как новая возможность позволяет решить вполне реальную задачу за пару часов с минимальными усилиями.


Предыстория


Для лучшего понимания задачи я опишу вам в двух словах свой первый коммерческий проект, которым я с коллегами занимался два года назад. Перед нами стояла задача реализовать на C# 2.0 своеобразный классификатор объектов, который давал ответ на вопрос, отвечает ли некий документ (пускай это будет что-то вроде налоговой декларации) определенному критерию. Критерий был сложным предикатом, который формировался офицерами гос.службы, ответственной за обработку этих документов и помогал выделять в потоке документов те, на которые службе стоило обратить более пристальное внимание. Ну а зачем им обращать внимание я вам рассказывать не буду, впрочем, догадаться не сложно :) Итак, выражение набивалось офицером через специальный редактор на аяксе, после чего могло использоватся двумя способами: применятся в рантайме для проверки объекта в памяти или же генерировать SQL-запрос, который позволял выбрать объекты, отвечающие критерию, из базы данных. Первый режим впоследствии убрали из системы, но я до сих пор помню, насколько сложно и одновременно интересно было его реализовать. Тогда на все про все у нас ушло несколько месяцев. Благодаря новым возможностям языка C# 4.0 мне удалось воссоздать подобный (хотя, конечно, значительно более примитивный) классификатор объектов буквально за пару часов.

Реализация


Итак, приступим. Как известно, любое выражение является по своей сути деревом, узлами которого выступают константы, переменные, операторы и функции. Наш классификатор будет таким деревом, которое будет принимать объект и давать ответ, принадлежит ли наш объект к данному классу.
Сначала выделим базовый класс для вершины дерева выражения:
  public abstract class ExpressionNode
  {
    public abstract dynamic Evaluate();

    public virtual void HandleObject(object obj)
    {
    }

    public abstract IEnumerable<ExpressionNode> GetChildren();
  }


* This source code was highlighted with Source Code Highlighter.

Метод Evaluate вычисляет значение для данной вершины и всего поддерева, вершиной которого она является. Обратите внимание, что значение, возвращаемое этим методом, имеет динамический тип. HandleObject позволяет обработать объект до вычисления, чтобы запомнить нужные данные. GetChildren используется для перемещения по дереву.
Класс для констант:
  public class Constant : ExpressionNode
  {
    private readonly dynamic _value;

    public Constant(dynamic value)
    {
      _value = value;
    }

    public override dynamic Evaluate()
    {
      return _value;
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      yield break;
    }
  }


* This source code was highlighted with Source Code Highlighter.

Константа это очень простая вершина выражения, которая хранит в себе значение и возвращает его при необходимости.
Класс, обеспечивающий обращение к свойствам обрабатываемого объекта (переменным нашего выражения):
  public class ObjectProperty : ExpressionNode
  {
    private dynamic _value = null;
    private readonly string _name;
    private bool _propertyNotFound;

    public ObjectProperty(string name)
    {
      _name = name;
    }

    public override void HandleObject(object obj)
    {
      try
      {
        _value = obj.GetType().GetProperty(_name).GetValue(obj, new object[0]);
      }
      catch (Exception ex)
      {
        _propertyNotFound = true;
        _value = null;
      }
    }

    public override dynamic Evaluate()
    {
      if (_propertyNotFound)
      {
        throw new PropertyNotFoundException();
      }
      return _value;
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      yield break;
    }
  }

* This source code was highlighted with Source Code Highlighter.

Очень напоминает константу, но значение вычисляется для каждого нового объекта во время его обработки. Обратите внимание, что если у объекта отсутствует свойство, то исключение будет выброшено только на этапе вычисления, а не на этапе обработки. Эта особенность позволяет создавать сложные выражения с ленивыми вычислениями.
Для проверки того, существует ли свойство у объекта, используется вот такая функция:
  public class HasPropertyChecker : ExpressionNode
  {
    private readonly string _name;
    private bool _result;

    public HasPropertyChecker(string name)
    {
      _name = name;
    }

    public override dynamic Evaluate()
    {
      return _result;
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      yield break;
    }

    public override void HandleObject(object obj)
    {
      _result = obj.GetType().GetProperty(_name) != null;
    }
  }

* This source code was highlighted with Source Code Highlighter.

Остальные вершины выражения – операторы, могут иметь три типа: унарный, бинарный и агрегатор.
Базовый класс для унарного:
  public abstract class UnaryOperator : ExpressionNode
  {
    private readonly ExpressionNode _argument;
    protected UnaryOperator(ExpressionNode arg)
    {
      _argument = arg;
    }

    protected abstract dynamic EvaluateUnary(dynamic arg);

    public override dynamic Evaluate()
    {
      return EvaluateUnary(_argument.Evaluate());
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      yield return _argument;
    }
  }

* This source code was highlighted with Source Code Highlighter.

И самый простой пример такого оператора, логическое отрицание:
  [Operator("not")]
  public class Not : UnaryOperator
  {
    public Not(params ExpressionNode[] args) : this(args[0]) { Debug.Assert(args.Length == 1); }
    public Not(ExpressionNode arg) : base(arg) { }

    protected override dynamic EvaluateUnary(dynamic arg)
    {
      return !arg;
    }
  }


* This source code was highlighted with Source Code Highlighter.

Заметьте, оператор помечен специальным атрибутом, который определяет его имя. Оно пригодится потом, для формирования дерева на основе XML.
Базовый бинарный оператор и его пример, оператор сложения:
  public abstract class BinaryOperator : ExpressionNode
  {
    private readonly ExpressionNode _argument1;
    private readonly ExpressionNode _argument2;

    protected BinaryOperator(ExpressionNode arg1, ExpressionNode arg2)
    {
      _argument1 = arg1;
      _argument2 = arg2;
    }

    protected abstract dynamic EvaluateBinary(dynamic arg1, dynamic arg2);

    public override dynamic Evaluate()
    {
      return EvaluateBinary(_argument1.Evaluate(), _argument2.Evaluate());
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      yield return _argument1;
      yield return _argument2;
    }
  }

  [Operator("add")]
  public class Add : BinaryOperator
  {
    public Add(params ExpressionNode[] args) : this(args[0], args[1]) { Debug.Assert(args.Length == 2); }
    public Add(ExpressionNode arg1, ExpressionNode arg2) : base(arg1, arg2) { }

    protected override dynamic EvaluateBinary(dynamic arg1, dynamic arg2)
    {
      return arg1 + arg2;
    }
  }

* This source code was highlighted with Source Code Highlighter.

И, наконец, базовый оператор агрегации и его пример, логическое «И»:
  public abstract class AgregateOperator : ExpressionNode
  {
    private readonly ExpressionNode[] _arguments;

    protected AgregateOperator(params ExpressionNode[] args)
    {
      _arguments = args;
    }

    protected abstract dynamic EvaluateAgregate(IEnumerable<dynamic> args);

    public override dynamic Evaluate()
    {
      return EvaluateAgregate(_arguments.ConvertAll(x => x.Evaluate()));
    }

    public override IEnumerable<ExpressionNode> GetChildren()
    {
      return _arguments;
    }
  }

  [Operator("and")]
  public class And : AgregateOperator
  {
    public And(params ExpressionNode[] args) : base(args) { }

    protected override dynamic EvaluateAgregate(IEnumerable<dynamic> args)
    {
      foreach(dynamic arg in args)
      {
        if (!arg)
        {
          return arg;
        }
      }
      return true;
    }
  }

* This source code was highlighted with Source Code Highlighter.

Все дерево (в виде ссылки на вершину-корень) хранится в классе-обертке:
  public class BooleanExpressionTree
  {
    public BooleanExpressionTree(ExpressionNode root)
    {
      Root = root;
    }

    public ExpressionNode Root { get; private set; }

    public bool EvaluateOnObject(dynamic obj)
    {
      lock (this)
      {
        PrepareTree(obj, Root);
        return (bool)Root.Evaluate();
      }
    }

    private void PrepareTree(dynamic obj, ExpressionNode node)
    {
      node.HandleObject(obj);
      foreach (ExpressionNode child in node.GetChildren())
      {
        PrepareTree(obj, child);
      }
    }
  }

* This source code was highlighted with Source Code Highlighter.

Сами выражения хранятся в виде XML и генерируются в рантайме. Кому интересно, как это происходит, можете посмотреть исходники – там все тривиально.

Тестовые объекты


Итак, для тестирования я подготовил несколько разношерстных классов (да, они странные, но, поверьте, с ними интереснее, чем с налоговыми декларациями):
  public class Human
  {
    public object Legs { get { return new object(); } }
    public object Hands { get { return new object(); } }
    public int LegsCount { get { return 2; } }
    public int HandsCount { get { return 2; } }
    public string Name { get; set; }
  }

  public class OldMan : Human
  {
    public DateTime BirthDate { get { return new DateTime(1933, 1, 1); } }
  }

  public class Baby : Human
  {
    public DateTime BirthDate { get { return new DateTime(2009, 1, 1); } }
  }

  public class Animal
  {
    public int LegsCount { get { return 4; } }
    public object Tail { get { return new object(); } }
    public string Name { get; set; }
  }

  public class Dog : Animal
  {
    public DateTime BirthDate { get; set; }
  }

* This source code was highlighted with Source Code Highlighter.

Создаем экземпляры тестовых объектов:
    static IEnumerable<object> PrepareTestObjects()
    {
      List<object> objects = new List<object>();
      objects.Add(new Human { Name = "Some Stranger" });
      objects.Add(new OldMan { Name = "Ivan Petrov" });
      objects.Add(new OldMan { Name = "John Smith" });
      objects.Add(new Baby { Name = "Vasya Pupkin" });
      objects.Add(new Baby { Name = "Bart Simpson" });
      objects.Add(new Dog { Name = "Sharik", BirthDate = new DateTime(2004, 11, 11) });
      objects.Add(new Dog { Name = "Old Zhuchka", BirthDate = new DateTime(1900, 11, 11) });
      return objects;
    }

* This source code was highlighted with Source Code Highlighter.


Выражения


Примеры выражений, использованные при тестировании:
IsHuman:
<and>
 <has-property name="Hands"/>
 <has-property name="Legs"/>
 <not>
  <has-property name="Tail"/>
 </not>
 <equals>
  <property name="HandsCount"/>
  <constant value="2" type="int"/>
 </equals>
 <equals>
  <property name="LegsCount"/>
  <constant value="2" type="int"/>
 </equals>
</and>
IsNotAmerican:
<and>
 <has-property name="Name"/>
 <or>
  <like>
   <property name="Name" />
   <constant value="Ivan" type="string"/>
  </like>
  <like>
   <property name="Name" />
   <constant value="Vasya" type="string"/>
  </like>
 </or>
</and>
IsOld:
<and>
 <has-property name="BirthDate"/>
 <gt>
  <sub>
   <constant value="2009-12-23" type="datetime" />
   <property name="BirthDate"/>
  </sub>
  <constant value="22000.00:00:00" type="timespan"/>
 </gt>
</and>

* This source code was highlighted with Source Code Highlighter.

Думаю, по внешнему виду XML понятно, что проверяют эти выражения (они нарочно примитивны, как и сами объекты, чтобы не утомлять читателей излишней сложностью). Вот как эти выражения классифицируют сгенерированные объекты:
Test Is Human:
Some Stranger — True
Ivan Petrov — True
John Smith — True
Vasya Pupkin — True
Bart Simpson — True
Sharik — False
Old Zhuchka — False
Test Is Old:
Some Stranger — False
Ivan Petrov — True
John Smith — True
Vasya Pupkin — False
Bart Simpson — False
Sharik — False
Old Zhuchka — True
Test Is Not American:
Some Stranger — False
Ivan Petrov — True
John Smith — False
Vasya Pupkin — True
Bart Simpson — False
Sharik — False
Old Zhuchka — False


Выводы


Динамики привнесли в C# совершенно новые возможности, которые позволяют быстро, используя минимум кода, реализовать ряд нетривиальных задач, например вычисление динамических выражений. За эту возможность часто приходится платить производительностью, но иногда ею стоит пренебречь, получив взамен компактный, хорошо читаемый код, который легко отладить и поддерживать. Конечно, в данном случае можно обойтись стандартными средствами рефлексии, применить кодогенерацию или же вообще использовать другой, возможно изначально динамический язык (тот же Python), но, ИМХО, использование динамиков в данном случае – самое простое и красивое решение.
Скачать код можно тут (60 Кб).
Progg it

Tags:
Hubs:
+6
Comments7

Articles