Saturday, August 9, 2014

Deep understanding expression. Part 3

В этой статье мы снова окунемся в мир деревьев выражений языка C#, рассматривая их с примерами под каждое дерево. Каждый пример по возможности будет подкреплен иллюстрацией  при условии наличия в чистом языке C# аналогии поведения с деревом выражений. Каждую функцию постараемся проанализировать как можно детальнее, чтобы максимально покрыть теорию по ней и ознакомиться с ее практическим применением.
MakeDynamic
В предыдущей статье  мы рассмотрели функцию Expression.Dynamic, которая создает выражение DynamicExpression, которое представляет динамическую операцию, привязанную с использованием указанного объекта CallSiteBinder
Функция MakeDynamic – это такая же функция, как и Dynamic, рассмотренная ранее, только MakeDynamic является функцией более низкого порядка, так как Dynamic использует в себе данную функцию. Вот как выглядит реализация данной функции, с исходников .NET Framework DynamicExpression.cs:
public static new DynamicExpression Dynamic(CallSiteBinder binder, Type returnType, Expression arg0, Expression arg1, Expression arg2, Expression arg3)
{
    return Expression.Dynamic(binder, returnType, arg0, arg1, arg2, arg3);
}

public static DynamicExpression Dynamic(CallSiteBinder binder, Type returnType, Expression arg0, Expression arg1, Expression arg2)
{
    ContractUtils.RequiresNotNull(binder, "binder");
    ValidateDynamicArgument(arg0);
    ValidateDynamicArgument(arg1);
    ValidateDynamicArgument(arg2);

    DelegateHelpers.TypeInfo info = DelegateHelpers.GetNextTypeInfo(
        returnType,
        DelegateHelpers.GetNextTypeInfo(
            arg2.Type,
            DelegateHelpers.GetNextTypeInfo(
                arg1.Type,
                DelegateHelpers.GetNextTypeInfo(
                    arg0.Type,
                    DelegateHelpers.NextTypeInfo(typeof(CallSite))
                )
            )
        )
    );

    Type delegateType = info.DelegateType;
    if (delegateType == null)
    {
        delegateType = info.MakeDelegateType(returnType, arg0, arg1, arg2);
    }

    return DynamicExpression.Make(returnType, delegateType, binder, arg0, arg1, arg2);
}
Как это использовать, рассмотрим на простом примере.
static void Main(string[] args)
{
    var x = Expression.Parameter(typeof(object), "x");
    var y = Expression.Parameter(typeof(object), "y");
    var binder = Binder.BinaryOperation(
        CSharpBinderFlags.None, ExpressionType.Add, typeof(Program),
        new CSharpArgumentInfo[] { 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)});
    Func<dynamicdynamicdynamic> f =
        Expression.Lambda<Func<objectobjectobject>>(
            Expression.MakeDynamic(typeof(Func<CallSiteobjectobjectobject>), binder, x, y),
            new[] { x, y }
        ).Compile();


    Console.WriteLine(f("Hello"" World!!!"));

    Console.ReadLine();
}
Результат:
Hello World!!!
MakeGoto
С меткой перехода goto, а также с функцией Expression.Goto для создания этой метки мы уже сталкивались. Функция MakeGoto – функция более низкого уровня, чем Goto, поэтому она используется самой функцией Goto. Обычно почти все функции в деревьях выражений с приставкой Make имеют функции более высокого ранга. 
Функция Goto в исходниках фреймворка GotoExpression.cs выглядит так:
public static GotoExpression Goto(LabelTarget target, Expression value, Type type)
{
    return MakeGoto(GotoExpressionKind.Goto, target, value, type);
}

