Списъци, речници и LINQ
Проблем
Количката расте
Клиентът на онлайн магазина слага в количката кафе, чай, мляко, после още филтри. После маха млякото. Колко продукта ще купи, не знаеш предварително. Масивът обаче иска размера си още при new:
string[] cart = new string[3];cart[0] = "Кафе на зърна";cart[1] = "Зелен чай";cart[2] = "Мляко за кафе";
try{ cart[3] = "Филтри за кафе";}catch (IndexOutOfRangeException e){ Console.WriteLine("Не се побира: " + e.Message);}Не се побира: Index was outside the bounds of the array.Има и втори проблем. Складът пази продукти по код като C-101. За да намериш количеството за един код в масив, обхождаш масива с цикъл всеки път. При 10 000 продукта и 100 поръчки в минута това са много цикли за нищо.
Виж го
List<T>: масив, който расте
Списъкът (List) пази елементи поред, като масива, но расте с Add и намалява с Remove:
List<string> cart = new List<string>();cart.Add("Кафе на зърна");cart.Add("Зелен чай");cart.Add("Мляко за кафе");cart.Add("Филтри за кафе");Console.WriteLine("В количката: " + cart.Count);Console.WriteLine("Първото: " + cart[0]);
cart.Remove("Мляко за кафе");Console.WriteLine("Има ли мляко? " + cart.Contains("Мляко за кафе"));foreach (string item in cart){ Console.WriteLine("- " + item);}В количката: 4Първото: Кафе на зърнаИма ли мляко? False- Кафе на зърна- Зелен чай- Филтри за кафе- В
List<string>ъгловите скоби казват какво има в списъка: текстове.List<int>е списък от цели числа,List<Product>е списък от продукти. Типът в скобите се казва типов параметър. Затова пишемList<T>, къдетоTе „какъвто тип избереш“. Countе броят на елементите (при масива еLength). Индексът е като при масива:cart[0]е първият.Removeмаха елемента, ако го има, аContainsказва дали го има.foreachминава през всички.
В този модул програмите са без обвивката class Program и Main: редовете стоят направо във файла. Това са top-level statements от урока „Подготовка на компютъра“: компилаторът сам слага Main около тях. Класовете, когато ги има, са след тези редове.
По-точно, за любопитните
- Списък може да се напълни още при създаването:
new List<int> { 2, 1, 3 }. Има и още методи:Insert(индекс, стойност)слага на място,RemoveAt(индекс)маха по място,Clear()изпразва.
List<int> quantities = new List<int> { 2, 1, 3 };quantities.Insert(0, 5);quantities.RemoveAt(2);Console.WriteLine(string.Join(", ", quantities));quantities.Clear();Console.WriteLine(quantities.Count);5, 2, 30string.Join(", ", списък)слепва елементите в един текст с разделител между тях.- Отвътре списъкът е масив с малко резерва. Когато се напълни,
Listправи нов, два пъти по-голям масив и копира елементите. ЗатоваAddобикновено е бърз, а ти не мислиш за размера. - Ако напишеш индекс извън списъка,
cart[10], изключението еArgumentOutOfRangeException, а неIndexOutOfRangeExceptionкато при масива.
Пробвай
Какво остава в списъка
Какво ще отпечата програмата?
List<int> quantities = new List<int> { 2, 5, 2, 1 };quantities.Remove(2);quantities.Add(3);Console.WriteLine(quantities.Count + " " + quantities[0] + " " + quantities[3]);Remove(2) маха само първото 2. Остава { 5, 2, 1 }, после Add(3) го прави { 5, 2, 1, 3 }. Четири елемента, първият е 5, четвъртият е 3.
Виж го
Речник: търсене по код
Речникът (Dictionary) пази двойки ключ → стойност. Тук ключът е кодът на продукта, а стойността е количеството на склад. Намирането по ключ е почти мигновено, без цикъл, колкото и продукти да има:
Dictionary<string, int> stock = new Dictionary<string, int>();stock["C-101"] = 12;stock["T-205"] = 30;stock["C-101"] = stock["C-101"] - 2;Console.WriteLine("Кафе на склад: " + stock["C-101"]);Console.WriteLine("Има ли X-999? " + stock.ContainsKey("X-999"));
if (stock.TryGetValue("T-205", out int tea)){ Console.WriteLine("Чай на склад: " + tea);}
try{ Console.WriteLine(stock["X-999"]);}catch (KeyNotFoundException){ Console.WriteLine("Няма продукт с код X-999");}
foreach (KeyValuePair<string, int> pair in stock){ Console.WriteLine(pair.Key + ": " + pair.Value + " бр.");}Кафе на склад: 10Има ли X-999? FalseЧай на склад: 30Няма продукт с код X-999C-101: 10 бр.T-205: 30 бр.Dictionary<string, int>: ключовете са текст, стойностите са цели числа.stock["C-101"] = 12записва. Ако ключът вече го има, стойността се заменя.stock["C-101"]чете.- Четене на ключ, който го няма, хвърля
KeyNotFoundException. Хващаш го сtry/catch, както в модул „Изключения“. По-добре е да не стигаш дотам:ContainsKeyказва дали ключът го има, аTryGetValueпроверява и чете наведнъж. Стойността идва презout, както приint.TryParse. foreachминава през двойките. Всяка еKeyValuePairсKeyиValue.
По-точно, за любопитните
Add(ключ, стойност)също записва, но хвърляArgumentException, ако ключът вече го има. Така не презаписваш цена по погрешка. Речник може да се напълни при създаването с двойки в къдрави скоби:
Dictionary<string, decimal> prices = new Dictionary<string, decimal>{ { "C-101", 18.90m }, { "T-205", 6.40m },};try{ prices.Add("C-101", 19.50m);}catch (ArgumentException){ Console.WriteLine("C-101 вече има цена: " + prices["C-101"] + " €");}prices.Remove("T-205");Console.WriteLine("Продукти с цена: " + prices.Count);C-101 вече има цена: 18.90 €Продукти с цена: 1- Ключовете са уникални, стойностите не са. Два продукта може да имат количество 30, но кодът
C-101е един. - Редът, в който
foreachдава двойките, не е обещан. Днес е редът на добавяне, ако не си махал нищо, но не разчитай на него. Ако ти трябва ред, подреди ключовете. - Речникът намира ключа бързо, защото пресмята от него число (хеш) и по него знае къде да гледа. Затова ключът трябва да не се променя, докато е в речника.
Пробвай
Код, който го няма
Програмата трябва да покаже цената на продукта с код C-102. Вместо това спира с изключение. На кой ред е проблемът?
Избери реда с грешката.
Ключът "C-102" не е в речника, затова prices[code] хвърля KeyNotFoundException. Когато кодът идва отвън (от клиента, от файл), провери го: if (prices.TryGetValue(code, out decimal price)) и в else отпечатай „Няма цена за C-102“.
Идеята
Правило като стойност: ламбда
Да намериш цените от 50 € нагоре е цикъл с if:
List<int> prices = new List<int> { 12, 75, 40, 130, 58 };List<int> expensive = new List<int>();foreach (int p in prices){ if (p >= 50) { expensive.Add(p); }}Console.WriteLine(string.Join(", ", expensive));75, 130, 58Утре ще ти трябват продуктите под 10 €, после само чайовете. Цикълът е същият, сменя се само условието в if. Удобно е условието да се подаде като стойност на метод, който прави цикъла вместо теб.
Такова условие се пише като ламбда: p => p >= 50. Чети го „за всяко p дай p >= 50“. Вляво от => е параметърът, вдясно е резултатът. Ламбдата е кратък метод без име, без return и без типове: C# ги извежда сам.
По-точно, за любопитните
Ламбдата има тип като всяка стойност. Func<int, bool> е „метод, който взима int и връща bool“. Можеш да я запишеш в променлива и да я викаш като метод. Ламбдата може да има и тяло с къдрави скоби и return, когато едно изражение не стига:
Func<int, bool> expensive = p => p >= 50;Console.WriteLine(expensive(75) + " " + expensive(12));
Func<decimal, bool> overWithVat = p =>{ decimal withVat = p * 1.2m; return withVat > 50;};Console.WriteLine(overWithVat(45.00m));True FalseTrueВиж го
Where и Select
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 не се променя.
- Резултатът е нов списък с три текста.
Методите Where и Select са от LINQ (Language Integrated Query), заявки към данни, вградени в езика. Работят за всеки списък и масив:
Where(p => p >= 50)пуска нататък само елементите, за които ламбдата даваtrue. Това е цикълът сifот предишната карта.Select(p => p + " €")превръща всеки елемент в нещо друго, тук в текст.ToList()събира резултата в новList. Списъкътpricesне се променя.
Елементите минават един по един: 12 отпада още на Where и до Select не стига.
Виж го
Продуктите на магазина
Истинският каталог е списък от обекти. Класът Product има само свойства, затова обектите се създават със свойствата в къдрави скоби: new Product { Id = 1, Name = "…" }. Това е същото като new Product() и после p.Id = 1; за всяко свойство.
List<Product> catalog = new List<Product>{ new Product { Id = 1, Name = "Еспресо на зърна", Price = 24.90m, Category = "кафе" }, new Product { Id = 2, Name = "Зелен чай сенча", Price = 9.80m, Category = "чай" }, new Product { Id = 3, Name = "Колумбия на зърна", Price = 31.50m, Category = "кафе" }, new Product { Id = 4, Name = "Мента", Price = 4.20m, Category = "чай" }, new Product { Id = 5, Name = "Кафемелачка", Price = 64.00m, Category = "уреди" },};
public class Product{ public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; }}Ламбдата получава цял продукт, затова условието чете свойствата му:
List<Product> coffee = catalog.Where(p => p.Category == "кафе").ToList();Console.WriteLine("Кафета: " + coffee.Count);
List<string> byPrice = catalog .OrderByDescending(p => p.Price) .Select(p => p.Name + ": " + p.Price + " €") .ToList();foreach (string line in byPrice){ Console.WriteLine(line);}Кафета: 2Кафемелачка: 64.00 €Колумбия на зърна: 31.50 €Еспресо на зърна: 24.90 €Зелен чай сенча: 9.80 €Мента: 4.20 €Where(p => p.Category == "кафе")оставя само кафетата.OrderByDescending(p => p.Price)подрежда по цена, от най-високата.OrderByподрежда от най-ниската. Ламбдата казва по какво се подрежда.- Методите се нижат един след друг: всеки получава резултата на предишния.
Каталогът е същият. Какво ще отпечата?
List<string> names = catalog .Where(p => p.Price < 30) .OrderBy(p => p.Price) .Select(p => p.Name) .ToList();Console.WriteLine(names.Count + ": " + names[0]);Под 30 € са три продукта: еспресото (24,90 €), сенчата (9,80 €) и ментата (4,20 €). OrderBy ги подрежда от най-евтиния, затова първа е ментата.
Виж го
Един отговор от целия списък
Част от методите на LINQ не връщат списък, а една стойност: сума, брой, „има ли“, един продукт:
Console.WriteLine("Общо: " + catalog.Sum(p => p.Price) + " €");Console.WriteLine("Средно: " + catalog.Average(p => p.Price) + " €");Console.WriteLine("Под 5 €: " + catalog.Count(p => p.Price < 5));Console.WriteLine("Има ли уреди? " + catalog.Any(p => p.Category == "уреди"));
Product tea = catalog.First(p => p.Category == "чай");Console.WriteLine("Първият чай: " + tea.Name);Product cocoa = catalog.FirstOrDefault(p => p.Category == "какао");Console.WriteLine(cocoa == null ? "Какао няма" : cocoa.Name);Общо: 134.40 €Средно: 26.88 €Под 5 €: 1Има ли уреди? TrueПървият чай: Зелен чай сенчаКакао нямаSumиAverageсъбират и усредняват това, което дава ламбдата.Count(правило)брои отговарящите,Any(правило)казва дали има поне един.First(правило)връща първия отговарящ, а ако няма такъв, хвърляInvalidOperationException.FirstOrDefaultв същия случай връщаnull. Ползвай го, когато е нормално да няма резултат, и провери заnull.
Кой метод дава общата стойност на каталога?
decimal total = catalog.(p => p.Price);Sum(p => p.Price) събира цените на всички продукти: 134,40 €.
По-точно, за любопитните
GroupByразделя елементите на групи по ключ. Всяка група имаKeyи се държи като списък, затова върху нея работятCountиSum. Типът на групата е дълъг (IGrouping<string, Product>), затова тук е удобенvar:
foreach (var group in catalog.GroupBy(p => p.Category)){ Console.WriteLine(group.Key + ": брой " + group.Count() + ", общо " + group.Sum(p => p.Price) + " €");}кафе: брой 2, общо 56.40 €чай: брой 2, общо 14.00 €уреди: брой 1, общо 64.00 €- Същата заявка може да се напише и със синтаксис на заявка, който прилича на SQL. Компилаторът го превръща в
Where,OrderByиSelect:
List<string> teas = (from p in catalog where p.Category == "чай" orderby p.Price select p.Name).ToList();Console.WriteLine(string.Join(", ", teas));Мента, Зелен чай сенча- Мързеливо изпълнение.
WhereиSelectне обхождат нищо, докато някой не поиска резултата:ToList,foreach,Sum,string.Join. Дотогава заявката е само рецепта, от типIEnumerable<int>(„нещо, през което можеш да минеш“). Затова тук 130 влиза в резултата, макар да е добавено следWhere:
List<int> prices = new List<int> { 12, 75 };IEnumerable<int> expensive = prices.Where(p => p >= 50);prices.Add(130);Console.WriteLine(string.Join(", ", expensive));75, 130- Ако ти трябва резултатът към даден момент, извикай
ToList()веднага: тогава списъкът е готов и по-късните промени не го засягат. - Цените тук са
decimal: при париdoubleдава грешки от закръгляне.Sumпази двата знака след точката: 134.40. Числата се печатат с точка, защото проверката на кода работи без български регионални настройки; на компютър с тях ще видиш 134,40.
Упражнение
Количка, склад и отчет
Реши задачите в редактора си. Ползвай класа Product от урока.
1. Количка. Сложи в List<Product> еспресо (24,90 €), мента (4,20 €) и кафемелачка (64,00 €). Отпечатай броя и общата сума. После намери кафемелачката по Id с First, махни я и отпечатай пак:
Продукти: 3, общо 93.10 €Махнах: КафемелачкаПродукти: 2, общо 29.10 €2. Склад. Речник код → количество: C-101 12 бр., T-205 30 бр., G-300 2 бр. Напиши метод Sell(stock, code, quantity), който намалява количеството и връща текст за резултата. Ако кода го няма или количеството не стига, речникът не се променя. Продай 5 от C-101, 3 от G-300 и 1 от X-999, после отпечатай склада:
C-101: продадени 5, остават 7G-300: има само 2Няма продукт X-999C-101: 7T-205: 30G-300: 23. Отчет. С каталога от урока отпечатай трите най-скъпи продукта и сумата на цените по категория. Take(3) взима първите три елемента. За сумите ползвай речник категория → сума (или GroupBy от „По-точно“):
Най-скъпите три: Кафемелачка 64.00 € Колумбия на зърна 31.50 € Еспресо на зърна 24.90 €По категории: кафе: 56.40 € чай: 14.00 € уреди: 64.00 €Тест