Lab 04
Члени класу
enum, struct, static, overload
Лаба 04 — Члени класу
Мета
Збагатити систему новими видами членів класу: іменованими константами (enum), структурами-значеннями (struct), статичними утилітними класами та індексаторами — і навчитися перевантажувати методи.
Контекст
Після Лаби 03 система працює, але код «брудний»: статуси — рядки "Scheduled",
групи крові — "A+", спеціальності — "Кардіологія". Одна опечатка — і логіка
зламана, а компілятор мовчить.
Ця лаба лікує це системно: замінюємо magic strings на типобезпечні
конструкції (enum, struct), виносимо форматування в статичний клас і
розширюємо API менеджерів індексаторами та перевантаженнями.
Працюєте в тому самому проєкті ClinicApp/. Гілка зливається в main.
Структура проєкту на початку лаби
Це результат Лаби 03 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 03)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Patient.cs
├── Doctor.cs
├── PatientManager.cs
├── DoctorManager.cs
├── Appointment.cs
├── AppointmentManager.cs
├── Clinic.cs
└── GrowablePatientManager.csСтруктуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
enum— іменовані константи;struct— власний value-тип;static class— клас без екземплярів;- індексатор
this[int index]; - перевантаження методів (кілька методів з одним іменем, різні параметри);
out-параметр і патернTryXxx;?.і??— тепер можна (раніше були заборонені).
Досі заборонено: List<T> / Dictionary (Лаба 09), LINQ (Лаба 14),
interface / abstract (Лаби 06–07).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain(Лаби 03+; 01–02 не зливались).
Проєкт ClinicApp/ уже існує з Лаби 03. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-04Коміт — на кожне завдання (Lab04 TaskNN).
Ваш домен
За замовчуванням — домен «клініка». Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Enum — замість магічних рядків ⭐⭐
Умова
У поточному коді статус запису зберігається як string Status = "Scheduled". Якщо хтось напише "Shedüled" — ніхто не помітить до виконання.
Вирішіть це через перерахування (enum): компілятор перевіряє допустимі значення на етапі збірки.
Що реалізувати:
enum AppointmentStatus— три стани запису.enum BloodType— дев'ять значень (у т.ч.Unknown).enum Speciality— вісім спеціальностей лікаря.- Замінити
string StatusвAppointmentнаAppointmentStatus. - Замінити
string BloodTypeвPatientнаBloodType. - Замінити
string SpecialityвDoctorнаSpeciality. - Виправити код, який через зміну типів перестав компілюватись: початкові дані та введення з меню в
Program.cs, пошук за спеціальністю вDoctorManager.
Специфікація
| Enum | Значення |
|---|---|
AppointmentStatus |
Scheduled, Cancelled, Completed |
BloodType |
Unknown, APositive, ANegative, BPositive, BNegative, ABPositive, ABNegative, OPositive, ONegative |
Speciality |
General, Cardiology, Neurology, Pediatrics, Surgery, Orthopedics, Dermatology, Emergency |
Приклад
// До (рядки — ніщо не захищає від помилки)
Status = "Cancelled";
if (Status == "Schdeuled") ... // компілятор мовчить!
// Після (enum — помилка компіляції при опечатці)
Status = AppointmentStatus.Cancelled;
if (Status == AppointmentStatus.Scheduled) ...Підказки
- Кожен
enum— окремий файл у просторі іменClinicApp. Оголошенняenum— коротке; синтаксис подивіться в документації. - Порядок значень має сенс. Перший елемент отримує число
0— тому на першу позицію ставте «природне за замовчуванням»:Unknownдля групи крові,Generalдля спеціальності. Так поле без явної ініціалізації матиме розумне значення. - Заміна типу поля — механічна, але наскрізна. У класах
Appointment,Patient,Doctorзмініть тип властивості зstringна відповіднийenum, а потім пройдіться по конструкторах і методах (Cancel,Complete), замінюючи рядкові літерали на значення enum. Компілятор сам покаже всі місця, де лишивсяstring. - Порівняння enum — звичайне
==/!=, як з числами. enum.ToString()поверне назву значення як у коді ("APositive"), а не"A+". Гарне відображення зробите в Задачі 3 (статичний форматер).- Що ще зламається після заміни типів — і це частина задачі.
Program.cs: початкові дані передавали групу крові й спеціальність рядками, а меню читало їх з консолі як текст.DoctorManager: пошук за спеціальністю порівнював текст запиту з рядковим полем. Без цих виправлень проєкт не збереться, а коміт задачі буде неповним. - Ввід enum з меню. Користувач вводить число, а поле тепер має тип enum, тож
потрібне явне приведення числа до типу перерахування — запис вигляду
(ТипEnum)число. Покажіть у меню пронумерований список значень і перетворіть введений номер. Чи існує в enum таке значення, це приведення не перевіряє — до цього повернемось у Лабі 05. - Пошук за спеціальністю на цьому кроці — найпростіше порівнювати текст запиту з текстовим поданням значення enum; повноцінний варіант з українськими назвами зробите в Задачах 3–4.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
AppointmentStatus |
BookingStatus |
ReservationStatus |
EnrollmentStatus |
RentalStatus |
LoanStatus |
SessionStatus |
BloodType |
RoomType |
DishCategory |
Faculty |
CarClass |
BookGenre |
FitnessLevel |
Speciality |
Department |
CuisineType |
Subject |
CarBrand |
LibrarySection |
TrainingType |
Коміт
git add ClinicApp/AppointmentStatus.cs ClinicApp/BloodType.cs ClinicApp/Speciality.cs
git add ClinicApp/Appointment.cs ClinicApp/Patient.cs ClinicApp/Doctor.cs
git add ClinicApp/DoctorManager.cs ClinicApp/Program.cs
git commit -m "Lab04 Task01"Задача 2. Struct WorkSchedule — value type ⭐⭐⭐
Умова
У Doctor є два окремих поля: int WorkStartHour і int WorkEndHour. Вони завжди разом — і разом мають зміст. Але нічого не заважає встановити Start = 20, End = 6 — безглузде розкладання.
struct дозволяє об'єднати пов'язані дані у нероздільний value type: значення копіюється при присвоєнні, не передається за посиланням.
Що реалізувати:
struct WorkScheduleз двомаget-only властивостямиStartіEnd.- Конструктор
WorkSchedule(int start, int end). - Обчислювані властивості:
HoursPerDay,Display(рядок"08:00–17:00"),IsNow(чи поточна година в межах розкладу). - Метод
Contains(int hour). override ToString().- Замінити
WorkStartHour/WorkEndHourуDoctorодним полемScheduleтипуWorkSchedule.
Специфікація struct
| Член | Тип | Опис |
|---|---|---|
Start |
public int (get only) |
Година початку |
End |
public int (get only) |
Година кінця |
HoursPerDay |
обчислювана int |
End - Start |
Display |
обчислювана string |
"08:00–17:00" |
IsNow |
обчислювана bool |
Contains(DateTime.Now.Hour) |
WorkSchedule(int, int) |
конструктор | Ініціалізує Start та End |
Contains(int hour) |
public bool |
hour >= Start && hour < End |
ToString() |
override | Display + " (" + HoursPerDay + " год)" |
Приклад
WorkSchedule morning = new WorkSchedule(8, 16);
WorkSchedule evening = new WorkSchedule(14, 22);
Console.WriteLine(morning); // 08:00–16:00 (8 год)
Console.WriteLine(morning.IsNow); // true/false залежно від годин
// Value type — копіюється при присвоєнні
WorkSchedule copy = morning;
// copy і morning — незалежні значенняПідказки
structописується майже якclass— тільки ключове слово інше. Синтаксис і повний перелік відмінностей — у документації.StartіEnd— незмінні після створення. Це властивості лише зget, які присвоюються один раз у конструкторі. Даліschedule.Start = 5не скомпілюється — і це добре: розклад не можна «покалічити».Displayбудує рядок"08:00–17:00"з двох годин, доповнених нулем (форматD2) — так само, якWorkSchedule-властивість у Лабі 03, тільки тепер логіка живе в одному місці.ContainsтаIsNow.Contains(hour)перевіряє діапазон[Start, End);IsNow— це простоContainsдля поточної години. Не дублюйте умову.- У
Doctorдва поля стають одним полем типуWorkSchedule(зget/set).IsAvailableNowтоді зводиться до звернення доSchedule.IsNow, аCanAcceptAt— доSchedule.Contains(...). - Ключова відмінність
structвідclass— семантика копіювання. Присвоєння однієїstruct-змінної іншій копіює значення (два незалежні розклади), тоді як присвоєння об'єкта класу копіює посилання (обидві змінні — на один об'єкт). Перевірте це експериментом уProgram.csі прочитайте розділ «Value types vs reference types». - Валідацію
Start < Endпоки не додаємо — це Лаба 05 (Інкапсуляція).
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
WorkSchedule (Start, End) |
BookingPeriod (CheckIn, CheckOut) |
ServiceHours (Open, Close) |
LectureSlot (StartHour, EndHour) |
RentalPeriod (PickupHour, ReturnHour) |
ShiftSchedule (Start, End) |
TrainingSlot (Start, End) |
Коміт
git add ClinicApp/WorkSchedule.cs ClinicApp/Doctor.cs ClinicApp/Program.cs
git commit -m "Lab04 Task02"Задача 3. Static клас та індексатор ⭐⭐⭐
Умова
Проблема 1: BloodType.APositive.ToString() повертає "APositive", але нам потрібно "A+". Логіка форматування потрібна в багатьох місцях — куди її помістити, якщо вона не належить жодному конкретному об'єкту?
Відповідь: static class — клас без екземплярів, тільки статичні методи.
Проблема 2: Отримати третього пацієнта зараз: clinic.Patients.FindById(3). Але якщо ми вже знаємо індекс — clinic.Patients[2] було б природніше.
Відповідь: індексатор this[int index].
Що реалізувати:
static class ClinicFormatterз методами:FormatBloodType(BloodType bt)→"A+","B-"тощоFormatSpeciality(Speciality s)→"Кардіологія"тощоFormatAge(int age)→"41 рік","33 роки","16 років"(правила відмінювання)FormatPhone(string phone)→"(050) 123-4567"
- Оновити
Patient.ToString()іDoctor.ToString()щоб використовували форматер. - Додати індексатор
this[int index]доPatientManager,DoctorManager,AppointmentManager.
Приклад
// static клас — викликається без екземпляру
Console.WriteLine(ClinicFormatter.FormatBloodType(BloodType.APositive)); // A+
Console.WriteLine(ClinicFormatter.FormatAge(1)); // 1 рік
Console.WriteLine(ClinicFormatter.FormatAge(3)); // 3 роки
Console.WriteLine(ClinicFormatter.FormatAge(11)); // 11 років
// індексатор
Patient first = clinic.Patients[0];
Doctor second = clinic.Doctors[1];Підказки
static classне має екземплярів —new ClinicFormatter()заборонено компілятором. Усередині лишеpublic staticметоди. Такий клас — просто «набір функцій під спільним іменем».FormatBloodType/FormatSpeciality— це зіставлення «значення enum → рядок». Найчистіше —switchяк вираз із гілкою_для решти. Кожній назві зі специфікації Задачі 1 поставте у відповідність потрібний рядок.FormatAge— правила українського відмінювання:- остача від ділення на 100 у діапазоні 11–19 → завжди «років»;
- інакше дивимось останню цифру:
1→ «рік»,2–4→ «роки», решта → «років». Перевірте на 1, 3, 11, 21, 111.
FormatPhone— спершу переконайтесь, що рядок має рівно 10 символів і всі вони цифри; якщо ні — поверніть як є. Якщо так — зберіть"(050) 123-4567"з підрядків. Метод виділення підрядка — у документаціїString.- Індексатор — це властивість із параметром. Синтаксис —
this[int index]з блокомget. Усередині перевірте межі (indexу[0, _count)); якщо поза межами — повернітьnull(тому тип —Patient?). Тількиget, безset. Patient.ToString()/Doctor.ToString()тепер викликаютьClinicFormatterзамість того, щоб форматувати самотужки.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicFormatter.FormatBloodType |
HotelFormatter.FormatRoomType |
RestaurantFormatter.FormatCategory |
UnivFormatter.FormatFaculty |
RentalFormatter.FormatCarClass |
LibraryFormatter.FormatGenre |
GymFormatter.FormatTrainingType |
PatientManager[i] |
GuestManager[i] |
CustomerManager[i] |
StudentManager[i] |
ClientManager[i] |
ReaderManager[i] |
MemberManager[i] |
Коміт
git add ClinicApp/ClinicFormatter.cs
git add ClinicApp/Patient.cs ClinicApp/Doctor.cs
git add ClinicApp/PatientManager.cs ClinicApp/DoctorManager.cs ClinicApp/AppointmentManager.cs
git commit -m "Lab04 Task03"Задача 4. Перевантаження методів та параметр out ⭐⭐⭐⭐
Умова
Перевантаження методів — декілька методів з однаковою назвою, але різними параметрами. Компілятор обирає потрібний за типом аргументів.
Параметр out — дозволяє методу повертати додаткове значення через аргумент. Класичний патерн — TryXxx: повертає bool (успіх/невдача) і через out — знайдений об'єкт.
Що реалізувати:
- Перевантаження в
DoctorManager:FindBySpeciality(string query)— існуючий (пошук за рядком, часткове співпадіння)FindBySpeciality(Speciality speciality)— новий (точне співпадіння за enum)
- Перевантаження в
AppointmentManager:GetByDate(DateTime date)— існуючийGetByDate(int year, int month, int day)— новий (три числа замістьDateTime)
- TryFindById у
PatientManager:bool TryFindById(int id, out Patient patient)
- TryFindById у
DoctorManager:bool TryFindById(int id, out Doctor doctor)
- Додати
FindByBloodType(BloodType bloodType)доPatientManager. - Продемонструвати
?.та??уProgram.cs.
Приклад
// Перевантаження — компілятор обирає за типом аргументу
Doctor[] cardiologists = clinic.Doctors.FindBySpeciality(Speciality.Cardiology); // enum версія
Doctor[] found = clinic.Doctors.FindBySpeciality("кардіо"); // string версія
// GetByDate overload
Appointment[] today = clinic.Appointments.GetByDate(2026, 5, 10); // зручніше, ніж new DateTime(...)
// TryFindById з out параметром
if (clinic.Patients.TryFindById(3, out Patient patient))
Console.WriteLine("Знайдено: " + patient.FullName);
else
Console.WriteLine("Пацієнта не знайдено.");
// ?. та ??
string name = clinic.Patients.FindById(99)?.FullName ?? "не знайдено";
Console.WriteLine(name); // не знайденоПідказки
- Перевантаження — це просто два методи з однаковим іменем і різними
параметрами.
FindBySpeciality(string)шукає за частиною рядка (як у Лабі 03),FindBySpeciality(Speciality)— точним порівняннямenum. Компілятор сам обере потрібний за типом аргументу на місці виклику. GetByDate(int, int, int)не дублює логіку — він будуєDateTimeз трьох чисел і викликає вже наявнийGetByDate(DateTime). Одна реалізація, зручніший вхід.TryFindById— патернTryXxx. Метод повертаєbool(знайшли чи ні), а сам об'єкт віддає через параметрout. Всередині він спирається на наявнийFindById. Прочитайте проoutі про те, чому цей патерн зручніший за «повернутиnullі сподіватись, що викличок перевірить».FindByBloodType(BloodType)— той самий двопрохідний патерн, щоFindByNameу Лабі 03, лише умова — точна рівністьenum(==), а не входження підрядка.?.(null-conditional) — звертання до члена, що безпечно даєnull, якщо ліворучnull.??(null-coalescing) — підставляє запасне значення замістьnull. Разом:FindById(99)?.FullName ?? "невідомий"не впаде на неіснуючому ID. Ці оператори дозволені саме з цієї лаби — продемонструйте їх уProgram.cs.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
FindBySpeciality(Speciality) |
FindByDepartment(Department) |
FindByCategory(DishCategory) |
FindByFaculty(Faculty) |
FindByClass(CarClass) |
FindBySection(LibrarySection) |
FindByType(TrainingType) |
TryFindById(id, out Patient) |
TryFindById(id, out Guest) |
TryFindById(id, out Customer) |
TryFindById(id, out Student) |
TryFindById(id, out Client) |
TryFindById(id, out Reader) |
TryFindById(id, out Member) |
Коміт
git add ClinicApp/PatientManager.cs ClinicApp/DoctorManager.cs ClinicApp/AppointmentManager.cs ClinicApp/Program.cs
git commit -m "Lab04 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі 4 завдання виконано:
oop-course/ ← гілка Lab-04 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т1 Т2 Т4
├── Patient.cs ✏ Т1 Т3
├── Doctor.cs ✏ Т1 Т2 Т3
├── PatientManager.cs ✏ Т3 Т4
├── DoctorManager.cs ✏ Т1 Т3 Т4
├── Appointment.cs ✏ Т1
├── AppointmentManager.cs ✏ Т3 Т4
├── Clinic.cs
├── GrowablePatientManager.cs
├── AppointmentStatus.cs 🆕 Т1
├── BloodType.cs 🆕 Т1
├── Speciality.cs 🆕 Т1
├── WorkSchedule.cs 🆕 Т2
└── ClinicFormatter.cs 🆕 Т3Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були в Лабі 03.
Усі файли поки що лежать в одній теці ClinicApp/ — у підпапки (Models/, Enums/, Managers/, Utils/) їх розкладемо в Лабі 05.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Проєкт компілюється без помилок і попереджень
- У коді немає рядкових літералів
"Scheduled","A+","Кардіологія"— усе черезenum -
BloodType.APositiveпоказується як"A+"(черезClinicFormatter) -
Doctor.Schedule.ToString()повертає"08:00–16:00 (8 год)" -
clinic.Patients[0]повертає першого пацієнта,clinic.Patients[999]—null -
clinic.Doctors.FindBySpeciality(Speciality.Cardiology)іFindBySpeciality("кардіо")обидва працюють (перевантаження) -
TryFindById(99, out var p)повертаєfalseі не кидає виняток -
FindById(99)?.FullName ?? "не знайдено"не падає зNullReferenceException -
FormatAge: 1 → «1 рік», 3 → «3 роки», 11 → «11 років», 21 → «21 рік»
Питання для самоперевірки
- Що саме перевіряє компілятор, коли статус —
enum, і чого він не може перевірити, коли статус —string? WorkSchedule a = b; a.Start = 10;— чому цей код навіть не скомпілюється, і що було б зb, якбиStartмавset?- Чому
ClinicFormatter—static class, а не звичайний клас, який просто ніхто не інстанціює? Що дає ключове словоstaticна класі? - Індексатор і звичайна властивість — у чому синтаксична й змістова різниця?
TryFindByIdповертаєbool+out. Чим це надійніше за «повернутиPatient?і сподіватись на перевірку!= null»?FindBySpeciality(string)іFindBySpeciality(Speciality)— це перевантаження. Чи можна було б обійтися одним методом з необов'язковим параметром? Чому ні?
Статус гілки
Після всіх 4 завдань (кожне — окремий коміт Lab04 TaskNN на гілці Lab-04):
git push -u origin Lab-04
git checkout main
git merge --no-ff Lab-04 -m "Merge Lab-04: Class Members"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-05.