DEV Community

Cover image for LINQ в C#. Огляд
Oleksandr Martyniuk
Oleksandr Martyniuk

Posted on • Edited on • Originally published at martyniuk.dev

LINQ в C#. Огляд

В цій статті пропоную розглянути LINQ як важливий компонент .NET фреймворку, його історію та роль. Чому він був створений і як врешті користуватись цим інструментом. В кінці розглянемо приклади на мові C#, які дадуть уявлення про те, що таке LINQ.

LINQ - (анг. language integrated query) - мова запитів до структурованих даних, що інтегрована в C#. Такими структурованими даними можуть бути колекції об'єктів в пам'яті, XML файли, таблиці баз даних, веб-сервіси і т.ін. Але для чого нам потрібен ще один інструмент в мові C#, яка і так дозволяє працювати з даними? Вся справа у легкості розуміння і сприйняття. Для того, щоб пояснити в чому саме полягає легкість LINQ, необхідно розглянути відмінність між декларативним та імперативним програмуванням.

Імперативне та декларативне програмування

Перші мови програмування задавали чіткий порядок команд. Це дуже зручно, коли ви маєте справу з регістрами процесору і прямим доступом до пам'яті. В таких мовах, як C та Assembler широко використовуються оператори виділення пам'яті, присвоєння, умовні оператори та підпрограми. Все це ознаки імперативного підходу. Слово імператив з англійської перекладається як наказ і це досить точне вмзначення для подібного підходу, тому що при імперативному підході програма є послідовністю чітких команд і комп'ютер виконує ці команди одна за одною.

До імперативних мов програмування відносяться:

  • C#
  • Python
  • JavaScript
  • Go

Всі ці мови базуються на змінних, операторах присвоєння і підпрограмах. Такий підхід близький до того як працює комп'ютер, але далекий від людської мови, звичної всім нам. Людині зручніше декларувати те, чого вона хоче, ніж описувати чіткий алгоритм досягнення цієї мети.

Тому імперативному підходу протиставляється декларативний. Його головною ознакою є те, що покроковий алгоритм не задається. Натомість задається джерело даних, описується бажаний результат і набір правил, завдяки яким цей результат буде досягнуто. Виконання запиту доручається інтерпретатору, який вміє перевести його у форму зручну для комп'ютера, тобто в імперативну форму.

До декларативних мов відносяться:

  • SQL
  • Regular Expressions
  • XSLT Transformation
  • Gremlin

Саме введення декларативного підходу до обробки даних в .NET і було головною метою створення LINQ.

Порівняння підходів

Розглянемо на прикладі два підходи, спочатку імперативний з використанням циклу, а потім декларативний з використанням LINQ.

Як тестовий набір даних візьмемо список супергероїв, кожен з яких має ім'я, рік народження (або першої згадки у коміксах) та назву серії, де він вперше з'явився.

Отже, створимо нову консольну програму в .NET Core:

> dotnet new console -n  LinqTestApp
The template "Console Application" was created successfully.
Enter fullscreen mode Exit fullscreen mode

Визначимо клас супергероя і додамо тестові дані.

private class Hero
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Comics { get; set; }
}

private static readonly List<Hero> _heroes = new List<Hero> 
{
    new Hero
    {
        Name = "Superman",
        YearOfBirth = 1938,
        Comics = "Action Comics"
    },
    new Hero
    {
        Name = "Batman",
        YearOfBirth = 1938,
        Comics = "Detective Comics"
    },
    new Hero
    {
        Name = "Captain America",
        YearOfBirth = 1941,
        Comics = "Captain America Comics"
    },
    new Hero
    {
        Name = "Ironman",
        YearOfBirth = 1963,
        Comics = "Tales of Suspense"
    },
    new Hero
    {
        Name = "Spiderman",
        YearOfBirth = 1963,
        Comics = "Amazing Fantasy"
    }
};
Enter fullscreen mode Exit fullscreen mode

Нехай, наша задача полягатиме в тому, щоб відібрати тих супергероїв, які мають слово "man" в імені та вивести їх імена на екран у алфавітному порядку. Тобто, ми повинні отримати імена всіх героїв (окрім Капітана Америки) у відсортованому вигляді.

Batman
Ironman
Spiderman
Superman
Enter fullscreen mode Exit fullscreen mode

При імперативному підході нам необхідно створити список в який ми будемо заносити імена відфільтрованих героїв, потім пройтись по списку героїв і додати до створеного списку лише тих, чиї імена містять слово "man". Після цього необхідно відсортувати отриманий список і вивести на екран:

static void Main(string[] args)
{
    var heroNames = new List<string>();
    foreach (var hero in _heroes)
    {
        if (hero.Name.Contains("man"))
        {
            heroNames.Add(hero.Name);
        }
    }
    heroNames.Sort();

    foreach (var heroName in heroNames)
    {
        Console.WriteLine(heroName);
    }
}
Enter fullscreen mode Exit fullscreen mode

Тепер давайте розв'яжемо ту ж задачу, але за допомогою LINQ.

Не забудьте піключити простір імен System.Linq

