русс | укр

Мови програмуванняВідео уроки php mysqlПаскальСіАсемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование


Linux Unix Алгоритмічні мови Архітектура мікроконтролерів Введення в розробку розподілених інформаційних систем Дискретна математика Інформаційне обслуговування користувачів Інформація та моделювання в управлінні виробництвом Комп'ютерна графіка Лекції


Спадкоємство


Дата додавання: 2015-01-08; переглядів: 952.


 

Клас в С# може мати довільну кількість нащадків і лише одного предка. При описі класу ім'я його предка записується в заголовку класу після двокрапки. Якщо ім'я предка не вказане, за предка вважається базовий клас всієї ієрархії System.Object:

 

[атрибути] [специфікатори] class ім'я_класу [: предки]

тіло_класу

 

Слово "предки" присутнє в описі класу в множині, хоча клас може мати тільки одного предка. Причина в тому, що клас разом з єдиним предком може успадковувати від інтерфейсів - спеціального виду класів, що не мають реалізації. Інтерфейси розглядаються в наступному розділі.

 

Розглянемо спадкоємство класів на прикладі. У 5 розділі був описаний клас Monster, моделюючий персонаж комп'ютерної гри. Допустимо, нам потрібно ввести в гру ще один тип персонажів, який повинен володіти властивостями об'єкту Monster. Буде логічне зробити новий об'єкт нащадком об'єкту Monster (лістинг 8.1).

 

Лістинг 8.1. Клас Daemon, предок класу Monster

using System;

namespace examp48

{

class Monster

{

public Monster()

{

this.name = "Noname";

this.health = 100;

this.ammo = 100;

}

 

public Monster(string name): this()

{

this.name = name;

}

public Monster(int health, int ammo, string name)

{

this.name = name;

this.health = health;

this.ammo = ammo;

}

public string GetName()

{

return name;

}

public int GetHealth()

{

return health;

}

public int GetAmmo()

{

return ammo;

}

public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}", name, health, ammo);

 

}

 

protected string name; // закриті поля

public int health, ammo;

}

 

class Daemon :Monster

{

public Daemon()

{

brain = 1;

}

 

public Daemon(string name, int brain)

: base(name) // 1

{

this.brain = brain;

}

 

public Daemon(int health, int ammo, string name, int brain)

: base(health, ammo, name) // 2

{

this.brain = brain;

}

 

new public void Passport() // 3

{

Console.WriteLine(

"Daemon {0} \t health = {1} ammo = {2} brain = {3}",

name, health, ammo, brain);

 

}

 

public void Think() // 4

{

Console.Write(name + " is ");

for (int i = 0; i < brain; ++i) Console.Write(" thinking");

Console.WriteLine("...");

}

int brain; // закрите поле

}

 

class Classl

{

static void Main()

{

Daemon Dima = new Daemon("Dima", 3); // 5

Dima. Passport(); // 6

Dima.Think(); // 7

Dima.GetHealth(); // 8

Dima.Passport();

}

}

}

У класі Daemon введені закрите поле brain і метод Think, визначені власні конструктори, а також перевизначений метод Passport. Всі поля і властивості класу Monster успадковуються в класі Daemon.

Результат роботи програми:

 

Daemon Dima health = 100 ammo = 100 brain = 3

Dima is thinking thinking thinking...

Daemon Dima health = 90 ammo = 100 brain = 3

 

Екземпляр класу Daemon використовує як власні (оператори 5-7), так і успадковані (оператор 8) елементи класу. Розглянемо загальні правила спадкоємства, використовуючи як приклад лістинг 8.1.

Конструктори не успадковуються, тому похідний клас повинен мати власні конструктори. Порядок виклику конструкторів визначається приведеними далі правилами:

§ Якщо в конструкторі похідного класу явний виклик конструктора базового класу відсутній, то автоматично викликається конструктор базового класу без параметрів. Це правило використане в першому з конструкторів класу Daemon.

§ Для ієрархії, що складається з декількох рівнів, конструктори базових класів викликаються, починаючи з самого верхнього рівня. Після цього виконуються конструктори тих елементів класу, які є об'єктами, в порядку їх оголошення в класі, а потім виконується конструктор класу. Таким чином, кожен конструктор ініціалізував свою частину об'єкту.

§ Якщо конструктор базового класу вимагає вказівки параметрів, він має бути явним чином викликаний в конструкторі похідного класу в списку ініціалізації (оператори 1 і 2). Виклик виконується за допомогою ключового слова base. Викликається та версія конструктора, список параметрів якої відповідає списку аргументів, вказаних після слова base.

Поля, методи і властивості класу успадковуються, тому за бажання замінити елемент базового класу новим елементом слід явним чином вказати компілятору свій намір за допомогою ключового слова new. У лістингу 8.1 таким чином перевизначений метод виведення інформації про об'єкт Passport.

