Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ЛабРаб_ООП

.pdf
Скачиваний:
145
Добавлен:
27.05.2015
Размер:
1.37 Mб
Скачать

Визначення 1. Класи А і В знаходяться у відношенні "клієнт-постачальник", якщо одним з полів класу В є об'єкт класу А. Класс А називається постачальником класу В, клас В називається клієнтом класу А.

Визначення 2. Класи А і В знаходяться у відношенні "батько - спадкоємець", якщо при оголошенні класу В клас А вказаний як батьківський клас. Клас А називається батьком класу В, клас В називається спадкоємцем класу А.

Обидва відношення - спадкоємство і вкладеність - є транзитивними. Якщо В - клієнт А і C - клієнт В, то звідси витікає, що C - клієнт А. Якщо В - спадкоємець А і C - спадкоємець В, то звідси витікає, що C - спадкоємець А.

Визначення 1 і 2 задають прямих або безпосередніх клієнтів і постачальників, прямих батьків і спадкоємців. Внаслідок транзитивності необхідно ввести поняття рівня. Прямі клієнти і постачальники, прямі батьки і спадкоємці відносяться до відповідного рівня 1 (клієнти рівня 1, постачальники рівня 1 і так далі). Потім слідує рекурсивне визначення: прямий клієнт клієнта рівня k відноситься до рівня k+1.

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

Відмітимо, що ланцюжки вкладеності і спадкоємства можуть бути достатньо довгими. На практиці цілком можуть зустрічатися ланцюжки довжини 10. Наприклад, бібліотечні класи, складові системи Microsoft Office, повністю побудовані на відношенні вкладеності. При програмній роботі з об'єктами Word можна почати з об'єкту, задаючого застосування Word, і дістатися до об'єкту, задаючого окремий символ в деякому слові деякої пропозиції однієї з відкритих документів Word. Для вибору потрібного об'єкту можна задати такий ланцюжок: застосування Word - колекція документів - документ - область документа - колекція абзаців - абзац - колекція пропозицій - пропозиція - колекція слів - слово - колекція символів - символ. У цьому ланцюжку кожному поняттю відповідає клас бібліотеки Microsoft Office, де кожна пара класів, що є сусідами, зв'язана відношенням "постачальник-клієнт".

Класи бібліотеки FCL зв'язані як відношенням вкладеності, так і відношенням спадкоємства. Довгі ланцюжки спадкоємства достатньо характерні для класів цієї бібліотеки.

Відносини «є» і «має»

При проектуванні класів часто виникає питання, яке ж відношення між класами потрібно побудувати. Розглянемо зовсім простій приклад двох класів - Square і Rectangle, що описують квадрати і прямокутники. Напевно, зрозуміло, що ці класи слід зв'язати скоріше відношенням спадкоємства, чим вкладеності; менш зрозумілим залишається питання, а якого з цих двох класів слід зробити батьківським. Ще один приклад двох класів - Car і Person, що описують автомобіль і персону. Якими відносинами з цими класами повинен бути зв'язаний клас Person_of_Car, що описує власника машини? Чи може він бути спадкоємцем обох класів? Знайти правильні відповіді на ці питання проектування класів допомагає розуміння того, що відношення "клієнт-

постачальник" задає відношення "має" ("has"), а відношення спадкоємства задає відношення "є" ("is а"). У разі класів Square і Rectangle зрозуміло, що кожен об'єкт квадрат "є" прямокутником, тому між цими класами має місце відношення спадкоємства, і батьківським класом є клас Rectangle, а клас Square є його нащадком.

У разі автомобілів, персон і власників авто також зрозуміло, що власник "має" автомобіль і "є" персоною. Тому клас Person_of_Car є клієнтом класу Car і спадкоємцем класу Person.

61

Основи спадкоємства

C# підтримує спадкоємство, дозволяючи в оголошенні класу вбудовувати інший клас. Це реалізується за допомогою завдання базового класу при оголошенні похідного. Краще всього почати з прикладу. Розглянемо клас TwoDShape, в якому визначаються атрибути «узагальненої» двовимірної геометричної фігури (наприклад, квадрата, прямокутника, трикутника і т.д.)

