понедельник, 27 декабря 2010 г.

Знакомство с Dynamic LINQ

- Dynamic LINQ, знакомьтесь, это мои читатели.
- Читатели, знакомьтесь, это Dynamic LINQ.    
                           Знакомство читателей с Dynamic LINQ

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

Язык программирования C# в отношении «строгости» типизации является скорее мультипарадигменым: его можно отнести к статическим языкам, однако он в значительной степени поддерживает и динамическую составляющую. Еще начиная с первой версии языка, вы могли достучаться к методу, свойству или полю по его имени с помощью механизма рефлексии, а с появлением ключевого слова dynamic (а точнее с появлением функциональности, скрытой за этим ключевым словом), этот процесс вообще здорово упростился.

LINQ и деревья выражений

Что касается LINQ-а (Language INtegrated Query), то это, прежде всего отличная штука, которая при правильной «готовке» может повысить декларативность кода и, соответственно, упростить его понимание и сопровождение. Что касается синтаксиса, то LINQ может быть представлен в одной из двух синтаксических форм: в форме выражений запросов (query expressions) и в форме вызовов методов. При этом любая конструкция, представленная в виде выражения запроса должна соответствовать некоторому паттерну, который называется Query Expression Pattern, для того, чтобы компилятор мог успешно преобразовать ее в форму вызовов методов.

На самом деле CLR  ни сном ни духом не знает о существовании выражений запросов. Это все лишь синтаксический сахар, который затем благополучно преобразовывается компиляторов в вызовы методов. Так, например, конструкция вида:

var res = from e in collection 
            select e;

Преобразовывается компилятором в

var res = collection.Select(e => e);

И будет успешно скомпилирована, если тип объекта collection содержит метод с именем Select, либо же такой метод доступен в виде метода расширения с правильной сигнатурой. Причем правильной сигнатурой в данном случае можем быть либо делегат, принимающий тип элемента коллекции и возвращающий его же, либо аналогичное выражение (expression).

Давайте рассмотрим следующий код:

var func = x => x + 1;

Похоже, что у компилятора есть вся необходимая информация для вывода типа, в результате он может спокойно «догадаться», что тип переменной func должен быть Func<int, int>. Однако этот код не компилируется, поскольку любое лямбда-выражение может быть присвоено переменным двух типов: делегату или выражению:

Func<int, int> func = x => x + 1;
Expression<Func<int, int>> expression = x => x + 1;

При этом первую строку кода можно легко представить в виде синтаксиса C# 2.0 с помощью анонимных делегатов:

Func<int, int> func = delegate(int x) { return x + 1; };

Однако со второй строкой кода все несколько сложнее. В этом случае компилятор преобразует указанное лямбда-выражение в дерево выражения (Expression Tree), где каждый узел дерева представляет собой выражение, такое как вызов метода, унарный или бинарный оператор и так далее.

var x = Expression.Parameter(typeof(int), "x");
var expression = Expression.Lambda<Func<int, int>>(
    Expression.Add(
        x,
        Expression.Constant(1)
    ),
    x
);

Однако эти два варианта кода отличаются не только синтаксически. Ключевое различие состоит в том, что в первом случае мы, по сути, получаем функтор, т.е. анонимный метод, располагающийся в сгенерированном компиляторе классе. Во втором же случае мы получаем код в виде данных, которые затем можно сериализовать, передать в другой контекст, проанализировать, преобразовать в совершенно другой формат (например, в SQL) и уже потом выполнить. Помимо этого, любое выражение можно скомпилировать прямо во время выполнения, просто вызвав метод Compile, в результате чего мы получим функтор, аналогичный тому, что мы получим в первом случае, однако сгенерирован он будет во время выполнения с помощью Reflection.Emit, а не во время компиляции компилятором C#.