public static GotoExpression MakeGoto(GotoExpressionKind kind, LabelTarget target, Expression value, Type type)
{
    ValidateGoto(target, ref value, "target""value");
    return new GotoExpression(kind, target, value, type);
}
Пример:
static void Main(string[] args)
{
    LabelTarget label = Expression.Label();
    Expression blockExpr = Expression.Block(
        Expression.Call(
            null,
            typeof(Console).GetMethod("Write"new Type[] { typeof(String) }),
            Expression.Constant("Hello ")
            ),
        Expression.MakeGoto(GotoExpressionKind.Goto, label, nulltypeof(void)),
        Expression.Call(
            null,
            typeof(Console).GetMethod("Write"new Type[] { typeof(String) }),
            Expression.Constant("World ")
            ),
        Expression.Label(label)
        );

    Console.WriteLine(Expression.Lambda<Action>(blockExpr).Compile().DynamicInvoke());
    Console.ReadLine();
}
Результат:
Hello
Всего в деревьях выражений различают 4 метки перехода: goto, return, break и continue. Они представляются с помощью enum GotoExpressionKind.
public enum GotoExpressionKind
{
    /// <summary>
    /// A <see cref="GotoExpression"/> that represents a jump to some location.
    /// </summary>
    Goto,
    /// <summary>
    /// A <see cref="GotoExpression"/> that represents a return statement.
    /// </summary>
    Return,
    /// <summary>
    /// A <see cref="GotoExpression"/> that represents a break statement.
    /// </summary>
    Break,
    /// <summary>
    /// A <see cref="GotoExpression"/> that represents a continue statement.
    /// </summary>
    Continue,
}
То есть, с помощью метода MakeGoto вы можете создать не только метку перехода для goto, но и для таких переходов, как continue/break/return.
MakeIndex
Метод Expression.MakeIndex создает IndexExpression для обращения по индексу к свойству или массиву. В C# языке такое поведение присущее индексаторам.
В отличие от предыдущих рассмотренных функций с приставкой Make, метод MakeIndex более высокого уровня, так как внутри себя он вызывает функции Expression.Property или Expression.ArrayAccess для доступа к массиву по индексам.
public static IndexExpression MakeIndex(Expression instance, PropertyInfo indexer, IEnumerable<Expression> arguments)
{
    if (indexer != null)
    {
        return Property(instance, indexer, arguments);
    }
    else
    {
        return ArrayAccess(instance, arguments);
    }
}
Поскольку недавно мы рассматривали  использование ArrayAccess, написать пример не должно составить особых проблем.
Пример:
public class Program
{
    static void Main(string[] args)
    {
        var instance = new ProgramZ { x = new List<string> { "a""b" } };

        Expression propertyExpr = Expression.Property(
                Expression.Constant(instance),
                "x"
            );

        PropertyInfo propertyInfo = typeof (List<string>).GetProperty("Item");

        Expression indexExpr =
                Expression.MakeIndex(
                        propertyExpr,
                        propertyInfo,
                        new[] { Expression.Constant(0) });

        var parameter = Expression.Parameter(typeof(ProgramZ), "arg");
        var expr = Expression.Lambda<Func<ProgramZstring>>(indexExpr, parameter);

        string result = expr.Compile().Invoke(instance);

        Console.WriteLine(result);

        Console.ReadLine();
    }

    public class ProgramZ
    {
        public List<string> x { getset; }
    }
}

Результат:
a
Небольшое пояснение к коду. Функция MakeIndex возвратит нам значение по индексу 0. Мы могли бы также передать индекс как параметр, чтобы обратиться к нужному индексу. Также можно использовать изученную раньше функцию Expression.ArrayIndex для получения значения по индексу.
MakeMemberAccess
Метод Expression.MakeMemberAccess создает MemberExpression для предоставления доступа к свойству или полю класса. Мы уже рассматривали пример с использованием функции Field, для того чтобы получить доступ к закрытому полю. Функция Make также относится к функциям более высокого уровня, поскольку она вызывает функции Field или Property, в зависимости от типа MemberInfo.
public static MemberExpression MakeMemberAccess(Expression expression, MemberInfo member)
{
    ContractUtils.RequiresNotNull(member, "member");

    FieldInfo fi = member as FieldInfo;
    if (fi != null)
    {
        return Expression.Field(expression, fi);
    }
    PropertyInfo pi = member as PropertyInfo;
    if (pi != null)
    {
        return Expression.Property(expression, pi);
    }
    throw Error.MemberNotFieldOrProperty(member);
}
Рассмотрим старый пример, для того чтобы прочитать значение с закрытого свойства.
public class A
{
    public A(string name)
    {
        _p = name;
    }

