classDiagram Worker <|-- Manager Worker <|-- SalesManager
Тема 8. Основи об’єктно-орінтованого програмування
ООП, C#, об’єкт, клас, інкапсуляція, наслідування, поліморфізм, абстракція, конструктор, деструктор, інтерфейс, метод, властивість, модифікатори доступу, sealed, virtual, override, abstract, new, base, this, static, instance, composition, aggregation.
Основи об’єктно-орієнтованого програмування (ООП) у C# включають ключові концепції: інкапсуляцію, наслідування, поліморфізм і абстракцію. Класи та об’єкти є основними будівельними блоками ООП. Інкапсуляція забезпечує контроль доступу до даних через модифікатори доступу. Наслідування дозволяє створювати нові класи на основі існуючих, зменшуючи дублікацію коду. Поліморфізм дає змогу перевизначати методи та працювати з об’єктами через інтерфейси та базові класи. Абстракція допомагає створювати загальні моделі поведінки, приховуючи деталі реалізації. ООП у C# сприяє структурованому підходу до розробки програм та спрощує їхній супровід.
Презентація
8.1. Поняття об’єкта та класу. Основні елементи класу
Об’єктно-орієнтоване програмування і проектування побудоване на класах. Будь-яку програмну систему, побудовану в об’єктному стилі, можна розглядати як сукупність класів, можливо, об’єднаних в проекти, простори імен, рішення, як це робиться при програмуванні у Visual Studio.
Клас - це шаблон, який визначає форму об’єкту. Він задає як дані, так і код, який оперує цими даними.
Об’єкти - це екземпляри класу.
Клас складається із:
- полів;
- властивостей;
- методів;
- подій;
- конструкторів;
- деструкторів;
- делегатів.
- …
Елементи класу називаються членами класу.
Клас оголошується за допомогою ключового слова class
. Синтаксис має наступний вигляд:
Лістинг 8.1. Синтаксис оголошення класу.
class ім’я_класу
{
//Оголошення полів
доступ тип імя_змінної;
доступ тип імя_змінної;
//Оголошення методів
доступ тип_повернення імя_метода(параметри)
{
тіло метода;
}
доступ тип_повернення імя_метода(параметри)
{
тіло метода;
}
}
Розглянемо приклад базового створення класу “Комплексне число”.
Лістинг 8.2. Оголошення класу ComplexNumber
.
public class ComplexNumber
{
//Поля
private double a;
private double b;
//Конструктор
public ComplexNumber(double a, double b)
{
this.a = a;
this.b = b;
}
//Метод
public override string ToString()
{
return a + " + " + b + "i";
}
}
Доступ до полів, методів та інших членів класу може здійснюватися з різним рівнем доступу:
private
доступний лише всередині класу (типу);protected
доступний лише всередині класу та класів-нащадків;internal
доступний лише в межах збірки;protected internal
доступний лише в межах збірки, лише всередині класу та класів-нащадків;public
доступний для усіх.
8.2. Будова класу
8.2.1. Поля класу
Поля класу синтаксично є звичайними змінними (об’єктами) мови. Їх опис задовольняє звичайним правилам оголошення змінних, про що детально говорилося раніше. Змістовно поля задають представлення тій самій абстракції даних, яку реалізує клас.
Поля характеризують властивості об’єктів класу. Коли створюється новий об’єкт класу, то цей об’єкт є набором полів класу. Два об’єкти одного класу мають один і той же набір полів, але різняться значеннями, що зберігаються в цих полях.
Синтаксис оголошення полів:
Наприклад, оголосимо клас Worker
, який має 3 поля: розмір з/п, прізвище, вік.
Лістинг 8.3. Оголошення полів класу Worker
.
class Worker
{
public double salary; //Розмір з/п
public string firstname; //Ім'я
public string lastname; //Прізвище
}
Зараз клас працівник нагадує структуру! І це не дивно, адже клас є більш розвиненою структурую.
8.2.2. Методи класу
Змінні(поля) екземплярів і методи - дві основні складові класів. Поки наш клас Worker
містить лише дані. Хоча такі класи (без методів) допустимі, більшість класів мають методи.
Методи - це процедури (підпрограми), які маніпулюють даними, визначеними в класі, і у багатьох випадках забезпечують доступ до цих даних. Зазвичай різні частини програми взаємодіють з класом за допомогою його методів. Будь-який метод містить одну або декілька інструкцій.
Кожен метод має ім’я, і саме це ім’я використовується для його виклику. У загальному випадку методу можна привласнити будь-яке ім’я. Але пам’ятаєте, що ім’я Main()
зарезервовано для методу, з якого починається виконання програми. Крім того, як імена методів не можна використовувати ключові слова С#.
Імена методів супроводжуються парою круглих дужок. Наприклад, якщо метод має ім’я GetVal
, то в тексті буде написано GetVal()
. Це допомагає відрізняти імена змінних від імен методів. Формат запису методу такий:
Лістинг 8.4. Оголошення класу Worker
. Метод.
class Worker
{
public double salary; //Розмір з/п
public string firstname; //Ім'я
public string lastname; //Прізвище
//Метод, виводить інформацію про працівника на консоль
public void DisplayInfo()
{
Console.WriteLine("{0} {1}, - {2} грн.", lastname, firstname, salary);
}
}
Зверніть увагу ось на що. Змінні екземпляра salary
, lastname
і firstname
використовуються всередині методу DisplayInfo()
без будь-яких атрибутів, тобто їм не передує ні ім’я об’єкту, ні оператор “крапка”. Це дуже важливий момент: якщо метод задіює змінну екземпляра, яка визначена в його класі, він робить це безпосередньо, без явного посилання на об’єкт і без оператора “крапка”. І Це логічно. Адже метод завжди викликається для деякого об’єкту конкретного класу. Таким чином, немає необхідності вказувати усередині методу об’єкт удруге. Це означає, що значення salalry
, lastname
і firstname
всередині методу DisplayInfo()
неявно вказують на копії цих змінних, що належать об’єкту, який викликає метод DisplayInfo()
.
Лістинг 8.5. Інші приклади методів
public int GetAge() {...}
protected string GetByName(string name) {...}
protected static bool IsEquals(Class obj1, Class obj2) {...}
Повернення значення методом.
У загальному випадку існує два варіанти умов для повернення з методу. Перший пов’язаний з виявленням закриваючої фігурної дужки, що позначає кінець тіла методу (як продемонстровано на прикладі методу DisplayInfo()
). Другий варіант полягає у виконанні інструкції return. Можливі дві форми використання інструкції return
: одна призначена для void-методів (які не повертають значень), а інша - для повернення значень.
Негайне завершення void
-методу можна організувати за допомогою наступної форми інструкції return
:
Лістинг 8.5. Інші приклади методів
public void DisplayInfo()
{
if(salary < 0)
return;
Console.WriteLine("{0} {1}, {2}", lastname, firstname, salary);
}
Хоча void
-методи - не рідкість, більшість методів все ж повертають значення. І справді, здатність повертати значення - одна з найкорисніших якостей методу. Ми вже розглядали приклад повернення значення під час роботи з масивами. Значення, які повертаються методами, використовуються в програмуванні по різному. У одних випадках повернене значення є результатом обчислень, в інших - воно просто означає, успішно чи ні виконана певна операція, а в третіх - воно може бути кодом-стану. Методи повертають викликаючим їх процедурам, використовуючи наступну форму інструкції return
:
return значення;
Додамо до нашого класу Працівник ще кілька полів і методів.
Лістинг 8.6. Клас “Працівник”. Методи. Продовження.
class Worker
{
public string firstname; //Ім'я
public string lastname; //Прізвище
public double salary; //Розмір з/п
public double bonus; //Бонус до з/п у % від з/п
//Метод, виводить інформацію про працівника на консоль
public void DisplayInfo()
{
Console.WriteLine("{0} {1}, - {2} грн.", lastname, firstname, salary);
}
//Повертає суму бонусу, яку отримає працівник.
public double GetBonusSum()
{
return bonus * salary;
}
//Повертає повну суму, яку отримає працівник.
public double GetFullSum()
{
return salary + GetBonusSum();
}
}
У цьому прикладі створено поле «бонус» та три додаткових методи, функціональність яких подана у коментарях. Розглянемо приклад програми:
Лістинг 8.7. Приклад роботи з класом “Працівник”.
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
Worker worker1 = new Worker();
worker1.salary = 250;
worker1.firstname = "Петро";
worker1.lastname = "Петров";
worker1.bonus = 0.12;
Console.WriteLine("Розмір бонусу: {0}\nВсього з/п: {1}",worker1.GetBonusSum(), worker1.GetFullSum());
}
або
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
Worker worker1 = new Worker();
worker1.salary = 250;
worker1.firstname = "Петро";
worker1.lastname = "Петров";
worker1.bonus = 0.12;
double bonusSum = worker1.GetBonusSum();
double fullSum = worker1.GetFullSum();
Console.WriteLine("Розмір бонусу: {0}\nВсього з/п: {1}",bonusSum,fullSum);
}
Результат виконання:
Розмір бонусу: 30
Всього з/п: 280
Як бачимо, результат виконання для обох програм ідентичний. Функції GetBonusSum()
та GetFullSum()
повертають значення типу double
та передають його у першому випадку одразу для виведення на консоль, у другому записують у проміжні змінні. Аналогічні повернення типів даних можна виконувати для усіх типів даних, як базових так і створених користувачем.
Використання параметрів
Під час виклику методу можна передати одне або декілька значень. Значення, яке передається методу, називається аргументом. Змінна всередині методу, яка набуває значення аргументу, називається параметром. Параметри оголошуються всередині круглих дужок, які слідують за ім’ям методу. Синтаксис оголошення параметрів аналогічний синтаксису, вживаному для змінних.
Наприклад, ми можемо визначати суму бонусу, передаючи процент бонусу від з/п у функцію і не записуючи бонус як окреме поле, оскільки є багато працівників, які можуть взагалі не отримати бонус.
Тоді виклик буде мати вигляд:
У метод можна передавати безліч аргументів різного типу.
8.2.3. Конструктори
У попередніх прикладах змінні кожного об’єкта встановлювалися “вручну” за допомогою наступної послідовності інструкцій:
Професіонал ніколи б не використав подібний підхід. І річ не стільки в тому, що таким чином можна просто “забути” про одне або декілька полів, скільки в тому, що існує набагато зручніший спосіб це зробити. Цей спосіб - використання конструктора.
Конструктор ініціалізує об’єкт при його створенні. Він має таке ж ім’я, що і сам клас, а синтаксично подібний до методу. Проте у визначенні конструкторів не вказується тип значення, що повертається. Формат запису конструктора такий:
Зазвичай конструктор використовується, аби додати змінним екземпляра, визначеним у класі, початкові значення або виконати вихідні дії, необхідні для створення повністю сформованого об’єкту. Крім того, зазвичай як елемент «доступ» використовується модифікатор доступу public, оскільки конструктори, як правило, викликаються поза їх класом.
Всі класи мають конструктори незалежно від того, визначите ви їх чи ні, оскільки С# автоматично надає конструктор за замовчуванням, який ініціалізував всі змінні-члени, що мають типи-значення, нулями, а змінні-члени посилального типу - null
-значеннями.
Отже створимо конструктор для класу Worker:
Лістинг 8.8. Приклад роботи з класом “Працівник”. Конструктор
class Worker
{
public string firstname; //Ім'я
public string lastname; //Прізвище
public double salary; //Розмір з/п
public double bonus; //Бонус до з/п у % від з/п
//Конструктор класу Worker
public Worker()
{
firstname = "empty";
lastname = "empty";
salary = 0.0;
bonus = 0.0;
}
//Перевантажений конструктор класу Worker
public Worker(string fname, string lname, double salary, double bonus)
{
firstname = fname;
lastname = lname;
this.salary = salary;
this.bonus = bonus;
}
//Метод, виводить інформацію про працівника на консоль
public void DisplayInfo()
{
Console.WriteLine("{0} {1}, - {2} грн.", lastname, firstname, salary);
}
//Повертає суму бонусу, яку отримає працівник.
public double GetBonusSum()
{
return bonus * salary;
}
//Повертає повну суму, яку отримає працівник.
public double GetFullSum()
{
return salary + GetBonusSum();
}
}
Як бачимо, створено два конструктори! Перший (public Worker()) не приймає значень, другий (public Worker(string fname, string lname, double salary, double bonus)) є параметризованим, тобто приймає значення. Створення двох методів з однаковими іменами, але різними сигнатурами називається перевантаженням методів. Сигнатурою методу є тип повертаємого значення та перелік параметрів. Аналогічно можна перевантажувати конструктори. Детальніше перевантаження методів ми розглянемо під час вивчення наслідування. Тепер програма маттиме вигляд:
Лістинг 8.9. Приклад роботи з класом “Працівник”. Конструктор. Приклад
Console.OutputEncoding = Encoding.Unicode;
Worker worker1 = new Worker();
worker1.DisplayInfo();
Worker worker2 = new Worker("Степан", "Петров", 25.5, 0.5);
worker2.DisplayInfo();
Результат:
empty empty, - 0 грн.
Петров Степан, - 25.5 грн.
8.3. Інкапсуляція
8.3.1. Реалізація інкапсуляції традиційними методами доступу і зміни даних
Інкапсуляція у програмуванні це приховування внутрішньої реалізації та даних класу від зовнішнього доступу. Зазвичай це відбувається закриванням полів за допомогою модифікатора private
та доступом до них через методи.
Продовжимо розвивати попередні приклади…
Якщо ви хочете, аби зовнішній світ міг взаємодіяти із закритим полем даних salary
, потрібно визначити метод доступу (get
) і метод зміни (set
). Наприклад:
Лістинг 8.10. Реалізація інкапсуляції у класі “Працівник”. Методи
class Worker
{
private string firstname; //Ім'я
private string lastname; //Прізвище
private double salary; //Розмір з/п
private double bonus; //Бонус до з/п у % від з/п
...
...
public double GetSalary()
{
return salary;
}
public void SetSalary(double s)
{
//здійснити перевірки
salary = s;
}
}
Тепер за допомогою методів GetSalary()
та SetSalary()
ми можемо маніпулювати змінною salary
всередині класу. Назвати ваші методи ви можете і по іншому, адже це просто методи, проте бажано робити їх назви відповідно до функцій. Тоді виклик у коді програми матиме наступний вигляд:
Лістинг 8.11. Реалізація інкапсуляції у класі “Працівник”
Worker worker2 = new Worker("Степан", "Петров", 25.5,0.5);
Console.WriteLine("Salary: {0}",worker2.GetSalary());
worker2.SetSalary(150.6);
Console.WriteLine("Salary: {0}", worker2.GetSalary());
Результат:
Salary: 25.5
Salary: 150.6
8.3.2. Друга форма інкапсуляції - властивості класу
На противагу традиційним методам доступу і зміни в .NET
-мовах інкапсуляцію переважно реалізовують за допомогою властивостей, які моделюють відкриті поля даних. Замість того аби заставляти користувача викликати два різні методи для отримання і зміни даних стану, користувач може викликати те, що здається відкритим полем. Для ілюстрації розглянемо властивість Salary
, яка замінить два наші методи GetSalary()
та SetSalary()
.
Синтаксис оголошення властивості:
Лістинг 8.12. Реалізація інкапсуляції у класі “Працівник”. Властивості
class Worker
{
private string firstname; //Ім'я
private string lastname; //Прізвище
private double salary; //Розмір з/п
...
public double Salary
{
get { return salary; }
set { salary = value; }
}
}
8.3.4. Властивості лише для читання і лише для запису
При створенні класів ви можете налаштувати властивість доступну лише для запису або лише для читання. Щоб це зробити, просто створіть властивість без відповідного блоку set
або get
. Наприклад, властивість тільки для читання:
Властивість тільки для запису:
8.4. Підтримка наслідування у С
8.4.1. Наслідування
Тепер, коли ми познайомилися з різними способами створення інкапсульованого класу, настав час звернути свою увагу на створення сімейства зв’язаних класів.
Як вже наголошувалося, наслідування - це той аспект ООП, який сприяє багатократному використанню коду. Наслідування — метод утворення нових класів на основі використання вже існуючих. Наслідування буває двох типів: класичне наслідування (відношення «є») і наслідування відповідно до моделі включения/делегування (відношення «має»).
Давайте почнемо з дослідження класичної моделі «є».
При створенні між класами відношення «є» ви створюєте залежність між типами. Основна ідея класичного наслідування полягає у тому, що нові класи можуть використовувати (і можливо розширювати) функціональність інших класів. Для ілюстрації припустимо, що ви хочете задіювати функціональність класу Worker для створення двох нових класів:SalesPerson
(торгівельний агент) і Manager
(менеджер). В цьому випадку ієрархія класів виглядатиме так, як показано на малюнку нижче.
Як показано на рис. 1, торгівельний агент «є» співробітником (так само як і менеджер). У класичній моделі наслідування базові класи (такі як Worker
) використовуються для визначення загальних характеристик всіх наслідників. Підкласи (такі як SalesPerson
і Manager
) розширюють цю загальну функціональність, додаючи більш специфічну поведінку.
Для нашого прикладу припустимо, що клас Manager розширює клас Worker
, додаючи запис про кількість акцій, якими володіє співробітник, а клас SalesPerson
містить обсяги продажів, здійснені цим агентом. У С# розширення класу виконується за допомогою оператора :
(двокрапка) у визначенні класу.
Тоді похідний клас «Торговий агент» матиме наступний вигляд:
Лістинг 8.13. Реалізація класу “SalesManager”
class SalesPerson : Worker
{
private double salesQuantity;
public double SalesQuantity
{
get { return salesQuantity; }
set { salesQuantity = value; }
}
}
Проте, як видно з прикладу, немає конструткора, який би передавав інформацію про агента для класу. Тому розширимо клас і додамо конструтор:
Лістинг 8.14. Реалізація класу SalesManager
. Властивість SalesQuantity
class SalesPerson : Worker
{
private double salesQuantity;
public SalesPerson(string fname, string lname, double salary, double bonus, double sQuantity)
: base(fname, lname, salary, bonus)
{
salesQuantity = sQuantity;
}
public double SalesQuantity
{
get { return salesQuantity; }
set { salesQuantity = value; }
}
}
Розберемо код:
- Наслідування здіснюється за домогою оператора
:
(двокрапка) - Наслідуватися можна одночасно лише від одного класу та багатьох інтерфейсів.
- Ми не створюємо полів
firstname
,lastname
,salary
і так далі, всі вони неявно наслідуються від базового класу Worker; - У конструктор ми передаємо всю ту ж саму інформацію яку передавали для конструктора
Worker
+ наше нове полеsalesQuantity
. - За допомогою ключовго слова base викликаємо конструктор базовго класу і передаємо йому параметри.
- Використовуємо властивість для доступу до інформації про обсяги продаж.
Реалізуємо у класі Worker властивості для усіх полів і визначимо метод, який дозволить виводити усю інформацію про працівника на екран:
Лістинг 8.15. Реалізація класу “Worker”. Властивості
public double Salary
{
get { return salary; }
set { salary = value; }
}
public string FirstName
{
get { return firstname; }
set { firstname = value; }
}
public string LastName
{
get { return lastname; }
set { lastname = value; }
}
public double Bonus
{
get { return bonus; }
set
{
if (bonus >= 0 && bonus < 1)
bonus = value;
}
}
Тепер напишемо наступну програму:
Лістинг 8.15. Приклад роботи з класом SalesPerson
Console.OutputEncoding = Encoding.Unicode;
SalesPerson sPerson = new SalesPerson("Петро", "Петренко", 125, 0.21, 154);
sPerson.FirstName = "Остап";
sPerson.DisplayInfo();
Результат
Петренко Остап - 125 грн.
У нашому класі SalesPerson
немає явно реалізованого методу DisplayInfo()
або властивості FirstName
, проте ми їх можеми викликати! Тобто ми їх успадкували від батьківського класу Worker
.
Майте на увазі, що при наслідуванні інкапсуляція зберігається. Тому похідний клас не може безпосередньо звертатися до закритих членів, визначених в його базовому класі. Тобто не можна наприклад у конструкторі класу SalesPerson
записати:
Лістинг 8.16. Приклад роботи з класом SalesPerson
public SalesPerson(string fname, string lname, double salary, double bonus, double sQuantity)
: base(fname,lname,salary,bonus)
{
//Не можна присвоїти значення.
firstname = fname; //Помилка
salesQuantity = sQuantity; //Помилка
}
Нагадаємо, що усі поля класі Worker приватні (private
), тобто закриті від «зовнішнього світу».
8.4.2. Ключове слово protected
Як ви вже знаєте, відкриті елементи безпосередньо доступні звідки завгодно, тоді як до закритих елементів не можна дістати доступ з якого-небудь об’єкту, окрім класу, що визначив їх. С# наслідує приклад багатьох інших сучасних об’єктно-орієнтованих мов і надає додатковий рівень доступу - захищений (protected
) доступ.
Коли базовий клас визначає захищені дані або члени, він створює множину елементів, які можуть бути безпосередньо доступні будь-якому насліднику. Якщо ви хочете дозволити класам SalesPerson
і Manager безпосередньо звертатися до даних, визначених в класі Worker
, початкове визначення класу Worker можна змінити таким чином:
Лістинг 8.17. Оголошення класу Worker
. Поля
class Worker
{
protected string firstname; //Ім'я
protected string lastname; //Прізвище
protected double salary; //Розмір з/п
protected double bonus; //Бонус до з/п у % від з/п
}
Після цього конструкція firstname = fname
; стане доступною і програма відкомпілюється.
Перевага визначення захищених членів в базовому класі полягає в тому, що похідним класам більше не доведеться діставати доступ до даних за допомогою відкритих методів або властивостей. Вочевидь, є і негатив: коли похідний клас має безпосередній доступ до внутрішніх даних його батьківського класу, існує можливість неумисного обходу бізнес-правил, визначених у відкритих властивостях. При визначенні захищених членів ви створюєте певний рівень довіри між батьківським і дочірнім класом, оскільки компілятор не намагатиметься виявляти які-небудь порушення бізнес-правил. І нарешті, знайте, що з точки зору користувача об’єкту захищені дані вважаються закритими (оскільки користувач знаходиться «поза родинним колом»).
Тому наступний код недопустимий:
SalesPerson sPerson = new SalesPerson("Петро", "Петров",125, 0.21, 154);
sPerson.firstname = "Іван"; // Помилка
sPerson.DisplayInfo();
8.4.3. Запобігання наслідування - запаковані класи (sealed)
При створенні відношень базовий клас/підклас можна використовувати поведінку існуючих типів. Проте що якщо ви хочете визначити клас, від якого не можна створювати похідні класи? Наприклад, припустимо, що ви додали ще один клас в простір імен, який розширює існуючий типSalesPerson
. На рис. 2. показана зміна ієрархії.
Класом, що представляє торгівельного агента, що працює за сумісництвом, є PSalesPerson
. Припустимо, що нам потрібно гарантувати, що ніхто не зможе створити підклас від PSalesPerson
. Аби цей клас не можна було розширювати, в С# використовується ключове слово sealed
:
sealed class PSalesPerson: SalesPerson
{
//Поля
//Властивості
//Методи
public PSalesPerson(string fname, string lname, double salary, double bonus, double sQuantity)
: base(fname, lname, salary, bonus, sQuantity)
{
//Інструкції конструктора
}
}
Оскільки клас PSalesPerson
запечатано, він не може служити базовим класом для інших типів. Отже якщо спробувати розширити PSalesPerson
, ви отримаєте помилку компіляції:
8.4.4. Програмування включення/ делегування
Як наголошувалося раніше, наслідування буває двох видів. Тільки що ми розглянули класичне відношення «є». Аби завершити дослідження цього другого стовпа ООП, давайте дослідимо відношення «має» (відоме так само, як модель включення/делегування). Припустимо, що ми створили новий клас, що моделює соціальний пакет співробітника:
class SocialPackage
{
//Сума виплати
private double socialSum;
public double SocialSum
{
get { return socialSum; }
set { socialSum = value; }
}
}
Вочевидь, було б досить дивним встановлювати відношення «є» між соціальним пакетом (класом SocialPackage
) і посадами співробітників. (Менеджер «є» соціальним пакетом? Сумнівно).) Проте повинно бути зрозуміло, що деякий тип відношення між цими двома класами може бути встановлений. Якщо не вдаватися до деталей, можна сказати, що кожен співробітник «має» (has-а
) соціальний пакет. Для цього можна додати до полів класу Worker
поле SocialPackage
таким чином:
Таким чином, ми успішно включили в клас інший об’єкт. Проте для надання функціональності об’єкту, що включається, зовнішньому світу необхідний делегування. Делегування - це просто додавання у клас членів, які використовують функціональність об’єкту, що включається. Найпростіший варіант – реалізація властивостей для включеного поля або методів Get
, Set
.
8.4.5. Вкладені визначення типів
Перш ніж досліджувати останній стовп ООП (поліморфізм), давайте познайомимося з технікою програмування під назвою вкладені типи. У С# можна визначити тип (перерахування, клас, інтерфейс, структуру або делегат) безпосередньо в області класу або структури. В цьому випадку вкладений (або «внутрішній») тип вважається членом класу, в який він вкладений (тобто «зовнішнього класу»), і з точки зору механізму часу виконання ним можна маніпулювати так само, як будь-яким іншим членом (полем, властивістю, методом, подією і т. д.). Синтаксис, що використовується для створення вкладених типів, досить простий. Розглянемо клас SocialPackage
як вкладений.
class Worker
{
protected string firstname; //Ім'я
protected string lastname; //Прізвище
protected double salary; //Розмір з/п
protected double bonus; //Бонус до з/п у % від з/п
public class SocialPackage
{
//Сума виплати
private double socialSum;
public double SocialSum
{
get { return socialSum; }
set { socialSum = value; }
}
}
...
}
Хоча цей синтаксис досить наочний, не завжди зрозуміло, навіщо потрібно так робити. Далі представлені аргументи, покликані допомогти в цьому розібратися:
- Модель вкладених типів схожа на відношення «має» за винятком того, що у вас є повний контроль над рівнем доступу не до об’єкту, що включається, а до внутрішнього типу.
- Оскільки вкладений тип - це член включеного класу, він може звертатися до закритих членів цього класу.
- Частенько вкладений тип корисний лише як допоміжний для класу і не призначений для використання зовнішнім світом.
- Коли тип містить інший тип-клас, він може створювати змінні-члени цього типу так само, як і інші елементи даних. Проте, якщо ви хочете використовувати вкладений тип ззовні включаючого типу, тип необхідно кваліфікувати вкладеним типом.
8.5. Підтримка поліморфізму у C
8.5.1. Реалізація поліморфізму у С
Тепер давайте дослідимо завершальний стовп ООП - поліморфізм. Реалізуємо у класі Worker
метод GiveBonus()
таким чином:
Оскільки цей метод був визначений як відкритий, ми можемо надавати бонуси як торгівельним агентам, так і менеджерам (а також торгівельним агентам, що працюють за сумісництвом):
SalesPerson sPerson = new SalesPerson("Петро", "Петров",125, 0.21, 154);
Console.WriteLine("З/п: {0}", sPerson.Salary);
Console.WriteLine("Додамо бонус - 12.5!");
sPerson.GiveBonus(12.5f);
Console.WriteLine("З/п: {0}", sPerson.Salary);
Результат:
З/п: 125
Додамо бонус: 12.5!
З/п: 137.5
Проблема поточного дизайну полягає в тому, що успадкований метод GiveBonus()
працює ідентично для всіх підкласів. У ідеалі в бонусі торгівельного представника або торгівельного представника за сумісництвом повинен враховуватися об’єм продажів. Можливо, менеджери повинні отримувати додаткові акції на додаток до збільшення зарплати.
8.5.2. Ключові слова virtual
і override
Поліморфізм надає підкласам можливість зміни реалізації методів, визначених у їх базовому класі. Для зміни поточного дизайну необхідно розуміти значення ключових слів virtual
та override
мови С#. Якщо у базовому класі визначається метод, який може бути перекритий підкласом, цей метод має бути віртуальним:
Коли підкласу потрібно перевизначити віртуальний метод, це робиться за допомогою ключового слова override
. Наприклад, типи SalesPerson
і Manager
можуть перекрити метод GiveBonus()
таким чином:
class SalesPerson : Worker
{
...
public override void GiveBonus(float bon)
{
if (salesQuantity > 100)
bon += bon * 0.1f;
salary += bon;
}
...
}
Зверніть увагу, як кожен перекритий метод може використовувати поведінку за замовчуванням за допомогою ключового слова base. Toбто вам не потрібно повністю повторно реалізовувати логіку методу GiveBonus()
, ви можете багато разів задіювати (і, можливо, розширювати) поведінку за замовчуванням батьківського класу.
Наприклад:
public override void GiveBonus(float bon)
{
if (salesQuantity > 100)
bon += bon * 0.1f;
base.GiveBonus(bon);
}
8.5.3. Поняття абстрактного класу
В даний момент базовий клас Worker надає захищені змінні-члени своїм наслідникам, а також підтримує віртуальний метод (GiveBonus()
), який може бути перекритий наслідниками.
Хоча це все і чудово, в поточному дизайні є дивний побічний ефект, що полягає в тому, що ви можете безпосередньо створити екземпляри базового класу Worker
:
У даному прикладі єдине дійсне призначення базового класу Worker полягає у визначенні загальних полів і членів для всіх підкласів. Зрозуміло, що немає сенсу створювати безпосередній екземпляр цього класу, оскільки тип Worker сам по собі дуже узагальнений. Наприклад, якби я підійшов до вас і сказав: «Я співробітник!», ваше перше питання було б: «Ну і що ти за співробітник?» (консультант, тренер, помічник адміністратора, редактор, співробітник Білого дому і т. д.).
Враховуючи, що багато базових класів є досить туманними сутностями, набагато краще в нашому прикладі було б запобігти можливості безпосереднього створення об’єктів класу Worker в коді. У С# це можна зробити програмно, використовуючи ключове слово abstract
:
Таким чином створити обєкт класу не вдасться.
Контрольні запитання
Загальні питання про ООП
- Що таке об’єктно-орієнтоване програмування (ООП)?
- Які основні принципи ООП?
- Що таке клас у C#?
- Що таке об’єкт і як він створюється у C#?
- Чим відрізняється клас від об’єкта?
- Що таке інкапсуляція і як її реалізують у C#?
- Які існують модифікатори доступу в C#?
- Що таке конструктор класу? Які його види?
- Чи можна створити клас без конструктора?
- Що таке деструктор і коли він викликається?
Наслідування
- Що таке наслідування у C#?
- Як успадковувати клас у C#? Наведіть приклад.
- Що таке ключове слово
base
і як його використовують? - Чи може клас C# успадковувати декілька класів? Чому?
- Як заборонити успадкування класу?
Поліморфізм
- Що таке поліморфізм у C#?
- Яка різниця між перевизначенням (
override
) і приховуванням (new
) методів? - Як використовувати віртуальні методи (
virtual
) у C#? - Що таке абстрактний клас і коли його слід використовувати?
- Чим абстрактний клас відрізняється від інтерфейсу?
Абстракція та інтерфейси
- Що таке абстракція у C#?
- Як створити інтерфейс у C#?
- Чи може інтерфейс містити реалізацію методів?
- Чи може клас реалізовувати декілька інтерфейсів одночасно?
- Чим відрізняється інтерфейс від класу?
Додаткові концепції ООП
- Що таке статичні (
static
) класи та методи? - Чим статичні поля відрізняються від нестатичних?
- Що таке композиція та агрегація у C#?
- Чим
sealed
клас відрізняється від звичайного класу? - У яких випадках слід використовувати ООП замість процедурного програмування?
Дорогі друзі, якщо Ви помітили, що для написання матеріалів використані джерела, які я не вказав - прошу надіслати мені інформацію на пошту. Дякую.