1. Коротка передісторія
Колись у C# було всього дві опції під час роботи з колекціями: використовувати масиви або так звані «слабо типізовані» колекції — на кшталт ArrayList, куди можна було складати об’єкти будь-якого типу. Здавалося б, свобода! Але варто було додати до такої колекції рядок і ціле число упереміш — і починався хаос: неможливо було безпечно дістати елемент без додаткових (і не завжди дієвих) перевірок типу. А ще — безглузді помилки нізвідки та танці з перетвореннями типів (casting).
Приклад неузагальненої колекції (ArrayList):
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("Привіт C#");
int number = (int)stuff[0]; // ОК
string text = (string)stuff[1]; // ОК
int fail = (int)stuff[1]; // БАХ! InvalidCastException (помилка під час виконання)
Так, компілятор не сваритиметься — помилка з’явиться лише під час виконання! Це як відкрити холодильник і знайти там гарячу каструлю — сюрприз!
У чому суть узагальнення?
Узагальнені колекції (Generics) з’явилися, щоб можна було створювати колекції з гарантованим типом вмісту. Це дає три ключові переваги:
- Безпека типів на етапі компіляції. Компілятор не дасть вам випадково покласти в колекцію щось зайве.
- Зручність: не доведеться щоразу виконувати явне перетворення типу.
- Продуктивність: не витрачається час на непотрібну "упаковку і розпаковку" значущих типів (boxing/unboxing).
2. Що таке Generics (узагальнення) взагалі?
Generics — це потужний спосіб описувати структури даних і методи так, щоб вони працювали з будь-яким типом, але при цьому залишалися суворо типізованими.
Уявіть універсальну коробку, яку можна використовувати як для книжок, так і для шкарпеток, але лише для одного типу одночасно. Якщо коробка оголошена як «тільки для книжок» — ніхто не зможе підмішати туди шкарпетки. Так само і з узагальненими колекціями: якщо ви створюєте колекцію з int, туди випадково не потрапить string.
Приклад оголошення узагальненого класу
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Привіт Generics!" };
T — це «параметр типу», що визначає, з чим працюватиме ваша коробка. Компілятор C# вимагатиме, щоб ви завжди чітко вказували, який тип ви кладете.
Узагальнені колекції в .NET
У .NET Framework майже всі сучасні колекції мають узагальнені версії. Це:
- List<T> — динамічний список елементів типу T.
- Dictionary<TKey, TValue> — асоціативний масив (словник) з ключами і значеннями.
- Queue<T>, Stack<T> — черги й стеки.
- та багато інших.
3. Як це працює: влаштування Generics
Параметри типу
Коли ви оголошуєте колекцію типу List<int>, компілятор C# створює окремий варіант (спеціалізацію, specialization) цього класу спеціально для типу int. Коли ж ви оголошуєте List<string>, компілятор C# створює ще один варіант цього класу, але вже для рядків тощо.
Для вас, як для розробника, усе виглядає просто:
List<int> numbers = new List<int>();
numbers.Add(1); // Можна додати тільки int
List<string> words = new List<string>();
words.Add("привіт"); // Можна додати тільки string
Якщо спробуєте додати елемент іншого типу — компілятор одразу повідомить про помилку:
numbers.Add("fail"); // ПОМИЛКА на етапі компіляції!
Безпека типів: Compile-time vs. Run-time
Це означає, що такі помилки не дійдуть до запуску програми (run-time): компілятор перехопить їх заздалегідь. Ваша колекція надійно захищена — компілятор стоїть на варті.
Під капотом
- Для посилальних типів (наприклад, string, object) — жодних зайвих перетворень типів.
- Для значущих типів (int, double) зникає проблема «упаковки/розпаковки» (boxing/unboxing).
- Використання generics у .NET не призводить до надлишкового коду — CLR оптимізує це на етапі JIT-компіляції.
4. Як generics підвищують продуктивність
Підкреслимо ще раз ті властивості, які Generics надають нашому коду:
Типобезпека (Type Safety):
Це, мабуть, найважливіше. Завдяки Generics компілятор стає вашим особистим охоронцем, який не пропустить «чужаків» у вашу колекцію. Ви можете бути певні, що List<Product> міститиме тільки об’єкти Product, а не якісь випадкові рядки чи числа. Це виключає цілий клас помилок, які раніше могли виявлятися лише під час роботи програми й призводити до неочікуваних збоїв (InvalidCastException). Ваш код стає надійнішим і передбачуванішим.
Продуктивність (Performance):
Як ми вже казали, для значущих типів (на кшталт int, double чи DateTime) Generics дають змогу ефективніше працювати з пам’яттю та процесорним часом. Для колекцій, які зберігають мільйони елементів або часто змінюються, це може бути критично важливо. Замість того, щоб постійно перекладати апельсини з ящика в мішок і назад, ви просто кладете їх прямо в потрібний ящик.
Повторне використання коду (Code Reusability):
Generics дають змогу писати один і той самий код, який працюватиме з різними типами даних без потреби його дублювати. Припустімо, вам потрібна функція для обміну місцями двох змінних. Без Generics довелося б писати SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduct(ref Product a, ref Product b) і так далі. З Generics ви пишете лише одну функцію: Swap<T>(ref T a, ref T b). Цей принцип працює і для колекцій: не потрібні IntList, StringList, ProductList — достатньо List<T>. Це робить ваш код компактнішим, зручним у підтримці та масштабованим.
5. Узагальнені методи і власні generic-класи
Узагальнені методи
Generics — це не лише про колекції! Ви можете писати власні узагальнені методи, які працюють з будь-яким типом. Це робить код універсальнішим.
// Вказуємо тип-параметр T одразу після імені методу
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// Використання:
int a = 10, b = 20;
Swap(ref a, ref b); // Тепер a == 20, b == 10
string one = "один", two = "два";
Swap(ref one, ref two); // Працює і для рядків!
Зверніть увагу, що компілятор C# може сам визначити параметр типу методу на підставі типів переданих змінних. Тому в прикладі вище до методу Swap не потрібно додатково передавати тип.
Власні узагальнені класи
Ви навіть можете створювати свої власні «коробки» (generic‑класи). Це простіше, ніж здається:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// Використання:
var pair = new Pair<int, string> { First = 42, Second = "відповідь" };
6. Приклад використання generic-колекцій
Повернімося до нашого «списку завдань» (To‑Do List), який ми почали будувати в прикладах попередніх лекцій. Раніше ми зберігали завдання в масиві або просто виводили їх на екран. Тепер зберігаємо їх динамічно в List<string>.
Приклад: динамічне додавання завдань у список
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("Введіть завдання (або порожній рядок для завершення):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("Завдання додано! Введіть ще (або порожній рядок для завершення):");
}
Console.WriteLine("\nВаш список завдань на сьогодні:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
Що тут зручно?
- Колекція автоматично розширюється у міру додавання нових завдань.
- Неможливо випадково додати до списку щось, окрім рядка.
- Можна легко перебирати список і виводити його вміст.
7. Типові помилки початківців, особливості та поради
Перехід від неузагальнених колекцій до узагальнених іноді спричиняє плутанину. Ось кілька поширених ситуацій, з якими стикаються початківці:
- Спроба додати елемент не того типу (List<int> numbers = new List<int>(); numbers.Add("hi"); // Помилка).
- Бажання змішувати типи в одній колекції — тут варто обрати базовий тип (наприклад, List<object>) — а потім доведеться завжди перетворювати назад до потрібного типу.
- Забувають про обмеження (constraints), пишуть надто «широкі» узагальнення й отримують дивні помилки на етапі компіляції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