Метод Passport класу Daemon заміщає відповідний метод базового класу, проте можливість доступу до методу базового класу з методу похідного класу зберігається. Для цього перед викликом методу указується слово base, наприклад:

 

base.Passport( );

 

Виклик однойменного методу предка з методу нащадка завжди дозволяє зберегти функції предка і доповнити їх, не повторюючи фрагмент коду. Окрім зменшення об'єму програми це полегшує її модифікацію, оскільки зміни, внесені до методу предка, автоматично відбиваються у всіх його нащадках. У конструкторах метод предка викликається після списку параметрів і двокрапки, а в решті методів - в будь-якому місці за допомогою приведеного синтаксису.

Ось, наприклад, як виглядав би метод Passport, якби ми в класі Daemon хотіли не повністю перевизначити поведінку його предка, а доповнити його:

 

new public void Passport()

{

base.Passport();

Console.WriteLine(" brain = {1}", brain );

}

Елементи базового класу, визначені як private, в похідному класі недоступні. Тому в методі Passport для доступу до полів name, health і ammo довелося використовувати відповідні властивості базового класу. Інше рішення полягає в тому, щоб визначити ці поля зі специфікатором protected, в цьому випадку вони будуть доступні методам всіх класів, похідних від Monster. Обидва рішення мають свої переваги і недоліки.

Під час виконання програми об'єкти зберігаються в окремих змінних, масивах або інших колекціях. У багатьох випадках зручно оперувати об'єктами однієї ієрархії одноманітно, тобто використовувати один і той же програмний код для роботи з екземплярами різних класів. Бажано мати можливість описати:

§ об'єкт, в який під час виконання програми заносяться посилання на об'єкти різних класів ієрархії;

§ контейнер, в якому зберігаються об'єкти різних класів, які відносяться до однієї ієрархії;

§ метод, в який можуть передаватися об'єкти різних класів ієрархії;

§ метод, з якого залежно від типу об'єкту, який викликав його, викликаються відповідні методи.

Все це можливо завдяки тому, що об'єкту базового класу можна привласнити об'єкт похідного класу .

Давайте спробуємо описати масив об'єктів базового класу і занести туди об'єкти похідного класу. У лістингу 8.2 в масиві типу Monster зберігаються два об'єкти типу Monster і один - типу Daemon

 

Лістинг 8.2. Масив об’єктів різних типів

using System;

namespace examp48

{

class Monster

{

// Див. лістинг 8.1

}

 

class Daemon : Monster

{

// Див. лістинг 8.1

}

 

class Classl

{

static void Main()

{

const int n = 3;

Monster[] stado = new Monster[n];

stado[0] = new Monster("Monia");

stado[1] = new Monster("Monk");

stado[2] = new Daemon ("Dimon", 3);

foreach (Monster elem in stado) elem.Passport(); //1

for (int i = 0; i < n; ++i) stado[i].ammo = 0; //2

Console.WriteLine();

foreach (Monster elem in stado) elem.Passport(); //3

}

}

}

 

Результат роботи програми:

Monster Monia health = 100 ammo = 100

Monster Monk health = 100 ammo = 100

Monster Dimon health = 100 ammo = 100

 

Monster Monia health = 100 ammo = 0

Monster Monk health = 100 ammo = 0

Monster Dimon health = 100 ammo = 0

 

Об'єкт типу Daemon дійсно можна помістити в масив, що складається з елементів типу Monster, але для нього викликаються тільки методи і властивості, успадковані від предка. Це влаштовує нас в операторові 2, а в операторах 1 і 3 хотілося б, щоб викликався метод Passport, перевизначений в нащадку.

Отже, привласнювати об'єкту базового класу об'єкт похідного класу можна, але викликаються для нього тільки методи і властивості, визначені в базовому класі. Іншими словами, можливість доступу до елементів класу визначається типом посилання, а не типом об'єкту, на який вона вказує.

Це і зрозуміло: адже компілятор повинен ще до виконання програми вирішити, який метод викликати, і вставити в код фрагмент, передавальний управління на цей метод (цей процес називається раннім зв’язуванням). При цьому компілятор може керуватися тільки типом змінної, для якої викликається метод або властивість (наприклад, stado[i].Ammo). Те, що в цій змінній в різні моменти часу можуть знаходитися посилання на об'єкти різних типів, компілятор врахувати не може.

Отже, якщо ми хочемо, щоб методи, що викликаються, відповідали типу об'єкту, необхідно відкласти процес зв’язування до етапу виконання програми, а точніше - до моменту виклику методу, коли вже точно відомо, на об'єкт якого типу вказує посилання. Такий механізм в С# є - він називається пізнім зв’язу-ванням і реалізується за допомогою так званих віртуальних методів.

 


<== попередня лекція | наступна лекція ==>
РОЗДІЛ 8. ІЄРАРХІЇ КЛАСІВ | Віртуальні методи


Онлайн система числення Калькулятор онлайн звичайний Науковий калькулятор онлайн