Sunday, August 10, 2014

Deep understanding expression. Part 4

Эта статья является заключительной в серии статьей по деревьям выражений, в которой я постарался описать функции, доступные в Expression. Мы рассмотрим оставшуюся часть функций, которые не успели рассмотреть в предыдущих статьях. Продолжим экскурсию в мир деревьев выражений.
Or/OrAssign
Данные методы представляют собой побитовою операцию или (OR). В C# представлено оператором "|".
Ниже приведена таблица соответствия.
Пример в виде таблицы:
Пример:
static void Main(string[] args)
{
    Expression orExpr = Expression.Or(Expression.Constant(5), Expression.Constant(3));
    Console.WriteLine(Expression.Lambda<Func<int>>(orExpr).Compile()());
}
Результат:
7
OrElse
Функция OrElse являет собой дерево выражений в виде условного оператора "или" и проверяет второй операнд, если результат сравнения первого операнда была ложь (false).
Пример:
static void Main(string[] args)
{
    Expression orElseExpr = Expression.OrElse(
        Expression.Constant(false),
        Expression.Constant(true)
    );

    // Print out the expression.
    Console.WriteLine(orElseExpr.ToString());

    // The following statement first creates an expression tree,
    // then compiles it, and then executes it.
    Console.WriteLine(Expression.Lambda<Func<bool>>(orElseExpr).Compile()());
}
Результат:
(False OrElse True)
True
Parameter
Функция Expression.Parameter создает ParameterExpression, который можно использовать для идентификация параметра функции или переменной в дереве выражений. До этого мы уже множество раз рассматривали использование функции Parameter. По логике работы с данной функцией ее использование можно разделить на две части: явное использование и неявное.

Пример:
static void Main(string[] args)
{
    //Impicit using
    Expression<Func<int, int, int>> sumExpr = (a, b) => a + b; //a and b is ParameterExpressions
    Console.WriteLine(sumExpr.Compile().Invoke(3, 4));

    //Explicit using
    ParameterExpression aParam = Expression.Parameter(typeof (int), "a");
    ParameterExpression bParam = Expression.Parameter(typeof(int), "b");
    Expression addExpr = Expression.AddAssign(aParam, bParam);

    var lambdaExpr = Expression.Lambda<Func<int, int, int>>(addExpr,
        new[] {aParam, bParam}
        ).Compile();

    Console.WriteLine(lambdaExpr.Invoke(5, 6));
}
Результат:
7
11
PostDecrementAssign/PostIncrementAssign
Функция PostDecrementAssign создает UnaryExpression, который предоставляет выражение уменьшения исходного выражения на единицу. Функция PostIncrementAssign, наоборот, увеличивает исходное значение на единицу. Это, по сути, аналогично постинкрементной и постдекрементной операции в C#.
Поэтому, как можно понять с таблицы выше, часто такую форму записи используют, например, в циклах, когда нужно что-то просуммировать и сохранить в переменной. По сути, эти две формы записи позволяют эмулировать работу цикла for с помощью деревьев выражений. Я даже не смог придумать пример, чтобы продемонстрировать постинкрементную операцию без цикла. Вы, надеюсь, помните заковыристые вопросы на собеседовании, наподобие такого кода и вопроса, который звучит примерно следующим образом: "Что будет выведено на экран в результате выполнения данного кода?".
int i = 5;
i = i++;
Console.WriteLine(i);
Это вопрос с подвохом, и обычно он включает в себя знание принципа работы постфиксной и префиксной нотации. Мы не будем углубляться в тему постинкрементной и постдекрементной записи, а также почему пример выше выведет значение 5, а не 6, как, возможно, некоторые ожидали. Но если вы поищите по ключевым словам "Преинкремент и Постинкремент", то вы сможете понять, в чем разница.
Пример:
static void Main(string[] args)
        {
            ParameterExpression value = Expression.Parameter(typeof(int), "value");

            // Creating an expression to hold a local variable.
            ParameterExpression result = Expression.Parameter(typeof(int), "result");

            ParameterExpression localValue = Expression.Parameter(typeof(int), "n");

            // Creating a label to jump to from a loop.
            LabelTarget label = Expression.Label(typeof(int));

            LabelTarget endLoop = Expression.Label();

            // Creating a method body. PostIncrementAssign
            Expression blockPostIncrement = Expression.Block(
                new[] { result },
                Expression.Assign(result, Expression.Constant(0)),
                Expression.Block(
                    new[] { localValue },
                    Expression.Assign(localValue, Expression.Constant(1)),
                    Expression.Loop(
                        Expression.Block(
                            Expression.IfThen(
                                Expression.Not(
                                    Expression.LessThanOrEqual(localValue, value)),
                                Expression.Break(endLoop)),
                            Expression.AddAssign(result, localValue),
                            Expression.PostIncrementAssign(localValue)),
                        endLoop)),
                result);

            // Creating a method body. PostDecrementAssign
            BlockExpression blockPostDecrement = Expression.Block(
                new[] { result },
                Expression.Assign(result, Expression.Constant(1)),
                    Expression.Loop(
                       Expression.IfThenElse(
                           Expression.GreaterThan(value, Expression.Constant(1)),
                           Expression.AddAssign(result,
                               Expression.PostDecrementAssign(value)),
                           Expression.Break(label, result)
                       ),
                   label
                )
            );

            // Compile and run an expression tree.
            int sum1 = Expression.Lambda<Func<int, int>>(blockPostDecrement, value).Compile()(5);

            int sum2 = Expression.Lambda<Func<int, int>>(blockPostIncrement, value).Compile()(5);

            Console.WriteLine(sum1);
            Console.WriteLine(sum2);

            Console.ReadLine();
        }