Теперь должно быть понятно, каким образом работают всякие LINQ 2 SQL, Entity Framework и тому подобные вещи. Если LINQ 2 Object, который реализован в виде методов расширения интерфейса IEnumerable<T>, принимают делегаты, то метода расширения интерфейса IQueryable<T> принимают выражения, которые сериализируются, передаются в SQL Server, там по ним генерируются соответствующие SQL запросы, которые затем выполняются, и только потом результат возвращается вызывающему коду. Не сложно догадаться, что таким же образом можно реализовать какой-нибудь LINQ 2 WCF, когда сервер приложений будет получать выражения от клиентского кода, разбирать и выполнять в собственном контексте.

Dynamic LINQ

Теперь, когда мы более или менее разобрались с принципами, положенными в основу LINQ можно переходить к той самой динамической составляющей, которая до сего момента упоминалась только во введении (конечно, не считая заголовка?).

Итак, «капитан» нам подсказывает, что Dynamic LINQ должен иметь какое-то отношение к «динамическому» созданию LINQ-запросов во время выполнения, когда информация о том, что же нам такого сделать, доступна не во время компиляции, а формируется уже непосредственно во время выполнения программы.

Центральной частью библиотеки Dynamic LINQ является класс DynamicQueryable, расположенный в пространстве имен System.Linq.Dynamic, который содержит ряд методов расширения интерфейса IQueryable и IQueryable<T>, для динамического создания запросов по предоставленной строке. Этот класс содержит методы Select, Where, GroupBy и некоторые другие, но в отличие от стандартных методов расширения, представленных классом Queryable, эти методы расширения принимают строку, а не готовое выражение.

Давайте в качестве примера рассмотрим операции над следующим классом Customer:

class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

При наличии такого класса мы можем воспользоваться Dynamic LINQ следующим образом:

// Создаем тестовый список заказчиков
var customers = new Customer[]
        {
            new Customer {Id = 1, Name = "Customer1"},
            new Customer {Id = 2, Name = "Customer2"}
        };
// Для использования Dynamic LINQ нам необходимо вызвать метод AsQueryable,
// поскольку методы расширения класса DynamicQueryable определены для
// интерфейса IQueryable, а не для интерфейса IEnumerable
var res = customers.AsQueryable().
    // Фильтруем исходный список и получаем только записи с Id равным 1
    Where("Id = 1").
    // Создаем объект анонимного класса, содержащего 3 свойства Id, Name
    // и GeneratedProperty (которое добавлено исключительно в демонстрационных
    // целях)
    Select("new(Id, Name,  \"Value\" as GeneratedProperty)");
foreach (var c in res)
{
    // При запуске этого кода получим следующий результат:
    // {Id=1, Name=Customer1, GeneratedProperty=Value}
    Console.WriteLine(c);
}

При этом, если провести аналогию с классическими LINQ запросами, то приведенный выше запрос может быть переписан таким образом:

var res = customers.
    Where(c => c.Id == 1).
    Select(c => new { c.Id, c.Name, GeneratedProperty = "Value" });

Конечно же, в подобном коде нет особенного смысла, когда запросы известны на этапе компиляции, но это может быть весьма полезным, когда запрос, передаваемый функции Where или Select будет вводиться пользователем непосредственно или хранится во внешнем источнике, таком как конфигурационный файл. Однако даже этот пример показывает основные функциональные возможности, предоставляемые библиотекой Dynamic LINQ.

Поскольку большинство методов класса DynamicQueryable принимают строки, а не делегаты или выражения, то основной задачей библиотеки является разбор строк для получения выражений, а также для динамического создания объектов анонимных классов. Сама архитектура библиотеки Dynamic LINQ, основанная на строках, накладывает одно важное ограничение на ее использование: Dynamic LINQ нельзя использовать в выражениях запросов, поскольку в этом случае необходимо обеспечить соответствие с Query Expression Pattern, который требует наличия лямбда-выражения, которое может быть преобразовано компилятором либо в делегат, либо в дерево выражения.

Для разбора выражений в Dynamic LINQ используется класс DynamicExpression, содержащий несколько перегруженных версий функций ParseLambda и Parse. Давайте вернемся к уже рассмотренному ранее выражению x => x + 1 и попробуем получить его путем разбора некоторой строки:

// Всю работу по созданию выражения берет на себя компилятор
Expression<Func<int, int>> expression = x => x + 1;

