Как многие уже наверняка знают, LINQ - встроенный в C# язык запросов, которые появился аж в 2007 году (.NET Framework 3.5) и существует он в виде двух реализаций: Query-синтаксис (синтаксис запросов) и Method-синтаксис (синтаксис методов). Несмотря на все возможности и достоинства синтаксиса запросов, он часто несправедливо игнорируется в пользу синтаксиса методов. И в этой статье я попытаюсь восстановить справедливость и на практических примерах покажу преимущества query-синтаксиса и его полезность для опытных разработчиков на C#.

Обзор Query-синтаксиса

По стилю и читабельности Query-синтаксис очень напоминает SQL. Он предоставляет собой декларативный способ работы с данными и является довольно выразительным и прямолинейным. Рассмотрим на примере интернет-магазина с товарами (Products).

Предположим, у нас есть список товаров, и мы хотим получить все с ценой выше 50.

class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

List<Product> products = new List<Product>
{
    new Product { Id = 1, Name = "Ноутбук", Price = 1200 },
    new Product { Id = 2, Name = "Клавиатура", Price = 80 },
    new Product { Id = 3, Name = "Мышь", Price = 30 },
    new Product { Id = 4, Name = "Монитор", Price = 300 }
};

Вот как будет выглядеть эта выборка с использованием синтаксиса запросов:

var expensiveProducts = from product in products
                        where product.Price > 50
                        select product;

А теперь сделаем то же самое, но синтаксисом методов:

var expensiveProducts = products.Where(p => p.Price > 50);

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

Однако, этот пример довольно спорный, поэтому давайте посмотрим на примеры, где более явно видны преимущества использования синтаксиса запросов.

Сложные запросы и несколько источников данных

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

Предположим, у нас есть два списка: один для продуктов и другой для категорий продуктов. Мы хотим получить все продукты с соответствующими названиями категорий и ценами выше 50.

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

List<Category> categories = new List<Category>
{
    new Category { Id = 1, Name = "Электроника" },
    new Category { Id = 2, Name = "Аксессуары" }
};

Вот как мы можем сделать это с использованием синтаксиса запросов:

var result = from product in products
             join category in categories on product.CategoryId equals category.Id
             where product.Price > 50
             select new { 
                ProductName = product.Name,
                CategoryName = category.Name,
                Price = product.Price 
             };

Опять же, многословно, но вполне читабельно. А теперь давайте сравним это с синтаксисом методов:

var result = products
    .Where(p => p.Price > 50)
    .Join(categories, 
          p => p.CategoryId, 
          c => c.Id, 
          (p, c) => new { 
            ProductName = p.Name,
            CategoryName = c.Name,
            Price = p.Price 
          });

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

Группировка и агрегация данных

Предположим, мы хотим сгруппировать продукты по категориям и рассчитать среднюю цену для каждой категории. Вот как мы можем сделать это с использованием синтаксиса запросов:

var groupedProducts = from product in products
                      join category in categories on product.CategoryId equals category.Id
                      group product by category.Name into productGroup
                      select new
                      {
                          CategoryName = productGroup.Key,
                          AveragePrice = productGroup.Average(p => p.Price)
                      };

Да, появилась новая абстракция - productGroup, однако, код остается вполне читабельным и понятным. Чего сказать о синтаксисе методов я уже не могу:

var groupedProducts = products
    .Join(categories, 
          p => p.CategoryId, 
          c => c.Id, 
          (p, c) => new { Product = p, Category = c })
    .GroupBy(pc => pc.Category.Name, pc => pc.Product)
    .Select(g => new { 
          CategoryName = g.Key,
          AveragePrice = g.Average(p => p.Price) 
    });

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

Комбинирование синтаксиса запросов и синтаксиса методов

Стоит отметить, что C# не накладывает на нас никакие ограничения по использованию только одного из этих синтаксисов, то есть мы можем легко (шутка) комбинировать Query-синтаксис и Method-синтаксис, чтобы использовать преимущества обоих подходов. Это может привести к более читабельному и удобному для поддержки коду в некоторых сценариях, но никаких гарантий я вам здесь не даю, действуйте на свой страх и риск.

Предположим, мы хотим получить уникальные имена категорий со средней ценой продуктов выше 100. Так, используя комбинацию синтаксисов, мы можем получить вполне себе милого Франкенштейна:

var filteredGroups = (from product in products
                      join category in categories on product.CategoryId equals category.Id
                      group product by category.Name into productGroup
                      select new
                      {
                          CategoryName = productGroup.Key,
                          AveragePrice = productGroup.Average(p => p.Price)
                      } into s
                      where s.AveragePrice > 100
                      select s.CategoryName)
                      .Distinct();

Заключение

Query-синтаксис - это мощный и выразительный инструмент, которым C# разработчики не должны пренебрегать при работе со сложными запросами, несколькими источниками данных и задачами связанными с агрегацией данных. И если вы хотите научиться самостоятельно так же лихо использовать синтаксис запросов, наряду с синтаксисом методов и их комбинацией, чтобы писать более удобный для поддержки и понятный код, то приглашаю вас на курс C# Developer. Professional. Для ознакомления с форматом обучения и знакомства с преподавателями приглашаю вас на бесплатный урок курса, где разработаем многопоточный сервер приложений с минимумом примитивов синхронизации. Рассмотрим вопросы синхронизации, масштабируемости приложения, обработку входящих сообщений, вопросы реализации длительных операций.

Комментарии (20)


  1. Oxoron
    00.00.0000 00:00
    +23

    лично мне уже приходится намного сильнее напрягать мозг, чтобы разобраться что же это за pc, и что вообще здесь происходит

    Для решения этой проблемы не нужно менять синтаксис. Поменяйте названия переменных ( p=>product, c=>category).

    Более того, в сложных запросах типа Join с 4-5 аргументами, можно прописывать названия аргументов. Будет у вас что-то вроде

    var result = products
        .Where(product => product.Price > 50) 
        .Join(categories, 
              outerKeySelector: product => product.CategoryId, 
              innerKeySelector: category => category.Id, 
              resultSelector: (product, category) => new { 
                ProductName = product.Name,
                CategoryName = category.Name,
                Price = product.Price 
              });

    , только красиво подсвеченное в VS.


    1. Lexo Автор
      00.00.0000 00:00

      Согласен, ваш вариант намного понятнее, однако, довольно громоздко выходит. Да и

      on product.CategoryId equals category.Id

      для меня как-то проще читается, чем

      outerKeySelector: product => product.CategoryId, 
      innerKeySelector: category => category.Id, 

      Хотя, возможно, я просто не достаточно к такому синтаксису привык.


    1. SergeyTatevosyan
      00.00.0000 00:00
      +2

      Тоже никогда не понимал что за общепринятое пренебрежение названиями переменных при использовании Linq. Начинаешь сомневаться- может это я чего то не понимаю.


  1. Evengard
    00.00.0000 00:00
    +3

    Скажу честно, я как правило тяготею к цепочке методов... Но только пока дело не доходит до джойнов. Особенно навроде left join-ов (хотя и inner join-ы тоже удобней в query syntax писать). В методном варианте это выглядит как-то уж совсем громоздко и неочевидно...


  1. Vasjen
    00.00.0000 00:00
    +4

    Как многие уже наверняка знают, LINQ - встроенный в C# язык запросов, которые появился аж в 2007 году (.NET Framework 3.5) и существует он в виде двух реализаций: Query-синтаксис (синтаксис запросов) и Method-синтаксис (синтаксис методов).

    Несмотря на все возможности и достоинства синтаксиса запросов, он часто несправедливо игнорируется в пользу синтаксиса методов. 

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

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

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


    1. freegemini
      00.00.0000 00:00

      Тут, наверное, ключевое слово "начинающий". Я неоднократно сталкивался с ситуацией, когда именно, ну совсем уж начинающие, достаточно тяжело усваивают то, что мне уже кажется таким элементарным, что аж бесит!!! Поэтому не нужно забывать, как мы сами изучали что-то "с нуля". Ведь изучить новую форму синтаксиса уже знакомого языка это не совсем одно и то же, что изучить "современные инструменты... делегаты и анонимные методы" человеку, не знающему еще ни одного языка... И как в любом обучении - всегда идут от простого к сложному...


    1. Lexo Автор
      00.00.0000 00:00

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


    1. Depact
      00.00.0000 00:00
      +1

      Можно ли пример инструментов более эффективных чем LINQ? Хочу расширить кругозор.

      В компаниях где я работал использовался по сути только он в связке с EF Core. Хотя схожие запросы писались иначе чем в статье.


  1. Dansoid
    00.00.0000 00:00
    +1

    После группировки для фильтрации можно и не отходить от Query Syntax:

    var filteredGroups = 
        from product in products
        join category in categories on product.CategoryId equals category.Id
        group product by category.Name into productGroup
        select new
        {
            CategoryName = productGroup.Key,
            AveragePrice = productGroup.Average(p => p.Price)
        } into s
        where s.AveragePrice > 100
        select s;
    


    1. Lexo Автор
      00.00.0000 00:00

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


      1. AgentFire
        00.00.0000 00:00
        +1

        Для обоснования совмещения используйте Distinct. Его тупо нет в query синтаксе.


        1. Lexo Автор
          00.00.0000 00:00

          Вы абсолютно правы, поправил пример.


  1. Proydemte
    00.00.0000 00:00
    +1

    Resharper сконвертирует query синтаксис в method синтаксис. Вот обратно не нашёл как сделать.

    А для удобства джойнов, можно пару вспомогательных обёрток написать и будет типа такого, для inner join

    products.InnerJoin(categories)
                    .On((product) => product.CategoryId,
                        (category) => category.Id)
                    .Select((product, category) => new { 
                        product.Id, 
                        product.Name, 
                        CategoryName = category.Name
                    });


    для left outer join:
    categories.LeftOuterJoin(products)
                    .On((category) => category.Id,
                        (product) => product.CategoryId)
                    .Select((category, product) => new
                    {
                        CategoryName = category.Name,
                        product?.Id,
                        product?.Name
                    });


    1. Lexo Автор
      00.00.0000 00:00

      Вот это, кстати, мне тоже видится проблемой. Что для лучшее решение которое я знаю для реализации джойнов - написать свой метод расширения. Хотя и он не скажу, что сильно проще читается, чем синтаксис запросов.


      1. Proydemte
        00.00.0000 00:00
        +3

        Есть ещё одно неявная проблема с query синтакс, если использовать Roslyn для работы с исходным кодом, то query синтакс выпадает из общего сценария и надо заморачиваться для его поддержки.


        Лично мне method синтакс гораздо понятнее, легче читать и я его всегда использую.


        1. Lexo Автор
          00.00.0000 00:00

          Да, согласен. Просто хотел ещё раз напомнить, что в шарпе есть не только method syntax.


          1. Proydemte
            00.00.0000 00:00
            +1

            Кстати есть ещё опция, при объявлении классов задать связь между классами.

            Т.е. Что-то типа (с возможностью использования generics в атрибутах)

             class Product
                {
                    public int Id { get; set; }
            
                    [Relationships<Category>(nameof(Category.Id))]
                    public int CategoryId { get; set; }
            
                    public string Name { get; set; }
                    public decimal Price { get; set; }
                }
            


            Если структура классов генерируется автоматически (типа из EF), то вообще без проблем такое добавить.

            Тогда условие связи вообще можно пропустить, оно будет через рефлекшн выведено, т.е. получится, что-то типа:
            categories.LeftOuterJoin(products)
                            .Select((category, product) => new
                            {
                                CategoryName = category.Name,
                                product?.Id,
                                product?.Name
                            });

            Если добавить аналайзер, то на этапе компиляции можно проверить что зависимости определены и определены корректно (т.е. совместимые типы, правильные имена и т.п.). И при кэшировании правил, будет работать почти также быстро как и без рефлекшена.

            А при желании, можно соурс генератор прикрутить и уже всё статично загенерировать, вообще железобетонно будет.


  1. zindur2
    00.00.0000 00:00

    Для маленьких баз сойдет. а вот для больших больше проблем. Очень часто сгенерированый SQL код не ефективный и приходится писать ручками...


  1. arTk_ev
    00.00.0000 00:00

    Непонятно почему не перенесут нормальный linq из F#. Он мощнее, быстрее и не генерит мусор.


  1. ioncorpse
    00.00.0000 00:00
    +1

    Для простых запросов Method-синтаксис удобнее. Просто привыкнуть надо к лямбдам и вообще не шарахаться от чего-то сложнее foreach.)
    Когда мне не хватало - пытался Query-синтаксис, получалось лучше, но это НЕ читабельно (мое мнение) и НЕ оптимально. Не хватает Method? EF разрешает в SQL, а дальше и до SP недалеко.
    Если нужен Query - что-то в архитектуре вашего ПО не так явно.