Lab 09
Generics
List<T>, Queue<T>, constraints
Лаба 09 — Generics (Узагальнені типи)
Мета
Навчитись використовувати List<T> замість масивів з ручним лічильником, і самостійно писати generic класи з параметром типу <T>. Побачити, як один клас може працювати з різними типами без дублювання коду.
Контекст
Після восьми лаб система працює, але з обмеженнями: PatientManager має фіксований масив Patient[100] і вручну керує лічильником. Remove() вимагає зсуву всіх елементів. Це ті самі «навмисні обмеження», що були в Лабі 03. Настав час замінити їх на List<T>.
Крім того, клініка потребує нову функціональність — чергу очікування: пацієнти приходять, стають у чергу і приймаються по порядку. Це окрема функція, яка природно виражається через Queue<T>.
Структура проєкту на початку лаби
Це результат Лаби 08 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 08)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (3 файли)
├── Models/
│ ├── Patient.cs
│ ├── Doctor.cs
│ ├── Appointment.cs
│ └── … ще 8 файлів без змін
├── Managers/
│ ├── PatientManager.cs
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs
│ ├── GrowablePatientManager.cs
│ ├── MedicalRecordManager.cs
│ └── BillingManager.cs
├── Utils/ (2 файли)
└── Interfaces/
├── ICancellable.cs
├── IPayable.cs
└── ISchedulable.csСтруктуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
List<T>іQueue<T>зі стандартної бібліотеки;- власні generic класи з параметром типу
<T>; - обмеження параметра типу
where T : ...; defaultдля параметра типу.
Досі заборонено: LINQ (Лаба 14), делегати й лямбди (Лаби 13–15).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-09Коміт — на кожне завдання (Lab09 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. `List` замість масиву з лічильником ⭐
Умова
PatientManager зберігає пацієнтів у Patient[] _patients з ручним int _count. Через це є штучне обмеження (MaxPatients = 100), Remove() потребує ручного зсуву елементів, а GetAll(), FindByName(), FindByBloodType() використовують двопрохідний патерн.
Замініть внутрішнє сховище на List<Patient>. Зовнішній API (Add, FindById, DisplayAll, Remove тощо) не змінюється — тільки внутрішня реалізація.
Що реалізувати:
- Поле
_patientsзмінити зPatient[]наList<Patient>; прибрати поле_countі константуMaxPatients. Count— повертати кількість елементів списку.Add()— додавати в список без перевірки ліміту.Remove()— видаляти зі списку замість ручного зсуву.FindByName(),FindByBloodType()— замість двопрохідного патерну наповнювати проміжнийList<Patient>і в кінці повертати масив.GetAll()— повертати масив однією операцією замість циклу.- Перевірити, що меню
1. Пацієнтипрацює так само, як раніше, але без обмеження на 100 пацієнтів.
Специфікація
Член PatientManager |
Було | Стало |
|---|---|---|
_patients |
Patient[] + _count + MaxPatients |
List<Patient> |
Count |
_count |
кількість елементів списку |
Add |
перевірка ліміту, запис у масив | додавання в список |
Remove |
ручний зсув | видалення зі списку |
FindByName, FindByBloodType |
два проходи | проміжний список → масив |
GetAll |
цикл копіювання | одна операція |
Сигнатури публічних методів не змінюються.
Підказки
List<T>— це динамічний масив зі стандартної бібліотеки. Розмір зростає автоматично при додаванні..Add(item)— додає в кінець..RemoveAt(index)— видаляє за індексом і зсуває решту автоматично..Count— кількість елементів (аналог_count).[i]— доступ за індексом (як у масиві)..ToArray()— повертає звичайний масивT[]з усіх елементівList<T>.- Конструктор без параметрів
new List<Patient>()— порожній список.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
PatientManager |
GuestManager |
CustomerManager |
StudentManager |
ClientManager |
ReaderManager |
MemberManager |
Patient[] → List<Patient> |
Guest[] → List<Guest> |
Customer[] → List<Customer> |
Student[] → List<Student> |
Client[] → List<Client> |
Reader[] → List<Reader> |
Member[] → List<Member> |
_count → _patients.Count |
_count → _guests.Count |
_count → _customers.Count |
_count → _students.Count |
_count → _clients.Count |
_count → _readers.Count |
_count → _members.Count |
Коміт
git add ClinicApp/Managers/PatientManager.cs
git commit -m "Lab09 Task01"Задача 2. Власний generic клас `WaitingQueue` ⭐⭐
Умова
Клініці потрібна черга очікування. Пацієнти приходять і стають у чергу (FIFO: перший прийшов — перший приймається). Це класична структура Queue<T>, але нам потрібна обгортка зі зрозумілим API і захистом від помилок.
Що реалізувати:
- Створити generic клас
WaitingQueue<T>уClinicApp/Models/WaitingQueue.cs— обгортку надQueue<T>. - Додати члени зі специфікації нижче.
Dequeue()іPeek()на порожній черзі мають кидатиInvalidOperationExceptionзі зрозумілим повідомленням.
Специфікація
| Член | Тип | Опис |
|---|---|---|
Count |
int (лише читання) |
Кількість у черзі |
IsEmpty |
bool |
Чи порожня черга |
Enqueue(T item) |
void |
Додати в кінець |
Dequeue() |
T |
Прийняти першого (видаляє з черги); порожня — InvalidOperationException |
Peek() |
T |
Подивитись, хто перший (не видаляє); порожня — InvalidOperationException |
ToArray() |
T[] |
Поточний стан черги у вигляді масиву (для виводу) |
Приклад
WaitingQueue<string> q = new WaitingQueue<string>();
q.Enqueue("A"); q.Enqueue("B"); q.Enqueue("C");
Console.WriteLine(q.Count); // 3
Console.WriteLine(q.Peek()); // A
Console.WriteLine(q.Dequeue()); // A
Console.WriteLine(q.Count); // 2Dequeue() на порожній черзі кидає InvalidOperationException.
Підказки
- Generic клас оголошується як
public class WaitingQueue<T>. ВикористовуйтеTскрізь, де раніше писали б конкретний тип. Queue<T>— стандартна колекція FIFO.Enqueue— додати,Dequeue— взяти перший,Peek— подивитись на перший.Queue<T>сама кидаєInvalidOperationExceptionприDequeue/Peekна порожній черзі — але явна перевірка черезIsEmptyдає зрозуміліше повідомлення.- Параметр
<T>не накладає жодних обмежень —WaitingQueue<Patient>,WaitingQueue<Doctor>,WaitingQueue<string>— усе компілюється. - Перевірити клас можна тимчасовим кодом у
Program.cs, як у прикладі вище; перед комітом його приберіть.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
WaitingQueue<Patient> |
WaitingQueue<Guest> |
WaitingQueue<Customer> |
WaitingQueue<Student> |
WaitingQueue<Client> |
WaitingQueue<Reader> |
WaitingQueue<Member> |
| черга на прийом | черга на заїзд | черга на столик | черга на зарахування | черга на авто | черга на книгу | черга до тренера |
Коміт
git add ClinicApp/Models/WaitingQueue.cs
git commit -m "Lab09 Task02"Задача 3. Черга в системі: нове меню ⭐⭐⭐
Умова
WaitingQueue<T> готова — тепер підключіть її до клініки: додайте чергу пацієнтів у Clinic і новий пункт головного меню «Черга».
Що реалізувати:
- У
Clinic.csдодати властивістьWaitingRoomтипуWaitingQueue<Patient>і створити чергу в конструкторі. - У головному меню додати пункт
6— «Черга — очікування, прийом»; «Звіт» переїжджає на7. - У
Program.csдодати функціюWaitingRoomMenu(Clinic clinic)з чотирма діями (див. специфікацію). - Дії «Прийняти першого» і «Хто перший?» обгорнути в
try/catchнаInvalidOperationException— на порожній черзі показати повідомлення, а не падати.
Специфікація
| Пункт | Дія | Що виводить |
|---|---|---|
1 — Додати пацієнта до черги |
запитує ID, знаходить пацієнта, ставить у чергу | підтвердження або «Пацієнта не знайдено» |
2 — Прийняти першого |
Dequeue |
ім'я прийнятого і скільки лишилось у черзі |
3 — Хто перший? |
Peek |
ім'я першого, без видалення |
4 — Переглянути всю чергу |
ToArray |
список із нумерацією |
0 — Назад |
Приклад
── Черга ───────────────────────
1. Додати пацієнта до черги
2. Прийняти першого
3. Хто перший?
4. Переглянути всю чергу
0. Назад
Оберіть: 2
Прийнято: Іван Петренко. У черзі лишилось: 1Підказки
WaitingRoomзберігає об'єктиPatient— тому можна одразу виводитиpatient.FullName.clinic.WaitingRoom.ToArray()— отримати масив для виводу переліку черги.Queue<T>гарантує порядок FIFO — порядок уToArray()відповідає порядку додавання.
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Clinic.WaitingRoom |
Hotel.CheckInQueue |
Restaurant.DiningQueue |
University.EnrollmentQueue |
CarRental.RentalQueue |
Library.BorrowQueue |
GymCenter.TrainingQueue |
6. Черга — очікування, прийом |
6. Черга — реєстрація |
6. Черга — на столик |
6. Черга — зарахування |
6. Черга — на авто |
6. Черга — на книгу |
6. Черга — до тренера |
Коміт
git add ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab09 Task03"Задача 4. `Repository`: generic CRUD з обмеженням типу ⭐⭐⭐⭐
Умова
WaitingQueue<T> не має обмежень — до неї можна додати будь-який тип. Іноді generic клас повинен гарантувати, що T має певні властивості. Наприклад, Repository<T> потрібен метод GetById(int id) — але для цього він повинен знати, що у T є властивість Id. Рішення — обмеження where T : IIdentifiable.
Що реалізувати:
- Створити інтерфейс
IIdentifiableуClinicApp/Interfaces/з властивістюint Id { get; }. - Додати
: IIdentifiableдо оголошенняPatient,DoctorіAppointment(властивістьIdу них уже є). - Створити generic клас
Repository<T> where T : IIdentifiableуClinicApp/Managers/Repository.csз методами зі специфікації. Усередині —List<T>.
Специфікація
Член Repository<T> |
Опис |
|---|---|
Add(T item) |
Додати |
GetById(int id) |
Знайти за Id; якщо немає — default |
GetAll() |
Усі елементи як T[] |
Remove(int id) |
Видалити за Id; bool — чи було що видаляти |
Count |
Кількість |
Приклад
Repository<Patient> repo = new Repository<Patient>();
repo.Add(p1);
repo.Add(p2);
Console.WriteLine(repo.GetById(p2.Id)?.FullName); // ім'я p2
Console.WriteLine(repo.Remove(p1.Id)); // True
Console.WriteLine(repo.Count); // 1Підказки
where T : IIdentifiable— компілятор дозволяє звертатись доitem.Idвсередині класу, бо гарантовано, що уTє цей член.default!— повертаєnullдля reference-типів і підходить як «не знайдено», аналогічно доnull!в існуючому коді.Repository<T>не замінюєPatientManager. Це окремий generic інструмент:PatientManagerмістить специфічну логіку (пошук за ім'ям, статистика), якоїRepositoryне знає.- Перевірити клас можна тимчасовим кодом у
Program.cs, як у прикладі; перед комітом його приберіть.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Repository<Patient> |
Repository<Guest> |
Repository<Customer> |
Repository<Student> |
Repository<Client> |
Repository<Reader> |
Repository<Member> |
Patient, Doctor, Appointment : IIdentifiable |
Guest, Staff, Booking |
Customer, Waiter, TableReservation |
Student, Lecturer, Enrollment |
Client, Manager, Rental |
Reader, Librarian, BookLoan |
Member, Trainer, Session |
Коміт
git add ClinicApp/Interfaces/IIdentifiable.cs ClinicApp/Managers/Repository.cs
git add ClinicApp/Models/Patient.cs ClinicApp/Models/Doctor.cs ClinicApp/Models/Appointment.cs
git commit -m "Lab09 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-09 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т3
├── Clinic.cs ✏ Т3
├── Enums/ (3 файли)
├── Models/
│ ├── Patient.cs ✏ Т4
│ ├── Doctor.cs ✏ Т4
│ ├── Appointment.cs ✏ Т4
│ ├── WaitingQueue.cs 🆕 Т2
│ └── … ще 8 файлів без змін
├── Managers/
│ ├── PatientManager.cs ✏ Т1
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs
│ ├── GrowablePatientManager.cs
│ ├── MedicalRecordManager.cs
│ ├── BillingManager.cs
│ └── Repository.cs 🆕 Т4
├── Utils/ (2 файли)
└── Interfaces/
├── ICancellable.cs
├── IPayable.cs
├── ISchedulable.cs
└── IIdentifiable.cs 🆕 Т4Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 08.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
-
1. Пацієнти— поведінка не змінилась, але немає ліміту на кількість -
6. Черга— новий пункт у головному меню, «Звіт» — на7 - Додати 3 пацієнтів у чергу → прийняти двох → у черзі 1
- «Прийняти» з порожньої черги → повідомлення про помилку, програма не падає
- (Експеримент, не для коміту)
Repository<Patient>компілюється;Repository<string>— ні - (Експеримент, не для коміту)
WaitingQueue<string>іWaitingQueue<int>компілюються (без обмеження)
Питання для самоперевірки
- В чому різниця між
List<T>іT[]? Коли перевага у масиву, коли уList<T>? - Що означає
<T>в оголошенні класу? Хто вказує конкретний тип — і коли? - Навіщо
where T : IIdentifiable? Що буде, якщо прибрати обмеження і звернутись доitem.Id? - Чому
WaitingQueue<T>не має обмеження, аRepository<T>має? В чому принципова різниця між ними? - FIFO чи LIFO:
Queue<T>абоStack<T>— що підходить для черги очікування і чому? PatientManagerтепер використовуєList<Patient>. Чи є сенс замінити весьPatientManagerнаRepository<Patient>? Що б втратилось?
Статус гілки
Після всіх 4 завдань (кожне — окремий коміт Lab09 TaskNN на гілці Lab-09):
git push -u origin Lab-09
git checkout main
git merge --no-ff Lab-09 -m "Merge Lab-09: Generics"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-10.