Результат:
15
15
Теперь рассмотрим, что приведено в этом примере. Первым делом мы обратим внимание на то, что же такого написано в примере с PostIncrementAssign. С помощью делегата пример выше можно было бы переписать следующим образом:
Func<int, int> sumDecrementFunc = value =>
{
    int result = 1;
    while (value > 1)
    {
        result += value--;
    }

    return result;
};

Console.WriteLine(sumDecrementFunc(5));

Func<int, int> sumFunc = value =>
{
    int result = 0;
    for (int n = 1; n <= value; n++)
        result += n;
    return result;
};

Console.WriteLine(sumFunc(5));
Этот код делает то же самое, что мы сделали с помощью дерева выражений. Думаю, особой сложности пример не должен вызвать, тем более, ниже приведено, как это может интерпретироваться в делегат.
Power/PowerAssign
Функция Power и PowerAssign позволяет создать BinaryExpression для возведения числа в степень.
Пример:
static void Main(string[] args)
{
    Expression powerExpr = Expression.Power(Expression.Constant(2.0), Expression.Constant(3.0));
    Console.WriteLine(Expression.Lambda<Func<double>>(powerExpr).Compile()());
    Console.ReadLine();
}

Результат:
8
Здесь нет никакой мистики и используется функция Math.Pow, которая достается через рефлексию.
public static BinaryExpression Power(Expression left, Expression right, MethodInfo method)
{
    RequiresCanRead(left, "left");
    RequiresCanRead(right, "right");
    if (method == null)
    {
        Type mathType = typeof(System.Math);
        method = mathType.GetMethod("Pow"BindingFlags.Static | BindingFlags.Public);
        if (method == null)
        {
            throw Error.BinaryOperatorNotDefined(ExpressionType.Power, left.Type, right.Type);
        }
    }
    return GetMethodBasedBinaryOperator(ExpressionType.Power, left, right, method, true);
}

PreIncrementAssign/PreDecrementAssign
Метод PreIncrementAssign увеличивает выражение на 1 и присваивает результирующее выражение в переменную. PreDecrementAssign, наоборот, уменьшает значение на единицу. В языке C# существует преинкрементная и предекрементная форма записи, которая показана на таблице ниже.
Пример:
static void Main(string[] args)
{
    ParameterExpression parameter = Expression.Parameter(typeof (int), "n");
    Expression preDecrementAssign = Expression.PreDecrementAssign(parameter);
    Expression preIncrementAssign = Expression.PreIncrementAssign(parameter);

    Console.WriteLine(Expression.Lambda<Func<intint>>(preDecrementAssign, parameter).Compile()(5));
    Console.WriteLine(Expression.Lambda<Func<intint>>(preIncrementAssign, parameter).Compile()(5));
    Console.ReadLine();
}