class TwoDShape { public double width; public double height;

public void showDim(){

Console.WriteLine(“Ширина і висота дорівнюють “ + width + “ і “ + height);

}

}

Клас TwoDShape можна використовувати як базового для класів, які описують специфічні типи двовимірних об'єктів. Наприклад, в наступній програмі клас TwoDShape використовується для виведення класу Triangle.

using System;

class TwoDShape { public double width; public double height;

public void showDim(){

Console.WriteLine(“Ширина і висота рівні “ + width + “ і “ + height);

}

}

class Triangle : TwoDShape{

public string style; //Тип трикутника public double area(){

return width*height/2;

}

public void showStyle(){ Console.WriteLine(“Трикутник “+style);

}

}

class Shapes{

public static void Main(){ Triangle t1 = new Triangle(); Triangle t2 = new Triangle(); t1.width = 4.0;

t1.height = 4.0;

t1.style = “рівнобедрений”;

t2.width = 8.0; t2.height = 12.0;

t2.style = “прямокутний”;

Console.WriteLine(“Інформація про t1: ”); t1.showStyle();

t1.showDim();

Console.WriteLine(“Площа дорівнює: ” + t1.area());

Console.WriteLine();

Console.WriteLine(“Інформація про t2: ”); t2.showStyle();

t2.showDim();

Console.WriteLine(“Площа дорівнює: ” + t2.area());

}

}

62

Конструктори батьків і нащадків

Кожен клас повинен поклопотатися про створення власних конструкторів. Він не може в цьому питанні покладатися на батька, оскільки, як правило, додає власні поля, про які батько нічого не може знати. Звичайно, якщо не задати конструкторів класу, то буде доданий конструктор за умовчанням, що ініціалізував всі поля значеннями за умовчанням. Але це рідкісна ситуація. Найчастіше, клас створює власні конструктори і, як правило, не один, задаючи різні варіанти ініціалізації полів.

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

Виклик конструктора батька відбувається не в тілі конструктора, а в заголовку, поки ще не створений об'єкт класу. Для виклику конструктора використовується ключове слово base, що іменує батьківський клас. Як це робиться, покажемо на прикладі конструкторів класу Derived:

public Derived() {

public Derived(string name, int cred, int deb):base (name,cred)

{

debet = deb;

}

Для конструктора без аргументів виклик аналогічного конструктора батька мається на увазі за умовчанням. Для конструкторів з аргументами виклик конструктора з аргументами батьківського класу повинен бути явним. Цей виклик синтаксично слідує відразу за списком аргументів конструктора, будучи відокремлений від цього списку символом двокрапки. Конструктору нащадка передаються всі аргументи, необхідні для ініціалізації полів, частина з яких передаються конструктору батька для ініціалізації батьківських полів.

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

Додавання методів і зміна методів батька

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

public void DerivedMethod()

{

Console.WriteLine("Це метод класу Derived");

}

На відміну від незмінних полів класів-предків, клас-нащадок може змінювати успадковані ним методи. Якщо нащадок створює метод з ім'ям, співпадаючим з ім'ям методу предків, то можливі три ситуації:

63

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

перевизначення методу. Метод батька в цьому випадку повинен мати модифікатор virtual або abstract. При перевизначенні зберігається сигнатура і модифікатори доступу успадкованого методу;

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

уточнити ключовим словом base ім'я методу.

Метод нащадка, що приховує метод батька, слід супроводжувати модифікатором new, вказуючим на новий метод.

Віртуальні методи

Розглянемо три механізми, що забезпечують поліморфізм [30]. Під поліморфізмом в ООП розуміють здатність одного і того ж програмного тексту x.M виконуватися по-різному, залежно від того, з яким об'єктом пов'язана суть x. Поліморфізм гарантує, що метод M, що викликається, належатиме класу об'єкту, пов'язаному з сутністю x. У основі поліморфізму, характерного для сімейства класів, лежать три механізми:

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

перевизначення нащадком методу, успадкованого від батька. Завдяки перевизначенню, в сімействі класів існує сукупність поліморфних методів з одним ім'ям і сигнатурою;

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

Усукупності це і називається поліморфізмом сімейства класів. Поліморфізм реалізується через механізм віртуальних функцій (дивися Вправу 2).

Абстрактні класи

Із спадкоємством тісно пов'язаний ще один важливий механізм проектування сімейства класів - механізм абстрактних класів. Клас називається абстрактним, якщо він має хоч би один абстрактний метод. Метод називається абстрактним, якщо при визначенні методу задана його сигнатура, але не задана реалізація методу. Оголошення абстрактних методів і абстрактних класів повинне супроводжуватися модифікатором abstract. Оскільки абстрактні класи не є повністю визначеними класами, то не можна створювати об'єкти абстрактних класів. Абстрактні класи можуть мати нащадків, що частково або повністю реалізовують абстрактні методи батьківського класу. Абстрактний метод найчастіше розглядається як віртуальний метод, що перевизначається нащадком, тому до них застосовується стратегія динамічного скріплення. Далі дивися приклад #2 Вправи 2.

64

Лабораторна робота №6. Перевантаження операцій в C#.

Мета роботи: засвоїти основи перевизначення операцій в мові C#, набути практичних навичок розробки класів з перевантаженими операціями.

Варіанти завдання.

Варіант 1. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“+” - додати елемент в множину (типу char + set); “+” - об'єднання множин; “==” - перевірка множин на рівність.

Варіант 2.. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“-” - видалити елемент з множини (типу set-char); “*” - перетин множин; “<” - порівняння множин .

Варіант 3. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“-” - видалити елемент з множини(типу set-char); “>”- перевірка на підмножину; “!=” - перевірка множин на нерівність.

Варіант 4. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“+” - додати елемент в множину (типу set + char); “*” - перетин множин;

“int ()” - потужність множини.

Варіант 5. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“()” - конструктор множини (у стилі конструктора Паскаля); “+” - об'єднання множин; “<=” - порівняння множин .

Варіант 6. Абстрактний тип даних (АТД ) – множина з елементами типу char. Додатково перенавантажувати наступні операції:

“>” -проверка на приналежність (char in set Паскаля); “*” -перетин множин; “<” -проверка на підмножину.

Варіант 7. Абстрактний тип даних (АТД ) – однонаправлений список з елементами типу char. Додатково перенавантажувати наступні операції:

“+” – об’єднати списки (list+list);

--” - видалити елемент з початку (типу --list); “==” перевірка на рівність.

Варіант 8. Абстрактний тип даних (АТД ) – однонаправлений список з елементами типу char. Додатково перенавантажувати наступні операції:

“+” - додати елемент в початок (char+list);

--” - видалити елемент з початку(типу --list);

65

==” перевірка на рівність.

Варіант 9. Абстрактний тип даних (АТД ) – однонаправлений список з елементами типу char. Додатково перенавантажувати наступні операції:

“+” -добавить елемент в кінець(list+char); “--” -удалить елемент з кінця(типу list--); “!=” перевірка на нерівність.

Варіант 10.. Абстрактний тип даних (АТД ) – однонаправлений список з елементами типу char. Додатково перенавантажувати наступні операції:

“[]” -доступ до елементу в заданій позиції, наприклад: int i; char c;

list L; c=L[i];

“+” - об'єднати два списки; “==” перевірка на рівність.

Варіант 11. АДТ-однонаправлений список з елементами типу

char. Додатково

перенавантажувати наступні операції:

 

“[]” -доступ до елементу в заданій позиції, наприклад:

 

int i; char c;

 

list L;

 

c=L[i];

 

“+” - об'єднати два списки;

 

“!=” перевірка на нерівність.

 

Варіант 12. Абстрактний тип даних (АТД ) – однонаправлений список з елементами типу char. Додатково перенавантажувати наступні операції:

“()” - видалити елемент в заданій позиції, наприклад : int i;

list L; L[i];

“()” - додати елемент в задану позицію, наприклад : int i; char c;

list L; L[с,i];

“!=” перевірка на нерівність.

Варіант 13. Абстрактний тип даних (АТД ) – стек. Додатково перенавантажувати наступні операції:

“+” додати елемент в стек; “—“ витягнути елемент із стека;

bool() - перевірка чи порожній стек?

Варіант 14. Абстрактний тип даних (АТД ) – черга. Додатково перенавантажувати наступні операції:

“+” додати елемент ; “—“ витягнути елемент ;

bool() - перевірка чи порожня черга?

Варіант 15. Абстрактний тип даних (АТД ) – одновимірний масив (вектор) дійсних чисел. Додатково перенавантажувати наступні операції:

“+” додавання векторів (а[i]+b[i] для всіх i);

66

“[]” доступ по індексу;

“+” додати число до вектора (double+vector).

Варіант 16. Абстрактний тип даних (АТД ) – одновимірний масив (вектор) дійсних чисел. Додатково перенавантажувати наступні операції:

“-” віднімання векторів(а[i]-b[i] для всіх i); “[]” доступ по індексу;

“-” відняти з вектора число(vector-double).

Варіант 17. Абстрактний тип даних (АТД ) – одновимірний масив (вектор) дійсних чисел. Додатково перенавантажувати наступні операції:

“*” множення векторів(а[i]*b[i] для всіх i); “[]” доступ по індексу;

“*” помножити вектор на число (vector*double).

Варіант 18. Абстрактний тип даних (АТД ) – одновимірний масив(вектор) дійсних чисел. Додатково перенавантажувати наступні операції:

“int()” розмір вектора;

“()” встановити новий розмір;

“-” відняти з вектора число(vector-double); “[]” доступ по індексу;

Варіант 19. Абстрактний тип даних (АТД ) – одновимірний масив(вектор) дійсних чисел. Додатково перенавантажувати наступні операції:

“=” привласнити всім елементам вектора значення(vector=double); “[]” доступ по індексу; “==” перевірка на рівність; “!=” перевірка на нерівність;

Варіант 20. Абстрактний тип даних (АТД ) – двомірний масив (матриця) дійсних чисел. Додатково перенавантажувати наступні операції:

“()” доступ по індексу; “*” множення матриць;

“*” множення матриці на число; “*” множення числа на матрицю.

Варіант 21. Абстрактний тип даних (АТД ) – двомірний масив(матриця) дійсних чисел. Додатково перенавантажувати наступні операції:

“()” доступ по індексу; “-” різниця матриць;

“-” відняти з матриці число; “==” перевірка матриць на рівність.

Варіант 22. Абстрактний тип даних (АТД ) – двомірний масив (матриця) дійсних чисел. Додатково перенавантажувати наступні операції:

“()” доступ по індексу;

“=” привласнити всім елементам матриці значення(matr=double); “+” додавання матриць;

“+” скласти матрицю з числом(matr+double).

Хід роботи.

1. Виконати Вправу.

67

2. Виконати завдання згідно своєму варіанту.

Вправа. Клас Fraction раціональних чисел (дробів).

Розглянемо клас Fraction раціональних чисел (дробів) з перевантаженими операціями “+”, “-”, “*”, “/” ++ і – (у префіксній і інфіксній формах), операції унарний мінус [29].

public struct Fraction

{

private long mNumerator; //чисельник private long mDenominator; //знаменник

public Fraction(long aNumerator, long aDenominator)

{

// Cancel the fraction and make the denominator positive

long gcd = GreatestCommonDivisor( aNumerator, aDenominator); mNumerator = aNumerator / gcd;

mDenominator = aDenominator / gcd;

if (mDenominator < 0)

{

mNumerator = -mNumerator; mDenominator = -mDenominator;

}

}

private static long GreatestCommonDivisor( long aNumber1, long aNumber2)

{

aNumber1 = Math.Abs(aNumber1);

aNumber2 = Math.Abs(aNumber2); while (aNumber1 > 0)

{

long newNumber1 = aNumber2 % aNumber1; aNumber2 = aNumber1;

aNumber1 = newNumber1;

}

return aNumber2;

}

public static Fraction operator +(Fraction aF1, Fraction aF2)

{

long num = aF1.mNumerator*aF2.mDenominator + aF2.mNumerator*aF1.mDenominator;

long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom);

}

public static Fraction operator -(Fraction aF1, Fraction aF2)

{

long num = aF1.mNumerator*aF2.mDenominator - aF2.mNumerator*aF1.mDenominator;

long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom);

}

public static Fraction operator *(Fraction aF1, Fraction aF2)

{

long num = aF1.mNumerator*aF2.mNumerator;

long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom);

}

public static Fraction operator /(Fraction aF1, Fraction aF2)

68

{

long num = aF1.mNumerator*aF2.mDenominator; long denom = aF1.mDenominator*aF2.mNumerator; return new Fraction(num, denom);

}

// Unary minus operator

public static Fraction operator -(Fraction aFrac)

{

long num = -aFrac.mNumerator; long denom = aFrac.mDenominator; return new Fraction(num, denom);

}

// Explicit conversion to double operator

public static explicit operator double(Fraction aFrac)

{

return (double) aFrac.mNumerator / aFrac.mDenominator;

}

//Operator ++ (the same for prefix and postfix form) public static Fraction operator ++(Fraction aFrac)

{

long num = aFrac.mNumerator + aFrac.mDenominator; long denom = aFrac.mDenominator;

return new Fraction(num, denom);

}

//Operator -- (the same for prefix and postfix form) public static Fraction operator --(Fraction aFrac)

{

long num = aFrac.mNumerator - aFrac.mDenominator; long denom = aFrac.mDenominator;

return new Fraction(num, denom);

}

public static bool operator true(Fraction aFraction)

{

return aFraction.mNumerator != 0;

}

public static bool operator false(Fraction aFraction)

{

return aFraction.mNumerator == 0;

}

public static implicit operator Fraction(double aValue)

{

double num = aValue; long denom = 1;

while (num - Math.Floor(num) > 0)

{

num = num * 10; denom = denom * 10;

}

return new Fraction((long)num, denom);

}

public override string ToString()

{

if (mDenominator != 0)

{

return String.Format("{0}/{1}", mNumerator, mDenominator);

}

69

else

{

return ("NAN"); // not а number

}

}

}

class FractionsTest

{

static void Main()

{

Fraction f1 = (double)1/4; Console.WriteLine("f1 = {0}", f1); Fraction f2 = (double)7/10; Console.WriteLine("f2 = {0}", f2); Console.WriteLine("-f1 = {0}", -f1);

Console.WriteLine("f1 + f2 = {0}", f1 + f2); Console.WriteLine("f1 - f2 = {0}", f1 - f2); Console.WriteLine("f1 * f2 = {0}", f1 * f2); Console.WriteLine("f1 / f2 = {0}", f1 / f2); Console.WriteLine("f1 / f2 as double = {0}", (double)(f1 / f2));

Console.WriteLine(

"-(f1+f2)*(f1-f2/f1) = {0}", -(f1+f2)*(f1-f2/f1));

}

}

Методичні вказівки та теоретичні відомості.

Мова C# дозволяє визначити значення операції щодо створюваного класу. Цей процес називається перевантаженням операцій. Перенавантажуючи операцію, ви розширюєте її використання для класу.

Основи перевантаження операцій

Перевантаження операцій тісно пов'язане з перевантаженням методів. Для перевантаження операцій використовується ключове слово operator, що дозволяє створити операційний метод, який визначає дію операції, пов'язану з її класом.

Існує дві форми методів operator: одна використовується для унарних операцій, а інша – для бінарних.

//загальний формат перевантаження для унарної операції

public static тип_возврата operator op( тип_параметра операнд){ //тіло_метода

}

// загальний формат перевантаження для бінарної операції public static тип_возврата operator op(

тип_параметра1 операнд1, тип_параметра2 операнд2) { //тіло_метода

}

Тут елемент op – це операція (наприклад “+” або “/”), яка перевантажується. Елемент тип_возврата – це тип значення, що повертається при виконанні заданої операції. Для унарних операцій тип операнда повинен співпадати з класом, для якого визначена операція. Що стосується бінарних операцій, то тип хоч би одного операнда повинен співпадати з відповідним класом. Таким чином, C#-операции не можна перенавантажувати для класів, не створених вами. Наприклад, ви не можете перенавантажувати операцію “+” для типів int або string.

70