JavaRush /Курси /C# SELF /Переваги узагальнених колекцій (Generics)

Переваги узагальнених колекцій (Generics)

C# SELF
Рівень 27 , Лекція 1
Відкрита

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), пишуть надто «широкі» узагальнення й отримують дивні помилки на етапі компіляції.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