
- •Ключове слово this
- •Ключове слово static
- •Статичний конструктор
- •Інкапсуляція з використанням методів get і set
- •Інкапсуляція з використанням властивостей
- •Організація робіт при описі класу. Атрибут partial
- •Спадкоємство
- •Int point; // поле
- •Додавання до класу запечатаного класу
- •Вкладеність класів
- •Поліморфізм
- •Абстрактні класи
- •Приховування членів класу
- •Оператори as і is
- •Структури
Операційні системи і системне програмування
Вступ в класи
За 50-60 років свого розвитку програмування пройшло дуже значний шлях: від початку розробки програм в машинних кодах через створення і застосування простих мов символічного кодування до випуску сучасних гігантських продуктів, які забезпечують роботу клієнта в розподільних мережах на основі новітніх підходів. Розробка і створення якої-небудь стандартної програми переведення даних з однієї системи числення в іншу, нової процедури сортування масиву даних, які налаштовані на потреби замовника програми обробки анкет, та інше було помітною подією в ті роки. Думка тих, хто був пов'язаний з програмуванням, невпинно працювала в напрямі, як спростити дуже складну працю програміста, підвищити продуктивність його праці, досягти надійності та якості програм. Спочатку було відмічено, що багато частин машинних алгоритмів в різних завданнях повторюються. Це привело до створення так званих стандартних програм - процедур, які виконували ці стандартні дії, наприклад, такі як переведення чисел з однієї системи числення в іншу, сортування масиву даних і т. п. Кожному програмістові вже не треба було, наприклад, турбуватися про створення ділянки сортування даних в своїй програмі. Він просто користувався стандартною процедурою, що постачається разом з математичним забезпеченням обчислювальної машини. І це прискорювало процес розробки програми. Потім прийшли до поняття структурного програмування. Це був значний крок вперед. Була розроблена методологія створення програми, в основі якої лежав принцип створення програми як структури, що складається з підпрограм, які створюються для обробки блоків програми, які повторюються, і принципу виконання програми зверху "вниз", коли в послідовності виконання операторів та окремих блоків програми не було повернення назад. Помри але зроби так, щоб не було повернень назад! Це забезпечувало швидше відлагодження програми. Вихід з циклу - тільки вперед (згадайте оператор break)! Повернення назад - тільки в межах оператора циклу! Передача управління з оператора if...else - тільки вперед! Геть оператор goto! Навіть була доведена теорема про те, що будь-яку схему алгоритму можна представити у вигляді композиції вкладених блоків begin та end, умовних операторів if, then, else, циклів з передумовою (while) і, можливо, додаткових логічних змінних (прапорців). Підхід був серйозний. Але життя не стоїть на місці. З появою новішої техніки - могутніх комп'ютерів – відкрилися й нові можливості, про які раніше навіть не мріяли. Та й про що ти можеш мріяти, коли оперативна пам'ять твого "комп’ютера" - всього 1024 маленьких комірок (1 Кб), а зовнішня - одна стрічка на 64 Кб, який практично весь час не працює! І швидкість у 3 тис. операцій в секунду.
Виявилось, що у процедурно-модульно-структурного підходу є значні недоліки, які можна усунути, використовуючи потужність сучасних комп'ютерів. По-перше, в програмі дані і підпрограми їх обробки (процедури і функції) формально ніяк не зв’язані. А хотілося б, щоб було навпаки. Наприклад, ви розв’язуєте задачу з обробки даних про якийсь будинок. Зручніше було б, щоб дані щодо будинку і функції з обробки цих даних зберігалися в одній "коробці". Причому, було б ще зручніше, якби ця "коробка" підійшла і для розв’язуванні завдання по іншому будинку з такими ж характеристиками, як і перший будинок. Тобто, кажучи мовою стандартизації, щоб "коробка" була стандартною для даного класу будинків. По-друге, дані в "коробці" не повинні бути доступні всім, хто працює з коробкою. Вони повинні бути захищені від прямого доступу до них. Тільки через функції, які ховаються в цій "коробці". По-третє було б корисним, щоб елементи одного будинку, заховані в "коробці", не зберігалися там без діла, коли дані будинку не обробляються, а були б використані, якщо вони підходять і до деяких інших будинків. Щоб можна було брати такі елементи, не створюючи їх наново для іншого класу будинків, додавати до них нові, потрібні для нового класу, і створювати свою нову "коробку" для нового класу будинків. І так далі. Це вже зовсім інший підхід до програмування. Він орієнтований не на модулі-процедури-функції, а на якісь об'єкти, на те, щоб в програмі вони створювалися (описувалися), щоб усередині них зберігалися функції з обробки даних цих об'єктів (функції природно, треба розробити, але в рамках опису об'єкта), щоб дані щодо об'єктів вводилися і потрапляли в сам об'єкт і нікуди більше, а корисні для інших об'єктів елементи вже готового об'єкту можна було б успадкувати іншому створюваному об'єкту. Раніше наприклад, могли ми, маючи деяку стандартну процедуру, формально узяти з неї якусь її частину і застосувати в іншій процедурі? Не могли. А в системі об'єктно-орієнтованого програмування (ООП), це цілком можливо. Ми бачимо, що у такого підходу в програмуванні більш високий рівень, кажучи мовою політики, усуспільнення. Від найпростіших стандартних програм з переведення чисел з десяткової системи числення у двійкову ми приходимо до створення стандартних конструкцій з опису та обробки даних людини, будинку, автомобіля, поліклініки, заводу, країни.
Раніше ми вивчили різні типи даних: цілих чисел, чисел з плаваючою крапкою, рядкових даних, типи організації даних, званих масивами, типи організації даних, званих перерахуваннями. Для ООП характерні типи даних, які називаються класами. Наприклад, для типів "масиви" і "перерахування" ми оголошували (описували згідно спеціальної схеми) сам тип, потім оголошували якісь змінні цього типу. Потім ініціалізували цю змінну деякими значеннями. У нас існували правила, як звертатися до елементів оголошеного типу даних за допомогою імені змінної. Наприклад, якщо у нас був оголошений масив М[] даних, то елемент масиву ми отримували, вказуючи в квадратних дужках номер (індекс) цього елемента в масиві: M[i]. Для типу даних, перерахування, була своя схема опису і своє правило звернення до елемента цього типу даних. Для типу "клас" дотримуються такі ж правила: цей тип даних описується за своєю схемою (шаблоном), змінна цього типу оголошується згідно загальних правил (<ім’я_типу> <ім’я_змінної>), за своїми правилами змінна ініціалізувалася, тобто їй надаються деякі початкові значення. Звернемо увагу на два моменти: коли ми описуємо тип даного (у нашому випадку тип "клас"), то описуємо фактично схему, шаблон, згідно якого надалі буде створений об'єкт даного типу. А коли ми цей шаблон наповнюємо змістом, то тим самим створюємо об'єкт з даним змістом, тобто щось, що можна помацати. Об'єкт вже треба розміщувати в пам'яті. Він вимагає простору. Схема, шаблон теж десь зберігаються, теж вимагають якоїсь пам'яті, але це як би допоміжна, не основна пам'ять. Ось, наприклад, ми оголосили цілочисельну змінну i. Компілятор для неї створює якийсь шаблон: виділяє в спеціальній пам'яті 4 байти. І все. Але коли ми цій змінній присвоюємо, скажімо, значення 5, то тим самим створюємо конкретний числовий об'єкт, з яким можна працювати. Просто з оголошенням i ще працювати не можна. Ні з чим. А з об'єктом, навіть можна сказати, з екземпляром цього шаблону int i, який дорівнює 5, працювати можна. Якщо ми присвоїмо змінній i інше значення, наприклад 6 можна сказати, що ми з шаблону int i отримали (створили) інший об'єкт: число 6.
Так само і з класом: ми його описали, отримали просто опис, шаблон. І нічого більшого. А як тільки ми за цим шаблоном створили змінну і наповнили її якимсь змістом, то отримали об'єкт відповідний даному змісту. Кажуть, що отримали екземпляр класу. Наповнили змінну іншим змістом - отримали інший об'єкт з іншим змістом, інший екземпляр класу. Об'єкти (екземпляри) вже розміщуються в пам'яті. Клас - це посилальний тип. Тому він розміщується в динамічній кучі оператором new. А чому зробили клас посилальним типом даного? Тому що об'єкти, отримувані з цього класу, можуть бути величезними (наприклад, якийсь гігантський завод). А переміщати в пам'яті, як ми бачили посилальні дані набагато простіше непосилальних: переслав тільки посилання кому треба і не чіпав величезний масив. Великий виграш у швидкості обробки.
Перш ніж вивчати конкретну структуру класу, відзначимо, що клас як сукупність елементів (членів класу) складається з членів, які називаються полями, і з членів, що оперують даними цих полів. Ці останні можуть бути конструкторами, методами, властивостями, подіями та ін. Методи - це функції. Так функції називаються у класах. Конструктори - це методи, які дозволяють ініціалізувати клас, тим самим створюють з класу об'єкт, розміщуючи його в пам'яті. Тобто конструктор - це звичайна (за структурою) функція, яка отримує на свій вхід дані, які присвоюються полям класу. Іншими словами, з порожнього шаблону за рахунок задання полів отримується об'єкт.
Наприклад, хай ми маємо клас MyCar. Це тип даного, як ми бачили. Оголошуємо змінну цього типу. Наприклад, car (автомобіль): MyCar car;. Але це поки ніщо: оголошення і не більш того. З такою змінною працювати не можна. Нехай, у автомобіля, клас якого ми хочемо описати, є такі характеристики (поля): марка (Type), ім'я власника (Name) і швидкість (Speed). Тоді функція, яка покликана задати ці поля, повинна мати в своєму заголовку ці три параметри (string Type, string Name, float Speed). А яким повинне бути ім'я у такої функції? Воно специфічне і співпадає з ім'ям класу. Цілком логічно. Адже конструктор - це те, що створює з класу об'єкт. Отже, для нашого передбачуваного класу його конструктор матиме вигляд
MyCar(string Type, string Name, float Speed)
А що повинно бути в тілі конструктора? Оператори, які присвоюють значення полям класу. Якщо поля в класі описані як string type, string name, float speed, то в тілі повинні бути оператори:
type=Type; name=Name; speed=Speed;
Тобто у результаті конструктор класу MyCar матиме вигляд:
MyCar(string Type, string Name, float Speed)
{
type=Type;
name=Name;
speed=Speed;
}
Якщо ми тепер з класу хочемо створити конкретний об'єкт, наприклад автомобіль Чака Норріса, то повинні записати:
MyCar Ch_nor_car = new MyCar("Porshe", "Chuck Norris", 250.0);
Згідно цього оператора конструктор створить об'єкт з ім'ям Ch_nor_car, оператор new розмістить об'єкт (або - екземпляр класу MyCar) в динамічній кучі і видасть адресу початку об'єкта в цій купі. Адреса буде покладена на поличку для змінної Ch_nor_car.
Ну а зараз треба розглянути, як створюється (описується) клас. Найпростіший вигляд опису класу такий:
сlass Car
{
}
Тут class - ключове слово, Car - ім'я класу. А де ж конструктор? Тут конструктор не вказаний. Він йде за замовчуванням. Взагалі, якщо у класі конструктор не вказується, то він за замовчуванням береться з класу Object - спеціального класу, з якого беруть свій початок решта класів. Вони, як то кажуть, нащадки класу Object. Конструктор за замовчуванням ініціалізує поля створюваного класу значеннями, прийнятими для полів класу за замовчуванням. Взагалі-то тіло класу не порожнє. Інакше навіщо цей клас створювати? У тілі повинні бути визначені члени класу і методи (тобто функції), які працюють з полями-членами та іншими членами класу. Ну і, звичайно, заданий конструктор класу. Конструкторів може бути більше одного, тому що може виникнути потреба у створенні об'єкта класу на основі не всіх полів а деяких. Але всі конструктори повинні обов'язково мати ім'я класу, але з різними параметрами. Члени класу групуються в дві секції: загальнодоступні члени (ті, що доступні для звернення до них не тільки з методів самого класу, але й з інших програм застосування) і приватні члени (до них мають доступ тільки методи даного класу). Члени загальнодоступної секції позначаються ключовим словом public, а приватної (особистої) - словом private. Є ще одне ключове слово, яке забезпечує доступність членів класу. Це слово protected (захищений). У класу є властивість успадковувати члени іншого класу (про це ми довідаємося докладніше далі). Успадковуються тільки ті члени класу, які мають атрибут public. Якщо ми хочемо щоб якісь члени створюваного нами класу, що потрапили в приватну секцію, теж успадковувалися, їм треба присвоїти атрибут protected. Тоді такі члени потраплятимуть в інші класи тільки шляхом спадкування.
Приклад програми, в якій створюється клас з двома конструкторами та одним методом обробки полів класу, приведений в лістингу 8.1.
Лістинг 8.1
// Приклад програми, в якій створюється клас з двома конструкторами
// та одним методом обробки полів класу, приведений в лістингу 8.1.
using System;
namespace app21_class
{
class MyCar
{
private string name;
private int speed;
private string owner_name;
// Конструктор класу
public MyCar(string Name, int Speed, string Owner_name)
{
name=Name;
speed=Speed;
owner_name=Owner_name;
}
// Другий конструктор класу
public MyCar(string Name, int Speed)
{
name=Name;
speed=Speed;
}
// Методи класу
public int M1()
{
speed=speed*2;
return(0);
}
} // class
class Program
{
public static void Main()
{
// Щоб створити об'єкт (з допомогою конструктора),
// треба, щоб конструктор був доступний в програмі
// Main(), яка знаходиться в іншому класі.
// Тому конструктор повинен мати атрибут public.
// Це ж стосується і метода M1()
MyCar car = new MyCar("Порше", 120, "Потапов");
car.M1();
// car вже вище оголошена як тип MyCar
car = new MyCar("Порше", 120);
car.M1();
Console.WriteLine("Press any key to continue... ");
Console.Read();
}
}
}
Отже, в цій програмі створюється клас MyCar з трьома приватними полямі (ім'я автомобіля, швидкість та ім'я власника).
private string name;
private int speed;
private string owner_name;
Через те, що всі поля приватні, то із зовнішньої програми, зокрема з Main(), до них доступу немає. Але є доступ через метод M1(), який має атрибут public (загальнодоступний). Тобто в цьому випадку, якщо ми хочемо щось з полями робити, то маємо тільки один засіб, який в класі визначений. Це метод M1(). Інше не дано. У класі визначені два конструктори, які дозволяють створити два різних об'єкти. Один - на підставі ініціалізації трьох полів, інший - двох полів. Конструктори повинні бути загальнодоступними, щоб зовні класу з їх допомогою створювати об'єкти. Тому у конструкторів наявний атрибут public. Єдиний метод класу (M1()) збільшує швидкість автомобіля в два рази. В основній програмі Main() створюються два об'єкти двома конструкторами, і в кожному випадку виконується метод М1(). Щоб було видно, що ж отримується при обчисленнях програма була запущена в режимі налаштування. Точки зупину були поставлені так, щоб можна було покроково стежити, як формується об'єкт car різними конструкторами. Значення, що отримуються полями об'єктів в результаті їх ініціалізації конструкторами і дії на них методу М1(), показані на рис. 8.1 і 8.2.
Відмітимо, що для звернення до елемента класу треба назвати клас і через крапку написати ім'я потрібного елемента. Відзначимо також, що після визначення полів класу, які в об'єктах, отриманих з цього класу, використовуватимуться для представлення їх стану, визначають інші члени, які моделюють поведінку об'єктів. У нашому випадку - це метод М1(). Розміщення одним рядком об'єкта, як показано в попередньому лістингу (MyCar car = new MyCar("Порше", 120, "Потапов");), не обов'язково: можна спочатку оголосити тип змінної (MyCar car;), а потім створити екземпляр класу (об'єкт):
car = new MyCar("Порше", 120, "Потапов");
Конструктор класу - це спеціальний метод, який викликається при створенні об'єкту. Цей метод не повертає ніякого значення має, на відміну від інших методів. У C# кожен клас забезпечується своїм конструктором за замовчуванням, хочете ви цього чи ні. Це метод з ім'ям класу але без параметрів. Тобто, коли ви створюєте екземпляр класу (або об'єкт, що те ж саме), ви повинні написати, наприклад
MyCar car = new MyCar();
Мал. 8.1. Значення полів об'єкту після роботи першого конструктора і методу М1()
Мал. 8.2. Значення полів об'єкту після роботи другого конструктора і методу М1()
У цьому випадку полям класу будуть присвоєні значення, прийняті в мові за замовчуванням. Якщо ж вам не підходять значення за замовчуванням, то ви самі можете створити свій конструктор за замовчуванням (тобто без параметрів), задавши в його тілі свої, потрібні вам, значення полів класу. Наприклад, для нашого випадку можна було б задати такий конструктор за замовчуванням:
public MyCar()
{
name="Порше";
speed=120;
owner_name="Потапов";
}
У нашому прикладі в класі визначено два конструктори. У них різне число параметрів, але типи параметрів однакові. Але може бути і так, що один конструктор від іншого відрізняється не тільки числом параметрів, але і їх типами. Визначення методів з одним і тим же ім'ям, але з різним числом і типом параметрів називають перевантаженням методу. Таким чином, клас MyCar має перевантажений конструктор, щоб надати більше одного способу створення з цього класу об'єкта.
Ключове слово this
У змінній з цим ім'ям зберігається адреса поточного об'єкта, тобто об'єкта, з яким у даному місці програми відбувається робота. Цей елемент введений в мову для вирішення неоднозначних ситуацій, коли наприклад, ви в конструкторі назвали параметр таким же ім'ям, як поле. Формально компілятор пропустить цей варіант, але при виконанні програми можна отримати не той результат. Щоб в таких випадках розрізняти, яка змінна до чого відноситься при однакових іменах, і введено це ключове слово. Наприклад, у разі нашого прикладу ми могли б визначити заголовок конструктора як
public MyCar(string name, int Speed, string Owner_name)
Тут перший параметр по імені співпадає з ім'ям поля класу. Тому в тілі конструктора ми вимушені записати рядок
{
name=name;
...
}
Компілятор буде вважати, що рядок присвоюється сам собі, і полю name класу нічого не присвоїть. Тому в таких ситуаціях треба використовувати слово this, яке в даному випадку підкаже компілятору, що name в лівій частині від знаку присвоєння належить поточному об'єкту, а саме тому, який створюється з класу, а не конструктора. У цьому випадку конструктор виглядатиме так:
public Mycar(string name, int Speed, string Owner_name)
{
this.name=Name;
speed=Speed;
owner_name=Owner_name;
}
Примітка: Члени класу можуть мати атрибут static (про це буде сказано далі). У цьому випадку використання this викличе помилку компіляції тому що статичні члени діють на рівні класу, а не об'єкта (вони для того і створюються, щоб бути доступними всім об'єктам), а на рівні класу this не існує.
Ключове слово static
Члени класу можуть мати атрибут static. Потреба у його введенні була викликана тими обставинами, що створювані з класу об'єкти повинні були мати по деяких полях, наприклад, спільні значення або володіти спільними методами. Це зручно, бо немає потреби мати в кожному об'єкті один і той же метод, досить отримати його на рівні класу, з якого створюється цей метод. Наприклад, ви створюєте клас щодо персоналу підприємства. Нехай, кожен об'єкт, який отримується з цього класу - це окремий працівник зі всіма своїми характеристиками. У всіх працівників можуть бути однакові, наприклад, деякі процентні надбавки. Такі поля треба робити статичними, і вони будуть у всіх об'єктах однакові. Досить змінити їх на рівні класу, як зміни відобразяться у всіх об'єктах. Або методи деяких розрахунків: змінився алгоритм, метод змінюється на рівні класу, і для всіх співробітників оновився метод розрахунку.
Раніше ми користувалися методом WriteLine(). Але ми нічого не говорили, звідки він і чому так записується (Console.WriteLine()). Тепер ми можемо сказати, що цей метод з класу Console, і він має атрибут static. Він не викликається на рівні об'єкту (ми не можемо написати Console con = new Console(), створюючи об'єкт з класу, а потім писати con.WriteLine(); компілятор не пропустить), а викликається безпосередньо з класу Console. Якщо в деякому класі визначені тільки статичні члени, такий клас можна назвати обслуговуючим. Приклад тому - клас Console. Є, наприклад, і клас Math, який надає можливість роботи з математичними величинами і функціями. Статичні члени класу можуть оперувати тільки іншими статичними членами, інакше на етапі компіляції виникне помилка.
Для звернення до статичного члена обов'язково треба указувати ім'я класу, якому цей член належить (як, наприклад, ми це робили у разі застосування функції (а зараз ми знаємо - статичного методу) консольного виводу).
Отже, при створенні статичних полів класу вони розподіляються по всіх екземплярах класу. Для них в пам'яті виділяється спеціальна ділянка, яка не скидається, коли об'єкт знищується. У той час як нестатичні поля - це незалежні копії полів для кожного об'єкта. І вони при ліквідації об'єкта знищуються разом з ним, тобто пам'ять від них звільняється. Статичні поля зберігаються а нестатичні - не зберігаються. Тому при створенні класу одним із завдань розробника є визначення, які поля будуть статичними, а які ні.
Для остаточного переконання ще один приклад. Хай у працівника потрібно розрахувати якусь процентну ставку. Вона для всіх працівників розраховується персонально згідно одного і того ж алгоритму, який не залежить від працівника. Нехай працівників на заводі три тисячі. Тобто створено три тисячі екземплярів класу Персонал. І в кожному присутній метод розрахунку процентної ставки. Якщо алгоритм розрахунку змінився треба буде з класу знову формувати три тисячі екземплярів з новим методом, якщо тільки метод не має атрибуту static. А з цим атрибутом формувати наново екземпляри немає потреби, тому що у кожному екземплярі звернення до цього методу йде із вказівкою класу, а не об'єкта, бо метод - статичний. А в класі метод вже змінений. І звідти він отримуватиметься вже зміненим.