Expressions в C# — impress yourself!

    .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.


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


    Поделиться публикацией

    Комментарии 23

      –3
      Забавные «выражения».
        +2
        Expression мощный инструмент, только как он с GC работает? С одной стороны, в .net нет инструментов для выгрузки неиспользуемого кода. Если подгрузил assembly, то и будет в памяти болтаться, а с другой можно динамически создавать Expression, потом их, опять же, динамически компилировать, использовать и забывать (сдавать в GC) и проблем с утечкой памяти вроде как нет.
        Как это реализовано поверх MSIL 2.0 для меня осталось загадкой :)
          0
          Вот такой вот код:
          int s = 0;
          for(int i=0;i<int.MaxValue;i++)
          {
              int i1 = i;
              Expression<Func<int, int>> x = j => j + i1;
              Func<int, int> f = x.Compile();
              s += f(i);
              if (i % 100 == 0)
              {
                  Console.WriteLine(s);
                  GC.Collect();
              }
          }
          Console.WriteLine(s);
          


          стабильно держится на 15 мегабайтах и не растет уже минут 10. А как сделано — тут я пас.
            0
            В Expression идет значение i1 или i1 идет как замыкание? Если как замыкание, то возможно используется кэширование.
              0
              i1 замыкается, но каждый цикл замыкается новая переменная, так что вроде все честно.
              +1
              Вот такое изменение вашего кода поможет вам понять, как это сделано:

              Assembly [] b = AppDomain.CurrentDomain.GetAssemblies();
              Console.WriteLine(«Before = » + b.Length);

              int s = 0;
              for (int i = 0; i < int.MaxValue; i++)
              {
                int i1 = i;
                 Expression<Func<int, int>> x = j => j + i1;
                 Func<int, int> f = x.Compile();

                 Assembly[] a = AppDomain.CurrentDomain.GetAssemblies();
                 Console.WriteLine(«After = » + a.Length);

                 s += f(i);
                 if (i % 100 == 0)
                 {
                   Console.WriteLine(s);
                   GC.Collect();
                 }
              }
              +1
              Как это нету возможности? В .net есть возможность выгружать динамически код — создаете Assembly, вгружаете в нее то, что надо, отрабатывает, выгружаете весь домен
                +3
                … Виноват, описАлся — «создаете Assembly» следует читать как «создаете AppDomain»
                  0
                  вот, вот, и я о том же…
                +1
                  +1
                  Expressions не в пример проще для создания, нежели Emit
                0
                Хорошая техника! Аналогичный подход используется в ASP.NET MVC 2 RC (например ActionMethodDispatcher.cs)
                  0
                  аналогичный подход используется когда SQL запросы надо оптимизировать. Смотришь как делает LINQ2SQL, разбираешься почему именно так(если надо) и делаешь на основе этого новый запрос, но с учетом твоей логике.
                  0
                  Только не говорите, что это очень крутая штука для создания своего ORM =)

                  Кстати, кому интересно, недавно рылся в блогах MSDN и нашел некого meek'а. У него крутая статья по
                  деревьям выражений

                    0
                    Кто-то же делает ORM, ведь «не на деревьях растут» :)
                      0
                      У нас используется генерация asp страниц из xml — там используется рефлекшн, думаем перейти на expressions.
                    0
                      0
                      Интересно с каких пор появился оператор methodof?
                      Эрик Липерт писал, что такого оператора у нас не будет даже в C# 4 — слишком дорого проектировать, тестировать, сопровождать (((
                        0
                        Оператора нет, а IL вызов есть. Именно такой код показывает Reflector.
                          0
                          Да, знаю. Просто очень жаль, что нет такого оператора в C#.
                        0
                        А смысл иметь оператор methodof? Сделайте свой статический класс хелпер со статическим методов methodof(Type type, String method_name, BindingAttributes attr) и юзайте на здоровье. Если же считаете, что оператор будет работать быстрее такого метода (типа нативная поддержка, как typeof), то возникает вопрос: зачем выигрышь в 10 микросекунд? Или Вы собираетесь миллион раз вызывать этот оператор? (что какбы редко бывает и не стоит делать)
                          0
                          Смысл — в строгой типизации. Если бы был оператор methodof, то, например, при рефакторинге я бы не заботился о том, что надо еще поменять имена функций в строковых константах. Поэтому, например, я предпочитаю использовать вот такой вот метод:
                          private static string GetMethodName<T>(Expression<Action<T>> action)
                          {
                          	Expression body = action.Body;
                          	var mce = body as MethodCallExpression;
                          	if (mce != null)
                          		return mce.Method.Name;
                          	return null;
                          }
                          

                          Который потом вызывается, например, так:
                          string beginMethodName = GetMethodName<Stream>(s => s.Read(null, 0, 0));
                          
                          0
                          ExpressionTrees — крутая штука =)
                          Чешутся руки что-нибудь сделать с помощью них, а работа все время отнимает…

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое