Предыдущая лекция |   | Следующая лекция :----------------:|:----------:|:----------------: [Ещё раз про классы. Интерфейсы.](./t6_oop_habr.md) | [Содержание](../readme.md#тема-6-основные-принципы-объектно-ориентированного-программирования) | [Шаблоны проектирования.](./t6_templates.md) # LINQ ## Основы LINQ **LINQ** (Language-Integrated Query) представляет простой и удобный язык запросов к источнику данных. В качестве источника данных может выступать объект, реализующий интерфейс **IEnumerable** (например, стандартные коллекции, массивы), набор данных **DataSet**, **документ XML**. Но вне зависимости от типа источника **LINQ** позволяет применить ко всем один и тот же подход для выборки данных. Существует несколько разновидностей **LINQ**: * **LINQ to Objects**: применяется для работы с массивами и коллекциями * **LINQ to Entities**: используется при обращении к базам данных через технологию Entity Framework * **LINQ to Sql**: технология доступа к данным в MS SQL Server * **LINQ to XML**: применяется при работе с файлами XML * **LINQ to DataSet**: применяется при работе с объектом DataSet * **Parallel LINQ (PLINQ)**: используется для выполнения параллельных запросов В этой главе речь пойдет прежде всего о **LINQ to Objects**. В чем же удобство **LINQ**? Посмотрим на простейшем примере. Выберем из массива строки, начинающиеся на определенную букву и отсортируем полученный список: ```cs // исходный массив string[] teams = {"Бавария", "Боруссия", "Реал Мадрид", "Манчестер Сити", "ПСЖ", "Барселона"}; // список для результатов var selectedTeams = new List(); foreach(string s in teams) { // ищем слова начинающиеся на букву "Б" if (s.ToUpper().StartsWith("Б")) selectedTeams.Add(s); } // сортируем получившийся список selectedTeams.Sort(); foreach (string s in selectedTeams) Console.WriteLine(s); ``` Теперь проведем те же действия с помощью **LINQ**: ```cs string[] teams = {"Бавария", "Боруссия", "Реал Мадрид", "Манчестер Сити", "ПСЖ", "Барселона"}; var selectedTeams = from t in teams // определяем каждый объект из teams как t where t.ToUpper().StartsWith("Б") //фильтрация по критерию orderby t // упорядочиваем по возрастанию select t; // выбираем объект foreach (string s in selectedTeams) Console.WriteLine(s); ``` Чтобы использовать функциональность **LINQ**, убедимся, что в файле подключено пространство имен *System.LINQ*. Итак, код стал меньше и проще. В принципе все выражение можно было бы записать в одну строку: `var selectedTeams = from t in teams where t.ToUpper().StartsWith("Б") orderby t select t`. Но для более понятной логической разбивки я поместил каждое отдельное подвыражение на отдельной строке. Простейшее определение запроса **LINQ** выглядит следующим образом: ``` from переменная in набор_объектов select переменная; ``` Итак, что делает этот запрос **LINQ**? Выражение `from t in teams` проходит по всем элементам массива *teams* и определяет каждый элемент как *t*. Используя переменную *t* мы можем проводить над ней разные операции. Несмотря на то, что мы не указываем тип переменной *t*, выражения **LINQ** являются строго типизированными. То есть среда автоматически распознает, что набор *teams* состоит из объектов **string**, поэтому переменная *t* будет рассматриваться в качестве строки. Далее с помощью оператора `where` проводится фильтрация объектов, и если объект соответствует критерию (в данном случае начальная буква должна быть "Б"), то этот объект передается дальше. Оператор `orderby` упорядочивает по возрастанию, то есть сортирует выбранные объекты. Оператор `select` передает выбранные значения в результирующую выборку, которая возвращается **LINQ**-выражением. В данном случае результатом выражения **LINQ** является объект `IEnumerable`. Нередко результирующая выборка определяется с помощью ключевого слова **var**, тогда компилятор на этапе компиляции сам выводит тип. Преимуществом подобных запросов также является и то, что они интуитивно похожи на запросы языка SQL, хотя и имеют некоторые отличия. ### Методы расширения LINQ Кроме стандартного синтаксиса `from .. in .. select` для создания запроса **LINQ** мы можем применять специальные методы расширения, которые определены для интерфейса **IEnumerable**. Как правило, эти методы реализуют ту же функциональность, что и операторы **LINQ** типа *where* или *orderby*. Например: ```cs string[] teams = { "Бавария", "Боруссия", "Реал Мадрид", "Манчестер Сити", "ПСЖ", "Барселона" }; var selectedTeams = teams .Where(t=>t.ToUpper().StartsWith("Б")) .OrderBy(t => t); foreach (string s in selectedTeams) Console.WriteLine(s); ``` Запрос `teams.Where(t=>t.ToUpper().StartsWith("Б")).OrderBy(t => t)` будет аналогичен предыдущему. Он состоит из цепочки методов *Where* и *OrderBy*. В качестве аргумента эти методы принимают делегат или лямбда-выражение. Не каждый метод расширения имеет аналог среди операторов **LINQ**, но в этом случае можно сочетать оба подхода. Например, используем стандартный синтаксис **LINQ** и метод расширения *Count()*, возвращающий количество элементов в выборке: ```cs int number = (from t in teams where t.ToUpper().StartsWith("Б") select t).Count(); ``` ### Список используемых методов расширения LINQ * **Select**: определяет проекцию выбранных значений * **Where**: определяет фильтр выборки * **OrderBy**: упорядочивает элементы по возрастанию * **OrderByDescending**: упорядочивает элементы по убыванию * **ThenBy**: задает дополнительные критерии для упорядочивания элементов возрастанию * **ThenByDescending**: задает дополнительные критерии для упорядочивания элементов по убыванию * **Join**: соединяет две коллекции по определенному признаку * **GroupBy**: группирует элементы по ключу * **ToLookup**: группирует элементы по ключу, при этом все элементы добавляются в словарь * **GroupJoin**: выполняет одновременно соединение коллекций и группировку элементов по ключу * **Reverse**: располагает элементы в обратном порядке * **All**: определяет, все ли элементы коллекции удовлятворяют определенному условию * **Any**: определяет, удовлетворяет хотя бы один элемент коллекции определенному условию * **Contains**: определяет, содержит ли коллекция определенный элемент * **Distinct**: удаляет дублирующиеся элементы из коллекции * **Except**: возвращает разность двух коллекцию, то есть те элементы, которые создаются только в одной коллекции * **Union**: объединяет две однородные коллекции * **Intersect**: возвращает пересечение двух коллекций, то есть те элементы, которые встречаются в обоих коллекциях * **Count**: подсчитывает количество элементов коллекции, которые удовлетворяют определенному условию * **Sum**: подсчитывает сумму числовых значений в коллекции * **Average**: подсчитывает cреднее значение числовых значений в коллекции * **Min**: находит минимальное значение * **Max**: находит максимальное значение * **Take**: выбирает определенное количество элементов * **Skip**: пропускает определенное количество элементов * **TakeWhile**: возвращает цепочку элементов последовательности, до тех пор, пока условие истинно * **SkipWhile**: пропускает элементы в последовательности, пока они удовлетворяют заданному условию, и затем возвращает оставшиеся элементы * **Concat**: объединяет две коллекции * **Zip**: объединяет две коллекции в соответствии с определенным условием * **First**: выбирает первый элемент коллекции * **FirstOrDefault**: выбирает первый элемент коллекции или возвращает значение по умолчанию * **Single**: выбирает единственный элемент коллекции, если коллекция содердит больше или меньше одного элемента, то генерируется исключение * **SingleOrDefault**: выбирает первый элемент коллекции или возвращает значение по умолчанию * **ElementAt**: выбирает элемент последовательности по определенному индексу * **ElementAtOrDefault**: выбирает элемент коллекции по определенному индексу или возвращает значение по умолчанию, если индекс вне допустимого диапазона * **Last**: выбирает последний элемент коллекции * **LastOrDefault**: выбирает последний элемент коллекции или возвращает значение по умолчанию ## Фильтрация выборки и проекция ### Фильтрация Для выбора элементов из некоторого набора по условию используется метод *Where*. Например, выберем все _четные_ элементы, которые больше `10`. Фильтрация с помощью операторов **LINQ**: ```cs int[] numbers = { 1, 2, 3, 4, 10, 34, 55, 66, 77, 88 }; var evens = numbers.Where(i => i % 2 == 0 && i > 10); ``` Если выражение в методе **Where** для определенного элемента будет равно **true** (в данном случае выражение `i % 2 == 0 && i > 10`), то данный элемент попадает в результирующую выборку. ### Выборка сложных объектов Допустим, у нас есть класс пользователя: ```cs class User { public string Name { get;set; } public int Age { get; set; } public List Languages { get; set; } public User() { Languages = new List(); } } ``` Создадим набор пользователей и выберем из них тех, которым больше `25` лет: ```cs List users = new List { new User { Name="Том", Age=23, Languages = new List { "английский", "немецкий" }}, new User { Name="Боб", Age=27, Languages = new List { "английский", "французский" }}, new User { Name="Джон", Age=29, Languages = new List { "английский", "испанский" }}, new User { Name="Элис", Age=24, Languages = new List { "испанский", "немецкий" }} }; var selectedUsers = users.Where(u => u.Age > 25); foreach (User user in selectedUsers) Console.WriteLine($"{user.Name} - {user.Age}"); ``` Консольный вывод: ``` Боб - 27 Джон - 29 ``` ### Сложные фильтры Теперь рассмотрим более сложные фильтры. Например, в классе пользователя есть список языков, которыми владеет пользователь. Что если нам надо отфильтровать пользователей по языку: Для создания запроса применяется метод *SelectMany*: ```cs var selectedUsers = users .SelectMany(u => u.Languages, (u, l) => new { User = u, Lang = l }) .Where(u => u.Lang == "английский" && u.User.Age < 28) .Select(u=>u.User); ``` Результат: ``` Том - 23 Боб - 27 ``` Метод *SelectMany()* в качестве первого параметра принимает последовательность, которую надо проецировать, а в качестве второго параметра - функцию преобразования, которая применяется к каждому элементу. На выходе она возвращает `8` пар "пользователь - язык" `new { User = u, Lang = l }`, к которым потом применяетс фильтр с помощью *Where*. >8 пар в конкретно этом примере, а вообще метод для каждого пользователя возвращает список объектов равный количеству языков пользователя. ### Проекция Проекция позволяет спроектировать из текущего типа выборки какой-то другой тип. Для проекции используется оператор **select**. Допустим, у нас есть набор объектов следующего класса, представляющего пользователя: ```cs class User { public string Name { get;set; } public int Age { get; set; } } ``` Но нам нужен не весь объект, а только его свойство *Name*: ```cs List users = new List(); users.Add( new User { Name = "Sam", Age = 43 }); users.Add( new User { Name = "Tom", Age = 33 }); var names = users.Select(u => u.Name); foreach (string n in names) Console.WriteLine(n); ``` Результат выражения **LINQ** будет представлять набор строк, поскольку метод `Select(u => u.Name)` выбирают в результирующую выборку только значения свойства *Name*. Аналогично можно создать объекты другого типа, в том числе анонимного: ```cs List users = new List(); users.Add( new User { Name = "Sam", Age = 43 }); users.Add( new User { Name = "Tom", Age = 33 }); var items = users.Select(u => new { FirstName = u.Name, DateOfBirth = DateTime.Now.Year - u.Age }); foreach (var n in items) Console.WriteLine($"{n.FirstName} - {n.DateOfBirth}"); ``` Здесь метод _select_ создает объект базового типа (object), используя текущий объект **User**. И теперь результат будет содержать набор объектов данного типа, в котором определены два свойства: *FirstName* и *DateOfBirth*. ### Переменые в запросах и оператор let Иногда возникает необходимость произвести в запросах **LINQ** какие-то дополнительные промежуточные вычисления. Для этих целей мы можем задать в запросах свои переменные с помощью оператора _let_: ```cs List users = new List() { new User { Name = "Sam", Age = 43 }, new User { Name = "Tom", Age = 33 } }; var people = from u in users let name = "Mr. " + u.Name select new { Name = name, Age = u.Age }; ``` В данном случае создается переменная name, значение которой равно `"Mr. " + u.Name`. Возможность определения переменных наверное одно из главных преимуществ операторов **LINQ** по сравнению с методами расширения. ### Выборка из нескольких источников В LINQ можно выбирать объекты не только из одного, но и из большего количества источников: Например, возьмем классы: ```cs class Phone { public string Name { get; set; } public string Company { get; set; } } class User { public string Name { get; set; } public int Age { get; set; } } ``` Создадим два разных источника данных и произведем выборку: ```cs List users = new List() { new User { Name = "Sam", Age = 43 }, new User { Name = "Tom", Age = 33 } }; List phones = new List() { new Phone { Name="Lumia 630", Company="Microsoft" }, new Phone { Name="iPhone 6", Company="Apple"}, }; var people = from user in users from phone in phones select new { Name = user.Name, Phone = phone.Name }; foreach (var p in people) Console.WriteLine($"{p.Name} - {p.Phone}"); ``` Консольный вывод: ``` Sam - Lumia 630 Sam - iPhone 6 Tom - Lumia 630 Tom - iPhone 6 ``` Таким образом, при выборке из двух источников каждый элемент из первого источника будет сопоставляться с каждым элементом из второго источника. То есть получиться 4 пары. ## Сортировка Для сортировки набора данных по возрастанию используется метод *OrderBy*: ```cs int[] numbers = { 3, 12, 4, 10, 34, 20, 55, -66, 77, 88, 4 }; IEnumerable orderedNumbers = numbers.OrderBy(i => i); foreach (int i in orderedNumbers) Console.WriteLine(i); ``` Метод *OrderBy* принимает критерий сортировки. В данном случае в качестве критерия выступает само число. Возьмем посложнее пример. Допустим, надо отсортировать выборку сложных объектов. Тогда в качестве критерия мы можем указать свойство класса объекта: ```cs List users = new List() { new User { Name = "Tom", Age = 33 }, new User { Name = "Bob", Age = 30 }, new User { Name = "Tom", Age = 21 }, new User { Name = "Sam", Age = 43 } }; var sortedUsers = users.OrderBy(u => u.Name); foreach (User u in sortedUsers) Console.WriteLine(u.Name); ``` Метод *OrderBy()* сортирует по возрастанию. Для сортировки по убыванию используется метод *OrderByDescending*: ```cs var sortedUsers = users.OrderByDescending(u => u.Name); ``` ### Множественные критерии сортировки В наборах сложных объектов иногда возникает ситуация, когда надо отсортировать не по одному, а сразу по нескольким полям. Для этого в запросе **LINQ** добавляются методы _ThenBy_ (для сортировки по возрастанию) или _ThenByDescending_ (для сортировки по убыванию): ```cs List users = new List() { new User { Name = "Tom", Age = 33 }, new User { Name = "Bob", Age = 30 }, new User { Name = "Tom", Age = 21 }, new User { Name = "Sam", Age = 43 } }; var result = users .OrderBy(u => u.Name) .ThenBy(u => u.Age); foreach (User u in result) Console.WriteLine($"{u.Name} - {u.Age}"); ``` Результат программы: ``` Alice - 28 Bob - 30 Sam - 43 Tom - 21 Tom - 33 ``` --- ## Задание на дом: 1. Реализовать примеры из лекции на своей предметной области (приветствуется использование методов не рассмотренных в рамках лекции: distinct, sum и т.п.). Предыдущая лекция |   | Следующая лекция :----------------:|:----------:|:----------------: [Ещё раз про классы. Интерфейсы.](./t6_oop_habr.md) | [Содержание](../readme.md#тема-6-основные-принципы-объектно-ориентированного-программирования) | [Шаблоны проектирования.](./t6_templates.md)