Tuesday, July 29, 2014

Under the hood Expression in C#


Сегодня мы с вами снова окунемся в волшебный мир языка C#. Чем больше и глубже я изучаю этот язык, тем больше он мне нравится. Вы, наверное, слыхали один из популярных вопросов, которые задают на собеседовании: "На сколько по десятибалльной шкале вы оцениваете свои знания?". Обычно за ним следует следующий вопрос о том, какие приобретенные знания помогли перейти с предыдущего уровня на текущий. Впервые я услышал о таком вопросе от Сергея Теплякова, известного MVP в Украине. Это хороший вопрос, но не для собеседования, как по мне. Этот вопрос хорош тем, что он позволяет взглянуть на себя с другой стороны. 
Я бы распределил иерархию в этой шкале по такому принципу: на первом месте с заслуженной "десяткой" − папа языка C# Андерс Хайлсберг; затем Скит, Рихтер, Барт де Смет и другие топ-авторы, которые разделяют оценку "9" этого пьедестала. Себе обычно в таком марафоне я выделяю 6-7 баллов. Эта градация позволяет взглянуть на подход к изучению языка как продвижение по ступенькам и развития себя как профессионала. 
Вернемся к нашей теме. Сегодня мы сменим перспективу взгляда на мир деревьев выражений Expression. Отправной точкой написания материала послужила достойная статья по expression автора Барта де Смета "Expression Trees, Take Two Introducing System Expressions". Назвать статью Барта простой для понимания сложно, но в ней настолько глубоко рассматривается expression, что не восхищаться этим невозможно.
Давайте рассмотрим классический пример (похожий разбор примера я увидел в блоге MSDN, после того как набросал свой) суммирования двух чисел. Классический функтор для этого будет иметь следующий вид:
Func<intintint> sum = (a, b) => a + b;

Console.WriteLine(sum(3, 4));
Теперь посмотрим, как это преобразуется в дерево выражений.
Expression<Func<intintint>> expression = (a, b) => a + b;

Console.WriteLine(expression.Compile().Invoke(3,4));
Отличие первой строки кода от второй, по сути, − только в том, что у нас во втором варианте добавлено ключевое слово Expression, которое полностью изменяет логику. Эти две строки кода не равны между собой. Концептуально Expression<Func<T>> полностью отличается от Func<T>. 
Func<T> представляет собой делегат, который в базовом приближении можно рассматривать как некоего рода безопасный указатель на метод. Expression<Func<T>> организовывается в структуру данных дерева для лямбда-выражения. Эта структура дерева описывает, что лямбда-выражение будет с конкретным выражением. Например, это дерево может содержать в себе информацию о переменных, методах вызова, константных значениях и т.д. То есть, мы можем сами скомпоновать то выражение, которое максимально подходит нам, и преобразовать его к актуальному методу, используя метод Expression.Compile(). Действие для получения лямбда-выражения как анонимного метода с дерева выражения происходит во время компиляции. То есть, у вас во время компиляции доступна проверка типов и т.д.
Для того чтобы посмотреть, какое дерево нам будет построено, мы можем запустить ExpressionTreeVisualizer с примеров, которые можно скачать по ссылке для Visual Studio 2008. Там мы сможем увидеть следующий результат:

Это дерево можем расписать так, чтобы был понятен принцип работы.
  1. Получаем с помощью метода Expression.Parameter аргумент a.
  2. Получаем с помощью метода Expression.Parameter аргумент b.
  3. Методом Expression.Add суммируем параметры с шагов 1 и 2.
  4. Приводим это все к Expression.Lambda, в котором первым аргументом передаем результат выполнения метода 3, а вторым аргументом  − массив входных аргументов с шага 1 и 2.
Вот как это все будет выглядеть:
Expression<Func<intintint>> expression = (a, b) => a + b;

BinaryExpression body = (BinaryExpression)expression.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ParameterExpression right = (ParameterExpression)body.Right;

Console.WriteLine(expression.Body);
Console.WriteLine(" The left part of the expression: " +
    "{0}{4} The NodeType: {1}{4} The right part: {2}{4} The Type: {3}{4}",
    left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);
После запуска на экране мы получим следующий результат:
Поскольку мы разобрали, в какое дерево выражений интерпретируется наш код, давайте построим сами это дерево выражений в обратном порядке.
var argA = Expression.Parameter(typeof(int), "a"); //step 1
var argB = Expression.Parameter(typeof(int), "b"); //step 2
var expressionFunc = Expression.Lambda<Func<intintint>>
    (
        Expression.Add(argA, argB), //step 3
        new[] { argA, argB } //step 4
    ).Compile();

Console.WriteLine(expressionFunc(3, 4));

Выше вы можете увидеть в виде комментариев, как будет пошагово выполнен наш результат. 
Давайте еще немного попрактикуемся с нашими деревьями выражений. Допустим, мы имеем следующую функцию:
public bool CompareNumber(int n)
{
    if (n > 5)
        return true;
    return false;
}
Если мы хотим получить функтор с этого метода, можно написать следующим образом:
Func<intbool> compareNumber = n =>
    {
        if (n > 5)
            return true;
        return false;
    };
Но над таким выражением мы не сможем написать Expression,  как в примере в начале главы. Такой код у вас попросту не скомпилируется.
Expression<Func<intbool>> expression = n =>
    {
        if (n > 5)
            return true;
        return false;
    };
Но ваш код скомпилируется и выполнится, если переписать его так:
Func<intbool> compareNumber = n =>
    {
        if (n > 5)
            return true;
        return false;
    };

Expression<Func<intbool>> expression = n => compareNumber(n);

Давайте посмотрим, как наше дерево выражений будет построено в нашем визуализаторе.
Это не то, что бы мы хотели увидеть. Поэтому напишем наше дерево выражений сами, поскольку это не так сложно, как кажется на первый взгляд. Наше дерево выражений будет состоять из следующих этапов:
А теперь распишем эти все этапы в коде.
LabelTarget returnTarget = Expression.Label(typeof(bool));
ParameterExpression argN = Expression.Parameter(typeof(int), "n");
Expression test = Expression.GreaterThan(argN, Expression.Constant(5));
Expression iftrue = Expression.Return(returnTarget, Expression.Constant(true));
Expression iffalse = Expression.Return(returnTarget, Expression.Constant(false));
Expression ifThenElse = Expression.IfThenElse(test, iftrue, iffalse);

var ex = Expression.Block(
    ifThenElse,
    Expression.Label(returnTarget, Expression.Constant(false)));

var compiled = Expression.Lambda<Func<intbool>>(
    ex,
    new[] { argN }
).Compile();
Выше мы можем увидеть один в один те шаги, которые показаны на рисунке. Если мы добавим проверку на то, работает ли наш код,
Console.WriteLine(compiled(5));     // prints "False"
Console.WriteLine(compiled(6));     // prints "True"
то сможем увидеть результат на экране:
Данный тип деревьев, которые мы использовали, называются деревьями сравнений (Comparison Expression).
Полный список мы можем посмотреть по ссылке #BinaryExpression.cs. Всего у нас существует 85 типов Expressions, которые мы можем посмотреть по ссылке #ExpressionType.cs. Деревья выражений очень сильно изменились с версии 3.5 фреймворка. Я взял код у Барта де Смета, чтобы получить список изменений между версиями 3.5 -> 4.0->4.5. Данный код представлен ниже.
var ops = from m in typeof(Expression).GetMethods()
            where m.IsStatic
            group m by m.Name into g
            orderby g.Key
            select new { g.Key, Overloads = g.Count() }.ToString();

foreach (var op in ops)
{
    Console.WriteLine(op);
}
Интересный факт: в деревьях выражений ничего не прибавилось в версии фреймворка 4.5. То есть, версия фреймворка 4.0 и версия 4.5 идентичные. А если сравнивать с версией 3.5 то у нас добавилось 69 новых выражений, и изменилось 6. В версии 3.5, у нас было доступно 57 деревьев выражений, в версии 4.0 их стало 120. Неплохое прибавление, согласитесь. Ниже будет приведена схема изменений в 4-м фреймворке, где черным цветом обозначены деревья выражений, которые остались без изменений в 3.5 фреймфорке, желтым  то, что изменилось в новой версии, и зеленым − то что было добавлено в новой версии фреймворка.
{ Key = Add, Overloads = 2 }
{ Key = AddAssign, Overloads = 3 }
{ Key = AddAssignChecked, Overloads = 3 }
{ Key = AddChecked, Overloads = 2 }
{ Key = And, Overloads = 2 }
{ Key = AndAlso, Overloads = 2 }
{ Key = AndAssign, Overloads = 3 }
{ Key = ArrayAccess, Overloads = 2 }
{ Key = ArrayIndex, Overloads = 3 }
{ Key = ArrayLength, Overloads = 1 }
{ Key = Assign, Overloads = 1 }
{ Key = Bind, Overloads = 2 }
{ Key = Block, Overloads = 12 }
{ Key = Break, Overloads = 4 }
{ Key = Call, Overloads = 14 }
{ Key = Catch, Overloads = 4 }
{ Key = ClearDebugInfo, Overloads = 1 }
{ Key = Coalesce, Overloads = 2 }
{ Key = Condition, Overloads = 2 }
{ Key = Constant, Overloads = 2 }
{ Key = Continue, Overloads = 2 }
{ Key = Convert, Overloads = 2 }
{ Key = ConvertChecked, Overloads = 2 }
{ Key = DebugInfo, Overloads = 1 }
{ Key = Decrement, Overloads = 2 }
{ Key = Default, Overloads = 1 }
{ Key = Divide, Overloads = 2 }
{ Key = DivideAssign, Overloads = 3 }
{ Key = Dynamic, Overloads = 6 }
{ Key = ElementInit, Overloads = 2 }
{ Key = Empty, Overloads = 1 }
{ Key = Equal, Overloads = 2 }
{ Key = ExclusiveOr, Overloads = 2 }
{ Key = ExclusiveOrAssign, Overloads = 3 }
{ Key = Field, Overloads = 3 }
{ Key = GetActionType, Overloads = 1 }
{ Key = GetDelegateType, Overloads = 1 }
{ Key = GetFuncType, Overloads = 1 }
{ Key = Goto, Overloads = 4 }
{ Key = GreaterThan, Overloads = 2 }
{ Key = GreaterThanOrEqual, Overloads = 2 }
{ Key = IfThen, Overloads = 1 }
{ Key = IfThenElse, Overloads = 1 }
{ Key = Increment, Overloads = 2 }
{ Key = Invoke, Overloads = 2 }
{ Key = IsFalse, Overloads = 2 }
{ Key = IsTrue, Overloads = 2 }
{ Key = Label, Overloads = 6 }
{ Key = Lambda, Overloads = 18 }
{ Key = LeftShift, Overloads = 2 }
{ Key = LeftShiftAssign, Overloads = 3 }
{ Key = LessThan, Overloads = 2 }
{ Key = LessThanOrEqual, Overloads = 2 }
{ Key = ListBind, Overloads = 4 }
{ Key = ListInit, Overloads = 6 }
{ Key = Loop, Overloads = 3 }
{ Key = MakeBinary, Overloads = 3 }
{ Key = MakeCatchBlock, Overloads = 1 }
{ Key = MakeDynamic, Overloads = 6 }
{ Key = MakeGoto, Overloads = 1 }
{ Key = MakeIndex, Overloads = 1 }
{ Key = MakeMemberAccess, Overloads = 1 }
{ Key = MakeTry, Overloads = 1 }
{ Key = MakeUnary, Overloads = 2 }
{ Key = MemberBind, Overloads = 4 }
{ Key = MemberInit, Overloads = 2 }
{ Key = Modulo, Overloads = 2 }
{ Key = ModuloAssign, Overloads = 3 }
{ Key = Multiply, Overloads = 2 }
{ Key = MultiplyAssign, Overloads = 3 }
{ Key = MultiplyAssignChecked, Overloads = 3 }
{ Key = MultiplyChecked, Overloads = 2 }
{ Key = Negate, Overloads = 2 }
{ Key = NegateChecked, Overloads = 2 }
{ Key = New, Overloads = 6 }
{ Key = NewArrayBounds, Overloads = 2 }
{ Key = NewArrayInit, Overloads = 2 }
{ Key = Not, Overloads = 2 }
{ Key = NotEqual, Overloads = 2 }
{ Key = OnesComplement, Overloads = 2 }
{ Key = Or, Overloads = 2 }
{ Key = OrAssign, Overloads = 3 }
{ Key = OrElse, Overloads = 2 }
{ Key = Parameter, Overloads = 2 }
{ Key = PostDecrementAssign, Overloads = 2 }
{ Key = PostIncrementAssign, Overloads = 2 }
{ Key = Power, Overloads = 2 }
{ Key = PowerAssign, Overloads = 3 }
{ Key = PreDecrementAssign, Overloads = 2 }
{ Key = PreIncrementAssign, Overloads = 2 }
{ Key = Property, Overloads = 7 }
{ Key = PropertyOrField, Overloads = 1 }
{ Key = Quote, Overloads = 1 }
{ Key = ReferenceEqual, Overloads = 1 }
{ Key = ReferenceNotEqual, Overloads = 1 }
{ Key = Rethrow, Overloads = 2 }
{ Key = Return, Overloads = 4 }
{ Key = RightShift, Overloads = 2 }
{ Key = RightShiftAssign, Overloads = 3 }
{ Key = RuntimeVariables, Overloads = 2 }
{ Key = Subtract, Overloads = 2 }
{ Key = SubtractAssign, Overloads = 3 }
{ Key = SubtractAssignChecked, Overloads = 3 }
{ Key = SubtractChecked, Overloads = 2 }
{ Key = Switch, Overloads = 6 }
{ Key = SwitchCase, Overloads = 2 }
{ Key = SymbolDocument, Overloads = 4 }
{ Key = Throw, Overloads = 2 }
{ Key = TryCatch, Overloads = 1 }
{ Key = TryCatchFinally, Overloads = 1 }
{ Key = TryFault, Overloads = 1 }
{ Key = TryFinally, Overloads = 1 }
{ Key = TryGetActionType, Overloads = 1 }
{ Key = TryGetFuncType, Overloads = 1 }
{ Key = TypeAs, Overloads = 1 }
{ Key = TypeEqual, Overloads = 1 }
{ Key = TypeIs, Overloads = 1 }
{ Key = UnaryPlus, Overloads = 2 }
{ Key = Unbox, Overloads = 1 }
{ Key = Variable, Overloads = 2 }
Если мы посмотрим мельком на данную таблицу, то увидим много методов с приставкой Assign, которые позволяют сразу присвоить значение чему-то. Пример:
var variableExpr = Expression.Parameter(typeof (string), "hello");
var assignExpr = Expression.Assign(variableExpr, Expression.Constant("Hello World"));
var blockExpr = Expression.Block(
    new[] { variableExpr},
    assignExpr
    );
Console.WriteLine(assignExpr.ToString());
Console.WriteLine(Expression.Lambda<Func<String>>(blockExpr).Compile()());
После запуска мы увидим результат на экране.

Итоги
В этой статье мы рассмотрели, как писать простые сценарии с помощью деревьев выражений. Надеюсь, статья позволит вам понять, как строить самостоятельно как минимум простые деревья выражений. В следующей статье мы постараемся более детально остановится на деревьях выражений и написать примеры для каждой из приведенных выше операций для 120 разных выражений, а также напишем таблицу соответствия деревьев выражений C# коду.

No comments:

Post a Comment