Lab 06
Наслідування
MedicalRecord, Diagnosis, LabResult
Лаба 06 — Успадкування
Мета
Навчитися будувати ієрархії класів через успадкування: виносити спільне в абстрактний базовий клас, зобов'язувати похідні класи реалізовувати абстрактні методи, перевизначати virtual-методи, безпечно працювати з об'єктами різних типів через базовий тип і розпізнавати реальний тип об'єкта під час виконання (is, as).
Контекст
Система вже вміє зберігати пацієнтів, лікарів і записи на прийом, а її класи захищені від некоректних даних (Лаба 05). Але медична картка пацієнта — окрема сутність: лікар додає до неї записи різних видів — діагноз, результат аналізу, рецепт. Усі вони мають спільні атрибути (кому, хто, коли), але відрізняються змістом і поведінкою: діагноз буває хронічним, рецепт «спливає», аналіз буває поза нормою.
Якщо зробити три незв'язані класи — це три копії однакових полів і три окремі сховища в менеджері (Diagnosis[], LabResult[], Prescription[]). Додати четвертий вид запису означало б переписувати все навколо. Успадкування вирішує це: одна база — різні похідні класи, а код, що працює із «записом узагалі», не знає й не мусить знати, яким саме він є.
Структура проєкту на початку лаби
Це результат Лаби 05 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 05)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/
│ ├── AppointmentStatus.cs
│ ├── BloodType.cs
│ └── Speciality.cs
├── Models/
│ ├── Patient.cs
│ ├── Doctor.cs
│ ├── Appointment.cs
│ └── WorkSchedule.cs
├── Managers/
│ ├── PatientManager.cs
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs
│ └── GrowablePatientManager.cs
└── Utils/
├── ClinicFormatter.cs
└── ClinicValidator.csСтруктуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що таке успадкування
Успадкування — механізм, за якого один клас (похідний, «нащадок») отримує все, що є в іншому (базовому), і може додати власне. У коді це двокрапка в заголовку: class Diagnosis : MedicalRecord.
Коли успадкування доречне. Перевірте фразою «X є різновидом Y»: діагноз є медичним записом — ієрархія доречна. Якщо ж «X має Y» (лікар має розклад роботи), це не успадкування, а звичайне поле, як Doctor.Schedule. Плутанина «є» / «має» — найпоширеніша помилка проєктування.
Що передається нащадку. public- і protected-члени (поля, властивості, методи). private-члени фізично існують в об'єкті, але код нащадка напряму їх не бачить. Конструктори не успадковуються: у кожного класу свої, а конструктор нащадка мусить викликати конструктор базового класу.
Три слова, що керують заміною поведінки:
| Слово | Де пишеться | Що означає |
|---|---|---|
abstract |
у базовому класі | метод без тіла: «кожен нащадок зобов'язаний його реалізувати». Клас, що має такий метод, теж abstract — створити його через new не можна |
virtual |
у базовому класі | метод з реалізацією за замовчуванням: «нащадок може її замінити, а може й лишити» |
override |
у похідному класі | сама заміна abstract- або virtual-методу базового класу |
Перший погляд на поліморфізм. Змінна типу MedicalRecord може вказувати на Diagnosis, LabResult чи Prescription. Виклик методу через таку змінну виконає версію реального об'єкта, а не типу змінної. Саме на цьому побудовано Задачі 2 і 4. Глибше про поліморфізм — у Лабі 08.
Звідки ви вже знаєте
override. З Лаби 03 ви пишетеoverride ToString(). Тепер видно, чому це працює: кожен клас у C# неявно успадковуєobject, у якому методToString()оголошено якvirtual.
Що нового дозволено (і тільки воно)
- заголовок нащадка
class Похідний : Базовий; abstract(для класу й методу),virtual,override;protectedта виклик конструктора базового класу: base(...);- оператори
is(зокремаis Тип змінна),asта явне приведення(Тип)об'єкт; винятокInvalidCastException; - масив базового типу, у якому лежать об'єкти різних похідних типів.
Про null: оператор as повертає «або об'єкт, або null», тому тип результату записується як Diagnosis? (див. розділ «Коротко про null і тип T?» в Лабі 03).
Досі заборонено: interface (Лаба 07), new-приховування методів і sealed (Лаба 08), List<T> / Dictionary та інші generic-колекції (Лаба 09), LINQ (Лаба 14). Усе робимо масивами й циклами, як і раніше.
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-06Коміт — на кожне завдання (Lab06 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» наприкінці кожного завдання. Структуру рішення зберігайте: одна абстрактна база + щонайменше три нащадки, де один із нащадків перевизначає virtual-метод IsActive() за власним правилом.
Як користуватися підказками
Підказки — напрям думки, а не готовий код. «Специфікація» каже, що має вийти; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Приклади коду в цій лабі показують загальну схему на вигаданій ієрархії (Vehicle, Bicycle, Truck) — перенесення її на MedicalRecord, Diagnosis, LabResult і Prescription робите самі. Приклади «Як має працювати» показують лише використання вашого коду й очікуваний вивід.
Задача 1. Абстрактний клас `MedicalRecord` та `Diagnosis` ⭐⭐
Умова
Усі медичні записи мають спільні поля: хто пацієнт, який лікар, коли зроблено. Але зміст кожного виду різний — діагноз, аналіз і рецепт несуть зовсім різну інформацію.
abstract class дозволяє оголосити спільний контракт: що є у кожного запису і що кожен нащадок зобов'язаний реалізувати. Поки клас абстрактний — new MedicalRecord(...) неможливий, лише new Diagnosis(...).
Що реалізувати:
abstract class MedicalRecordуClinicApp/Models/(простір іменClinicApp.Models):Id(статичний лічильник, як уPatient),PatientId,DoctorId,Date,Notes;protected-конструктор — перевіряєPatientId > 0іDoctorId > 0черезClinicValidator.ValidatePositive, ініціалізує спільні поля;Idприсвоюється останнім (принцип Лаби 05);abstract string GetSummary()— зміст запису, кожен нащадок реалізує по-своєму;virtual string GetRecordType()— базова реалізація:"Медичний запис";virtual bool IsActive()— базова реалізація: запис активний, якщо він не старший за 6 місяців;override ToString()— складається зGetRecordType()іGetSummary().
Перший конкретний нащадок
Diagnosis : MedicalRecord:- приватні поля
_diagnosisCode,_descriptionз явними сеттерами — валідація черезClinicValidator.ValidateName; - публічні властивості
DiagnosisCode,Description,IsChronic; - конструктор, що викликає
base(...)і присвоює власні поля через властивості; override GetSummary()—"I10: Гіпертонічна хвороба [хронічне]";override GetRecordType()—"Діагноз".
- приватні поля
Як перевіряти, поки меню не змінене: Меню для медичної картки з'явиться лише в Задачі 4. До того класи перевіряйте тимчасовим кодом у Program.cs — між блоком тестових даних і головним циклом меню: створіть об'єкти, виведіть результат у консоль, порівняйте з прикладами. Цей тимчасовий код у коміти Задач 1–3 не потрапляє: git add там перелічує лише файли завдання. Тож git status показуватиме Program.cs як змінений — це нормально. У Задачі 4 ви замінюєте тимчасовий код справжніми тестовими даними й меню та комітите Program.cs разом з рештою.
Специфікація
MedicalRecord:
| Член | Тип | Опис |
|---|---|---|
Id |
int (лише get) |
Авто-лічильник |
PatientId |
int (лише get) |
ID пацієнта, має бути > 0 |
DoctorId |
int (лише get) |
ID лікаря, має бути > 0 |
Date |
DateTime (лише get) |
Дата запису |
Notes |
string (get; set) |
Додаткові нотатки, за замовчуванням "", без перевірок |
| конструктор | protected |
(int patientId, int doctorId, DateTime date) |
GetSummary() |
abstract string |
Зміст запису |
GetRecordType() |
virtual string |
Тип: "Медичний запис" |
IsActive() |
virtual bool |
Не старший за 6 місяців |
ToString() |
override |
"[1] Діагноз | 21.09.2026 | I10: Гіпертонічна хвороба"; якщо Notes не порожні — у кінці додається " | " + Notes |
Diagnosis:
| Член | Тип | Опис |
|---|---|---|
DiagnosisCode |
string (get; set з валідацією) |
Код діагнозу ("I10"); порожній або довший за 50 символів — ArgumentException |
Description |
string (get; set з валідацією) |
Опис; ті самі правила |
IsChronic |
bool (get; set) |
Ознака хронічного, за замовчуванням false |
| конструктор | public |
(int patientId, int doctorId, DateTime date, string diagnosisCode, string description, bool isChronic = false) |
GetSummary() |
override |
код: опис, а для хронічного — ще " [хронічне]" |
GetRecordType() |
override |
"Діагноз" |
Приклад
Схема ієрархії — на вигаданому класі Vehicle (у вашому проєкті такі конструкції робите самі):
public abstract class Vehicle
{
public int Wheels { get; }
protected Vehicle(int wheels)
{
Wheels = wheels;
}
public abstract string Describe(); // нащадок ЗОБОВ'ЯЗАНИЙ реалізувати
public virtual string Kind() => "Транспорт"; // нащадок МОЖЕ замінити
}
public class Bicycle : Vehicle
{
public Bicycle() : base(2) { } // виклик конструктора базового класу
public override string Describe() => "Велосипед із педалями";
public override string Kind() => "Двоколісний";
}Як така ієрархія використовується (Truck — інший нащадок Vehicle):
// Vehicle v = new Vehicle(4); // помилка компіляції: клас абстрактний
Vehicle v = new Bicycle(); // змінна базового типу тримає нащадка
Console.WriteLine(v.Kind()); // "Двоколісний" — виконується версія BicycleЯк має працювати ваш код (дати залежать від дня запуску, номер [1] — від кількості вже створених записів):
// abstract — не можна створити безпосередньо:
// MedicalRecord r = new MedicalRecord(1, 1, DateTime.Today); // помилка компіляції!
// Тільки через нащадка:
Diagnosis d = new Diagnosis(1, 1, DateTime.Today, "I10", "Гіпертонічна хвороба", isChronic: true);
Console.WriteLine(d.GetRecordType()); // Діагноз
Console.WriteLine(d.GetSummary()); // I10: Гіпертонічна хвороба [хронічне]
Console.WriteLine(d); // [1] Діагноз | 21.09.2026 | I10: Гіпертонічна хвороба [хронічне]
Console.WriteLine(d.IsActive()); // True (щойно створено)
// Змінна базового типу може тримати нащадка:
MedicalRecord record = new Diagnosis(1, 1, DateTime.Today, "J06.9", "Ринофарингіт");
Console.WriteLine(record.GetRecordType()); // Діагноз — виклик іде в нащадка!Підказки
- Що йде в базовий клас. Питання-фільтр: «чи є це поле у кожного запису?» Пацієнт, лікар, дата — так, тож вони в
MedicalRecord. Код діагнозу чи дозування препарату є лише в одного виду — вони належать нащадкам. - Файл і клас. Створіть
ClinicApp/Models/MedicalRecord.csз простором іменClinicApp.Models. Словоabstractставиться передclassу заголовку. Щоб скористатисьClinicValidator, потрібенusingдля простору іменClinicApp.Utils(як у Лабі 05). - Властивості лише для читання.
Id,PatientId,DoctorId,Dateприсвоюються один раз у конструкторі — так само, якIdуPatient(Лаба 03: статичний лічильник і_nextId++).Notes— властивість ізgetіset, початкове значення — порожній рядок; перевірок їй не потрібно. - Конструктор —
protected. Модифікатор означає «видимий у самому класі та в його нащадках». Порядок дій у конструкторі — принцип із Лаби 05: спершу перевірки (ValidatePositiveдляpatientIdіdoctorId; у виклик передавайтеnameofвідповідного параметра), потім присвоєння, аId— останнім. abstract-метод не має тіла. Замість{ … }після заголовка стоїть крапка з комою. Якщо нащадок не реалізує такий метод — помилка компіляції. Тому й сам клас, що містить такий метод, мусить бутиabstract. Проте не все в абстрактному класі абстрактне: він може мати й звичайні поля, властивості та методи з тілом — саме так влаштованийMedicalRecord.virtual-метод має тіло за замовчуванням, яке нащадок може замінити (а може й ні). Тут цеGetRecordType()(текст «Медичний запис») таIsActive().- Правило
IsActive()у базі: «запис активний, якщо він не старший за 6 місяців». ПорівняйтеDateз датою «шість місяців тому» — її даєDateTime.AddMonthsз від'ємним аргументом. Межа включно: запис рівно піврічної давності ще активний. ToString(). Складайте рядок із викликів методівGetRecordType()іGetSummary()(не з полів!), дату виводьте у форматіdd.MM.yyyy; нотатки додавайте лише коли вони непорожні. Базовий клас не знає, який нащадок виконаєGetSummary()— і не мусить: це вирішується під час виконання.- Клас
Diagnosis. Оголошується як нащадок: після імені — двокрапка й ім'я базового класу. Поля й властивості робіть за схемою Лаби 05: приватне поле_camelCase, властивість із валідацією черезClinicValidator.ValidateName, у виклик передавайтеnameof(...)властивості. Назва методу говорить про «ім'я», але він просто перевіряє «непорожній рядок до 50 символів» — для коду діагнозу й опису це саме те, що треба. - Конструктор нащадка мусить викликати конструктор бази. Після списку параметрів ставиться двокрапка й
base(...)з потрібними аргументами. Тіло конструктора нащадка виконується вже після базового; у ньому присвоюйте власні поля через властивості (щоб спрацювала валідація), а не напряму в поля. Спробуйте прибратиbase(...): компілятор поскаржиться на відсутній аргумент (кодCS7036) — у базового класу немає конструктора без параметрів, тож нащадок мусить сказати, який саме викликати. override— обов'язкове слово. ІGetSummary(), іGetRecordType()уDiagnosisзаписуються зoverride. Без нього дляGetSummary()код не збереться (CS0534: абстрактний метод не реалізовано). ДляGetRecordType()наслідки підступніші: компілятор видасть лише попередженняCS0114. Експеримент. На хвилину приберітьoverrideзGetRecordType()уDiagnosis. Збережіть діагноз у змінну типуMedicalRecordі виведітьGetRecordType()та сам запис (ToString) — а тоді те саме через змінну типуDiagnosis. Що бачите? Повернітьoverride. Компілятор запропонує ще й «додати словоnew» — це інша конструкція (Лаба 08), вона вам не потрібна. Висновок: попередження компілятора в ієрархіях — майже завжди помилка.- Експеримент з порядком конструкторів. У
try/catchдвічі створітьDiagnosisз порожнім кодом, а тоді коректний — який у ньогоId? Тепер зробіть те саме зpatientId = 0. Чому результати різні? Підказка: конструктор бази виконується повністю до тіла конструктора нащадка. Якщо помилку виявила база —Idще не призначено; якщо нащадок — база вже «з'їла» номер. Принцип «Idостаннім» у ієрархії повністю не досяжний — це відомий компроміс; у Лабі 17 нумерацією займеться база даних. - Ще два експерименти з компілятором:
new MedicalRecord(...)(кодCS0144) і змінаprotectedнаprivateу конструкторі бази (кодCS0122у всіх нащадках — вони більше не можуть викликатиbase(...)). Зауваження: для абстрактного класуpublic-конструктор поводиться так само, якprotected(створити об'єкт все одно не можна), алеprotectedчесніше показує намір. - (За бажанням) запис не може бути датований майбутнім —
ClinicValidator.ValidateDateце вже вміє. Подумайте, де його викликати.
📖 Документація:
- Успадкування
- Абстрактні класи та члени
abstract,virtual,overridebaseтаprotectedobject.ToStringDateTime.AddMonths
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
MedicalRecord (abstract) |
GuestRecord (abstract) |
OrderRecord (abstract) |
AcademicRecord (abstract) |
ServiceRecord (abstract) |
LibraryRecord (abstract) |
GymRecord (abstract) |
Diagnosis (перший нащадок) |
Complaint |
FeedbackEntry |
GradeEntry |
DamageReport |
LoanRecord |
ProgressEntry |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
virtual GetRecordType() → "Медичний запис" |
→ "Запис гостя" |
→ "Замовлення" |
→ "Академічний запис" |
→ "Сервісний запис" |
→ "Бібліотечний запис" |
→ "Запис у клубі" |
Коміт
git add ClinicApp/Models/MedicalRecord.cs ClinicApp/Models/Diagnosis.cs
git commit -m "Lab06 Task01"Задача 2. `LabResult`, `Prescription` та `MedicalRecordManager` ⭐⭐⭐
Умова
Diagnosis — лише один із видів медичних записів. Результат аналізу (LabResult) має числове значення, одиниці виміру, норму й ознаку «в нормі». Рецепт (Prescription) — назву препарату, дозування й тривалість курсу.
Кожен нащадок реалізує GetSummary() по-своєму і за потреби перевизначає virtual-методи. Наприклад, Prescription змінює логіку IsActive(): рецепт активний, доки не закінчився курс, незалежно від 6-місячного правила.
MedicalRecordManager зберігає поліморфний масив MedicalRecord[] — в одному масиві живуть діагнози, аналізи й рецепти. DisplayAll() перебирає масив і виводить кожен запис — кожен об'єкт показує свій рядок.
Що реалізувати:
LabResult : MedicalRecord(ClinicApp/Models/):- приватні поля
_testName,_unit,_referenceRange— валідація черезClinicValidator.ValidateName; Value(double) іIsNormal(bool) — автовластивості без валідації;override GetSummary()→"Гемоглобін: 145 г/л (норма: 120–160)"; якщо поза нормою — додати" ⚠ поза нормою";override GetRecordType()→"Аналіз".
- приватні поля
Prescription : MedicalRecord(ClinicApp/Models/):- приватні поля
_medicationName,_dosage— валідація черезClinicValidator.ValidateName; - приватне поле
_durationDays— валідація черезClinicValidator.ValidatePositive; Instructions— автовластивість (необов'язкове поле, не валідується);- обчислювана властивість
ExpiresAt→Date.AddDays(DurationDays); override GetSummary()→"Лізиноприл 10 мг × 30 днів (1 раз на добу вранці)";override GetRecordType()→"Рецепт";override IsActive()→ExpiresAt >= DateTime.Today(замість 6-місячного правила).
- приватні поля
MedicalRecordManagerуClinicApp/Managers/:- поліморфний масив
MedicalRecord[] _records(ліміт 1000); Add(MedicalRecord),FindById(int);GetByPatient(int)→MedicalRecord[];GetByDoctor(int)→MedicalRecord[];DisplayAll(),DisplayList(MedicalRecord[]);- індексатор
this[int index].
- поліморфний масив
Специфікація
LabResult:
| Член | Тип | Опис |
|---|---|---|
TestName |
string (get; set з валідацією) |
Назва аналізу |
Value |
double (get; set) |
Виміряне значення |
Unit |
string (get; set з валідацією) |
Одиниці виміру |
ReferenceRange |
string (get; set з валідацією) |
Норма текстом: "120–160", "< 5.2" |
IsNormal |
bool (get; set) |
Результат у нормі? |
| конструктор | public |
(int patientId, int doctorId, DateTime date, string testName, double value, string unit, string referenceRange, bool isNormal) |
GetSummary() / GetRecordType() |
override |
див. вище / "Аналіз" |
Prescription:
| Член | Тип | Опис |
|---|---|---|
MedicationName |
string (get; set з валідацією) |
Препарат |
Dosage |
string (get; set з валідацією) |
Дозування, "10 мг" |
DurationDays |
int (get; set з валідацією) |
Тривалість курсу, > 0 (ArgumentOutOfRangeException) |
Instructions |
string (get; set) |
Як приймати; за замовчуванням "" |
ExpiresAt |
DateTime (лише get, обчислюється) |
Date + DurationDays днів |
| конструктор | public |
(int patientId, int doctorId, DateTime date, string medicationName, string dosage, int durationDays, string instructions = "") |
GetSummary() / GetRecordType() |
override |
див. вище / "Рецепт"; текст інструкції в дужках — лише якщо вона непорожня |
IsActive() |
override |
Курс ще не закінчився (включно з днем закінчення) |
MedicalRecordManager:
| Член | Тип результату | Опис |
|---|---|---|
Count |
int |
Кількість записів |
Add(MedicalRecord) |
void |
Перевірка ліміту (1000) з повідомленням; після додавання — повідомлення на кшталт Запис [1] Діагноз додано. |
FindById(int) |
MedicalRecord? |
Лінійний пошук; null, якщо не знайдено |
GetByPatient(int) |
MedicalRecord[] |
Усі записи пацієнта |
GetByDoctor(int) |
MedicalRecord[] |
Усі записи лікаря |
DisplayAll() |
void |
Усі записи; якщо їх немає — повідомлення |
DisplayList(MedicalRecord[]) |
void |
Виводить масив; якщо порожній — повідомлення |
this[int index] |
MedicalRecord? |
Індексатор (лише get); поза межами — null |
Приклад
Як має працювати ваш код:
LabResult lr = new LabResult(1, 1, DateTime.Today, "Холестерин", 6.2, "ммоль/л", "< 5.2", isNormal: false);
Console.WriteLine(lr);
// [3] Аналіз | 21.09.2026 | Холестерин: 6.2 ммоль/л (норма: < 5.2) ⚠ поза нормою
Prescription rx = new Prescription(1, 1, DateTime.Today.AddDays(-5), "Лізиноприл", "10 мг", 30, "вранці");
Console.WriteLine(rx.IsActive()); // True — курс 30 днів, минуло лише 5
Console.WriteLine(rx.ExpiresAt.ToString("dd.MM.yyyy")); // 16.10.2026 — через 25 днівПоліморфний масив — різні типи, одне сховище:
MedicalRecord[] records = manager.GetByPatient(1);
for (int i = 0; i < records.Length; i++)
Console.WriteLine(records[i]); // кожен виводить свій ToString()
// [1] Діагноз | 22.08.2026 | I10: Гіпертонічна хвороба [хронічне]
// [3] Аналіз | 14.09.2026 | Гемоглобін: 145 г/л (норма: 120–160)
// [5] Рецепт | 16.09.2026 | Лізиноприл 10 мг × 30 днів (1 раз на добу вранці)Підказки
- Той самий шаблон, що й у
Diagnosis. Кожен новий вид запису — окремий файл уModels/, клас-нащадокMedicalRecord, конструктор із викликомbase(...), приватні поля з валідованими властивостями, дваoverride(GetSummary,GetRecordType). Порядок параметрів конструкторів — за таблицями специфікації, бо на нього спираються тестові дані та меню в Задачі 4. - Які поля валідуються. Текстові (
TestName,Unit,ReferenceRange,MedicationName,Dosage) — черезValidateName;DurationDays— черезValidatePositive. Без перевірок лишаютьсяValue(це просто число),IsNormal(прапорець) таInstructions(необов'язковий текст). У викликах валідатора знову передавайтеnameof(...)властивості. Це той самийClinicValidatorз Лаби 05: новий клас — нові поля, але правила лишаються в одному місці, а не копіюються в кожен клас. - Про
IsNormal. Це свідомий компроміс: норма записана текстом ("< 5.2","120–160"), тож автоматично порівняти з неюValueне вийде, і прапорець виставляє той, хто вводить результат. У реальній системі норму зберігали б двома числами (мінімум і максимум), аIsNormalбув би обчислюваною властивістю — за бажанням спробуйте так зробити. GetSummary()для аналізу — назва, значення, одиниці й норма в дужках; суфікс" ⚠ поза нормою"лише колиIsNormalдорівнюєfalse. Якщо в консолі замість⚠,×,–з'являються знаки питання — це кодування консолі: уProgram.cs(теж після блокуusing) встановітьConsole.OutputEncodingу UTF-8 (див. документацію).- Роздільник дробової частини залежить від регіональних налаштувань. Додаючи
doubleдо рядка, .NET бере поточну культуру: на комп'ютері з українськими налаштуваннями6.2виведеться як6,2(у прикладах вище — з крапкою). Це та сама проблема, що в Лабі 01: зафіксуйтеInvariantCultureдвома рядками вProgram.cs— одразу після блокуusing(розділ «Як виконувати завдання» Лаби 01; докладніше — підказка 2 Задачі 4). Ці рядки потраплять у коміт Задачі 4 разом з рештоюProgram.cs. ExpiresAt— обчислювана властивість: лишеget, приватного поля немає, значення щоразу рахується зDateіDurationDaysметодомDateTime.AddDays. Такі властивості вже траплялись у Лабі 03.override IsActive()уPrescriptionповністю замінює правило бази: рецепт активний, поки не минув день закінчення курсу (сам день закінчення ще активний). Базову реалізацію можна було б викликати черезbase.IsActive(), але тут вона не потрібна. УLabResultметод не перевизначайте — аналіз користується базовим правилом.MedicalRecordManagerбудується за шаблономPatientManager(Лаба 03): масив фіксованого розміру, лічильник_count, константа ліміту,Addз перевіркою ліміту та повідомленням. Вибірки (GetByPatient,GetByDoctor) — у два проходи: спершу порахувати відповідні записи, потім створити масив потрібного розміру й заповнити його. ЖоднихList<T>.- Тип масиву — базовий:
MedicalRecord[]. Це і є «поліморфний масив»: у нього можна класти іDiagnosis, іLabResult, іPrescription, бо кожен із них єMedicalRecord. Окремі масиви під кожен вид не потрібні. DisplayAll()таDisplayList(...)— це один цикл, у якому кожен елемент просто виводиться. Ніякихifіis: кожен об'єкт сам знає, як себе показати (ToString()нащадка). Це і є поліморфізм — один рядок коду поводиться по-різному залежно від реального типу об'єкта.FindByIdта індексатор — за зразком менеджерів із Лаб 03–04: результат має типMedicalRecord?,nullозначає «не знайдено» (для індексатора — «індекс поза межами»).- Перевірка тимчасовим кодом: створіть менеджер, додайте по одному запису кожного виду й виведіть
DisplayAll(). Потім спробуйте покласти в масив запис дляpatientId = 0— конструктор має відхилити його ще доAdd.
📖 Документація:
- Поліморфізм
overrideDateTime.AddDays- Індексатори
CultureInfo.InvariantCultureтаConsole.OutputEncoding
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
MedicalRecord |
GuestRecord |
OrderRecord |
AcademicRecord |
ServiceRecord |
LibraryRecord |
GymRecord |
Diagnosis |
Complaint |
FeedbackEntry |
GradeEntry |
DamageReport |
LoanRecord |
ProgressEntry |
LabResult |
RoomInspection |
QualityCheck |
ExamResult |
TechInspection |
BookReturn |
FitnessTest |
Prescription |
ServiceRequest |
SpecialOrder |
Assignment |
RepairOrder |
FineNotice |
TrainingPlan |
У вашому домені щонайменше один нащадок має власне правило IsActive() (як Prescription: «поки не минув термін»), а один — числове поле з ознакою (як LabResult).
Коміт
git add ClinicApp/Models/LabResult.cs ClinicApp/Models/Prescription.cs ClinicApp/Managers/MedicalRecordManager.cs
git commit -m "Lab06 Task02"Задача 3. `is`, `as` — фільтрація за типом ⭐⭐⭐
Умова
Масив MedicalRecord[] зберігає об'єкти різних типів. Але часто потрібно отримати лише діагнози, або лише хронічні, або лише активні рецепти. Для цього треба перевірити реальний тип об'єкта під час виконання.
is — оператор перевірки типу. as — безпечне приведення: повертає null, якщо тип не збігається, замість InvalidCastException.
Що реалізувати. Додати до MedicalRecordManager:
GetDiagnoses(int patientId)→Diagnosis[]— усі діагнози пацієнта;GetLabResults(int patientId)→LabResult[]— усі аналізи пацієнта;GetPrescriptions(int patientId)→Prescription[]— усі рецепти пацієнта;GetChronicDiagnoses(int patientId)→Diagnosis[]— лише хронічні;GetActivePrescriptions(int patientId)→Prescription[]— лише активні (черезIsActive());DisplayPatientSummary(int patientId)— зведена картка:- кількість записів кожного типу;
- список хронічних діагнозів (якщо є);
- список активних рецептів з датою закінчення (якщо є).
Приклад
Синтаксис — на вигаданій ієрархії з Задачі 1 (Truck — ще один нащадок Vehicle):
Vehicle v = new Bicycle();
// is — перевірка типу, повертає bool:
if (v is Bicycle)
Console.WriteLine("це велосипед");
// is зі змінною — перевірка і приведення одночасно:
if (v is Bicycle b)
Console.WriteLine(b.Wheels); // b уже має тип Bicycle
// as — спробувати привести, або null:
Bicycle? asBike = v as Bicycle;
if (asBike != null)
Console.WriteLine(asBike.Wheels);
// явне приведення — при невдачі кидає виняток:
Truck t = (Truck)v; // InvalidCastException: Bicycle не є TruckЯк має працювати ваш DisplayPatientSummary (дати залежать від дня запуску):
=== Медична картка пацієнта #1 ===
Всього записів: 5 (діагнозів: 2, аналізів: 2, рецептів: 1)
Хронічні діагнози (1):
[1] Діагноз | 22.08.2026 | I10: Гіпертонічна хвороба [хронічне]
Активні рецепти (1):
[5] Рецепт | 16.09.2026 | Лізиноприл 10 мг × 30 днів (1 раз на добу вранці) | до 16.10.2026Підказки
Три способи працювати з реальним типом:
isas(Тип)об'єктЩо повертає boolоб'єкт потрібного типу або nullоб'єкт потрібного типу Якщо тип не збігається falsenullкидає InvalidCastExceptionКидає виняток? ніколи ніколи так, при невдачі isзі змінною (is Тип змінна) — сучасний стиль: перевірка й приведення в одній операції. Змінна доступна там, де компілятор певен, що перевірка вдалась: у тіліifі праворуч від&&в тій самій умові. Це замінює зв'язку «is— а потім окреме приведення».asзавжди даєТип?. З увімкненими nullable-типами результатasзаписується якDiagnosis?, і перед використанням його треба перевірити наnull— так само, якFindByIdз Лаби 03.Явне приведення небезпечне. Спробуйте
(Diagnosis)recordдля запису-рецепта й прочитайте текст виняткової ситуації. Використовуйтеis/as, коли не впевнені в типі; явне приведення — лише коли тип гарантований.Метод-фільтр за типом — знайомий двопрохідний алгоритм. Перший прохід рахує записи, які належать потрібному пацієнту і мають потрібний тип. Потім створюється масив результатів потрібного розміру (тип елементів — конкретний нащадок:
Diagnosis[], а неMedicalRecord[]). Другий прохід перевіряє ту саму умову й заповнює масив; саме тут зручна формаis Тип змінна, бо вона одразу дає змінну потрібного типу для запису в масив результатів.Додаткові умови (
IsChronic,IsActive()) додаються до тієї самої умови через&&: змінна, яку давis, доступна в правій частині. ВибіркиGetChronicDiagnosesіGetActivePrescriptions— це фільтри за типом і ознакою водночас.isозначає «є різновидом», а не «точно цього типу». Якби існував класChronicDiagnosis : Diagnosis, тозапис is Diagnosisбуло бtrueі для нього. Якщо колись знадобиться саме точний тип — див.GetType()у документації. У нашій ієрархії нащадки не мають власних нащадків, тож різниці ви не побачите, — але знати її треба.DisplayPatientSummary. Один прохід по записах пацієнта з трьома лічильниками; далі — розділи «Хронічні діагнози» та «Активні рецепти», які виводяться лише непорожніми (для пацієнта без хронічних діагнозів порожнього заголовка бути не повинно). Якщо в пацієнта взагалі немає записів — одне коротке повідомлення.Де перевірка типу доречна, а де ні. У менеджері фільтр за типом — законне використання
is: потрібні саме діагнози. А отDisplayListне має перевіряти типи — він просто виводить те, що дали (це задача поліморфізму, а неis). Якщо у вашDisplayListзахотілось додатиif (… is …)— зупиніться й подумайте, чи не вирішує цеToString().Зверніть увагу на дублювання. П'ять методів вийшли майже однаковими — різниться тільки тип. Це прямий наслідок того, що ми поки не вміємо писати код «для будь-якого типу»; у Лабі 09 з'явиться інструмент, що прибирає таке дублювання. Зараз нічого не переписуйте.
📖 Документація:
- Оператори перевірки типу та приведення (
is,as,(T)x) - Огляд зіставлення зі зразком (pattern matching)
- Шаблони: оголошення та типи
InvalidCastExceptionobject.GetType- Nullable-типи посилань (
T?)
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
GetDiagnoses(patientId) |
GetComplaints(guestId) |
GetFeedback(customerId) |
GetGrades(studentId) |
GetDamageReports(clientId) |
GetLoanRecords(readerId) |
GetProgress(memberId) |
GetLabResults(patientId) |
GetRoomInspections(guestId) |
GetQualityChecks(customerId) |
GetExamResults(studentId) |
GetTechInspections(clientId) |
GetReturns(readerId) |
GetFitnessTests(memberId) |
GetActivePrescriptions(patientId) |
GetActiveRequests(guestId) |
GetActiveOrders(customerId) |
GetActiveAssignments(studentId) |
GetActiveRepairs(clientId) |
GetActiveFines(readerId) |
GetActivePlans(memberId) |
DisplayPatientSummary |
DisplayGuestSummary |
DisplayCustomerSummary |
DisplayStudentSummary |
DisplayClientSummary |
DisplayReaderSummary |
DisplayMemberSummary |
Коміт
git add ClinicApp/Managers/MedicalRecordManager.cs
git commit -m "Lab06 Task03"Задача 4. Інтеграція: `Clinic`, меню й тестові дані ⭐⭐⭐⭐
Умова
Ієрархія класів і менеджер готові. Тепер треба підключити їх до системи: Clinic отримує новий менеджер, Program.cs — новий розділ меню, а в тестових даних з'являються приклади кожного виду запису.
Ця задача показує силу поліморфізму в реальному контексті: код меню не знає реальних типів записів. Він викликає DisplayList(MedicalRecord[]) — і кожен запис виводить себе правильно.
Що реалізувати:
- У
Clinic.csдодати властивістьMedicalRecordManager MedicalRecords. - У
Program.csзамінити тимчасовий тестовий код (із Задач 1–3) справжніми тестовими даними — приклади всіх трьох видів для кількох пацієнтів (див. таблицю нижче). - У
Program.csодразу після блокуusingзафіксуватиInvariantCulture(як у Лабі 01) — інакше дробові числа поводитимуться по-різному на різних комп'ютерах. - Головне меню: новий пункт
4— «Медична картка»; пункт «Звіт» переїжджає на5. - Підменю «Медична картка» з пунктами:
1— Картка пацієнта (зведення черезDisplayPatientSummary);2— Усі записи пацієнта;3— Додати діагноз;4— Додати аналіз;5— Додати рецепт;6— Записи лікаря;0— Назад.
- Продемонструвати поліморфізм явно: вивести всі записи одного пацієнта — масив
MedicalRecord[]містить різні типи, а цикл ізGetRecordType()іGetSummary()(абоToString()) дає правильний рядок для кожного.
Формат вводу
Дата нового запису — завжди DateTime.Today. Кожен пункт додавання запитує дані у такому порядку (перед запитом ID корисно показати списки пацієнтів і лікарів через їхні DisplayAll()):
| Пункт | Що запитує програма (у порядку) |
|---|---|
3 — діагноз |
ID пацієнта (ціле) → ID лікаря (ціле) → код діагнозу (J06.9) → опис → «Хронічне? (1=так, 0=ні)» |
4 — аналіз |
ID пацієнта → ID лікаря → назва аналізу → значення (число з крапкою) → одиниці виміру → норма (текст, напр. 4.0–9.0) → «В нормі? (1=так, 0=ні)» |
5 — рецепт |
ID пацієнта → ID лікаря → препарат → дозування (10 мг) → кількість днів (ціле) → інструкція (Enter — пропустити) |
1, 2 |
ID пацієнта (ціле) |
6 |
ID лікаря (ціле) |
Що має відбуватись при помилках:
| Ситуація | Реакція програми |
|---|---|
| ID або кількість днів — не число | повідомлення про некоректне число, повернення в підменю |
| Пацієнта чи лікаря з таким ID немає | повідомлення «не знайдено», повернення в підменю |
| Значення аналізу — не число | повідомлення, повернення в підменю (а не тихий 0) |
Порожній код діагнозу, кількість днів 0 тощо |
Помилка: … з тексту винятку; програма не падає |
Специфікація — тестові дані
Додайте щонайменше такі записи (ID пацієнтів і лікарів — ті, що вже є в даних Лаби 05):
| Пацієнт | Запис | Що це демонструє |
|---|---|---|
| #1 | діагноз I10, хронічний, 30 днів тому |
GetChronicDiagnoses, [хронічне] |
| #1 | діагноз J06.9, гострий, 5 днів тому |
не потрапляє в «хронічні» |
| #1 | аналіз «Гемоглобін» — в нормі | рядок без ⚠ |
| #1 | аналіз «Холестерин» — поза нормою | ⚠ поза нормою |
| #1 | рецепт «Лізиноприл», 30 днів, виписаний 5 днів тому | IsActive() == true |
| #2 | рецепт, 10 днів, виписаний 40 днів тому | IsActive() == false — не потрапляє в «Активні рецепти» |
| #2, #3 | по одному запису іншого виду (діагноз, аналіз) | різні пацієнти й лікарі, GetByDoctor |
Приклад
Як має виглядати додавання аналізу через меню:
── Медична картка ────────────
...
Оберіть: 4
ID пацієнта: 1
ID лікаря: 1
Назва аналізу: Гемоглобін
Значення (число): 145
Одиниці виміру: г/л
Норма (напр. 4.0–9.0): 120–160
В нормі? (1=так, 0=ні): 1
Запис [9] Аналіз додано.А так — відхилений запис (порожній код діагнозу); програма повертається в підменю. Точний текст після Помилка: залежить від повідомлень вашого ClinicValidator:
Оберіть: 3
ID пацієнта: 1
ID лікаря: 1
Код діагнозу (напр. J06.9):
Опис: Ринофарингіт
Хронічне? (1=так, 0=ні): 0
Помилка: DiagnosisCode не може бути порожнім.Поліморфний вивід — один код, різна поведінка (цикл по GetByPatient(1)):
Діагноз: I10: Гіпертонічна хвороба [хронічне]
Діагноз: J06.9: Гострий ринофарингіт
Аналіз: Гемоглобін: 145 г/л (норма: 120–160)
Аналіз: Холестерин: 6.2 ммоль/л (норма: < 5.2) ⚠ поза нормою
Рецепт: Лізиноприл 10 мг × 30 днів (1 раз на добу вранці)Підказки
Clinic. Нова властивість — за зразком інших менеджерів: лишеget, значення створюється в конструкторі. ТипMedicalRecordManagerлежить уClinicApp.Managers, тож уClinic.csпотрібенusing ClinicApp.Managers;— після Лаби 05 він там уже є для інших менеджерів; перевірте.- Культура. Потрібні ті самі два рядки, що й у Лабі 01 (розділ «Як виконувати завдання»):
Thread.CurrentThread.CurrentCultureвстановлюється вCultureInfo.InvariantCulture. Місце — одразу після блокуusingі до решти коду: якщо поставити їх вище заusing, компілятор видасть помилкуCS1529(директивиusingмають іти першими). Побічний ефект — дробові числа в усій програмі (наприклад, «Середній вік» у статистиці пацієнтів) тепер виводяться з крапкою:29.2, а не29,2. - Головне меню. Змінюються дві речі: текст меню (новий пункт, «Звіт» —
5) іswitchіз виводом відповідних методів. Легко забути одне з двох — перевірте обидва. - Підменю — окремий метод (наприклад,
MedicalRecordsMenu, що приймаєClinic) за зразкомPatientsMenu,DoctorsMenu,AppointmentsMenu(а не гігантський блок усередині головного циклу). - Не передавайте мовчки нулі.
int.TryParseповертаєbool— перевіряйте його. Якщо ввели «abc» для кількості днів, а ви проігнорували результат, змінна стане0, і користувач побачить «тривалість має бути більше нуля» — хоч насправді помилка була у форматі числа. - Число з крапкою — пастка. З
InvariantCultureзвичайнийdouble.TryParse("6,2", out …)повернеtrueі значення62: кома там — роздільник розрядів, а не десяткова. Щоб відхиляти такий ввід, використайте перевантаженняTryParseіз параметрамиNumberStyles(потрібне значення —NumberStyles.Float) іIFormatProvider(CultureInfo.InvariantCulture). Перевірте на трьох вводах:6.2,6,2,abc. - Перевіряйте існування пацієнта й лікаря. Запис для пацієнта з ID
99, якого немає, — некоректний стан системи (той самий інваріант, що в Лабі 05). Знайдіть пацієнта й лікаря за ID (FindByIdабоTryFindByIdз Лаб 03–04) і лише тоді створюйте запис; інакше — повідомлення й повернення в меню. try/catchнавколо створення запису. Уtry— усе, що може кинути виняток (створенняDiagnosis/LabResult/Prescription). Два блокиcatchу правильному порядку (Лаба 05): спершуArgumentOutOfRangeException, потімArgumentException. У кожному — повідомленняПомилка: …і повернення в меню. Перевірте вручну: порожній код діагнозу, кількість днів0.- Завершений рецепт у тестових даних. Достатньо дати «40 днів тому» і курсу 10 днів: він закінчився 30 днів тому, тож
IsActive()повернеfalse, а в зведенні пацієнта цей рецепт не з'явиться. - Зведення для пацієнта без хронічних діагнозів не має виводити порожній розділ «Хронічні діагнози». Перевірте це на пацієнті #3.
- Поліморфний вивід (пункт 6 умови). Цикл по масиву, який повернув
GetByPatient: для кожного елемента виведіть тип запису й зміст. Поза межамиProgram.csця логіка не потрібна — ви лише демонструєте, що код не знає реальних типів. - Ключовий момент для самоперевірки: у методі
DisplayList(MedicalRecord[] records)немає жодногоif, жодногоis. Це і є поліморфізм — код не знає типів, але поводиться правильно. Якщо ви таки додали перевірку типу — приберіть її й дайте працюватиToString().
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Clinic.MedicalRecords |
Hotel.GuestHistory |
Restaurant.OrderHistory |
University.AcademicHistory |
Fleet.ServiceHistory |
Library.LibraryRecords |
GymCenter.GymRecords |
| Меню "Медична картка" | "Історія гостя" | "Замовлення" | "Успішність" | "Сервісна книжка" | "Картка читача" | "Картка учасника" |
Коміт
git add ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab06 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-06 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т4
├── Clinic.cs ✏ Т4
├── Enums/
│ ├── AppointmentStatus.cs
│ ├── BloodType.cs
│ └── Speciality.cs
├── Models/
│ ├── Patient.cs
│ ├── Doctor.cs
│ ├── Appointment.cs
│ ├── WorkSchedule.cs
│ ├── Diagnosis.cs 🆕 Т1
│ ├── LabResult.cs 🆕 Т2
│ ├── MedicalRecord.cs 🆕 Т1
│ └── Prescription.cs 🆕 Т2
├── Managers/
│ ├── PatientManager.cs
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs
│ ├── GrowablePatientManager.cs
│ └── MedicalRecordManager.cs 🆕 Т2 ✏ Т3
└── Utils/
├── ClinicFormatter.cs
└── ClinicValidator.csЛегенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 05. Файл може мати кілька позначок: MedicalRecordManager.cs створюється в Т2, а в Т3 до нього додаються методи-фільтри.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Проєкт компілюється без помилок і без попереджень (зокрема без
CS0114— це забутийoverride) -
new MedicalRecord(...)не компілюється — клас абстрактний (перевірте й закоментуйте назад) -
Diagnosis,LabResult,Prescriptionуспішно створюються -
MedicalRecord record = new Diagnosis(...)— присвоєння нащадка змінній базового типу працює -
record.GetRecordType()повертає"Діагноз", а не"Медичний запис" -
Prescription.IsActive()повертаєfalseдля рецепта, що закінчився, іtrueдля чинного -
Diagnosis.IsActive()за замовчуванням:trueдля запису молодшого за 6 місяців,false— для старшого -
DisplayList(MedicalRecord[])виводить різні рядки для різних типів — без жодногоif (r is ...) -
GetChronicDiagnosesповертає лише хронічні;GetActivePrescriptions— лише активні -
DisplayPatientSummaryправильно рахує типи, не виводить порожніх розділів - Меню «Медична картка» — пункт
4головного меню, «Звіт» —5, усі підпункти працюють -
new Diagnosis(1, 1, DateTime.Today, "", "Ринофарингіт")кидаєArgumentException -
new Prescription(1, 1, DateTime.Today, "Аспірин", "500 мг", 0)кидаєArgumentOutOfRangeException - При введенні порожнього коду діагнозу в меню — програма показує повідомлення про помилку, а не падає
- Значення аналізу
6.2(з крапкою) сприймається як6.2;6,2іabc— відхиляються з повідомленням - Запис для неіснуючого пацієнта чи лікаря (ID
99) відхиляється з повідомленням - У коді немає
interface,new/sealed(приховування),List<T>,Dictionary<,>, LINQ
Питання для самоперевірки
- Чому
abstract classне можна інстанціювати? Що відбувається при спробіnew MedicalRecord(...)і який код помилки видає компілятор? - Яка різниця між
abstractіvirtualметодом? Що станеться, якщо нащадок не реалізуєabstract-метод? А якщо не перевизначитьvirtual? - Чому
override ToString()у базовому класі викликаєGetSummary()нащадка, а не базового? Як це називається? - Навіщо
protected-конструктор у базовому класі? Чим він відрізняється відpublicіprivate(що покаже компілятор приprivate)? - Яка різниця між
is,asі явним приведенням(Diagnosis)record? Коли кожен із них кидає виняток? - Чому
Prescription.IsActive()перевизначає логіку, аLabResult.IsActive()— ні? Як базовий клас «знає», яку реалізацію викликати? - Метод
DisplayList(MedicalRecord[])не містить жодногоif (r is ...), але виводить різні рядки для різних типів. Чому це можливо? - Чому
ClinicValidatorвикликається і в базовому конструкторі (ValidatePositiveдляpatientId,doctorId), і в сеттерах нащадків (ValidateNameдля текстових полів)? Де саме «живе» відповідальність за кожну перевірку? - Що відбудеться, якщо забути
overrideбіляGetRecordType()уDiagnosis? Яке попередження побачите і що виведеrecord.GetRecordType()для змінної базового типу? - Чому невдале створення
Diagnosis(порожній код) «з'їдає» номерId, а невдале створення зpatientId = 0— ні? Що це говорить про порядок виконання конструкторів бази й нащадка? - Чому
запис is Diagnosisне гарантує, що реальний тип об'єкта — самеDiagnosis? Коли це може мати значення?
Статус гілки
Після всіх завдань (кожне — окремий коміт Lab06 TaskNN на гілці Lab-06):
git push -u origin Lab-06
git checkout main
git merge --no-ff Lab-06 -m "Merge Lab-06: Inheritance"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-07.