Результат:
4
6
Property/PropertyOrField
Метод Expression.Property позволяет получить доступ к проперти поля. Метод PropertyOrField позволяет также получить доступ к свойству или поля класса, только данный метод для обращения к свойству использует функцию Property.
public static MemberExpression PropertyOrField(Expression expression, string propertyOrFieldName)
{
    RequiresCanRead(expression, "expression");
    // bind to public names first
    PropertyInfo pi = expression.Type.GetProperty(propertyOrFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);
    if (pi != null)
        return Property(expression, pi);
    FieldInfo fi = expression.Type.GetField(propertyOrFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);
    if (fi != null)
        return Field(expression, fi);
    pi = expression.Type.GetProperty(propertyOrFieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);
    if (pi != null)
        return Property(expression, pi);
    fi = expression.Type.GetField(propertyOrFieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);
    if (fi != null)
        return Field(expression, fi);

    throw Error.NotAMemberOfType(propertyOrFieldName, expression.Type);
}
Давайте сразу рассмотрим практический пример использования данных функций.
class Program
{
    static void Main(string[] args)
    {

        Program program = new Program {B = 25};
        Expression propertyExpr = Expression.Property(Expression.Constant(program), "B");

        Console.WriteLine(Expression.Lambda<Func<int>>(propertyExpr).Compile()());

        program.B = 40;
        Expression propertyOrFieldExpr = Expression.PropertyOrField(Expression.Constant(program), "B");
        Console.WriteLine(Expression.Lambda<Func<int>>(propertyOrFieldExpr).Compile()());
        Console.ReadLine();
    }

    public int B { getset; }
}
Результат:
25
40
Quote
Метод Expression.Quote создает UnaryExpression и представляет собой выражение, которое имеет постоянное (const) значение типа Expression.
Пример:
static void Main(string[] args)
{
    string[] array = { "one""two""three" };

    // This example constructs an expression tree equivalent to the lambda:
    // str => str.AsQueryable().Any(ch => ch == 'e')

    Expression<Func<charbool>> innerLambda = ch => ch == 'e';

    var str = Expression.Parameter(typeof(string), "str");
    var expr =
        Expression.Lambda<Func<stringbool>>(
            Expression.Call(typeof(Queryable), "Any"new Type[] { typeof(char) },
                Expression.Call(typeof(Queryable), "AsQueryable",
                                new Type[] { typeof(char) }, str),
                Expression.Quote(innerLambda)    
            ),
            str
        );

    // Works like a charm (prints one and three)
    foreach (var value in array.AsQueryable().Where(expr))
        Console.WriteLine(value);
    Console.ReadLine();
}

Результат:
one
three
В этом примере есть одна интересная штука. Вместо Expression.Quote, мы можем написать Expression.Constant, и пример выше также будет работать. Этот пример я обнаружил в интернете, и соглашусь, пожалуй, с тем, что метод Expression.Quate является излишним. Компилятор запросто может скомпилировать вложенные лямбда-выражения в дерево выражений, заменяя Constant вместо Quote. Еще небольшое дополнение по поводу Quote: выражение, которое принимает данный метод, должно быть LambdaExpression. Об этом особо в MSDN не расписано, но в таких случаях я просто смотрю, как это реализовано в исходниках, так как Microsoft выложила в открытый доступ исходники, и теперь каждый желающий может с ними ознакомиться по ссылке.
public static UnaryExpression Quote(Expression expression)
{
    RequiresCanRead(expression, "expression");
    bool validQuote = expression is LambdaExpression;
#if SILVERLIGHT
    validQuote |= SilverlightQuirks;
#endif
    if (!validQuote) throw Error.QuotedExpressionMustBeLambda();
    return new UnaryExpression(ExpressionType.Quote, expression, expression.GetType(), null);
}

