Pull to refresh

Expressions в C# — impress yourself!

Reading time 9 min
Views 104K
.NET 4.0 уже не за горами и принесет кучу всего нового, нужного и не очень, крутого и суперкрутого. Однако и в старом добром .NET 3.5 есть много разных интересных фич, которые не используются в повседенвной работе, но иногда здорово облегчают жизнь разработчикам. Одна из таких замечательных штук — это Expressions.



Expressions (или, правильнее, expression trees) есть ни что иное, как дерево выражений, знакомое с университетских времен, когда вы только-только учились программировать. Вот напирмер, дерево для выражения 2+3*4.


System.Linq.Expressions, в принципе, те же самые деревья, только больше и сложнее (всего в .NET 56 различных выражений, начиная с простых математичских выражений, заканчивая инициализацией списков и вызовом методов).

Expressions можно построить двумя методами — в compile-time, и в run-time. В сompile-time компилятор сам разберет наш код и соберет из него Expression. Например, для вот такой строчки:
Expression<Func<string, int>> ex = s => s.Replace("x", "yy").Length*2;

* This source code was highlighted with Source Code Highlighter.

компилятор, на самом деле, выдаст такой код:
ParameterExpression CS$0$0000;
Expression<Func<string, int>> ex = Expression.Lambda<Func<string, int>>(Expression.Multiply(Expression.Property(Expression.Call(CS$0$0000 = Expression.Parameter(typeof(string), "s"), (MethodInfo) methodof(string.Replace), new Expression[] { Expression.Constant("x", typeof(string)), Expression.Constant("yy", typeof(string)) }), (MethodInfo) methodof(string.get_Length)), Expression.Constant(2, typeof(int))), new ParameterExpression[] { CS$0$0000 });

* This source code was highlighted with Source Code Highlighter.

Таким образом, мы получаем доступ к внутренней структуре выражения и можем сопоставить ему некий внешний язык — так, в частности, работает LINQ to SQL, который разбирает полученное выражение (я сказал, что LINQ запрос на самом деле это цепочка вызовов и тоже переводится в выражение?) и выполняет соответствующий T-SQL запрос. Я не буду рассказывать, как это делается — для этого необходима новая статья.


Я хочу рассказать про второй способ построения expressions — в run-time. Например, перед нами стоит задача написать десериалайзер, который будет собирать объект по набору пар «имя свойства»:«значение». Стандартный способ решить задачу — reflection, который, как известно, очень медленный. Вот код создателя через reflection (объяснять не буду, должно быть все понятно):
internal class ReflectionCreator<T>:ICreator<T>
{
  private readonly List<PropertyInfo> _infos;

  public ReflectionCreator()
  {
    _infos = new List<PropertyInfo>(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty));
  }
  public T Create(Dictionary<string, object> props)
  {
    var newObject = Activator.CreateInstance<T>();
    foreach (var propertyInfo in _infos)
    {
      object value;
      if (props.TryGetValue(propertyInfo.Name, out value))
      {
        propertyInfo.SetValue(newObject, value, null);
      }
    }
    return newObject;
  }
}

* This source code was highlighted with Source Code Highlighter.

Этот создатель тратит 0.001 сек на создание создателя и 1.20 сек на создание 10000 объектов (да, создание создателя...). Долго, но не смертельно. Но давайте взглянем со стороны expressions.

