Lab 04
Члени класу
enum, struct, static, overload
Лаба 04 — Члени класу
Мета
Збагатити систему новими видами членів класу: іменованими константами (enum), структурами-значеннями (struct), статичними утилітними класами та індексаторами — і навчитися перевантажувати методи.
Контекст
Після Лаби 03 система працює, але має «брудний» код: статуси зберігаються як рядки "Scheduled", групи крові як "A+", спеціальності як "Кардіологія". Будь-яка опечатка — і логіка зламана, а компілятор нічого не скаже.
Ця лаба вирішує це системно: замінюємо magic strings на типобезпечні конструкції та розширюємо API менеджерів колекцій.
Гілка
git checkout main
git checkout -b feature/class-membersГілка зливається в
mainпісля завершення всіх завдань.
Задача 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.
Специфікація
| 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:namespace ClinicApp; public enum AppointmentStatus { Scheduled, Cancelled, Completed } - Перший елемент
enumотримує числове значення0.Unknown,General— природні значення за замовчуванням. - У конструкторах замініть рядки на enum значення:
Status = AppointmentStatus.Scheduled; BloodType = BloodType.Unknown; - У
Cancel()іComplete():if (Status != AppointmentStatus.Scheduled) return false; Status = AppointmentStatus.Cancelled; enum.ToString()дає назву значення ("Scheduled"). Для відображення у зручному форматі ("Scheduled" → "A+") знадобиться Задача 3.
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
AppointmentStatus |
BookingStatus |
ReservationStatus |
EnrollmentStatus |
RentalStatus |
LoanStatus |
SessionStatus |
BloodType |
RoomType |
DishCategory |
Faculty |
CarClass |
BookGenre |
FitnessLevel |
Speciality |
Department |
CuisineType |
Subject |
CarBrand |
LibrarySection |
TrainingType |
Коміт
git add src/AppointmentStatus.cs src/BloodType.cs src/Speciality.cs
git add src/Appointment.cs src/Patient.cs src/Doctor.cs
git commit -m "Lab04 Task1: add enums for status, blood type and speciality"Задача 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, але ключове словоstruct:public struct WorkSchedule { public int Start { get; } public int End { get; } public WorkSchedule(int start, int end) { Start = start; End = end; } }get-only властивості ({ get; }) можна ініціалізувати тільки в конструкторі — це забезпечує незмінність після створення.Display— форматування черезToString("D2"):public string Display => Start.ToString("D2") + ":00–" + End.ToString("D2") + ":00";- У
Doctorзамініть два поля одним:
У конструкторі:public WorkSchedule Schedule { get; set; }Schedule = new WorkSchedule(8, 17); - Після зміни у
Program.cs:d1.Schedule = new WorkSchedule(8, 16); // замість WorkStartHour/WorkEndHour IsAvailableNowу Doctor спрощується до:public bool IsAvailableNow => Schedule.IsNow;- Різниця struct vs class: присвоєння
WorkSchedule a = bкопіює значення, аPatient a = bкопіює посилання. Перевірте: змінившиa.StartпісляWorkSchedule a = b,b.Startне зміниться (але для struct з readonly властивостями взагалі не можна змінити після створення).
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
WorkSchedule (Start, End) |
BookingPeriod (CheckIn, CheckOut) |
ServiceHours (Open, Close) |
LectureSlot (StartHour, EndHour) |
RentalPeriod (PickupHour, ReturnHour) |
ShiftSchedule (Start, End) |
TrainingSlot (Start, End) |
Коміт
git add src/WorkSchedule.cs src/Doctor.cs src/Program.cs
git commit -m "Lab04 Task2: add WorkSchedule struct, replace int hours in Doctor"Задача 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:public static class ClinicFormatter { public static string FormatBloodType(BloodType bt) => bt switch { BloodType.APositive => "A+", BloodType.ANegative => "A-", // ... _ => "Невідомо" }; }- Правила відмінювання для
FormatAge:- 11–19 → завжди "років" (виняток для підлітків)
- закінчення 1 → "рік" (21 рік, 31 рік, але не 11)
- закінчення 2,3,4 → "роки" (22 роки, 33 роки)
- інше → "років"
if (age % 100 >= 11 && age % 100 <= 19) return age + " років"; switch (age % 10) { case 1: return age + " рік"; case 2: case 3: case 4: return age + " роки"; default: return age + " років"; } - Індексатор синтаксично схожий на властивість, але з параметром
this[...]:public Patient this[int index] { get { if (index < 0 || index >= _count) return null!; return _patients[index]; } } - Індексатор — лише
get(readonly).PatientManager[0] = new Patient(...)не потрібно. FormatPhone: перевірте довжину 10 символів, усі цифри, потім форматуйте:return "(" + phone.Substring(0, 3) + ") " + phone.Substring(3, 3) + "-" + phone.Substring(6);
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
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 src/ClinicFormatter.cs
git add src/Patient.cs src/Doctor.cs
git add src/PatientManager.cs src/DoctorManager.cs src/AppointmentManager.cs
git commit -m "Lab04 Task3: add ClinicFormatter static class and indexers on managers"Задача 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); // не знайденоПідказки
- Перевантаження — просто два методи з однаковою назвою:
C# обере правильний варіант залежно від типу аргументу при виклику.public Doctor[] FindBySpeciality(string query) { /* рядковий пошук */ } public Doctor[] FindBySpeciality(Speciality speciality) { /* точне порівняння enum */ } GetByDateз трьома числами — делегує до основного:public Appointment[] GetByDate(int year, int month, int day) { return GetByDate(new DateTime(year, month, day)); }TryFindById— класичний TryXxx патерн:
Виклик:public bool TryFindById(int id, out Patient patient) { patient = FindById(id); return patient != null; }if (manager.TryFindById(5, out Patient p)) { ... }FindByBloodType— двопрохідний патерн з Lab03, але умова —== bloodTypeзамість.Contains():public Patient[] FindByBloodType(BloodType bloodType) { ... }?.— null-conditional:obj?.Propertyповертаєnullякщоobj == null, інакшеobj.Property.??— null-coalescing:expr ?? defaultValueповертаєdefaultValueякщоexpr == null.- Комбінація:
clinic.Patients.FindById(99)?.FullName ?? "невідомий"— безпечне звернення до властивості з fallback значенням.
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
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 src/PatientManager.cs src/DoctorManager.cs src/AppointmentManager.cs src/Program.cs
git commit -m "Lab04 Task4: add method overloads, TryFindById with out, FindByBloodType"Перевірка перед здачею
cd src
dotnet build
dotnet runПереконайтесь, що:
- Проєкт компілюється без помилок
-
AppointmentStatus.Scheduled— у коді немає рядка"Scheduled" -
BloodType.APositiveвідображається як"A+"черезClinicFormatter -
Doctor.Schedule.ToString()повертає"08:00–16:00 (8 год)" -
clinic.Patients[0]повертає першого пацієнта -
clinic.Doctors.FindBySpeciality(Speciality.Cardiology)повертає кардіологів -
TryFindById(99, out p)повертаєfalseі не кидає виняток -
FindById(99)?.FullName ?? "не знайдено"працює без NullReferenceException
Питання для самоперевірки
- Чому
enumбезпечніший заstringдля статусів? Що конкретно перевіряє компілятор? - Яка різниця між
classіstruct? Що станеться приWorkSchedule a = b; a.Start = 10? - Навіщо
static class? Чому не можна просто написати звичайний клас і не створювати екземпляри? - Що таке індексатор? Як він відрізняється від звичайної властивості?
- Чому
TryFindByIdповертаєboolі маєout, а не просто повертаєnullпри невдачі? - Яка різниця між перевантаженням методів та параметрами за замовчуванням?
Злиття
git checkout main
git merge --no-ff feature/class-members -m "Merge feature/class-members: Lab04 Class Members"Наступна лаба:
git checkout -b feature/encapsulation— інкапсуляція, приватні поля, валідація.