Saturday, July 19, 2014

Введение в Expressions в языке C#

Здравствуйте, уважаемые читатели моего блога. В этой статье мы рассмотрим понятие expression в языке C# и его использование. Признаться честно, expression в языке C# для меня – одна из самых страшных тем, поскольку, как и у многих разработчиков, у меня есть так называемые "слабые места" (темы в языке разработки, которые специалист знает лишь поверхностно). Причины этого разные, начиная с недостатка знаний, непонимания некоторых аспектов конкретной логики того или иного кода, проблематичного восприятия сложного построения кода и заканчивая банальным страхом, ленью. Моя боязнь перед темой expression (прим.: в русском языке существует два названия: выражения или деревья выражений) в основном заключалась в том, что о деревьях выражений нет хорошей литературы. Множество серьезных авторов по языку C# упоминают деревья выражений лишь вскользь, не придавая данной теме достаточно внимания. Но благодаря блогу я научился преодолевать свои страхи, стараясь выходить из своей зоны комфорта, чтобы узнать что-то новое и стать на уровень выше. Постараюсь рассмотреть все, что я узнал нового о деревьях выражений, а также о том, как с их помощью писать профессиональный и мощный программный код. Сразу хочу предупредить читателей, что читать код для построения деревьев выражений местами бывает очень сложно.
Мне очень понравился вопрос, прозвучавший на stackoverflow следующим образом: "Why would you use Expression<Func<T>> rather than Func<T>?". Имеется в виду вопрос: "В каких условиях вы бы использовали Expression<Func<T>>, вместо старого доброго Func<T>?". Что бы вы ответили, если бы у вас попросили объяснить разницу для приведенных ниже строк кода?

Func<stringint> getLenghtFunc = s => s.Length;
Expression<Func<stringint>> getExpLenghtFunc = s => s.Length;