// Для получения выражения из строки вначале нужно создать
// экземпляр ParameterExpression, а уже затем разобрать строку
var xExpression = Expression.Parameter(typeof(int), "x");
var parsedExpression =
    (Expression<Func<int, int>>)DynamicExpression.ParseLambda(
    new[] { xExpression }, null, "x + 1");

// Теперь мы можем скомпилировать каждое выражение и выполнить его

// Получим 2
Console.WriteLine(expression.Compile()(1));
// Опять же, получим 2, поскольку parseExpression
// представлет собой то же самое выражение, что и expression
Console.WriteLine(parsedExpression.Compile()(1));

Для разбора строкового выражения с конкретными типами параметров, нужно создать массив экземпляров ParameterExpression для каждого параметра строкового выражения и передать его функции ParseLambda. Вторым параметром нужно передать тип возвращаемого значения выражения; если этот тип не указан (как в нашем случае), то компилятор сам справится с этой задачей, в результате чего типом возвращаемого значения будет выведен автоматически (в нашем случае это будет int). В результате метод ParseLambda возвращает выражение, которое будет представлять дерево выражение абсолютно аналогичное тому, которое создает за нас компилятор в первом случае.

Другим, весьма популярным сценария использования выражений являются предикаты, которые принимают экземпляр некоторого кастомного класса и возвращают bool. Типичным примером выражений такого рода являются выражения, активно используемые в методе Where. И хотя совершенно необязательно, чтобы возвращаемым значением был именно bool, мы рассмотрим именно такой пример.

// Это выражение опять за нас создает компилятор
Expression<Func<Customer, bool>> expression =
    c => c.Id == 1 && c.Name.Length > 6;

// А это выражение мы получаем путем разбора строки
Expression<Func<Customer, bool>> parsedExpression =
    DynamicExpression.ParseLambda<Customer, bool>("Id == 1 AND Name.Length > 6");

// В этом случае фактические значения параметров
// передаются через параметр object[] values
Expression<Func<Customer, bool>> parsedExpression2 =
    DynamicExpression.ParseLambda<Customer, bool>("Id == @0 AND Name.Length > @1", 1, 6);

// Создаем двух подопытных кроликов, т.е. заказчиков,
// на которых мы будем проверять наши выражения
var cust1 = new Customer { Id = 1, Name = "Cust1" };
var cust2 = new Customer { Id = 1, Name = "Customer1" };

// Копилируем выражения; это позволит использовать их
// как обычные "функторы" без дополнительных накладных
// расходов на производительность
var func = expression.Compile();
var parsedFunc = parsedExpression.Compile();

// Выведет false, поскольку Name.Length для объекта
// cust1 вернет 5
Console.WriteLine(func(cust1));
// Выведет true, поскольку Name.Length объекта cust2
// равна 9, что удовлетворяет нашему предикату
Console.WriteLine(func(cust2));

// При вызове делегата parsedFunc мы получим аналогичные результаты
// поскольку предикаты func и parsedFunc эквивалентны
Console.WriteLine(parsedFunc(cust1));
Console.WriteLine(parsedFunc(cust2));

В классе DynamicExpression содержится обобщенная версия метода ParseLambda, в обобщенных параметрах которого указывается тип параметра и возвращаемого значения. При этом если обычное лямбда-выражение представлено в виде: c => c.PropertyName > 3, то в строковом представлении у нас остается только вторая часть выражения, а в качестве имени может быть использовано зарезервированное ключевое слово it (например, “it.PropertyName > 3”), либо просто указано имя свойства, метода или поля без дополнительного идентификатора (например, “PropertyName > 3”). После того, как мы распарсили строковое выражение, мы можем использовать его как обычное выражение, сгенерированное компилятором или сконструированное руками каким-то иным способом.