    private string _p;
    public string P
    {
        get { return _p; }
    }
}
Давайте посмотрим, как мы использовали старый вариант с функцией, и как можно использовать функцию MakeMemberAccess.
Пример:
static void Main(string[] args)
{
    A a = new A("Hello");
    ParameterExpression paramExpr = Expression.Parameter(typeof(A), "arg");

    MemberExpression member = Expression.Field(paramExpr, "_p");

    string result = Expression.Lambda<Func<Astring>>(member, paramExpr).Compile()(a);
    Console.WriteLine(result);

    MemberInfo getMethod = 
        typeof(A).GetMember("_p"BindingFlags.Instance | BindingFlags.NonPublic)[0];
    Expression memeberAccessExpr = Expression.MakeMemberAccess(paramExpr, getMethod);

    string result2 = Expression.Lambda<Func<Astring>>(memeberAccessExpr, paramExpr).Compile()(a);
    Console.WriteLine(result2);

    Console.ReadLine();
}
Результат:
Hello
Hello
MakeUnary
Метод MakeUnary создает UnaryExpression для работы с унарным оператором и представляет собой фабричный метод над такими унарными функциями, как Negate, Throw, Increment и другие. Весь список доступных функций можно посмотреть ниже.
public static UnaryExpression MakeUnary(ExpressionType unaryType, Expression operand, Type type, MethodInfo method)
{
    switch (unaryType)
    {
        case ExpressionType.Negate:
            return Negate(operand, method);
        case ExpressionType.NegateChecked:
            return NegateChecked(operand, method);
        case ExpressionType.Not:
            return Not(operand, method);
        case ExpressionType.IsFalse:
            return IsFalse(operand, method);
        case ExpressionType.IsTrue:
            return IsTrue(operand, method);
        case ExpressionType.OnesComplement:
            return OnesComplement(operand, method);
        case ExpressionType.ArrayLength:
            return ArrayLength(operand);
        case ExpressionType.Convert:
            return Convert(operand, type, method);
        case ExpressionType.ConvertChecked:
            return ConvertChecked(operand, type, method);
        case ExpressionType.Throw:
            return Throw(operand, type);
        case ExpressionType.TypeAs:
            return TypeAs(operand, type);
        case ExpressionType.Quote:
            return Quote(operand);
        case ExpressionType.UnaryPlus:
            return UnaryPlus(operand, method);
        case ExpressionType.Unbox:
            return Unbox(operand, type);
        case ExpressionType.Increment:
            return Increment(operand, method);
        case ExpressionType.Decrement:
            return Decrement(operand, method);
        case ExpressionType.PreIncrementAssign:
            return PreIncrementAssign(operand, method);
        case ExpressionType.PostIncrementAssign:
            return PostIncrementAssign(operand, method);
        case ExpressionType.PreDecrementAssign:
            return PreDecrementAssign(operand, method);
        case ExpressionType.PostDecrementAssign:
            return PostDecrementAssign(operand, method);
        default:
            throw Error.UnhandledUnary(unaryType);
    }
}
Возьмем те методы, которые мы уже проходили, например, Increment и Decrement, и напишем пример с методом MakeUnary.
Пример:
static void Main(string[] args)
{
    Expression decrementExpr = Expression.MakeUnary(ExpressionType.Decrement, Expression.Constant(2),
        typeof (int));
    Console.WriteLine(Expression.Lambda<Func<int>>(decrementExpr).Compile().Invoke());

    Expression incrementExpr = Expression.MakeUnary(ExpressionType.Increment, Expression.Constant(2),
        typeof(int));
    Console.WriteLine(Expression.Lambda<Func<int>>(incrementExpr).Compile().Invoke());

    Console.ReadLine();
}
Результат:
1
3
MemberBind
Метод Expression.MemberBind создает MemberMemberBinding, который предоставляет рекурсивную инициализацию элементов поля или свойства.
К сожалению, у меня так и не получилось найти пример использования данного дерева выражений. Это второе дерево выражений после MemberListBinding, для которого я не смог найти ни одного примера.
MemberInit
Представляет выражение, создающее новый объект, и инициализирует свойство объекта.
Пример:
class Program
{
    static void Main(string[] args)
    {
        var parameter = Expression.Parameter(typeof(int), "i");
        var newExpr = Expression.New(typeof(A));
        var bindExprs = new[]
    {
        Expression.Bind(typeof(A).GetProperty("B"), parameter)
    };

        var body = Expression.MemberInit(newExpr, bindExprs);
        var lambda = Expression.Lambda<Func<int, A>>(body, parameter);
        Console.WriteLine(lambda.Compile()(25).B);
        Console.ReadLine();
    }
}