ReferenceEqual/ReferenceNotEqual
Метод ReferenceEqual являет собой BinaryExpression для сравнения ссылок на равенство. Метод же ReferenceNotEqual сравнивает, наоборот, на неравенство. В языке C# мы можем вызвать метод класса Object, для того чтобы проверить ссылки на равенство.
Пример:
static void Main(string[] args)
{
    Expression referenceEqualExpr = Expression.ReferenceEqual(Expression.Constant("Hello"), Expression.Constant("Hello"));
    Expression referenceNotEqualExpr = Expression.ReferenceNotEqual(Expression.Constant("Hello"), Expression.Constant("World"));
    Console.WriteLine(Expression.Lambda<Func<bool>>(referenceEqualExpr).Compile()());
    Console.WriteLine(Expression.Lambda<Func<bool>>(referenceNotEqualExpr).Compile()());
    Console.ReadLine();
}

Результат:
True
True
Return
Метод Expression.Return создает GotoExpression которой представляет собой оператор возврата.
Пример:
static void Main(string[] args)
{
    LabelTarget returnTarget = Expression.Label();

    // This block contains a GotoExpression that represents a return statement with no value. 
    // It transfers execution to a label expression that is initialized with the same LabelTarget as the GotoExpression. 
    // The types of the GotoExpression, label expression, and LabelTarget must match.
    BlockExpression blockExpr =
        Expression.Block(
            Expression.Call(typeof(Console).GetMethod("WriteLine"new Type[] { typeof(string) }), Expression.Constant("Return")),
            Expression.Return(returnTarget),
            Expression.Call(typeof(Console).GetMethod("WriteLine"new Type[] { typeof(string) }), Expression.Constant("Other Work")),
            Expression.Label(returnTarget)
        );

    // The following statement first creates an expression tree, 
    // then compiles it, and then runs it.
    Expression.Lambda<Action>(blockExpr).Compile()();
    Console.ReadLine();
}

Результат:
Return
RightShift/RightShiftAssign
Метод RightShift представляет собой операцию побитого сдвига вправо. Если мы посмотрим операцию LeftShift с предыдущей статьи, то сможем понять лучше, как работает данная операция.
Пример в таблице, как это работает:
В приведенной таблице мы сдвинули значение 14 на два бита вправо и получили значение 3, что показано на рисунке ниже.
Пример:
static void Main(string[] args)
{
    Expression rightShiftExpt = Expression.RightShift(Expression.Constant(14), Expression.Constant(2));
    Console.WriteLine(Expression.Lambda<Func<int>>(rightShiftExpt).Compile()());
    Console.ReadLine();
}

Результат:
3
RuntimeVariables
Позволяет оперировать списком параметров или локальных переменных. Больше всего этот метод играет, наверное, вспомогательную роль.
Пример:
static void Main(string[] args)
{
    ParameterExpression parameterAExpr = Expression.Parameter(typeof (int), "a");
    ParameterExpression parameterBExpr = Expression.Parameter(typeof(int), "a");
    RuntimeVariablesExpression variables = Expression.RuntimeVariables(parameterAExpr, parameterBExpr);

    Expression addAssign = Expression.AddAssign(parameterAExpr, parameterBExpr);
    Console.WriteLine(Expression.Lambda<Func<intintint>>(addAssign,
        variables.Variables).Compile()(4,5));
    Console.ReadLine();
}

Результат:
9
Substract/SubstractAssign
Представляет собой математическую операцию отрицания. Это классическая операция отрицания без каких-либо сложностей или скрытого подтекста, поэтому предлагаю сразу перейти к примерам.
Пример:
static void Main(string[] args)
{
    Expression subtractExpr = Expression.Subtract(Expression.Constant(5), Expression.Constant(3));
    Console.WriteLine(Expression.Lambda<Func<int>>(subtractExpr).Compile()());

    ParameterExpression parameterAExpr = Expression.Parameter(typeof (int), "a");
    ParameterExpression parameterBExpr = Expression.Parameter(typeof (int), "b");

    Expression subtractAssignExpr = Expression.Subtract(parameterAExpr, parameterBExpr);
    Console.WriteLine(Expression.Lambda<Func<intintint>>(subtractAssignExpr,
        new[] { parameterAExpr, parameterBExpr }).Compile()(7,3));
    Console.ReadLine();
}