Помимо приведенных выше примеров, строка с выражением может содержать булевы операторы, такие как “&&”, “II”, “!” или “AND”, “OR”, “NOT”, операторы сравнения, умножения, деления, различные литералы, такие как “0”, “123”, “2.25”, “hello” и т.п., обращение к полям и свойствам объекта, “x.m”, “x.m()”, “x[5]”, к полям и свойствам типа “MyType.m”, тернарный оператор ?:, а также создание экземпляров анонимных классов “new(Id, Name, AnotherProperty as MyProperty)”. В результате чего мы получаем такой себе небольшой парсер выражений со своим простым языком, который вполне может быть использован для каких-то собственных специфических нужд.

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

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

UPD: Да, совсем забыл сказать. В моем текущем проекте Dynamic LINQ нашел свое применение для чтения выражений из конфигурационного файла, о чем я писал в предыдущей заметке под названием “Конфигурябельность”. Так что эта штука не просто сферический конь, который совершенно никому не нужен, а в целом, вполне себе неплохой инструмент для решения определенного круга задач.

17 комментариев:

  1. Эпиграф порадовал.
    И статья интересная, правда пока не представляю где это применить.

    ОтветитьУдалить
  2. Я нашел применение в совершенно реальном проекте, историю о котором рассказывал в статье "Конфигурябельность"

    ОтветитьУдалить
  3. А Вы на Java, случайно, не пишете?

    ОтветитьУдалить
  4. Огромное спасибо за статью. Даже не подозревал о такой возможности, как парсинг лямбда-выражений. Возьму на заметку.

    ОтветитьУдалить
  5. а вот странно, почему описания этого нет в текущей версии MSDN? И статья, которую вы ссылаетесь, довольная старая. Как дела обстоят в настоящий момент?

    ОтветитьУдалить
  6. Тоже пытались применить Dynamic Linq в проекте, но неполучилось, т.к. он не мог вывести типы (это было для LinqToSql, мы пытались генерить динамические запросы).
    Поэтому пришлось использовать PredicateBuilder. Оч. классная штука, рекомендую.
    http://www.albahari.com/nutshell/predicatebuilder.aspx

    ОтветитьУдалить
  7. Это только с Linq To Objects или с EF тоже можно использовать?

    ОтветитьУдалить
  8. Я думал, может Вы сделаете цикл статей про программированию системы на Google App Engine + Google Web Toolkit :) Его паттерны, типа EventBus и т.д. :)

    ОтветитьУдалить
  9. Естественно это можно использовать с любым LINQ провайдером.

    ОтветитьУдалить
  10. @Андрей: этого нет в MSDN поскольку эта библиотека не является частью BCL или .Net Framework.
    О дальнейшем развитии Dynamic LINQ я ничего не слышал, хотя после выхода .Net Framework 4, в деревьях выражений появилось много чего нового и вкусного.

    ОтветитьУдалить
  11. @SoftDed: со статьями о Google App Engine или с чем-то подобным помочь, увы не смогу, ибо по работе занимаюсь совсем другим, а в свободное время осилить такую тему, боюсь, не смогу:(

    ОтветитьУдалить
  12. Сергей, громадное спасибо, что объяснили довольно сложную тему с помощью простых и понятных шагов :-)

    Вот только я не понял фразу «Dynamic LINQ нельзя использовать в выражениях запросов». Объясните (возможно, не могу понять, т.к. литературу по .Net читаю в основном на английском и не могу найти сопоставление с привычным для меня термином)

    ОтветитьУдалить
  13. Мда... Чего-то я забыл привести оригинальный термин (обычно я стараюсь такого не пропускать).

    Выражения запросов - это LINQ query comprehension syntax.

    Например, вот это:

    var r = from l in list select l;

    Это LINQ запрос в формате выражения запросов (тот самый query comprehesion syntax).

    А вот это:

    var r = list.Select(l => l);

    Это LINQ запрос в формате вызова методов (method call expression).

    ОтветитьУдалить
  14. а я его использую c плагином jqGrid, где для сортировки передается стринговый параметр

    ОтветитьУдалить
  15. "C# в отношении «строгости» типизации является скорее мультипарадигменым" - разные понятия - мультипарадигмальный и типизация

    ОтветитьУдалить
  16. а как пременить такое для XElement ?

    ОтветитьУдалить