public class A
{
    public int B { get; set; }
}
Результат:
25
Код аналогичен методу
i => new A { B = i }
Modulo/ModuloAssign
Метод Expression.Modulo представляет собой дерево выражений для выполнения операции получения остатка. Операция ModuleAssign – это та же операция, но с присвоением значения.
Пример:
static void Main(string[] args)
{
    Expression moduloExpr = Expression.Modulo(Expression.Constant(5), Expression.Constant(2));
    Console.WriteLine(Expression.Lambda<Func<int>>(moduloExpr).Compile()());

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

    Console.WriteLine(Expression.Lambda<Func<int, int, int>>(moduloAssignExpr,
        new[] { parameterAExpr, parameterBExpr }
        ).Compile().Invoke(4, 5));

    Console.ReadLine();
}
Результат:
1
4
Multiply/MultiplyAssign
Функция Multiply позволяет выполнить операцию сложения с помощью деревьев выражений. Операция MultiplyAssign позволяет сделать то же самое, но с возможностью присвоить значение конкретной переменной. Эти две функции не проверяют выполняемую операцию на переполнение. Для этого есть аналогичные функции с приставкой Checked, которые мы рассмотрим ниже.
Пример:
static void Main(string[] args)
{
    Expression multipleExpr = Expression.Multiply(Expression.Constant(3), Expression.Constant(4));
    Console.WriteLine(Expression.Lambda<Func<int>>(multipleExpr).Compile()());

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

    Console.WriteLine(Expression.Lambda<Func<int, int, int>>(multipleAssignExpr,
        new[] { parameterAExpr, parameterBExpr }
        ).Compile().Invoke(4, 5));

    Console.ReadLine();
}
Результат:
12
20
MultiplyChecked/MultiplyAssignChecked
Эти две функции отличаются от функций, которые указаны выше, только тем, что в данных функциях добавлена проверка на переполнение. В случае переполнения при умножении мы получим OverflowException.

Пример:
static void Main(string[] args)
{
    Expression multipleExpr = Expression.MultiplyChecked(Expression.Constant(3), Expression.Constant(int.MaxValue));

    TryExpression tryExpression = Expression.TryCatch(
        Expression.Block(multipleExpr,
            Expression.Constant("Try block")),
        Expression.Catch(typeof (OverflowException),
            Expression.Constant("Overflow Exception was caught"))
        );
    Console.WriteLine(Expression.Lambda<Func<string>>(tryExpression).Compile()());

    Console.ReadLine();
}
Результат:
Overflow Exception was caught
Negate/NegateChecked
Метод Expression.Negate создает UnaryExpression и представляет собой выражение для операции отрицания. NegateChecked добавляет для операции отрицания проверку на переполнение. Если вы захотите попробовать использовать NegateChecked, посмотрите пример, который будет приведен ниже для метода Negate, замените Negate на NegateChecked и в параметр передайте, например, int.MaxValue.

Пример:
static void Main(string[] args)
{
    Expression negateExpr = Expression.Negate(Expression.Constant(5));

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

    Console.ReadLine();
}
Результат:
-5
New
Выражение Expression.New позволяет создать новый объект, используя либо конструктор по умолчанию, либо перегруженный конструктор.

Пример:
class Program
{
    public Program()
    {
        A = 5;
    }
    public int A { get; set; }
    static void Main(string[] args)
    {
        Expression newPropgramExpr = Expression.New(typeof(Program));

        Console.WriteLine(Expression.Lambda<Func<Program>>(newPropgramExpr).Compile()().A);

        Console.ReadLine();
    }

}
Результат:
5
NewArrayBound
Довольно редко используемая функция которая позволяет задать размерность массива. Разве что вы часто работаете с двумерными и выше размерностью массивами.