Рассмотрим проблему поближе. Если бы перед нами стояла задача написать десериалайзер для конкретного класса, например для класса Foo:
class Foo
{
  public string Name { get; set; }
  public int Value { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

мы бы могли просто собрать нужный нам функтор, вроде такого:
Func<Dictionary<string, object>, Foo> fooCreator =
  d => new Foo
       {
         Name = d.GetValue<string>("Name"),
         Value = d.GetValue<int>("Value")
       };

* This source code was highlighted with Source Code Highlighter.

Где GetValue() — метод-расширение для словаря:
static class DictionaryExtension
{
  public static TType GetValue<TType>(this Dictionary<string, object> d, string name)
  {
    object value;
    return d.TryGetValue(name, out value) ? (TType)value : default(TType);
  }
}


* This source code was highlighted with Source Code Highlighter.



Мы уже знаем, что в принципе любой кусок кода можно представить в виде expression tree просто путем замены Func<> на Expression<Func<>>. Давайте глянем, что нам покажет компилятор, когда мы завернем этот функтор в выражение (прошу прощения за простыню, но без нее сложно объяснить):
  1. ParameterExpression CS0 = Expression.Parameter(typeof (Dictionary<string, object>), "d");
  2. var fooCreator = Expression.Lambda<Func<Dictionary<string, object>, Foo>>
  3.   (
  4.     Expression.MemberInit
  5.     (
  6.       Expression.New
  7.       (
  8.         (ConstructorInfo)methodof(Foo..ctor),
  9.         new Expression[0]
  10.       ),
  11.       new MemberBinding[]
  12.       {
  13.         Expression.Bind
  14.         (
  15.           (MethodInfo)methodof(Foo.set_Name),
  16.           Expression.Call
  17.           (
  18.             null,
  19.             (MethodInfo)methodof(DictionaryExtension.GetValue),
  20.             new Expression[]
  21.             {
  22.               CS0,
  23.               Expression.Constant("Name",typeof(string))
  24.             }
  25.           )
  26.         ),
  27.         Expression.Bind
  28.         (
  29.           (MethodInfo)methodof(Foo.set_Value),
  30.           Expression.Call
  31.           (
  32.             null,
  33.             (MethodInfo)methodof(DictionaryExtension.GetValue),
  34.             new Expression[]
  35.             {
  36.               CS0,
  37.               Expression.Constant("Value",typeof(string))
  38.             }
  39.           )
  40.         )
  41.       }
  42.     ),
  43.     new ParameterExpression[] {CS0}
  44.   );
* This source code was highlighted with Source Code Highlighter.



Попытаемся разобраться.
1. Параметр для нашего выражения (что логично, потому что наш фуктор тоже принимает один параметр).
2. Наше выражение — это лямбда-выражение от функтора.
4. Мы инициализируем члены класса
6-10.… вызывая конструктор без парамтров
11.… и со следующим набором иницилизаторов:
13-15. Foo.Name
16.… чье значение получаем как результат вызова метода
18.… статического
19. ...DictionaryExtension.GetValue (заметьте, вызов extension-метода не отличается от вызова статического метода)
20.… с параметрами
22. ...d, который нам передадут как параметр функтора
23.… и строчным константным параметром «Name», которое есть ни что иное, как имя свойства
27-40. Все тоже самое для Foo.Value
43. Собственно, сам параметр для функтора.


Думаю, вполне понятно, что мы можем повторить этод код, только вместо хардкодного массива MemberBindings[] подставить свой, собранный для данного типа. После этого нам останется только как-то выполнить это выражение. Для этого у LambdaExpression есть замечательный метод Compile(), который скомпилирует наш функтор из данного выражения.


Вот код нашего нового создателя:
class ExpressionCreator<T> : ICreator<T>
{
  private readonly Func<Dictionary<string, object>, T> _creator;

  public ExpressionCreator()
  {
    var type = typeof(T);
    var newExpression = Expression.New(type);
    var dictParam = Expression.Parameter(typeof(Dictionary<string, object>), "d");
    var list = new List<MemberBinding>();
    var propertyInfos = type.GetProperties(BindingFlags.Instance |
                        BindingFlags.Public |
                        BindingFlags.SetProperty);
    foreach (var propertyInfo in propertyInfos)
    {
      Expression call = Expression.Call(
                         typeof (DictionaryExtension),
                         "GetValue", new[] {propertyInfo.PropertyType},
                         new Expression[]
                           {
                             dictParam,
                             Expression.Constant(propertyInfo.Name)
                           });

      MemberBinding mb = Expression.Bind(propertyInfo.GetSetMethod(), call);
      list.Add(mb);
    }

    var ex = Expression.Lambda<Func<Dictionary<string, object>, T>>(
                                      Expression.MemberInit(newExpression, list),
                                      new[] {dictParam});
    _creator = ex.Compile();
  }
  public T Create(Dictionary<string, object> props)
  {
    return _creator(props);
  }
}

* This source code was highlighted with Source Code Highlighter.

Мы, фактически, повторяем код, созданный компилятором, но динамически обрабатываем все свойства любого данного объекта. (Построение полностью аналогично разобранному выше).


Новый создатель создается 0,01 секунду (что в 10 раз медленнее, чем у reflection, но конструктор вызывается только один раз) и тратит 0,017 секунд на создание 10000 объектов (что в 70 раз быстрее).


Кстати, если создавать объект Foo напрямую
internal class DirectCreator : ICreator<Foo>
{
  public Foo Create(Dictionary<string, object> props)
  {
    return new Foo
    {
      Name = props.GetValue<string>("Name"),
      Value = props.GetValue<int>("Value")
    };
  }
}

* This source code was highlighted with Source Code Highlighter.

то это получается всего в два раза быстрее, чем через expressions.


Вот такие штуки позволяют нам делать Expression trees.


Исходный текст можно найти здесь: тыц.


Tags:
Hubs:
+27
Comments 23
Comments Comments 23

Articles