Списъци
Накратко: Списъкът е колекция, която расте и се свива сама; List<T> пази елементите поред като масив, а List<string> казва какъв тип са те.
Проблем
Количката е пълна
Пишеш количката на онлайн магазин и пазиш продуктите в масив. Масивът обаче има размер, който не се променя. Когато клиентът добави четвърти продукт, правиш по-голям масив и копираш всичко. Когато махне продукт, остава дупка:
string[] cart = { "Хляб", "Мляко", "Кафе" };
// Четвърти продукт: масивът е пълен, правим по-голям и копираме.string[] bigger = new string[cart.Length + 1];for (int i = 0; i < cart.Length; i++){ bigger[i] = cart[i];}bigger[cart.Length] = "Масло";cart = bigger;
// Клиентът маха млякото.cart[1] = null;
foreach (string item in cart){ Console.WriteLine(item);}Хляб
КафеМаслоnull: Console.WriteLine го печата като празен ред. А броят продукти вече не е cart.Length.Това е много код за нещо, което всеки магазин прави постоянно.
Идеята
List<T>: масив, който се грижи сам
C# има готов клас за това: List<T>. Той пази елементите в масив, но копирането и местенето прави сам. Такава колекция се казва списък:
List<string> cart = new List<string>();cart.Add("Хляб");cart.Add("Мляко");cart.Add("Кафе");cart.Add("Масло");cart.Remove("Мляко");Console.WriteLine(string.Join(", ", cart));Console.WriteLine("Продукти: " + cart.Count);Хляб, Кафе, МаслоПродукти: 3List<T>е в пространството от именаSystem.Collections.Generic.usingза него не пишеш: новите проекти го добавят сами, кактоusing System;.Addдобавя в края,Removeмаха и затваря дупката: „Кафе“ се мести на мястото на „Мляко“.Countе колко елемента има в момента. Пише се без скоби, защото е свойство. Това не еLength, защото списъкът не е масив.Console.WriteLine(cart)не печата елементите, а името на типа:
List<string> cart = new List<string>();cart.Add("Хляб");Console.WriteLine(cart);System.Collections.Generic.List`1[System.String]Затова string.Join(", ", cart) слепва елементите в един текст, разделени със запетая.
Ето какво прави List<T> вътре. Числото в скобите е началният размер на масива. Без него масивът е празен и при първото Add става с 4 места. Тук е 3, за да се види какво става, когато масивът се напълни:
List<string> cart = new List<string>(3);cart.Add("Хляб");cart.Add("Мляко");cart.Add("Кафе");cart.Add("Масло");cart.Remove("Мляко");Console.WriteLine(string.Join(", ", cart));Console.WriteLine("Продукти: " + cart.Count);Стъпка по стъпка, с думи
- cart сочи обект List<string>. Вътре има масив _items с 3 места и брояч _size.
- Add слага всеки елемент на първото свободно място и увеличава _size.
- Масивът е пълен. List<T> прави нов с 6 места, двойно повече, и копира трите елемента в него.
- _items вече сочи новия масив, а старият не трябва на никого. „Масло“ влиза на свободното място.
- Remove маха „Мляко“, а всичко след него се мести с едно място наляво. Дупка не остава.
- Отвън се вижда само списък с три продукта: копирането и местенето ги прави List<T>. Колко места има, казва Capacity.
По-точно, за любопитните
- Колко места има масивът отвътре, казва свойството
Capacity. Когато се напълни,List<T>прави нов масив с двойно повече места:
List<string> cart = new List<string>();Console.WriteLine(cart.Capacity);cart.Add("Хляб");Console.WriteLine(cart.Capacity);for (int i = 0; i < 4; i++){ cart.Add("Мляко");}Console.WriteLine(cart.Count + " " + cart.Capacity);045 8Идеята
Какво пише в ъглите
List<string> значи „списък от текст“. Типът в ъглите се казва типов параметър (generics). Той казва на компилатора какво има в списъка, и компилаторът пази това:
List<string> cart = new List<string>();cart.Add("Хляб");cart.Add(5);error CS1503: Argument 1: cannot convert from 'int' to 'string'Затова cart[0] връща направо string, без превръщане.
Отдясно типът се пише още веднъж: new List<string>(). Има и кратък запис, new(). Тогава компилаторът взима типа от лявата страна:
List<string> cart = new();cart.Add("Хляб");Console.WriteLine(cart[0]);ХлябВ C# променливата обикновено е от самия клас, List<string>. Списъкът има и интерфейс, IList<string>, с I отпред като всички интерфейси в C#. Той казва какво може списъкът, а List<string> е един начин да го прави:
IList<string> cart = new List<string>();cart.Add("Хляб");Console.WriteLine(cart[0]);ХлябТова е полиморфизмът от модул „Наследяване и полиморфизъм“. Интерфейса ще срещаш най-често като тип на параметър.
Идеята
Индекси: [i], Insert, IndexOf
Списъкът има индекси като масива, от 0 до Count - 1, и те се пишат със същите [ ]:
List<string> tasks = new List<string>();tasks.Add("Фактура за март");tasks.Add("Обаждане до банката");tasks.Add("Отчет за склада");
Console.WriteLine(tasks[0]);tasks[1] = "Среща в банката";tasks.Insert(0, "Кафе с екипа");Console.WriteLine(string.Join(", ", tasks));Console.WriteLine(tasks.IndexOf("Отчет за склада"));Console.WriteLine(tasks.Contains("Обаждане до банката"));Фактура за мартКафе с екипа, Фактура за март, Среща в банката, Отчет за склада3Falsetasks[i]чете,tasks[i] = xзаменя.Insert(i, x)вмъква на индексiи мести останалите надясно.IndexOf(x)търси и връща индекса или-1, аContains(x)казва само дали го има.
По-точно, за любопитните
- Индекс извън списъка спира програмата. Изключението е друго, отколкото при масив:
ArgumentOutOfRangeException, неIndexOutOfRangeException.
Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')^1брои отзад:tasks[^1]е последната задача, „Отчет за склада“.ContainsиIndexOfсравняват сEquals. За текст това значи „същите символи“, затова намират текст, който е равен, а не само същия обект.- Вмъкването и махането отпред са бавни при голям списък: всички елементи след индекса се местят с едно място. За един милион елемента това е един милион премествания.
Пробвай
Кой е на ред?
Списъкът с чакащите при лекаря. Иван влиза, а Ана има час и се нарежда втора.
Какво ще отпечата програмата?
List<string> waiting = new List<string>();waiting.Add("Иван");waiting.Add("Мария");waiting.Add("Петър");waiting.RemoveAt(0);waiting.Insert(1, "Ана");Console.WriteLine(string.Join(", ", waiting) + " " + waiting.Count);RemoveAt(0) маха Иван и Мария става първа, на индекс 0. Insert(1, "Ана") вмъква Ана на индекс 1 и бута Петър надясно. Остават трима.
Идеята
Обхождане
Списъкът се обхожда като масив: с foreach, когато ти трябват само елементите, или с for и индекс, когато ти трябва и номерът:
List<string> cart = new List<string>();cart.Add("Хляб");cart.Add("Кафе");cart.Add("Масло");
foreach (string item in cart){ Console.WriteLine("- " + item);}for (int i = 0; i < cart.Count; i++){ Console.WriteLine((i + 1) + ". " + cart[i]);}- Хляб- Кафе- Масло1. Хляб2. Кафе3. МаслоПо-точно, за любопитните
- Не махай елементи от списъка, докато го обхождаш с
foreach. Програмата спира:
foreach (string item in cart){ if (item == "Хляб") { cart.Remove(item); }}Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.Изключението идва винаги, дори когато махнеш предпоследния елемент: на следващата стъпка foreach проверява дали списъкът е променян.
- Ако трябва да махаш, обхождай с индекс отзад напред. Така махането не мести елементите, които още не си проверил:
List<string> cart = new List<string>();cart.Add("Хляб");cart.Add("Мляко");cart.Add("Хляб");for (int i = cart.Count - 1; i >= 0; i--){ if (cart[i] == "Хляб") { cart.RemoveAt(i); }}Console.WriteLine(string.Join(", ", cart));МлякоRemoveAll.Идеята
Числа в списък: List<int>
Списък от оценки е List<int>. В ъглите може да стои и прост тип като int, double или bool, затова числото стои в списъка, както си е:
List<int> scores = new List<int>();scores.Add(85);scores.Add(64);scores.Add(91);
int total = 0;foreach (int score in scores){ total += score;}Console.WriteLine("Средно: " + (double) total / scores.Count);Средно: 80(double) превръща сбора в дробно число, за да не е делението целочислено. C# печата 80, а не 80.0: дробно число без дробна част излиза без точка.
По-точно, за любопитните
- Опаковане (boxing) в C# също има. Когато прост тип се сложи в променлива от тип
object, C# го слага в обект. А==на дваobjectпроверява дали сочат един и същ обект:
object a = 127;object b = 127;Console.WriteLine(a == b);Console.WriteLine(a.Equals(b));
int c = 127;int d = 127;Console.WriteLine(c == d);FalseTrueTrueДвете 127 са опаковани поотделно, затова обектите са два. Equals сравнява стойностите.
- Старият клас
ArrayList(без ъгли, отSystem.Collections) пази всичко катоobject. Той приема и число, и текст, а всяко число опакова:
ArrayList old = new ArrayList();old.Add(85);old.Add("Хляб");Console.WriteLine(old.Count);2Затова днес се ползва List<int>.
intне може да еnull. Число, което може и да липсва, се пишеint?:
int? discount = null;Console.WriteLine(discount.HasValue);discount = 10;Console.WriteLine(discount.HasValue + " " + discount);FalseTrue 10Пробвай
Намери грешката
Залата пази заетите места в списък. Клиентът се отказва от място 2 и програмата трябва да отпечата 12, 7, 5. Вместо това печата 12, 2, 5. На кой ред е грешката?
Избери реда с грешката.
RemoveAt(i) маха по индекс: RemoveAt(2) маха елемента на индекс 2, тоест мястото 7. По стойност маха Remove: seats.Remove(2). В C# двата метода са с различни имена, затова Remove(2) винаги значи стойността 2. При числа обаче лесно се бърка кой от двата ти трябва, защото и индексът, и стойността са числа.По-точно, за любопитните
- Ако стойността я няма,
Removeне спира програмата и не маха нищо, а връщаfalse:
List<int> seats = new List<int> { 12, 7, 5 };Console.WriteLine(seats.Remove(2));Console.WriteLine(string.Join(", ", seats));False12, 7, 5Идеята
Подреждане и списък само за четене
Sort() подрежда списъка на място: текста по азбучен ред, числата по големина.
List<string> cities = new List<string>();cities.Add("София");cities.Add("Пловдив");cities.Add("Варна");cities.Sort();Console.WriteLine(string.Join(", ", cities));
List<int> prices = new List<int> { 30, 5, 12 };prices.Sort();Console.WriteLine(string.Join(", ", prices));Варна, Пловдив, София5, 12, 30new List<int> { 30, 5, 12 } прави списък с изброените стойности. Това е обикновен списък и можеш да го променяш.
Списък, който не се променя, се обявява с интерфейса IReadOnlyList<T>. Той има само това, което чете: индекса и Count. Затова компилаторът не дава да добавяш, махаш или подреждаш:
IReadOnlyList<string> days = new List<string> { "пн", "вт", "ср" };days.Add("чт");error CS1061: 'IReadOnlyList<string>' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'IReadOnlyList<string>' could be found (are you missing a using directive or an assembly reference?)IReadOnlyList не прави копие. Той само скрива методите за промяна. Ако някой има самия List, промяната му се вижда и през days:
List<string> week = new List<string> { "пн", "вт", "ср" };IReadOnlyList<string> days = week;week.Add("чт");Console.WriteLine(days.Count);4По-точно, за любопитните
Sort()подрежда текста по правилата на езика, който е зададен на компютъра (културата). Главните и малките букви не са отделно, затова „ананас“ е преди „Ябълка“. Ако подадешStringComparer.Ordinal, текстът се подрежда по кодовете на символите и главните букви са преди малките:
List<string> fruit = new List<string> { "ананас", "Ябълка", "банан", "Круша" };fruit.Sort();Console.WriteLine(string.Join(", ", fruit));fruit.Sort(StringComparer.Ordinal);Console.WriteLine(string.Join(", ", fruit));ананас, банан, Круша, ЯбълкаКруша, Ябълка, ананас, банан- Как се подреждат твои обекти, например продукти по цена, ще видиш в урока „Вложени класове“.
Упражнение
Списъци в класове
private поле на класа, а Main вика само методите му.1. Рафт с книги. Клас BookShelf с методи AddBook(string title), който слага книгата в края, и TakeBook(), който връща и маха първата книга (или null, ако рафтът е празен). Добави свойство Count и ToString(). В Main сложи „Под игото“, „Тютюн“ и „Железният светилник“, вземи една книга и отпечатай останалите.
Взе: Под иготоОстават 2: [Тютюн, Железният светилник]2. Журнал на събитията. Клас Event с дата и описание и клас EventLog с методи AddEvent(string date, string description) и GetEventsBetween(string from, string to), който връща списък със събитията между двете дати, включително. Датите са текст във вида 2026-03-31. В този вид текстът се подрежда като датите, затова string.CompareOrdinal(a, b) ги сравнява вярно. Той връща отрицателно число, ако a е преди b, 0 при равни и положително, ако a е след b. Добави четири събития: 2026-01-15 „Годишна инвентаризация“, 2026-03-31 „Отчет за първото тримесечие“, 2026-05-20 „Нов доставчик на кафе“, 2026-09-01 „Нов учебен сезон“. Отпечатай тези от 2026-03-01 до 2026-06-30.
2026-03-31 Отчет за първото тримесечие2026-05-20 Нов доставчик на кафе3. Тетрадка. Клас Note със заглавие и съдържание и клас Notebook с методи AddNote, FindNote(string title) (връща бележката или null), DeleteNote(string title) (връща true, ако е изтрил) и свойство Count. Добави бележки „ООП“, „Колекции“ и „SQL“, намери „Колекции“, изтрий „ООП“ и провери, че вече я няма.
Колекции: List<T> расте самИзтрита: ООПБележки: 2Няма бележка ООП.4. Смятане наум. Клас Question с текст и верен отговор и клас MathGame с списък от въпроси, метод AddQuestion и метод Start(). Start задава въпросите един по един, чете отговора и накрая казва колко са верните. Въпросите са 2 + 2, 3 * 4 и 15 - 9.
41272 + 2 = Вярно!3 * 4 = Вярно!15 - 9 = Грешно, отговорът е 6.Резултат: 2 от 3Още задачи
- Без повторения. Прочети 6 имена и ги събери в списък, като пропускаш тези, които вече са в него. При вход Иван, Мария, Иван, Ана, Мария, Петър изходът е
Иван, Мария, Ана, Петър. - Най-скъпият продукт. В списък List<double> има цени. Намери най-високата и на кой индекс е, без
Max. - Обърни списъка. Напиши метод, който връща нов списък със същите елементи в обратен ред. Не променяй първоначалния.
- Списък за пазаруване. Чети команди
add мляко,remove мляко,listиendи пази списъка между тях. Командата за триене на продукт, който го няма, казва „Няма такъв продукт.“
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Капсулация · Полиморфизъм · Интерфейси · Изключения
- Продължава в: Стек, опашка и свързан списък · Dictionary и HashSet · Хеш код и хеш функции · Вложени класове
- Задачи по темата: Колекции