Пример:
static void Main(string[] args)
{
    NewArrayExpression newArrayExpression = Expression.NewArrayBounds(
        typeof(string),
        Expression.Constant(3),
        Expression.Constant(2));

    // Output the string representation of the Expression.
    Console.WriteLine(newArrayExpression.ToString());
}
Результат:
new System.String[,](3, 2)
NewArrayInit
Метод Expression.NewArrayInit позволяет создать одномерный массив и сразу инициализировать его значениями.

Пример:
static void Main(string[] args)
{
    var data = new List<Expression>();
    for (int i = 0; i < 10; i++)
    {
        data.Add(Expression.Constant(i));
    }

    NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof (int), data);

    Console.WriteLine(newArrayExpression.ToString());
}
Результат:
new [] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Not
Поначалу по названию можно предположить, что данная операция делает какое-то отрицание, наподобие оператора "!" в языке C#.  Но это не так. Это еще одна из побитовых операций, которая представляет собой поразрядное дополнение (В английском языке есть подходящее слово для действия, которое проводит этот метод  flip). В C# есть специальный оператор, обозначающий операцию поразрядного дополнения "~".
Пример:
static void Main(string[] args)
{
    var list = new List<Expression>
    {
        Expression.Constant(0),
        Expression.Constant(0x111),
        Expression.Constant(0xfffff),
        Expression.Constant(0x8888),
        Expression.Constant(0x22000022)
    };

    foreach (var v in list)
    {
        var compileExpr = Expression.Lambda<Func<int>>(v).Compile();
        var notExpr = Expression.Lambda<Func<int>>(Expression.Not(v)).Compile();

        Console.WriteLine("~0x{0:x8} = 0x{1:x8}", compileExpr(), notExpr());
    }
}
Результат:
~0x00000000 = 0xffffffff
~0x00000111 = 0xfffffeee
~0x000fffff = 0xfff00000
~0x00008888 = 0xffff7777
~0x22000022 = 0xddffffdd
NotEqual
Данный метод позволяет сравнить два выражение на неравенство.
Пример:
static void Main(string[] args)
{
    Expression notEqualExpr = Expression.NotEqual(Expression.Constant(5), Expression.Constant(4));
    Console.WriteLine("{0} {1}", notEqualExpr, Expression.Lambda<Func<bool>>(notEqualExpr).Compile()());
}
Результат:
(5 != 4) True
OnesComplement
Метод представляет побитовое дополнение. Тот же самый оператор "~" в языке C#. Меня немного озадачивает, почему Microsoft создала две разные функции для одних и тех же целей. Ну а мы давайте посмотрим на тот же пример выше, просто заменим функцию Not на функцию OnesComplement.
Пример:
static void Main(string[] args)
{
    var list = new List<Expression>
    {
        Expression.Constant(0),
        Expression.Constant(0x111),
        Expression.Constant(0xfffff),
        Expression.Constant(0x8888),
        Expression.Constant(0x22000022)
    };

    foreach (var v in list)
    {
        var compileExpr = Expression.Lambda<Func<int>>(v).Compile();
        var notExpr = Expression.Lambda<Func<int>>(Expression.OnesComplement(v)).Compile();

        Console.WriteLine("~0x{0:x8} = 0x{1:x8}", compileExpr(), notExpr());
    }
}
Результат:
~0x00000000 = 0xffffffff
~0x00000111 = 0xfffffeee
~0x000fffff = 0xfff00000
~0x00008888 = 0xffff7777
~0x22000022 = 0xddffffdd
Результат получился аналогичным примеру, который был продемонстрирован до этого.

На этом закончим третью часть статьи, посвященной деревьям выражений, впереди нас ждет продолжение материала в еще одной статье. Как видим, деревья выражений  это мощный инструмент, который при умелом подходе может сослужить хорошую службу. Статья получилась суховатой. Надеюсь, после четвертого этапа погружения в мир деревьев выражений мы рассмотрим парочку реальных сложных примеров, а также ознакомимся с тем, как можно применять деревья выражений в своих программах. 

No comments:

Post a Comment