using System.Linq;
...
static void Main(string[] args)
{
    var heroNames =
        from hero in _heroes
        where hero.Name.Contains("man")
        orderby hero.Name
        select hero.Name;

    foreach (var hero in heroNames)
    {
        Console.WriteLine(hero.Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Програма стала на 5 рядків коротшою і легшою для розуміння, адже вибірка даних здійснюється мовою дуже схожою на англійську: From heroes where heroName contains "man" ordered by name select name, що можна перекласти як "З колекції героїв обери імена тих героїв які містять слово "man" в імені і відсортуй їх за ім'ям".

Розберемо, що робить даний код.

from hero in _heroes задає джерело даних. У нас це константний список _heroes. До кожного елементу списку ми будемо звертатися у виразі через змінну hero. І хоча ми ніде не вказували її тип, вираз залишається строго типізованим, адже компілятор має змогу вивести тип з типу колекції _heroes.

where hero.Name.Contains задає умову фільтрації вхідного списку. Якщо елемент відповідає умові, він передається далі. Таким чином, всі подальші оператори у виразі будуть працювати вже з відфільтрованим списком. Оператор where ще називають оператором фільтрації.

orderby hero.Name задає поле і спосіб сортування.

select hero.Name задає значення, що потрапить у результуючу вибірку. Оскільки нас цікавлять лише імена героїв, ми вказуємо тут поле Name. Саме цей оператор задає тип результату, тому у нашому випадку це буде IEnumerable<string>. Даний оператор ще називають оператором проекції, оскільки він перетворює дані, що містить джерело у вигляд необхідний нам для конкретної задачі.

Методи розширення

Варто зазначити, що хоча синтаксис LINQ значно відрізняється від синтаксису C#, все ж під капотом LINQ використовує методи розширення C#, тож наведений вище запит можна переписати в більш звичному об'єктному стилі:

var heroNames = _heroes
    .Where(hero => hero.Name.Contains("man"))
    .OrderBy(hero => hero.Name)
    .Select(hero => hero.Name);
Enter fullscreen mode Exit fullscreen mode

Такий синтаксис називається синтаксисом методів розширення (або лямбда синтаксисом) і він може застосовуватись разом з синтаксисом запитів LINQ. Часто, коли говорять про LINQ мають на увазі методи розширення, тому що вони реалізовані в просторі імен System.Linq і є частиною LINQ як компоненту .NET. У своїй більшості ці методи розширюють інтерфейс IEnumerable і є базою для реалізації LINQ. Це може спочатку збивати з пантелику, але LINQ це не лише синтаксис from ... in ... select, але також і синтаксис методів розширення.

Так як методи розширення є базою для реалізації LINQ, вони більш потужні ніж синтаксис запитів. Наприклад, метод Where має перевантажену версію, в якій при фільтрації доступний індекс елементу у вихідній колекції. Цей індекс може бути використаний при формуванні логічного виразу (предикату).

    ...
    .Where((hero, index) => hero.Name.Contains("man") && index > 2)
Enter fullscreen mode Exit fullscreen mode

Нажаль, даний індекс недоступний, якщо ми використовуємо синтаксис запиту from ... in ... select.

При використанні синтаксису запиту також недоступні скалярні функції Count, Max, Sum та інші методи (наприклад, Intersect).

Також, з методами розширення ми можемо розбити LINQ вираз на декілька частин і сформувати його згідно певної умови, що для синтаксису запиту неможливо. Наприклад:

var query = _heroes.Where((hero, index) => hero.Name.Contains("man"));

if (shouldBeSorted)
    query = query.OrderBy(hero => hero.Name);

var heroNames = query
    .Select(hero => hero.Name);
Enter fullscreen mode Exit fullscreen mode

Ми додаємо сортування тільки якщо вхідний параметр shouldBeSorted дорівнює true. Використовуючи синтаксис запиту нам необхідно записати вираз двічі в залежності від умови: в першому випадку зі сортуванням, а в другому - без нього.

В подальшому огляді ми будемо використовувати синтаксис методів розширення, так як він більш потужний і дозволяє показати можливості LINQ в повній мірі.

Трохи історії

У 2007 році мова C# мала версію 2.0 і не мала LINQ. Обробка даних відбувалась в імперативному стилі. В той час вже існували Python 2.4 та JavaScript 1.6, які мали потужні вбудовані засоби роботи з колекціями, такі як filter, map і reduce. C# значно програвав їм у зручності коли йшлося про роботу з колекціями, і це не могло продовжуватись довго.

Восени 2007 року компанія Microsoft випустила .NET Framework 3.5 в якому були значні нововведення. Ці зміни дали можливість створити LINQ та підняти версію мови C# до 3.0 Серед нововведень були:

  • Лямбда вирази зробили можливим просте визначення предикатів для методів типу Where, Select у вигляді () => {...}.
  • Анонімні типи дозволили створювати об'єкти довільної структури на льоту і прибрали необхідність оголошувати тип для результату LINQ виразу.
  • Дерева виразів зробили можливим збереження предикатів у вигляді об'єктів, на основі яких різні провайдери даних могли сформувати власний оптимізований запит. Це стосується LINQ to SQL або LINQ to XPath.
  • Методи розширення дозволили розширяти вже існуючі типи без їх модифікації і наслідування. Це дозволило застосовувати LINQ до великої кількості сторонніх типів, що підтримують IEnumerable або IQueryable.
  • Ініціалізатори об’єктів та колекцій довзолили створювати об'єкти та ініціалізувати їх поля без використання конструкторів, що значно спростило синтаксис для методів проекції LINQ, коли нові об'єкти створюються як частина виразу.
  • Оголошення змінних через var значно спростило визначення типу результату запиту і зробило можливим повернення даних анонімних типів з LINQ виразу.

Всі ці можливості вивели C# і платформу .NET на якісно новий рівень і довзолили створити LINQ. З моменту випуску він став невід'ємною частиною .NET фреймворку і як бібліотека для роботи з даними не поступається, а багато в чому і перевершує, вбудовані засоби інших мов, таких як Java, Python, Go і JavaScript.

В наступних статтях розглянемо детальніше всі основні аспекти роботи з LINQ. Ви переконаєтесь, що це не тільки зручний, але й доволі ефективний та зрілий інструмент.


Оригінальна стаття на моєму сайті.

Top comments (0)