Отличие первой строки кода от второй, по сути, − только в том, что у нас во втором варианте добавлено ключевое слово Expression, которое полностью изменяет логику. Эти две строки кода не равны между собой. Концептуально Expression<Func<T>> полностью отличается от Func<T>. Func<T> представляет собой делегат, который в базовом приближении можно рассматривать как некоего рода безопасный указатель на метод. Expression<Func<T>> организовывается в структуру данных дерева для лямбда-выражения. Эта структура дерева описывает, что лямбда-выражение будет с конкретным выражением. Например, это дерево может содержать в себе информацию о переменных, методах вызова, константных значениях и т.д. То есть, мы можем сами скомпонировать то выражение, которое максимально подходит нам, и преобразовать его к актуальному методу, используя метод Expression.Compile(). Действие для получения лямбда-выражения как анонимного метода с дерева выражения происходит во время компиляции. То есть, у вас во время компиляции доступна проверка типов и т.д.
Для примера рассмотрим простой вариант использования приведенных выше строк кода, которые использовались для разбора деревьев выражений.
static void Main(string[] args)
{
    Func<stringint> getLenghtFunc = s => s.Length;
 
    Expression<Func<stringint>> getExpLenghtFunc = s => s.Length;
 
    Console.WriteLine(getLenghtFunc("Hello World!"));
 
    Console.WriteLine(getExpLenghtFunc.Compile()("Hello"));
 
    Console.ReadLine();
}
Если мы скомпилируем  и запустим данный код, то на выходе получим вот такой результат:
Компилятор следующую строку
Expression<Func<stringint>> getExpLenghtFunc = s => s.Length;
преобразует в строку такого вида:
.method private hidebysig static
    void Main (
        string[] args
    ) cil managed
{
    // Method begins at RVA 0x2068
    // Code size 139 (0x8b)
    .maxstack 4
    .entrypoint
    .locals init (
        [0] class [System.Core]System.Func`2<stringint32> getLenghtFunc,
        [1] class [System.Core]System.Linq.Expressions.Expression`1<class [System.Core]System.Func`2<stringint32>> getExpLenghtFunc,
        [2] class [System.Core]System.Linq.Expressions.ParameterExpression CS$0$0000,
        [3] class [System.Core]System.Linq.Expressions.ParameterExpression[] CS$0$0001
    )

    IL_0000: nop
    IL_0001: ldsfld class [System.Core]System.Func`2<stringint32> SwitchOperatorSample.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0006: brtrue.s IL_001b

    IL_0008: ldnull
    IL_0009: ldftn int32 SwitchOperatorSample.Program::'<Main>b__0'(string)
    IL_000f: newobj instance void class [System.Core]System.Func`2<stringint32>::.ctor(objectnative int)
    IL_0014: stsfld class [System.Core]System.Func`2<stringint32> SwitchOperatorSample.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0019: br.s IL_001b

    IL_001b: ldsfld class [System.Core]System.Func`2<stringint32> SwitchOperatorSample.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0020: stloc.0
    IL_0021: ldtoken [mscorlib]System.String
    IL_0026: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_002b: ldstr "s"
    IL_0030: call class [System.Core]System.Linq.Expressions.ParameterExpression [System.Core]System.Linq.Expressions.Expression::Parameter(class [mscorlib]System.Type, string)
    IL_0035: stloc.2
    IL_0036: ldloc.2
    IL_0037: ldtoken method instance int32 [mscorlib]System.String::get_Length()
    IL_003c: call class [mscorlib]System.Reflection.MethodBase [mscorlib]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]System.RuntimeMethodHandle)
    IL_0041: castclass [mscorlib]System.Reflection.MethodInfo
    IL_0046: call class [System.Core]System.Linq.Expressions.MemberExpression [System.Core]System.Linq.Expressions.Expression::Property(class [System.Core]System.Linq.Expressions.Expression, class [mscorlib]System.Reflection.MethodInfo)
    IL_004b: ldc.i4.1
    IL_004c: newarr [System.Core]System.Linq.Expressions.ParameterExpression
    IL_0051: stloc.3
    IL_0052: ldloc.3
    IL_0053: ldc.i4.0
    IL_0054: ldloc.2
    IL_0055: stelem.ref
    IL_0056: ldloc.3
    IL_0057: call class [System.Core]System.Linq.Expressions.Expression`1<!!0> [System.Core]System.Linq.Expressions.Expression::Lambda<class [System.Core]System.Func`2<stringint32>>(class [System.Core]System.Linq.Expressions.Expression, class [System.Core]System.Linq.Expressions.ParameterExpression[])
    IL_005c: stloc.1
    IL_005d: ldloc.0
    IL_005e: ldstr "Hello World!"
    IL_0063: callvirt instance !1 class [System.Core]System.Func`2<stringint32>::Invoke(!0)
    IL_0068: call void [mscorlib]System.Console::WriteLine(int32)
    IL_006d: nop
    IL_006e: ldloc.1
    IL_006f: callvirt instance !0 class [System.Core]System.Linq.Expressions.Expression`1<class [System.Core]System.Func`2<stringint32>>::Compile()
    IL_0074: ldstr "Hello"
    IL_0079: callvirt instance !1 class [System.Core]System.Func`2<stringint32>::Invoke(!0)
    IL_007e: call void [mscorlib]System.Console::WriteLine(int32)
    IL_0083: nop
    IL_0084: call string [mscorlib]System.Console::ReadLine()
    IL_0089: pop
    IL_008a: ret
// end of method Program::Main

Этот код выглядит на первый взгляд довольно устрашающе. Хотя если мы внимательно посмотрим на класс Expression в MSDN, то увидим очень много методов, которые имеет данный класс. Чтобы освоить хотя бы часть этих методов, нужно потратить достаточно много времени. Всего на момент написания статьи можно было выделить 25 разных деревьев выражений.
  1. System.Linq.Expressions.BinaryExpression
  2. System.Linq.Expressions.BlockExpression
  3. System.Linq.Expressions.ConditionalExpression
  4. System.Linq.Expressions.ConstantExpression
  5. System.Linq.Expressions.DebugInfoExpression
  6. System.Linq.Expressions.DefaultExpression
  7. System.Linq.Expressions.DynamicExpression
  8. System.Linq.Expressions.GotoExpression
  9. System.Linq.Expressions.IndexExpression
  10. System.Linq.Expressions.InvocationExpression
  11. System.Linq.Expressions.LabelExpression
  12. System.Linq.Expressions.LambdaExpression
  13. System.Linq.Expressions.ListInitExpression
  14. System.Linq.Expressions.LoopExpression
  15. System.Linq.Expressions.MemberExpression
  16. System.Linq.Expressions.MemberInitExpression
  17. System.Linq.Expressions.MethodCallExpression
  18. System.Linq.Expressions.NewArrayExpression
  19. System.Linq.Expressions.NewExpression
  20. System.Linq.Expressions.ParameterExpression
  21. System.Linq.Expressions.RuntimeVariablesExpression
  22. System.Linq.Expressions.SwitchExpression
  23. System.Linq.Expressions.TryExpression
  24. System.Linq.Expressions.TypeBinaryExpression
  25. System.Linq.Expressions.UnaryExpression
На практике я использовал лишь горсть из этих деревьев. Давайте рассмотрим, как приведенный выше пример интерпретируется в C#-код. Выше код можно было бы переписать следующим образом:
static void Main(string[] args)
{
    var propertyInfo = typeof(string).GetProperty("Length");

    var entityParam = Expression.Parameter(typeof(string), "e");
    Expression columnExpr = Expression.Property(entityParam, propertyInfo);

    if (propertyInfo.PropertyType != typeof(int))
        columnExpr = Expression.Convert(columnExpr, typeof(int));

    var expression = Expression.Lambda<Func<stringint>>(columnExpr, entityParam);
    Console.WriteLine(expression.Compile()("Hello World"));
    Console.ReadLine();
}
Или более универсальный вариант, который я подсмотрел на stackoverflow Dynamic MemberExpression − и который использует для таких целей Expression.Convert.
Мы с вами прекрасно понимаем, что некоторые деревья выражений могут быть достаточно сложными для понимания. Если мы хотим узнать, как строятся деревья выражений, нам может понадобиться своего рода viewer для таких целей. В примерах, которые идут к Visual Studio 2008, включена программа, которая называется ExpressionTreeVisualizer. Она позволяет визуализировать дерево выражений. Ниже вы можете посмотреть скриншот этого дерева, который визуализирует то выражение, рассмотренное в примере выше.
Этот простой пример позволяет показать, как отображается дерево выражений.
  •  Body: извлечь тело выражения.
  • Parameters: получить параметры лямбда-выражения.
  • NodeType: получает ExpressionType некоторого узла в дереве.
  • Type: получает статичный тип выражения. В нашем случае выражение имеет тип Func <string, int>.
А теперь позвольте вам представить небольшой "хак". Пример, который идет в поставке, для того чтобы показать дерево выражений, работает по .NET Framework 3.5, поэтому есть большая вероятность того, что вы сможете посмотреть только небольшую часть из того, что представляют собой деревья выражений. Много чего добавилось в .NET Framework 4.0 и 4.5. К большому сожалению, если вы захотите просто изменить .NET Framework в Visual Studio (я компилировал и собирал проект в Visual Studio 2012 и 2013, поэтому ваша версия студии может отличаться), то программа у вас запустится, но при нажатии на кнопку "Show Visualizer"
вы получите вот такую неприятную ошибку:
Эта проблема возникает из-за сборки Microsoft.VisualStudio.DebuggerVisualizers.
Дело в том, что в .NET Framework изменилась реализация данной сборки. Поэтому в каждом проекте удалите эту сборку, а затем добавьте новую, как показано на рисунке ниже.
Затем перейдите на Assemblies -> Extensions, найдите по имени сборку, которую вы удалили (смотреть выше) и добавьте сборку версии 11.0.0.0.
После этого вы сможете пересобрать проект и запустить его. Результат в визуальном представлении дерева выражений будет немного отличаться.
Как видите, визуально наше дерево стало заметно отличаться. Более детальную разницу можно посмотреть на рисунке ниже, на котором оранжевым цветом я отобразил серьезные изменения построения дерева выражений, а зеленым цветом − новые добавленные свойства.
  • CanReduce: указывает на то, что узел может быть приведен к более простому узлу. Если этот параметр возвращает true, то можно с помощью функции Reduce() получить сокращенную форму.
  • TailCall: получает значение, которое определяет, можно ли компилировать лямбда-выражение с оптимизацией с помощью вызова с префиксом tail.
  • ReturnType: указывает на тип возвращаемого значения.
  • Name: возвращает имя лямбда-выражения.
Эти все свойства можно посмотреть здесь: LambdaExpression Properties. Класс PrimitiveParameterExpression представляет собой класс, наследуемый от класса ParameterExpression. Если вы посмотрите на скриншот выше, где написано "Old Version", то сами сможете увидеть, что, по сути, у нас просто добавилось наследование и класс PrimitiveParameterExpression стал типизированным.
/// <summary>
/// Generic type to avoid needing explicit storage for primitive data types
/// which are commonly used.
/// </summary>
internal sealed class PrimitiveParameterExpression<T> : ParameterExpression
{
    internal PrimitiveParameterExpression(string name)
        : base(name)
    {
    }

    public sealed override Type Type
    {
        get { return typeof(T); }
    }
}
Этот класс вы можете посмотреть по ссылке ParameterExpression.cs, так как компания Microsoft к недавнему времени открыла доступ к исходникам .NET Framework, которые вы можете посмотреть онлайн на сайте referencesource.microsoft.com или же скачать исходники с того же сайта, выбрав меню "Download". Если вы хотите добавить возможность сделать сразу навигацию к нужном методе на referencesource, то можете поставить расширение для студии Ref12. Более подробно с этим вы можете ознакомиться в статье "Как отлаживать .NET Framework Source". Класс ParameterExpression очень простой по реализации и имеет всего три самостоятельных метода, которые предлагаю рассмотреть для общего развития. Первый метод − это статический внутренний метод (static internal) Make(), который, в зависимости от входных параметров, возвращает либо экземпляр класса ByRefParameterExpression, либо PrimitiveParameterExpression, либо TypeParameterExpression. Ниже приведена реализация данного метода.
internal static ParameterExpression Make(Type type, string name, bool isByRef)
{
    if (isByRef)
    {
        return new ByRefParameterExpression(type, name);
    }
    else
    {
        if (!type.IsEnum)
        {
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Boolean: return new PrimitiveParameterExpression<Boolean>(name);
                case TypeCode.Byte: return new PrimitiveParameterExpression<Byte>(name);
                case TypeCode.Char: return new PrimitiveParameterExpression<Char>(name);
                case TypeCode.DateTime: return new PrimitiveParameterExpression<DateTime>(name);
                case TypeCode.DBNull: return new PrimitiveParameterExpression<DBNull>(name);
                case TypeCode.Decimal: return new PrimitiveParameterExpression<Decimal>(name);
                case TypeCode.Double: return new PrimitiveParameterExpression<Double>(name);
                case TypeCode.Int16: return new PrimitiveParameterExpression<Int16>(name);
                case TypeCode.Int32: return new PrimitiveParameterExpression<Int32>(name);
                case TypeCode.Int64: return new PrimitiveParameterExpression<Int64>(name);
                case TypeCode.Object:
                    // common reference types which we optimize go here.  Of course object is in
                    // the list, the others are driven by profiling of various workloads.  This list
                    // should be kept short.
                    if (type == typeof(object))
                    {
                        return new ParameterExpression(name);
                    }
                    else if (type == typeof(Exception))
                    {
                        return new PrimitiveParameterExpression<Exception>(name);
                    }
                    else if (type == typeof(object[]))
                    {
                        return new PrimitiveParameterExpression<object[]>(name);
                    }
                    break;
                case TypeCode.SByte: return new PrimitiveParameterExpression<SByte>(name);
                case TypeCode.Single: return new PrimitiveParameterExpression<Single>(name);
                case TypeCode.String: return new PrimitiveParameterExpression<String>(name);
                case TypeCode.UInt16: return new PrimitiveParameterExpression<UInt16>(name);
                case TypeCode.UInt32: return new PrimitiveParameterExpression<UInt32>(name);
                case TypeCode.UInt64: return new PrimitiveParameterExpression<UInt64>(name);
            }
        }
    }

    return new TypedParameterExpression(type, name);

}
Реализации данного метода довольно простая и не нуждается в комментировании. Также класс ParameterExpression имеет метод GetIsByRef(), который используется в связке со свойством IsByRef и указывает на то, что данный ParameterExpression должен рассматриваться в качестве ByRef параметра.
/// <summary>
/// Indicates that this ParameterExpression is to be treated as a ByRef parameter.
/// </summary>
public bool IsByRef
{
    get
    {
        return GetIsByRef();
    }
}

internal virtual bool GetIsByRef()
{
    return false;

}
И последний метод – Accept, который позволяет сделать обход дерева выражений.
protected internal override Expression Accept(ExpressionVisitor visitor)
{
    return visitor.VisitParameter(this);

}
Основная сложность, которая может возникнуть у вас в понимании этого метода, может заключаться в том,что вы не сталкивались с тем, как работает паттерн Посетитель (анг. Visitor). Вы можете посмотреть пример использования данного паттерна в языке C# на dofactory, но там пример, как по мне, слишком искусственный.

Итоги
В этой статье мы прошли введение в использование деревьев выражений, а также рассмотрели простой пример, который мусолили на протяжении всей статьи, для того чтобы понять, как строится дерево выражений, как можно посмотреть дерево выражений, как его отлаживать и другие нюансы. Если вы хотите более детально узнать о деревьях выражений, рекомендую для ознакомления статью Барта де Смета "EXPRESSION TREES, TAKE TWO – INTRODUCING SYSTEM.LINQ.EXPRESSIONS V4.0". Это достойная статья, автор которой является профессиональным разработчиком и автором книги "C# 5.0 Unleashed", позволяет окунуться в такие дебри в понимании деревьев выражений, что в некоторых местах, как говорят в народе, "без 100 грамм не разберешься". Постараюсь в ближайшее время подготовить для вас что-то еще более захватывающее по деревьям выражений.

No comments:

Post a Comment