Результат:
2
4
SubstractChecked/SubstractCheckedAssign
Методы SubstractChecked и SubstractCheckedAssign аналогичные методам Substract и SubstractAssign, с одним исключением: данные методы позволяют отслеживать переполнение. Другими словами, если у нас происходит переполнение, мы получим OverflowException.
Пример:
static void Main(string[] args)
{
    Expression tryCatchExpression = Expression.TryCatch(
        Expression.Block(
            Expression.SubtractChecked(Expression.Constant(int.MinValue), Expression.Constant(5)),
            Expression.Constant("Substract operation")
            ),
        Expression.Catch(
            typeof (OverflowException),
            Expression.Constant("Catch block")
            ));

    Console.WriteLine(Expression.Lambda<Func<string>>(tryCatchExpression).Compile()());

    Console.ReadLine();
}

Результат:
Catch block
Swicth/SwitchCase
Метод Switch создает SwitchExpression и представляет собой оператор switch. Это, по сути, самый обычный оператор switch, только представленный в виде дерева.
Пример:
static void Main(string[] args)
{
    ConstantExpression switchValue = Expression.Constant(3);

    // This expression represents a switch statement  
    // that has a default case.
    SwitchExpression switchExpr =
        Expression.Switch(
            switchValue,
            Expression.Call(
                        null,
                        typeof(Console).GetMethod("WriteLine"new Type[] { typeof(String) }),
                        Expression.Constant("Default")
                    ),
            new SwitchCase[] 
            {
                Expression.SwitchCase(
                    Expression.Call(
                        null,
                        typeof(Console).GetMethod("WriteLine"new Type[] { typeof(String) }),
                        Expression.Constant("First")
                    ),
                    Expression.Constant(1)
                ),
                Expression.SwitchCase(
                    Expression.Call(
                        null,
                        typeof(Console).GetMethod("WriteLine"new Type[] { typeof(String) }),
                        Expression.Constant("Second")
                    ),
                    Expression.Constant(2)
                )
            }
        );

    // The following statement first creates an expression tree, 
    // then compiles it, and then runs it.
    Expression.Lambda<Action>(switchExpr).Compile()();

    Console.ReadLine();
}

Результат:
Default
TryGetActionType/TryGetFuncType
Мы уже рассматривали методы GetActionType, GetDelegateType и GetFuncType, которые позволяют создавать делегат заданного типа. Более детально можно посмотреть здесь: Deep understanding expression. Part 2. Так вот, методы с приставкой Try позволяют получить тот же делегат в виде out параметра. Это сделано по аналогии с методами TryParse, TryConvert и другими методами, которые возвращают результат в случае успеха в виде out параметра. Давайте посмотрим тот же пример, что был рассмотрен нами в предыдущей статье.
Пример:
class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        dynamic delegateType = GetDelegate<A>(a, "Method1");
        delegateType(4);

        Console.ReadLine();
    }

    private static Delegate GetDelegate<T>(object target, string methodName)
    {
        MethodInfo method = typeof(T).GetMethod(methodName);
        List<Type> args = new List<Type>(
            method.GetParameters().Select(p => p.ParameterType));
        Type delegateType;
        if (method.ReturnType == typeof(void))
        {
            Expression.TryGetActionType(args.ToArray(), out delegateType);
        }
        else
        {
            args.Add(method.ReturnType);
            Expression.TryGetFuncType(args.ToArray(), out delegateType);
        }
        return Delegate.CreateDelegate(delegateType, target, method);
    }

    public class A
    {
        public void Method1(int number)
        {
            Console.WriteLine(number);
        }
    }
}

