Към съдържанието
wannacode

Списъци

Накратко: Списъкът е колекция, която расте и се свива сама; 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);
Изход
Хляб, Кафе, Масло
Продукти: 3
  • List<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);
Стъпка по стъпка, с думи
  1. cart сочи обект List<string>. Вътре има масив _items с 3 места и брояч _size.
  2. Add слага всеки елемент на първото свободно място и увеличава _size.
  3. Масивът е пълен. List<T> прави нов с 6 места, двойно повече, и копира трите елемента в него.
  4. _items вече сочи новия масив, а старият не трябва на никого. „Масло“ влиза на свободното място.
  5. Remove маха „Мляко“, а всичко след него се мести с едно място наляво. Дупка не остава.
  6. Отвън се вижда само списък с три продукта: копирането и местенето ги прави 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);
Изход
0
4
5 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("Обаждане до банката"));
Изход
Фактура за март
Кафе с екипа, Фактура за март, Среща в банката, Отчет за склада
3
False
  • tasks[i] чете, tasks[i] = x заменя.
  • Insert(i, x) вмъква на индекс i и мести останалите надясно.
  • IndexOf(x) търси и връща индекса или -1, а Contains(x) казва само дали го има.
По-точно, за любопитните
  • Индекс извън списъка спира програмата. Изключението е друго, отколкото при масив: ArgumentOutOfRangeException, не IndexOutOfRangeException.
tasks[3] при 3 задачи
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);
Отговор

Идеята

Обхождане

Списъкът се обхожда като масив: с 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));
Изход
Мляко
В модул „Модерен C#“ ще видиш по-кратък начин: 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);
Изход
False
True
True

Двете 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);
Изход
False
True 10

Пробвай

Намери грешката

Залата пази заетите места в списък. Клиентът се отказва от място 2 и програмата трябва да отпечата 12, 7, 5. Вместо това печата 12, 2, 5. На кой ред е грешката?

Избери реда с грешката.

По-точно, за любопитните
  • Ако стойността я няма, Remove не спира програмата и не маха нищо, а връща false:
List<int> seats = new List<int> { 12, 7, 5 };
Console.WriteLine(seats.Remove(2));
Console.WriteLine(string.Join(", ", seats));
Изход
False
12, 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, 30

new 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.

Вход
4
12
7
Изход
2 + 2 = Вярно!
3 * 4 = Вярно!
15 - 9 = Грешно, отговорът е 6.
Резултат: 2 от 3

Още задачи

  • Без повторения. Прочети 6 имена и ги събери в списък, като пропускаш тези, които вече са в него. При вход Иван, Мария, Иван, Ана, Мария, Петър изходът е Иван, Мария, Ана, Петър.
  • Най-скъпият продукт. В списък List<double> има цени. Намери най-високата и на кой индекс е, без Max.
  • Обърни списъка. Напиши метод, който връща нов списък със същите елементи в обратен ред. Не променяй първоначалния.
  • Списък за пазаруване. Чети команди add мляко, remove мляко, list и end и пази списъка между тях. Командата за триене на продукт, който го няма, казва „Няма такъв продукт.“

Тест

Провери се

1. С какво List<T> е по-удобен от масив за количка?
2. Какво казва <string> в List<string> cart?
3. List<int> seats съдържа 12, 2, 7, 5. Какво остава след seats.Remove(2)?
4. Може ли List<int>?
5. IReadOnlyList<string> days = new List<string> { "пн", "вт" }; Какво става при days.Add("ср")?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·