Lab 10
Ітератори та компаратори
IComparable, IComparer, analytics
Лаба 10 — Iterators & Comparators (Ітератори і компаратори)
Мета
Навчитись реалізовувати IComparable<T> і IComparer<T> для управління порядком сортування об'єктів, а також IEnumerable<T> з yield return для лінивої генерації послідовностей. Побудувати аналітичний модуль клініки, де ці концепції мають природний практичний сенс.
Контекст
Система вже накопичує дані: пацієнти, лікарі, записи на прийом. Але відповісти на питання «хто з лікарів прийняв найбільше пацієнтів?» або «хто з пацієнтів витратив найбільше?» неможливо. Це задача аналітики: обчислити статистику по кожному об'єкту і відсортувати за різними критеріями.
Порівняння і сортування в C# будуються на двох інтерфейсах:
IComparable<T>— природний порядок: клас сам знає, як порівнювати себе з іншим. Один порядок, вбудований у тип.IComparer<T>— зовнішній компаратор: окремий клас реалізує один критерій сортування. Таких компараторів можна мати скільки завгодно.
Генерація даних для аналітики природно виражається через IEnumerable<T> з yield return — ліниве обчислення статистики для кожного об'єкта по черзі, замість того щоб спочатку побудувати весь масив у пам'яті.
Структура проєкту на початку лаби
Це результат Лаби 09 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 09)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (3 файли)
├── Models/ (12 файлів)
├── Managers/ (7 файлів)
├── Utils/ (2 файли)
└── Interfaces/ (4 файли)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
- інтерфейси
IComparable<T>іIComparer<T>; Array.Sort()іList<T>.Sort()— без аргументу і з компаратором;IEnumerable<T>іyield return.
Досі заборонено: LINQ (Лаба 14), делегати й лямбди (Лаби 13–15).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-10Коміт — на кожне завдання (Lab10 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. `DoctorStats` і природний порядок через `IComparable` ⭐⭐
Умова
Клініці потрібен об'єкт — аналітичний знімок по лікарю: скільки прийомів провів, яка загальна виручка, коли був останній прийом. Цей об'єкт має вміти порівнювати себе з іншим таким об'єктом, щоб масив DoctorStats[] можна було відсортувати одним викликом Array.Sort().
Що реалізувати:
- Створити клас
DoctorStatsуClinicApp/Models/DoctorStats.csз властивостями зі специфікації (лише для читання). - Конструктор отримує всі п'ять значень і присвоює їх властивостям.
- Реалізувати
IComparable<DoctorStats>: лікар із більшою кількістю прийомів стоїть першим післяArray.Sort(). - Перевизначити
ToString(): один рядок — ID, ім'я, кількість прийомів, виручка, дата останнього прийому.
Специфікація
| Властивість | Тип | Опис |
|---|---|---|
DoctorId |
int |
ID лікаря |
FullName |
string |
Повне ім'я |
AppointmentCount |
int |
Загальна кількість прийомів |
TotalRevenue |
decimal |
Сума GetCost() по всіх прийомах |
LastAppointmentDate |
DateTime |
Дата останнього прийому; DateTime.MinValue, якщо прийомів немає |
Приклад
DoctorStats[] arr =
{
new DoctorStats(1, "Олег Сидоренко", 2, 600m, DateTime.Today),
new DoctorStats(2, "Наталія Мороз", 5, 1500m, DateTime.Today),
};
Array.Sort(arr);
Console.WriteLine(arr[0].FullName); // Наталія Мороз — 5 прийомівПідказки
IComparable<T>вимагає один метод:int CompareTo(T? other). Він повертає від'ємне число, якщоthisйде передother, нуль — якщо рівні, додатне — якщоthisйде після.- Щоб більша кількість прийомів опинилась першою, порівнюйте навпаки —
otherзthis, а неthisзother. Перевірте на папері: лікар А = 5 прийомів, лікар Б = 2 — післяSort()А має бути першим. DateTime.MinValue— константа «найраніша можлива дата». Зручна як маркер «прийомів не було».- Конструктор
DoctorStatsне рахує статистику сам — лише зберігає готові значення. Підрахунок буде вAnalyticsManager(Задача 4). - Перевірити сортування можна тимчасовим кодом у
Program.cs, як у прикладі; перед комітом його приберіть.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
DoctorStats |
StaffStats |
WaiterStats |
LecturerStats |
ManagerStats |
LibrarianStats |
TrainerStats |
AppointmentCount |
к-сть check-in | к-сть столів | к-сть курсів | к-сть оренд | к-сть видач | к-сть сесій |
TotalRevenue |
загальна виручка | загальна виручка | к-сть студентів | загальна виручка | к-сть повернень | загальна виручка |
| більше прийомів = вище | більше обслугованих = вище | більше столів = вище | більше курсів = вище | більше оренд = вище | більше видач = вище | більше сесій = вище |
Коміт
git add ClinicApp/Models/DoctorStats.cs
git commit -m "Lab10 Task01"Задача 2. `PatientStats` і `IComparable` ⭐⭐
Умова
За аналогією з DoctorStats створіть статистичний об'єкт для пацієнта: скільки візитів, скільки витрачено, дата останнього візиту. Природний порядок — за кількістю візитів (найактивніший пацієнт — перший).
Що реалізувати:
- Створити клас
PatientStatsуClinicApp/Models/PatientStats.csз властивостями зі специфікації. - Реалізувати
IComparable<PatientStats>: більша кількість візитів — вища позиція. - Перевизначити
ToString(): один рядок з усіма даними; якщо візитів не було, замість01.01.0001вивести—.
Специфікація
| Властивість | Тип | Опис |
|---|---|---|
PatientId |
int |
ID пацієнта |
FullName |
string |
Повне ім'я |
VisitCount |
int |
Кількість візитів |
TotalSpent |
decimal |
Сума GetCost() по всіх візитах |
LastVisitDate |
DateTime |
Дата останнього візиту; DateTime.MinValue, якщо візитів не було |
Приклад
[3] Максим Бойко | Візитів: 0 | Витрачено: 0.00 грн | Останній візит: —Підказки
- Структура ідентична
DoctorStats— той самий патерн, інші поля. Мета задачі — закріпити патерн на другому прикладі. - Порівняння з
DateTime.MinValueуToString()дає змогу показати «—» замість безглуздої дати.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
PatientStats |
GuestStats |
CustomerStats |
StudentStats |
ClientStats |
ReaderStats |
MemberStats |
VisitCount |
к-сть ночей | к-сть відвідин | к-сть курсів | к-сть оренд | к-сть видач | к-сть тренувань |
TotalSpent |
загальна сума | загальна сума | середній бал | загальна сума | штрафи сплачені | загальна сума |
| більше візитів = вище | більше ночей = вище | більше відвідин = вище | більше курсів = вище | більше оренд = вище | більше видач = вище | більше тренувань = вище |
Коміт
git add ClinicApp/Models/PatientStats.cs
git commit -m "Lab10 Task02"Задача 3. Кілька критеріїв сортування через `IComparer` ⭐⭐⭐
Умова
IComparable<T> дає один фіксований порядок. Але аналітичному модулю потрібно кілька: лікарів можна ранжувати за навантаженням, за виручкою, за алфавітом. Для цього є IComparer<T> — окремий клас, що реалізує один критерій і передається в List<T>.Sort(comparer).
Що реалізувати:
- Створити папку
ClinicApp/Comparators/(простір іменClinicApp.Comparators). - Створити в ній чотири компаратори зі специфікації.
Специфікація
| Клас | Реалізує | Сортує за | Порядок |
|---|---|---|---|
DoctorStatsByRevenue |
IComparer<DoctorStats> |
TotalRevenue |
спадання (більша виручка вище) |
DoctorStatsByName |
IComparer<DoctorStats> |
FullName |
зростання (А → Я), string.Compare(x, y, StringComparison.CurrentCulture) |
PatientStatsBySpent |
IComparer<PatientStats> |
TotalSpent |
спадання |
PatientStatsByLastVisit |
IComparer<PatientStats> |
LastVisitDate |
спадання (найновіший візит вище) |
Приклад
List<DoctorStats> list = new List<DoctorStats> { a, b, c };
list.Sort(); // за кількістю прийомів (IComparable)
list.Sort(new DoctorStatsByRevenue()); // за виручкою
list.Sort(new DoctorStatsByName()); // за ім'ямПідказки
IComparer<T>вимагає один метод:int Compare(T? x, T? y). Та сама семантика, що йCompareTo: від'ємне —xпередy, нуль — рівні, додатне —xпісляy.- Обробляйте
nullявно: обидваnull→0; лишеx == null→-1; лишеy == null→1. Тип параметрів nullable, тож компілятор попередить, якщо цього не зробити. - Щоб отримати спадний порядок, міняйте місцями
xіyу порівнянні. DateTimeтеж реалізуєIComparable— дати порівнюються так само, як числа.- Перевірити компаратори можна тимчасовим кодом у
Program.cs, як у прикладі; перед комітом його приберіть.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
DoctorStatsByRevenue |
StaffStatsByRevenue |
WaiterStatsByRevenue |
LecturerStatsByStudents |
ManagerStatsByRevenue |
LibrarianStatsByIssues |
TrainerStatsByRevenue |
DoctorStatsByName |
StaffStatsByName |
WaiterStatsByName |
LecturerStatsByName |
ManagerStatsByName |
LibrarianStatsByName |
TrainerStatsByName |
PatientStatsBySpent |
GuestStatsBySpent |
CustomerStatsBySpent |
StudentStatsByGrade |
ClientStatsBySpent |
ReaderStatsByIssues |
MemberStatsBySpent |
PatientStatsByLastVisit |
GuestStatsByLastStay |
CustomerStatsByLastVisit |
StudentStatsByLastCourse |
ClientStatsByLastRental |
ReaderStatsByLastLoan |
MemberStatsByLastSession |
Коміт
git add ClinicApp/Comparators/
git commit -m "Lab10 Task03"Задача 4. `AnalyticsManager` і ліниве обчислення через `yield return` ⭐⭐⭐
Умова
Статистику можна обчислити методом, що будує весь масив DoctorStats[] одразу. Кращий підхід — IEnumerable<T> з yield return: метод обчислює статистику для кожного лікаря по черзі і одразу віддає результат, не накопичуючи весь масив. Коли лікарів тисячі, можна зупинитись після першого десятка — решта не обчислюватиметься взагалі.
Що реалізувати:
- Створити клас
AnalyticsManagerуClinicApp/Managers/AnalyticsManager.cs, який отримуєAppointmentManager,DoctorManagerіPatientManagerчерез конструктор. - Метод
ComputeDoctorStats(): для кожного лікаря порахувати кількість його прийомів, сумуGetCost()і найпізнішу дату — і віддатиDoctorStatsчерезyield return. - Метод
ComputePatientStats()— те саме для пацієнтів. - У
Clinic.csдодати властивістьAnalyticsі створитиAnalyticsManagerу конструкторі.
Специфікація
Член AnalyticsManager |
Повертає | Опис |
|---|---|---|
конструктор (AppointmentManager appointments, DoctorManager doctors, PatientManager patients) |
зберігає залежності | |
ComputeDoctorStats() |
IEnumerable<DoctorStats> |
по одному DoctorStats на кожного лікаря, через yield return |
ComputePatientStats() |
IEnumerable<PatientStats> |
по одному PatientStats на кожного пацієнта, через yield return |
Для лікаря чи пацієнта без прийомів: кількість 0, сума 0, дата DateTime.MinValue.
Приклад
foreach (DoctorStats s in clinic.Analytics.ComputeDoctorStats())
Console.WriteLine(s);
// [1] Олег Сидоренко | Прийомів: 2 | Виручка: 600.00 грн | Останній: 15.10.2026
// [2] Наталія Мороз | Прийомів: 0 | Виручка: 0.00 грн | Останній: —Підказки
- Отримайте всіх лікарів через
_doctors.GetAll()і всі прийоми через_appointments.GetAll(); для кожного лікаря — цикл по прийомах з умовою наDoctorId. yield returnу методі з типомIEnumerable<T>перетворює метод на ітератор. Після кожногоyield returnвиконання «призупиняється» і відновлюється, коли запитують наступний елемент.- Щоб побачити лінивість: тимчасово виведіть рядок «обчислюю …» перед
yield return. Приforeachрядки з'являтимуться по одному під час ітерації, а не всі на початку. - Найпізніша дата: почніть з
DateTime.MinValueі оновлюйте, коли дата прийому пізніша.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ComputeDoctorStats() |
ComputeStaffStats() |
ComputeWaiterStats() |
ComputeLecturerStats() |
ComputeManagerStats() |
ComputeLibrarianStats() |
ComputeTrainerStats() |
ComputePatientStats() |
ComputeGuestStats() |
ComputeCustomerStats() |
ComputeStudentStats() |
ComputeClientStats() |
ComputeReaderStats() |
ComputeMemberStats() |
Коміт
git add ClinicApp/Managers/AnalyticsManager.cs ClinicApp/Clinic.cs
git commit -m "Lab10 Task04"Задача 5. Меню «Аналітика» ⭐⭐⭐
Умова
DoctorStats, PatientStats, компаратори і AnalyticsManager готові. Підключіть їх до меню: новий пункт головного меню 8 — «Аналітика» з п'ятьма звітами.
Що реалізувати:
- У головному меню додати пункт
8— «Аналітика». - У
Program.csдодати функціюAnalyticsMenu(Clinic clinic)з п'ятьма звітами зі специфікації. - Додати дві допоміжні функції:
CollectDoctorStats(Clinic clinic)іCollectPatientStats(Clinic clinic)— збирають результат ітератора вList<...>черезforeach.
Специфікація
| Пункт | Звіт | Як сортувати |
|---|---|---|
1 |
Лікарі за навантаженням | Sort() без аргументу (IComparable) |
2 |
Лікарі за виручкою | Sort(new DoctorStatsByRevenue()) |
3 |
Лікарі за іменем | Sort(new DoctorStatsByName()) |
4 |
Пацієнти за кількістю візитів | Sort() без аргументу (IComparable) |
5 |
Пацієнти за витратами | Sort(new PatientStatsBySpent()) |
0 |
Назад |
Приклад
── Аналітика ───────────────────
1. Лікарі за навантаженням
2. Лікарі за виручкою
3. Лікарі за іменем
4. Пацієнти за кількістю візитів
5. Пацієнти за витратами
0. Назад
Оберіть: 2
[2] Наталія Мороз | Прийомів: 1 | Виручка: 780.00 грн | Останній: 16.10.2026
[1] Олег Сидоренко | Прийомів: 2 | Виручка: 600.00 грн | Останній: 15.10.2026Підказки
clinic.Analytics.ComputeDoctorStats()повертає ітератор, а не список. Щоб сортувати, потрібенList<DoctorStats>— збирайте черезforeachі.Add().- Одну й ту саму
CollectDoctorStats()можна викликати для кожного пункту меню — ітератор щоразу починає обчислення заново. .Sort()без аргументу вимагає, щоб тип реалізовувавIComparable<T>..Sort(comparer)використовує переданий компаратор.- Не забудьте
using ClinicApp.Comparators;на початкуProgram.cs.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
| Лікарі за навантаженням | Персонал за check-in | Офіціанти за столами | Викладачі за курсами | Менеджери за орендами | Бібліотекарі за видачами | Тренери за сесіями |
| Лікарі за виручкою | Персонал за виручкою | Офіціанти за виручкою | Викладачі за студентами | Менеджери за виручкою | Бібліотекарі за відділами | Тренери за виручкою |
| Пацієнти за кількістю візитів | Гості за ночами | Клієнти за відвідинами | Студенти за курсами | Клієнти за орендами | Читачі за видачами | Учасники за тренуваннями |
| Пацієнти за витратами | Гості за витратами | Клієнти за витратами | Студенти за балом | Клієнти за витратами | Читачі за штрафами | Учасники за витратами |
Коміт
git add ClinicApp/Program.cs
git commit -m "Lab10 Task05"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-10 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т5
├── Clinic.cs ✏ Т4
├── Enums/ (3 файли)
├── Models/
│ ├── DoctorStats.cs 🆕 Т1
│ ├── PatientStats.cs 🆕 Т2
│ └── … ще 12 файлів без змін
├── Managers/
│ ├── AnalyticsManager.cs 🆕 Т4
│ └── … ще 7 файлів без змін
├── Utils/ (2 файли)
├── Interfaces/ (4 файли)
└── Comparators/
├── DoctorStatsByName.cs 🆕 Т3
├── DoctorStatsByRevenue.cs 🆕 Т3
├── PatientStatsByLastVisit.cs 🆕 Т3
└── PatientStatsBySpent.cs 🆕 Т3Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 09.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
-
8. Аналітиказ'явилась у головному меню - «Лікарі за навантаженням» і «Лікарі за виручкою» дають різний порядок (якщо тестові дані різноманітні)
- «Лікарі за іменем» дає алфавітний порядок
- Пацієнт без записів показує
Візитів: 0і дату— - (Експеримент, не для коміту) якщо прибрати
IComparableзDoctorStats, проєкт збирається, але.Sort()без аргументу кидаєInvalidOperationExceptionпід час виконання
Питання для самоперевірки
- Чим
IComparable<T>відрізняється відIComparer<T>? Коли використовувати перше, коли друге? - Що повертає
CompareToпри рівних значеннях? Що станеться, якщо завжди повертати0? - Чому
yield returnуComputeDoctorStats()дає «ліниве» обчислення? Коли саме виконується тіло циклу? - Як отримати з
IEnumerable<T>лише перші N елементів без LINQ? (підказка:foreach+ лічильник) - Що станеться, якщо викликати
.Sort()наList<DoctorStats>після того, як прибратиIComparable<DoctorStats>з класу? Чому це помилка часу виконання, а не компіляції? - Порівняйте:
ComputeDoctorStats()зyield returnі метод, що будує й повертаєDoctorStats[]. В чому різниця у поведінці при великій кількості лікарів?
Статус гілки
Після всіх 5 завдань (кожне — окремий коміт Lab10 TaskNN на гілці Lab-10):
git push -u origin Lab-10
git checkout main
git merge --no-ff Lab-10 -m "Merge Lab-10: Iterators & Comparators"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-11.