Результат:
4
TypeAs
Создает UnaryExpression, представляющий преобразование явной ссылки или упаковки, где null поддерживается в случае неудачного преобразования. В некоторых случаях в языке C# это подобно преобразование типа с помощью оператора as.
Пример:
static void Main(string[] args)
{
    UnaryExpression typeAsExpression = Expression.TypeAs(
            Expression.Constant(34, typeof(int)),
            typeof(int?));

    Console.WriteLine(typeAsExpression.ToString());

    Console.ReadLine();
}

Результат:
(34 As Nullable`1)
TypeIs
Позволяет проводить проверку конкретного типа данных на соответствие с нужным типом данных.
Пример:
static void Main(string[] args)
{
    TypeBinaryExpression typeIsExpression = Expression.TypeIs(
            Expression.Constant("Hello"),
            typeof(int));

    Console.WriteLine(typeIsExpression.ToString());
    Console.WriteLine(Expression.Lambda<Func<bool>>(typeIsExpression).Compile()());

    Console.ReadLine();
}

Результат:
("Hello" Is Int32)
False
TypeEqual
Данная функция позволяет сравнить идентификацию типов во время выполнения.
Пример:
static void Main(string[] args)
{
    TypeBinaryExpression typeEqualExpr = Expression.TypeEqual(
            Expression.Constant("Hello"),
            typeof(string));

    Console.WriteLine(typeEqualExpr.ToString());
    Console.WriteLine(Expression.Lambda<Func<bool>>(typeEqualExpr).Compile()());

    Console.ReadLine();
}

Результат:
("Hello" TypeEqual String)
True
UnaryPlus
Метод UnaryPlus представляет собой операцию "унарный плюс". Результатом унарного оператора сложения является значение его операнда.
Пример:
static void Main(string[] args)
{
    UnaryExpression unaryPlusexpr = Expression.UnaryPlus(Expression.Constant(5));
    Console.WriteLine(Expression.Lambda<Func<int>>(unaryPlusexpr).Compile()());

    Console.ReadLine();
}

Результат:
5
Unbox
Данный метод создает выражение, которое предоставляет явную распаковку.
Пример:
static void Main(string[] args)
{
    UnaryExpression unboxExpr = Expression.Unbox(Expression.Constant(5, typeof(object)), typeof(int));
    Console.WriteLine(Expression.Lambda<Func<int>>(unboxExpr).Compile()());

    Console.ReadLine();
}

Результат:
5
Variable
Создает узел ParameterExpression, который можно использовать для идентификации параметра или переменной в дереве выражения. Использовать его можно по аналогии с функцией Parameter, которую мы рассматривали ранее.
Пример:
static void Main(string[] args)
{
    ParameterExpression varA = Expression.Variable(typeof (int), "a");
    ParameterExpression varB = Expression.Variable(typeof(int), "b");

    Expression addAssignExpr = Expression.AddAssign(varA, varB);
    Console.WriteLine(Expression.Lambda<Func<intintint>>(addAssignExpr,
        new[] {varA, varB}).Compile()(3, 6));

    Console.ReadLine();
}

Результат:
9
На этой позитивной ноте мы закончили серию статей о деревьях выражений. Как видите, деревья выражений − довольно нетривиальный подход в разработке программного обеспечения. Возможно, причина заключается в том, что разработчику для умелого использования деревьев выражений нужно иметь достаточно неплохую базу знаний. Так, умение разбиение куска программы на отдельные лексические единицы для некоторых новичков − очень непростая задача. Также одна из проблем, которые также выплывают при использовании деревьев выражений, − это невозможность отладки кода. Нужны глубокие знания, чтобы понять, как можно построить то или иное дерево. Этот процесс уж точно никак нельзя назвать простым. Но чем сложнее предметная область, тем она увлекательнее. Если мы будем бросать вызов самим себе и будем побеждать такие трудности, это будет еще одной нашей маленькой победой. Надеюсь, что данная серия статей была не очень скучной. В следующих статьях мы с вами углубимся в примеры построения сложных деревьев выражений, а также научимся разбивать наше выражение на логически завершенные блоки. Строить своеобразное дерево с помощью деревьев выражений, поверьте, очень увлекательно. 

No comments:

Post a Comment