LINQ
Накратко: LINQ обработва колекция като поредица от стъпки: Where пресява, Select преобразува, OrderBy подрежда, а Sum и Average смятат.
Проблем
Отчет в цикъл
Поръчките вече имат и град:
class Order{ public string Customer { get; } public string City { get; } public decimal Total { get; }
public Order(string customer, string city, decimal total) { Customer = customer; City = city; Total = total; }}List<Order> orders = new List<Order>{ new Order("Мария", "София", 75.50m), new Order("Георги", "Варна", 12.90m), new Order("Елена", "София", 40.00m), new Order("Димитър", "Пловдив", 120.00m), new Order("Ана", "Варна", 64.00m),};Искаш клиентите с поръчки поне 50 €, подредени по азбучен ред:
List<string> result = new List<string>();foreach (Order o in orders){ if (o.Total >= 50) { result.Add(o.Customer); }}result.Sort();Console.WriteLine(string.Join(", ", result));Ана, Димитър, МарияДесет реда, а въпросът е един. Кодът казва как: направи празен списък, обиколи, провери, добави, подреди. За да разбереш какво прави, трябва да го изпълниш наум. Добавиш ли „само първите трима“ или „без повторения“, цикълът ще расте.
Идеята
LINQ: поредица от стъпки
Същият отчет, написан като стъпки:
List<string> result = orders .Where(o => o.Total >= 50) .Select(o => o.Customer) .Order() .ToList();Console.WriteLine(string.Join(", ", result));Ана, Димитър, МарияЧете се отгоре надолу: вземи поръчките, остави тези поне за 50 €, вземи клиента, подреди, събери в списък.
Тези методи са LINQ (Language Integrated Query, „заявки, вградени в езика“). Те са в System.Linq и работят върху всичко, което може да се обходи с foreach: списък, масив, речник. Резултатът им е заявка (IEnumerable<T>). Тя не пази данни. Тя е конвейер, по който елементите минават един по един. Всяка стъпка приема ламбда от предишния урок:
Where(условие)пропуска нататък само елементите, за които условието е вярно. Ламбдата еFunc<Order, bool>.Select(функция)превръща всеки елемент в друг: отOrderправиstring. Това еFunc<Order, string>.Order()подрежда по естествения ред, аOrderBy(o => o.Total)по ключ, който ти избираш.ToList()събира каквото е стигнало до края в новList<string>.
Списъкът orders не се променя. Заявката само го чете.
По-точно, за любопитните
Where,Select,Order,OrderByиTakeвръщат нова заявка и още нищо не правят.ToList,Count,Sum,Firstиforeachискат резултат и пускат конвейера.ToList()връща обикновенList<T>, който можеш да променяш. Масив даваToArray().Order()е от .NET 7. В по-стар код ще видишOrderBy(name => name): прави същото.- Има и втори запис, който прилича на SQL. Компилаторът го превръща в същите извиквания. В този курс се пишат методите:
List<string> result = (from o in orders where o.Total >= 50 orderby o.Customer select o.Customer).ToList();Console.WriteLine(string.Join(", ", result));Ана, Димитър, МарияВиж го
Какво минава през филтъра
List<int> prices = new List<int> { 12, 75, 40, 130, 58 };List<string> labels = prices .Where(p => p >= 50) .Select(p => p + " €") .ToList();Console.WriteLine(string.Join(", ", labels));Стъпка по стъпка, с думи
- Пет цени. Заявката само подготвя пътя: още нищо не е минало.
- 12 не минава p >= 50 и отпада. До Select не стига.
- 75 минава Where, Select го превръща в текста „75 €“, ToList() го събира.
- 40 също отпада. Всеки елемент минава целия път, преди да тръгне следващият.
- 130 и 58 минават. Списъкът prices не се променя.
- Резултатът е нов списък с три текста.
Петте цени тръгват по конвейера една по една. 12 стига до Where и отпада. 75 минава, Select го прави на текста „75 €“ и той влиза в списъка. После 40 отпада, а 130 и 58 минават.
Всеки елемент изминава целия път, преди да тръгне следващият. И нищо не тръгва, докато ToList() не поиска резултат.
По-точно, за любопитните
Това се казва мързеливо изпълнение. Методите на заявката само записват какво трябва да се направи. Виж го с отпечатване във всяка стъпка:
IEnumerable<int> query = new List<int> { 12, 75, 40 } .Where(p => { Console.WriteLine("Where " + p); return p >= 50; }) .Select(p => { Console.WriteLine("Select " + p); return p * 2; });Console.WriteLine("Заявката е готова, нищо не е минало през нея.");Console.WriteLine(string.Join(", ", query.ToList()));Заявката е готова, нищо не е минало през нея.Where 12Where 75Select 75Where 40150- Първият ред на изхода е отпечатан, преди през заявката да е минал и един елемент. В документацията на C# това е отложено изпълнение (deferred execution).
Select 75е веднага следWhere 75: елементите не чакат всички да минат презWhere.- Затова
Take(2)спира заявката, щом два елемента стигнат до края. Останалите дори не се проверяват:
IEnumerable<int> firstTwo = new List<int> { 75, 130, 58, 12, 99 } .Where(p => { Console.WriteLine("Where " + p); return p >= 50; }) .Take(2);Console.WriteLine(string.Join(", ", firstTwo));Where 75Where 13075, 130- Заявката не се изразходва. Всяко обхождане я пуска отначало: двата
Count()минават по четирите цени, и проверките са 8. Ако резултатът ти трябва няколко пъти, събери го веднъж сToList().
int checks = 0;IEnumerable<int> expensive = new List<int> { 12, 75, 40, 130 }.Where(p =>{ checks++; return p >= 50;});Console.WriteLine(expensive.Count());Console.WriteLine(expensive.Count());Console.WriteLine("Проверки: " + checks);22Проверки: 8AsParallel()пуска елементите в няколко нишки едновременно (PLINQ). Помага при много данни и тежки сметки. При малки списъци е по-бавно, а ламбдите не трябва да пипат общи данни.
Идеята
Сбор, брой, средно
Резултатът може да е число вместо списък. Сборът на четните числа:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };int sum = numbers.Where(n => n % 2 == 0).Sum();Console.WriteLine("Сума на четните числа: " + sum);Сума на четните числа: 6Sum() събира числата, които са стигнали до края. Със селектор, Sum(o => o.Total), първо взима числото от всеки елемент.Същото за поръчките:
int big = orders.Count(o => o.Total >= 50);decimal revenue = orders.Sum(o => o.Total);decimal average = orders.Average(o => o.Total);decimal maxTotal = orders.Max(o => o.Total);
Console.WriteLine("Поръчки от 50 € нагоре: " + big);Console.WriteLine($"Оборот: {revenue:F2} €");Console.WriteLine($"Средно: {average:F2} €");Console.WriteLine($"Най-голяма: {maxTotal:F2} €");Поръчки от 50 € нагоре: 3Оборот: 312.40 €Средно: 62.48 €Най-голяма: 120.00 €Count(условие)брои елементите, за които условието е вярно. Типът еint.AverageиMaxне могат да дадат отговор за празен списък: средно на нищо няма. Тогава хвърлятInvalidOperationException.Sumна празен списък е 0. Какво да правиш с резултат, който може да липсва, е следващата идея.
По-точно, за любопитните
С празен списък. DefaultIfEmpty(0) дава резерва: ако елементи няма, заявката дава един елемент, 0.
List<decimal> prices = new List<decimal>();Console.WriteLine(prices.Sum());Console.WriteLine(prices.DefaultIfEmpty(0).Max());try{ Console.WriteLine(prices.Max());}catch (InvalidOperationException e){ Console.WriteLine("InvalidOperationException: " + e.Message);}00InvalidOperationException: Sequence contains no elementsИдеята
Подреди, отрежи, без повторения
List<int> ratings = new List<int> { 5, 3, 5, 4, 2, 4, 5 };Console.WriteLine(string.Join(", ", ratings.Distinct().Order()));
List<string> top = orders .OrderByDescending(o => o.Total) .Take(3) .Select(o => o.Customer) .ToList();Console.WriteLine(string.Join(", ", top));
foreach (int i in Enumerable.Range(1, 3)){ Console.WriteLine("Маса " + i);}2, 3, 4, 5Димитър, Мария, АнаМаса 1Маса 2Маса 3Distinct()маха повторенията. Сравнява сEqualsиGetHashCode.OrderByDescending(o => o.Total)подрежда от най-голямата поръчка.Take(3)пропуска само първите три. Заедно с подреждането това е „топ 3“.Enumerable.Range(1, 3)дава числата 1, 2, 3. Вторият параметър е броят на числата, а не краят.ForEachима самоList<T>. Заявка се обхожда с обикновенforeach.
Пробвай
Кое стига до края?
Какво ще отпечата програмата?
List<string> drinks = new List<string> { "кафе", "чай", "кафе", "вода", "чай", "сок" };List<string> result = drinks .Distinct() .Order() .Take(3) .Select(d => d.ToUpper()) .ToList();Console.WriteLine(string.Join(", ", result));Distinct() оставя кафе, чай, вода, сок. Order() ги подрежда по азбука: вода, кафе, сок, чай. Take(3) оставя първите три, а Select ги прави с главни букви.Идеята
null: резултат, който може да липсва
Коя е най-голямата поръчка? Ако списъкът е празен, отговор няма. Тогава резултатът е null: „няма обект“. Но викащият трябва да помни да провери. Забрави ли, програмата спира с NullReferenceException някъде по-късно.
Java за това има Optional: кутия, в която има стойност или е празна. В C# такава кутия няма. Методите на LINQ, които търсят един елемент, връщат null, ако такъв няма, а проверката е твоя работа:
Order biggest = orders.MaxBy(o => o.Total);if (biggest != null){ Console.WriteLine("Най-голяма: " + biggest.Customer);}
Order huge = orders.FirstOrDefault(o => o.Total > 500);Console.WriteLine(huge != null);Console.WriteLine(huge?.Customer ?? "няма такава");Най-голяма: ДимитърFalseняма такаваMaxBy(o => o.Total)дава поръчката с най-голяма сума, а не самата сума. За празен списък от обекти даваnull.FirstOrDefault(условие)дава първия елемент, за който условието е вярно, илиnull, ако такъв няма.huge?.Customer: акоhugeеnull, целият израз еnull, вместо програмата да спре.x ?? yдаваy, акоxеnull, иначеx.
Твоите методи също връщат null, когато нямат отговор:
static string Nickname(string user){ // Връща прякора или null, ако човекът няма. return user == "maria" ? "Мими" : null;}string maria = Nickname("maria");if (maria != null){ Console.WriteLine("Прякор: " + maria);}
string georgi = Nickname("georgi");Console.WriteLine(georgi ?? "без прякор");Прякор: Мимибез прякорПо-точно, за любопитните
- Ако забравиш проверката:
Order huge = orders.FirstOrDefault(o => o.Total > 500);Console.WriteLine(huge.Customer);Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.Main()- Ако празният резултат е грешка, пиши
First, а неFirstOrDefault. Името казва честно какво ще стане:
try{ Order huge = orders.First(o => o.Total > 500); Console.WriteLine(huge.Customer);}catch (InvalidOperationException e){ Console.WriteLine("InvalidOperationException: " + e.Message);}InvalidOperationException: Sequence contains no matching element- За
int,decimalи другите стойностни типовеnullняма. ТогаваFirstOrDefaultдава 0, освен ако не му дадеш друга резерва:
List<int> ratings = new List<int> { 5, 3, 4 };Console.WriteLine(ratings.FirstOrDefault(r => r > 5));Console.WriteLine(ratings.FirstOrDefault(r => r > 5, -1));0-1- В нов проект (
dotnet new console) проверките заnullса включени (<Nullable>enable</Nullable>). Там тип, който може да еnull, се пише с?:Order? huge. Ако запишеш резултата наFirstOrDefaultвOrderбез?, компилаторът предупреждава. AnyиAllотговарят сTrueилиFalseдали някой или всички елементи отговарят на условието:
Console.WriteLine(orders.Any(o => o.Total > 100));Console.WriteLine(orders.All(o => o.Total >= 10));TrueTrueПробвай
Намери грешката
Избери реда с грешката.
Заявката не е списък, а рецепта. На ред 2 тя още нищо не е филтрирала. Count() я пуска веднъж, а string.Join на ред 5 я пуска наново, върху списъка, какъвто е в този момент, с 99 вътре:
Брой: 2Цени: 75, 130, 99Ако ти трябва резултатът към определен момент, събери го на ред 2 с ToList(). Тогава expensive е отделен списък и новата цена не го засяга:
List<int> prices = new List<int> { 12, 75, 40, 130 };List<int> expensive = prices.Where(p => p >= 50).ToList();Console.WriteLine("Брой: " + expensive.Count);prices.Add(99);Console.WriteLine("Цени: " + string.Join(", ", expensive));Брой: 2Цени: 75, 130Обратното вече е грешка: да добавяш в списъка, докато заявка върху него се обхожда. Тогава програмата спира:
List<int> prices = new List<int> { 12, 75, 40, 130 };try{ foreach (int price in prices.Where(p => p >= 50)) { prices.Add(price * 2); }}catch (InvalidOperationException e){ Console.WriteLine("InvalidOperationException: " + e.Message);}InvalidOperationException: Collection was modified; enumeration operation may not execute.Идеята
Групиране и съединяване
Колко поръчки има от всеки град и какъв е оборотът?
var byCity = orders .GroupBy(o => o.City) .OrderBy(g => g.Key);foreach (var group in byCity){ Console.WriteLine($"{group.Key}: брой {group.Count()}, оборот {group.Sum(o => o.Total):F2} €");}
string customers = string.Join(", ", orders.Select(o => o.Customer).Order());Console.WriteLine(customers);Варна: брой 2, оборот 76.90 €Пловдив: брой 1, оборот 120.00 €София: брой 2, оборот 115.50 €Ана, Георги, Димитър, Елена, МарияGroupBy(o => o.City) дели поръчките на групи по град. Всяка група има Key (градът) и се обхожда като списък с поръчките си, затова Count() и Sum(...) работят върху нея.
OrderBy(g => g.Key)подрежда групите по град. Без него групите излизат в реда, в който градовете се срещат за първи път: София, Варна, Пловдив.string.Join(", ", ...)съединява текстовете в един, със запетая между тях.
Типът на групите е дълъг (IOrderedEnumerable<IGrouping<string, Order>>), затова е var: компилаторът го извежда сам.
По-точно, за любопитните
ToDictionaryправи речник от групите: за всеки град списък с имената. Редът на ключовете вDictionaryне е гарантиран, затова градовете се подреждат изрично:
Dictionary<string, List<string>> namesByCity = orders .GroupBy(o => o.City) .ToDictionary(g => g.Key, g => g.Select(o => o.Customer).ToList());foreach (string city in namesByCity.Keys.Order()){ Console.WriteLine(city + ": " + string.Join(", ", namesByCity[city]));}Варна: Георги, АнаПловдив: ДимитърСофия: Мария, ЕленаToDictionary(ключ, стойност)прави речник с една стойност за ключ и хвърля изключение, ако ключът се повтори:
try{ Dictionary<string, string> customerByCity = orders.ToDictionary(o => o.City, o => o.Customer);}catch (ArgumentException e){ Console.WriteLine("ArgumentException: " + e.Message);}ArgumentException: An item with the same key has already been added. Key: СофияУпражнение
Отчети с LINQ
System.Linq е включен сам.1. Продажби по категория. Клас Sale с продукт, категория, брой и единична цена (decimal) и свойство Revenue (брой по цена). Групирай по категория с GroupBy, сумирай всяка група със Sum, отпечатай категориите по азбука и общия оборот. Продажбите: кафе на зърна, напитки, 12 бр. по 18,50 €; чай, напитки, 30 по 4,20 €; шоколад, сладки, 40 по 2,80 €; бисквити, сладки, 25 по 3,10 €; мед, храни, 8 по 12 €; пак кафе на зърна, 5 по 18,50 €.
Напитки: 440.50 €Сладки: 189.50 €Храни: 96.00 €Общо: 726.00 €2. Топ 3 продукта. Със същите продажби намери трите продукта с най-голям оборот. Кафето е в две продажби, затова първо групирай по продукт и сумирай, после подреди групите по сбора. Подсказка: GroupBy(s => s.Product), после OrderByDescending(g => g.Sum(s => s.Revenue)) и Take(3): всяка група има Key (продуктът) и продажбите си.
Кафе на зърна: 314.50 €Чай: 126.00 €Шоколад: 112.00 €3. Средна оценка. Прочети един ред с оценки, разделени с интервал. Отпечатай средната, най-високата и колко са отличните (5,50 и нагоре). Подсказка: Console.ReadLine().Split(' ').Select(double.Parse).
4.50 5.75 6.00 3.00 5.50Средно: 4.95Най-висока: 6.00Отлични: 34. Думите в текст. Прочети ред текст и отпечатай думите в него без повторения, по азбучен ред, с малки букви, разделени със запетая. Направи всеки знак, който не е буква, на интервал: text.Select(c => char.IsLetter(c) ? c : ' '), а new string(...) от тези знаци. После раздели с Split(' ', StringSplitOptions.RemoveEmptyEntries): празните части се махат.
Кафе с мляко, кафе без захар. Чай с мед!без, захар, кафе, мед, мляко, с, чайОще задачи
- Най-дългата дума. С
MaxBy(w => w.Length)намери най-дългата дума в изречение. Какво ще върне програмата за празен ред? - Средно по клас. Ученици с име, клас и оценка: средната оценка за всеки клас с
GroupByиAverage. - Големи и малки поръчки. С
Anyпровери дали има поръчка над 1000 €, а сAllдали всички са поне 10 €.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Списъци · Dictionary и HashSet · Вложени класове · Ламбди и делегати
- Продължава в: record, switch и шаблони · Тестове с xUnit · Бази данни · Reflection и атрибути
- Задачи по темата: Модерен C#