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 — ліниве обчислення статистики для кожного об'єкта по черзі, замість того щоб спочатку побудувати весь масив у пам'яті.
Гілка
git checkout main
git pull
git checkout -b feature/iteratorsЗавдання 1 — `DoctorStats` і `IComparable`: природній порядок ⭐⭐
Умова
Клініці потрібен об'єкт що представляє аналітичний знімок по лікарю: скільки прийомів провів, яка загальна виручка, коли був останній прийом. Цей об'єкт повинен вміти порівнювати себе з іншим таким об'єктом — щоб масив DoctorStats[] можна було відсортувати одним викликом Array.Sort().
Що реалізувати
Створи клас DoctorStats у src/Models/DoctorStats.cs.
Властивості (тільки читання):
int DoctorId— ID лікаряstring FullName— повне ім'яint AppointmentCount— загальна кількість прийомівdecimal TotalRevenue— сумаGetCost()по всіх прийомахDateTime LastAppointmentDate— дата останнього прийому (DateTime.MinValueякщо прийомів немає)
Конструктор: отримує всі п'ять значень, присвоює властивостям.
IComparable<DoctorStats>: реалізуй інтерфейс — визнач природній порядок: більша кількість прийомів = вища позиція в рейтингу. Тобто при сортуванні за зростанням (Array.Sort()) перший — той у кого найбільше прийомів.
override ToString(): один рядок — ID, ім'я, кількість прийомів, виручка, дата останнього прийому.
Підказки
IComparable<T>вимагає один метод:int CompareTo(T? other). Повертає від'ємне число якщоthisйде передother, нуль якщо рівні, додатнє якщоthisйде після.Щоб більша кількість прийомів опинилась першою (тобто сортування за спаданням через
Sort()за зростанням), порівнюй навпаки:return other.AppointmentCount.CompareTo(this.AppointmentCount). Перевір цю логіку на папері з прикладом: лікар А = 5 прийомів, лікар Б = 2 прийоми — післяSort()А повинен бути першим.DateTime.MinValue— константа "найраніша можлива дата" в C#. Зручно як маркер "прийомів не було".Конструктор
DoctorStatsне рахує статистику сам — він лише зберігає готові значення. Підрахунок буде вAnalyticsManager(Завдання 4).
📖 IComparable<T> — Microsoft Docs
📖 Array.Sort — Microsoft Docs
Що перевірити
Створи вручну кілька DoctorStats з різною кількістю прийомів, помісти в масив DoctorStats[], виклич Array.Sort() — переконайся що лікар з найбільшою кількістю прийомів стоїть першим.
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
DoctorStats |
StaffStats |
WaiterStats |
LecturerStats |
ManagerStats |
LibrarianStats |
TrainerStats |
AppointmentCount |
к-сть check-in | к-сть столів | к-сть курсів | к-сть оренд | к-сть видач | к-сть сесій |
TotalRevenue |
загальна виручка | загальна виручка | к-сть студентів | загальна виручка | к-сть повернень | загальна виручка |
| більше прийомів = вище | більше обслугованих = вище | більше столів = вище | більше курсів = вище | більше оренд = вище | більше видач = вище | більше сесій = вище |
Коміт
git add src/Models/DoctorStats.cs
git commit -m "Lab10 Task1: add DoctorStats with IComparable<DoctorStats> by appointment count"Завдання 2 — `PatientStats` і `IComparable` ⭐⭐
Умова
За аналогією з DoctorStats — статистичний об'єкт для пацієнта: скільки візитів, скільки витрачено, дата останнього візиту. Природній порядок — за кількістю візитів (найактивніший пацієнт — перший).
Що реалізувати
Створи клас PatientStats у src/Models/PatientStats.cs.
Властивості: int PatientId, string FullName, int VisitCount, decimal TotalSpent, DateTime LastVisitDate.
IComparable<PatientStats>: природній порядок — більша кількість візитів = вища позиція.
override ToString(): один рядок з усіма даними.
Підказки
Структура ідентична
DoctorStats— той самий патерн, різні поля і логіка порівняння. Одна з цілей цього завдання — закріпити патерн на другому прикладі.Зверни увагу на
LastVisitDate == DateTime.MinValueуToString()— якщо прийомів не було, виводь щось читабельне замість01.01.0001.
Що перевірити
Виклич Array.Sort() на масиві PatientStats[] — пацієнт з найбільшою кількістю візитів повинен бути першим.
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
PatientStats |
GuestStats |
CustomerStats |
StudentStats |
ClientStats |
ReaderStats |
MemberStats |
VisitCount |
к-сть ночей | к-сть відвідин | к-сть курсів | к-сть оренд | к-сть видач | к-сть тренувань |
TotalSpent |
загальна сума | загальна сума | середній бал | загальна сума | штрафи сплачені | загальна сума |
| більше візитів = вище | більше ночей = вище | більше відвідин = вище | більше курсів = вище | більше оренд = вище | більше видач = вище | більше тренувань = вище |
Коміт
git add src/Models/PatientStats.cs
git commit -m "Lab10 Task2: add PatientStats with IComparable<PatientStats> by visit count"Завдання 3 — `IComparer`: множинні критерії сортування ⭐⭐⭐
Умова
IComparable<T> дає один фіксований порядок. Але аналітичний модуль потребує кілька: лікарів можна ранжувати за навантаженням, за виручкою, за алфавітом. Для цього є IComparer<T> — окремий клас що реалізує один критерій і передається в List<T>.Sort(comparer).
Що реалізувати
Створи папку src/Comparators/. В ній — чотири класи:
DoctorStatsByRevenue — реалізує IComparer<DoctorStats>. Сортує за TotalRevenue за спаданням (більша виручка = вище).
DoctorStatsByName — реалізує IComparer<DoctorStats>. Сортує за FullName за зростанням (А → Я). Для порівняння рядків використовуй string.Compare(x, y, StringComparison.CurrentCulture).
PatientStatsBySpent — реалізує IComparer<PatientStats>. Сортує за TotalSpent за спаданням.
PatientStatsByLastVisit — реалізує IComparer<PatientStats>. Сортує за LastVisitDate за спаданням (найновіший візит = вище). Для порівняння дат: DateTime реалізує IComparable, тому y.LastVisitDate.CompareTo(x.LastVisitDate) дає спадний порядок.
Підказки
IComparer<T>вимагає один метод:int Compare(T? x, T? y). Та сама семантика що йCompareTo— від'ємне якщоxпередy, нуль якщо рівні, додатнє якщоxпісляy.Обробляй
nullявно: якщоx == null && y == null→0; якщо тількиx == null→-1; якщо тількиy == null→1. Компілятор вимагає цього бо тип може бути nullable.Щоб отримати спадний порядок з
CompareTo, просто міняй місцямиxіy:return y.TotalRevenue.CompareTo(x.TotalRevenue)дає спадний замість зростаючого.Використання:
list.Sort(new DoctorStatsByRevenue())—List<T>.Sort()приймаєIComparer<T>як аргумент.
📖 IComparer<T> — Microsoft Docs
📖 List<T>.Sort(IComparer) — Microsoft Docs
Що перевірити
Зроби один список DoctorStats, відсортуй його чотирма різними способами і виведи — переконайся що порядок щоразу різний:
.Sort()— за кількістю прийомів (IComparable).Sort(new DoctorStatsByRevenue())— за виручкою.Sort(new DoctorStatsByName())— за ім'ям
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
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 src/Comparators/
git commit -m "Lab10 Task3: add IComparer<T> comparators for DoctorStats and PatientStats"Завдання 4 — `AnalyticsManager` і `yield return`: ліниве обчислення ⭐⭐⭐
Умова
Де і як обчислювати статистику? Можна зробити метод що будує весь масив DoctorStats[] одразу і повертає його. Але є кращий підхід: IEnumerable<T> з yield return — метод обчислює статистику для кожного лікаря по черзі і одразу повертає результат, не накопичуючи весь масив.
Це важливо коли лікарів тисячі — ти можеш зупинитись після першого десятка і решту не обчислювати взагалі.
Що реалізувати
Створи клас AnalyticsManager у src/Managers/AnalyticsManager.cs.
Залежності через конструктор:
AnalyticsManager(AppointmentManager appointments, DoctorManager doctors, PatientManager patients)Метод IEnumerable<DoctorStats> ComputeDoctorStats():
- Отримай всіх лікарів через
_doctors.GetAll()і всі прийоми через_appointments.GetAll() - Для кожного лікаря (цикл
for) — пройди по всіх прийомах і знайди його прийоми: порахуй кількість, сумуGetCost(), знайди максимальну дату - Замість
return new DoctorStats(...)— використовуйyield return new DoctorStats(...) - Метод повертає
IEnumerable<DoctorStats>, тобто компілятор перетворює його на state machine
Метод IEnumerable<PatientStats> ComputePatientStats(): аналогічно для пацієнтів.
Оновлення Clinic.cs: додай public AnalyticsManager Analytics { get; } і ініціалізуй у конструкторі: Analytics = new AnalyticsManager(Appointments, Doctors, Patients).
Підказки
yield returnв методі з типом поверненняIEnumerable<T>перетворює метод на ітератор. Після кожногоyield returnвиконання методу "призупиняється" і відновлюється при наступному запиті елемента. Ось чому це "ліниво" — наступнийDoctorStatsне обчислюється поки його не запросили.Щоб переконатись в ліниві: поклади
Console.WriteLine("обчислюю " + doctors[i].FullName)передyield return. Приforeachпо результату побачиш що рядки виводяться по одному під час ітерації, а не всі одразу на початку.Щоб зібрати результати в
List<DoctorStats>:List<DoctorStats> list = new List<DoctorStats>(); foreach (DoctorStats s in analytics.ComputeDoctorStats()) list.Add(s);Тут
foreachспоживає ітератор по одному елементу.Максимальна дата: починай з
DateTime.MinValueі оновлюй колиappointments[j].ScheduledAt > lastDate.
📖 yield return — Microsoft Docs
📖 IEnumerable<T> — Microsoft Docs
📖 Iterators — C# Guide
Що перевірити
ComputeDoctorStats()повертає правильну кількість елементів (рівно стільки скільки лікарів)- Для лікаря без прийомів:
AppointmentCount == 0,TotalRevenue == 0,LastAppointmentDate == DateTime.MinValue foreachпо результату без збереження в список — також працює
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
AnalyticsManager |
AnalyticsManager |
AnalyticsManager |
AnalyticsManager |
AnalyticsManager |
AnalyticsManager |
AnalyticsManager |
ComputeDoctorStats() |
ComputeStaffStats() |
ComputeWaiterStats() |
ComputeLecturerStats() |
ComputeManagerStats() |
ComputeLibrarianStats() |
ComputeTrainerStats() |
ComputePatientStats() |
ComputeGuestStats() |
ComputeCustomerStats() |
ComputeStudentStats() |
ComputeClientStats() |
ComputeReaderStats() |
ComputeMemberStats() |
yield return по кожному лікарю |
по кожному співробітнику | по кожному офіціанту | по кожному викладачу | по кожному менеджеру | по кожному бібліотекарю | по кожному тренеру |
Коміт
git add src/Managers/AnalyticsManager.cs src/Clinic.cs
git commit -m "Lab10 Task4: add AnalyticsManager with yield return for IEnumerable<DoctorStats/PatientStats>"Завдання 5 — Меню аналітики: все разом ⭐⭐⭐
Умова
DoctorStats, PatientStats, компаратори, AnalyticsManager — все готово. Тепер підключи це до меню. Новий пункт "8. Аналітика" з п'ятьма варіантами звітів.
Що реалізувати
Program.cs — додай "8. Аналітика" до головного меню і функцію AnalyticsMenu(Clinic clinic).
Підменю з варіантами:
- Лікарі за навантаженням —
ComputeDoctorStats()→ зібрати вList<DoctorStats>→.Sort()(використовуєIComparable) → вивести - Лікарі за виручкою —
.Sort(new DoctorStatsByRevenue()) - Лікарі за іменем —
.Sort(new DoctorStatsByName()) - Пацієнти за кількістю візитів —
ComputePatientStats()→.Sort()(IComparable) - Пацієнти за витратами —
.Sort(new PatientStatsBySpent())
Два допоміжних методи для збору результатів:
CollectDoctorStats(Clinic clinic)→ збираєIEnumerable<DoctorStats>уList<DoctorStats>черезforeachCollectPatientStats(Clinic clinic)→ аналогічно
Підказки
Виклик
clinic.Analytics.ComputeDoctorStats()повертаєIEnumerable<DoctorStats>— ітератор, не список. Щоб сортувати, потрібенList<DoctorStats>. Збирай черезforeach+.Add().Один і той самий
CollectDoctorStats()можна викликати для кожного пункту меню — метод щоразу починає ітерацію заново.При сортуванні зверни увагу:
.Sort()без аргументу вимагає що тип реалізуєIComparable<T>..Sort(comparer)з аргументом — використовує переданий компаратор. Обидва методи існують наList<T>.using ClinicApp.Comparators;— не забудь додати у верхній частиніProgram.cs.
📖 List<T>.Sort() — Microsoft Docs
Що перевірити
Запусти dotnet run. Відкрий "8. Аналітика". Перевір кожен з п'яти пунктів:
- Лікарі виводяться у правильному порядку для кожного критерію
- Після сортування за навантаженням і за виручкою порядок різний (якщо тестові дані різноманітні)
- Пацієнти без жодного запису виводяться з
Візитів: 0і датою—
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
8. Аналітика |
8. Аналітика |
8. Аналітика |
8. Аналітика |
8. Аналітика |
8. Аналітика |
8. Аналітика |
| Лікарі за навантаженням | Персонал за check-in | Офіціанти за столами | Викладачі за курсами | Менеджери за орендами | Бібліотекарі за видачами | Тренери за сесіями |
| Лікарі за виручкою | Персонал за виручкою | Офіціанти за виручкою | Викладачі за студентами | Менеджери за виручкою | Бібліотекарі за відділами | Тренери за виручкою |
| Пацієнти за кількістю візитів | Гості за ночами | Клієнти за відвідинами | Студенти за курсами | Клієнти за орендами | Читачі за видачами | Учасники за тренуваннями |
| Пацієнти за витратами | Гості за витратами | Клієнти за витратами | Студенти за балом | Клієнти за витратами | Читачі за штрафами | Учасники за витратами |
Коміт
git add src/Program.cs
git commit -m "Lab10 Task5: add Analytics menu item 8 with sort options for doctors and patients"Перевірка перед здачею
cd src
dotnet build
dotnet run-
8. Аналітиказ'явилась у головному меню - "Лікарі за навантаженням" і "Лікарі за виручкою" дають різний порядок
- "Лікарі за іменем" дає алфавітний порядок
- Пацієнт без записів показує
Візитів: 0і дату— - Видалення
IComparableзDoctorStatsпризводить до помилки компіляції при.Sort()без аргументу -
DoctorStatsByRevenue— окремий клас, реалізуєIComparer<DoctorStats>
Питання для самоперевірки
- Чим
IComparable<T>відрізняється відIComparer<T>? Коли використовувати перше, коли друге? - Що повертає
CompareToпри рівних значеннях? Що станеться якщо завжди повертати0? - Чому
yield returnуComputeDoctorStats()дає "ліниве" обчислення? Коли саме виконується тіло циклу? - Як отримати з
IEnumerable<T>лише перші N елементів без LINQ? (підказка:foreach+ лічильник) - Що станеться якщо викликати
.Sort()наList<DoctorStats>після того як видалишIComparable<DoctorStats>з класу? - Порівняй:
ComputeDoctorStats()зyield returnvs метод що будує і повертаєDoctorStats[]. В чому різниця у поведінці при великій кількості лікарів?
Злиття
git checkout main
git merge --no-ff feature/iterators -m "Merge feature/iterators: Lab10 — DoctorStats, PatientStats, IComparable, IComparer, AnalyticsManager"
git pushНаступна лаба:
git checkout -b feature/reflection— атрибути і рефлексія.