Класи та структури
Алгоритмічна
мова C#
є цілковито об'єктно-орієнтованою. Програма на C# - це набір
класів та маніпулювання ними.
Класи
та структури дуже подібні між собою. Головні розбіжності між ними:
•
об'єкти класів мають тип даних за посиланням, а
структури - за значенням;
•
структури не підтримують спадковість;
•
для структури не можна оголосити конструктор за
замовчуванням;
•
для структури не можна оголосити деструктор;
•
неможливо використати ініціалізатори для надання
значень полям.
Оскільки
екземпляри структур розташовані у стеку, доцільно їх використовувати для
представлення невеликих об'єктів. Зокрема, клас Point, який
розглядатимемо нижче, з успіхом можна було б реалізувати як структуру. Вибір
класу зумовлено лише бажанням продемонструвати на простій логічній побудові
більше понять класу та структури.
Розглянемо
поняття класу на такому прикладі:
public
class Point { //статичне
поле
private
static uint count = 0; //поля
public
double x = 0; public double y = 0; //властивість лише для читання public
static uint Count { get {return count;}
}
//перекритий метод
public
override string ToString() { return "(" + x +
"," + y + ")";
}
// метод
public
void Set(double x, double y) {
this.x =
x;
this.y = y;
}
//конструктор без
параметрів public
Point () { count++ ;
}
//конструктор з параметрами public
Point(double x, double y) { count++ ; Set (x,y);
}
//конструктор копій
public
Point(Point a) : this(a.x,a.y) { }
//деструктор ~Point (
) { count—
;
}
Зауваження. В .NET Framework 2 з допомогою
модифікатора класу partial оголошення
класу можна розбити на декілька частин, які можна розташувати як в одному, так
і в різних файлах.
Дані
та функції всередині класу називають членами класу. Дані класу - це поля, константи та події, тобто ті члени класу, які містять дані для класу.
Поле - це довільна змінна, оголошена на рівні класу: count,
x,
y. Якщо поле оголошене як статичне (static), то
воно єдине для всього класу і жоден екземпляр об' єкта класу не матиме окремого
екземпляра цього поля. У протилежному випадку для кожного екземпляра об'єкта
цього класу утворюються власні незалежні екземпляри полів.
Клас Point описує
точку: кожен екземпляр класу - це точка на площині з координатами x та y. Статичне
поле count містить кількість утворених і
активних в поточний момент часу екземплярів класу.
Подія - це член класу, який дає змогу
об'єкту надіслати повідомлення про початок певної події (зміна значення поля,
взаємодія з користувачем і т.п.). Клієнт (код, який використовує об' єкт класу)
може містити код обробки події.
Функції класу - це ті члени класу, які забезпечують функціональність
для роботи з даними класу. Вони містять методи, властивості,
конструктори, деструктори, оператори та індексатори.
Методи
Методи
Види
функцій класу визначаються синтаксисом оголошення. Синтаксис оголошення методів
у C#
такий:
[модифікатори] тип_результату
НазваМетоду([параметри]){
//тіло методу
}
Модифікатори
методу аналогічні модифікаторам змінних. Додатково можна використовувати такі
модифікатори:
Модифікатор Опис
virtual Метод може бути
переозначений у дочірньому класі
abstract Віртуальний
метод, який визначає сигнатуру методу, однак не містить його реалізації. За
наявності абстрактних методів екземпляр класу не може бути утворений
override Метод переозначує успадкований віртуальний або абстрактний метод
override Метод переозначує успадкований віртуальний або абстрактний метод
sealed Метод переозначує
успадкований віртуальний метод і забороняє
його переозначення у дочірніх
класах. Використовують разом із override
extern Метод реалізований поза програмою на іншій мові
Клас Point містить
перекритий (override) метод ToString класу object.
Для
активізації методу потрібно вказати ім' я об' єкта, для якого активізують
метод, а після крапки - ім'я методу та список аргументів у дужках:
Point a =
new
Point(1,1); string s = a.ToString();
Для
активізації статичного методу потрібно зазначити назву класу, а не ім'я
об'єкта. Статичний метод може працювати лише зі статичними полями класу.
Якщо
метод активізується всередині класу, то назву класу не використовують.
Аргументи
методів можуть передаватися за посиланням або за значенням.
За
замовчуванням параметри передаються за значенням, тобто метод одержує копію
значення аргументу. Якщо параметр має тип даних за значенням, то довільні зміни
цього параметра всередині методу ніяк не вплинуть на значення аргументу-
оригіналу. Якщо ж параметр має тип за посиланням (масив, клас), то метод
працюватиме безпосередньо з даними аргументу, оскільки передана копія значення
- адреса розташування об' єкта.
Зауважимо,
що стрічки не поводять себе як тип за посиланням, отож зміни у стрічці, що
відбулися всередині методу, не зачіпають її оригінал.
Якщо
потрібно змінні за значенням передати в метод за посиланням, використовують
ключове слово ref перед типом параметра. Слово ref повинно
вказуватися і при виклику методу. Довільна модифікація змінної методом викликає
відповідні зміни аргументу-оригіналу.
Перед передачею
значень методу всі змінні-аргументи повинні бути ініціалізованими. Інакше компілятор
C# видасть
повідомлення про помилку. Обійти це обмеження можна шляхом використання
ключового слова out перед типом параметра. Слово out необхідно
зазначити і при виклику методу. Усередині методу параметрові, позначеному як out, необхідно
присвоїти значення.
Властивості
використовують з метою зробити виклик методу подібним на поле. їх і оголошують
подібно до поля. Однак додатково після оголошення у фігурних дужках
розташовують блок коду для контролю даних та реалізації потрібної функціональності.
Цей блок може мати два методи доступу: аксесор get та
аксесор set.
Клас Point має
властивість Count, яка повертає кількість утворених
екземплярів класу. Розширимо опис цієї властивості:
public
static uint Count { get {return count;}
set {if (value >=
0) count
= value;}
}
Зауважимо,
що аксесору set неявно передається параметр з іменем value такого
ж типу, як і властивість. Аналогічно, значення типу властивості повинен
повертати аксесор get.
Оскільки
властивість подібна до поля, то й активізацію її здійснюють подібно:
uint cnt = Point.Count; // аксесор get
Point.Count =
cnt; // аксесор set
Якщо властивість
містить лише аксесор get, то
її використовують тільки для читання значення. Якщо властивість містить лише
аксесор set, то її використовують тільки для
запису значення.
Конструктори
- це методи класу, які використовуються разом з оператором new для
утворення об'єкта.
Якщо
клас не містить власного конструктора, то компілятор утворить конструктор за
замовчуванням (без параметрів).
Конструкторів
може бути кілька. Вони відрізнятимуться кількістю і типом параметрів, однак
матимуть одну й ту ж назву - ім'я класу. Клас Point містить
три конструктори. Перший із них збільшує лічильник утворених об'єктів, а другий
додатково ініціалізує поля x та y. Третій
конструктор public Point (Point a) належить до
так званих конструкторів копій, оскільки ініціалізує
екземпляр класу на основі значень іншого екземпляра цього ж класу.
Для
утворення об'єкта можна використати довільний з конструкторів класу, проте лише
один.
Конструктор
не може повертати значення, отож тип результату не вказують при визначенні
конструктора.
Якщо
конструктор оголошений як private, то
його можна використовувати лише всередині класу, а якщо як protected - то
в класах-нащадках. Обмеження доступу до конструктора може бути корисним,
наприклад, для методів Clone (), Copy () або для аналогічних методів класу, яким потрібно
утворювати інші екземпляри цього класу. Очевидно, що клас повинен мати хоча б
один конструктор з доступом public, якщо
передбачається утворення об' єктів цього класу клієнтським кодом.
Клас може також мати
статичний конструктор без параметрів. Такий конструктор буде використано лише
один раз. Його можна використати для ініціалізації статичних змінних.
Наприклад,
static Point()
{ count
= 0; }
Статичний
конструктор не має модифікатора доступу, оскільки він активізується лише
середовищем .NET при завантаженні класу.
Зауважимо,
що клас може водночас мати конструктор екземплярів без параметрів і статичний
конструктор. Це єдиний випадок, коли може бути два методи класу з однаковими
назвами та однаковим списком параметрів.
Конструктор може викликати
інший конструктор цього ж класу. Для цього випадку існує спеціальний синтаксис:
public
Point() : this(0,0)
{
// додатковий код
}
Такий
синтаксис повідомляє компілятору, що у випадку використання конструктора
спочатку потрібно виконати інший конструктор цього класу з переданими йому
аргументами після ключового слова this, а
потім виконати код (якщо є) зазначеного конструктора.
Ключове слово this вказує,
що використовують елемент поточного класу. Якщо ми використаємо ключове слово base, то
дамо вказівку шукати відповідний елемент у базовому класі (клас- предок).
Деструктор
класу використовують для виконання дій, необхідних при знищенні екземпляра
класу. У нашому прикладі класу Point деструктор
зменшує лічильник екземплярів класу.
Як
і конструктор, деструктор має те ж ім' я, що й клас, однак з префіксом тильда (~): -Point. Деструктор
не має параметрів та не повертає результат.
Явним
чином деструктори у C# не викликають. Виконання коду деструктора ініціюється
механізмом прибирання „сміття". Отож не можна передбачити, коли буде
виконано код деструктора.
Ініціювати негайне
прибирання „сміття"
можна з допомогою методу Collect() об'єкта .NET System.GC, який
реалізує „прибиральника". Однак цим доцільно користуватися лише у випадку, коли
ви впевнені в необхідності
такого кроку.
Якщо
при знищенні екземпляра класу необхідно негайно звільнити ресурси, зайняті
екземпляром класу (наприклад, закрити файл) або надіслати повідомлення іншим
об'єктам, доцільно утворити спеціальні методи. Типові назви таких методів - Close
та Dispose. Клієнтський
код повинен явно активізувати ці методи. І це є недоліком такого підходу.
Інший варіант звільнення
ресурсів - використання оператора using. У цьому випадку клас повинен
успадковувати інтерфейс IDisposable, означений у просторі імен System:
class Point :
IDisposable
{
//--------
public
void Dispose () { // код
}
}
Нехай
задано точки A(xi,yi) та B(x2,y2). Точку C(x,y) назвемо сумою точок A та
B, якщо x= x1+ x2, y= y1+ y2.
Для
додавання точок у класі Point можна
утворити метод. Наприклад:
public
Point Add(Point p) {//код}; Тоді додавання виглядатиме так: C =
A.Add(B);
Якщо потрібно додати
декілька точок, то вираз ускладниться. Значно зручніше використовувати звичний
нам синтаксис для простих типів:
C = A + B;
C# дає змогу
перевантажувати операції. Наприклад, до означення класу Point можна
додати такий код:
public
static Point operator + (Point p1, Point p2) {
return new Point(p1.x + p2.x,
pl.y + p2.y);
}
Операція
оголошується аналогічно методу, за винятком того, що замість імені методу
пишуть ключове слово operator і
знак операції. Тепер, якщо A, B та C мають
тип Point, то ми можемо записати: C
= A + B;
Перевантажимо тепер
операцію множення на число.
public static Point operator * (double a, Point p) {
return new Point(a * p.x,
a * p.y);
}
public static Point operator * (Point p, double a) {
return a*p;
}
Для
операції множення ми утворили два варіанти перевантаження, щоб компілятор
коректно сприймав, наприклад,
код 10 *A та A*10.
Зауважимо,
що перевантаження операцій +, -, * та / використовуються компілятором для реалізації операцій +=, -=, *= та /= відповідно.
У C# є
шість операторів порівняння, які утворюють три пари:
== та != > та <= < та >=
C# вимагає
перевантаження операцій порівняння тільки парами. Окрім цього, у випадку
перевантаження == та ! =
потрібно також перекрити метод Equals(), успадкований
від System.Object.
Наведемо
приклад перевантаження операцій порівняння:
public static bool operator == (Point a, Point b) { return
a.x == b.x && a.y == b.y ? true
: false;
}
public static bool operator != (Point a, Point b){ return !(a == b);
}
public
override bool Equals(object obj) {
return
(obj is Point) && (this == (Point)obj);
}
public override int GetHashCode() { return ToString().GetHashCode();
}
У
цьому коді перекривається метод GetHashCode класу
object. Поки
що цей метод не є предметом нашого розгляду. Його наведено з метою уникнення
повідомлення від компілятора, що при перекритті методу Equals потрібно
також перекрити GetHashCode.
C# дає змогу
перевантажувати лише такі операції:
Категорія
|
Операції
|
|
Арифметичні
|
+ - * / % ++ —
|
|
Бітові
|
<< >> & | А ! ~
|
true false
|
Порівняння
|
== != < >= <= >
|
Перевантаження
методів використовують у тому випадку, коли потрібно, щоб клас виконував деякі
дії, але при цьому
існувало
кілька способів передачі інформації методу, який виконує завдання.
Ми вже
демонстрували перевантаження методів на прикладі конструкторів класу Point -
одна назва за різних наборів параметрів.
Наведемо ще один приклад - перевантаження методу
ToString:
public string
ToString(string format) { return String.Format(format,x,y) ;
}
Тепер
можна записати код:
Point p = new Point(1,1);
string s = p.ToString();
//s = "x=1 y=1"
s = p.ToString("x:{0}
y:{1}",
x, y); //s = "x:1 y:1"
Кількість
перевантажених методів не обмежена. Тобто клас може містити багато методів з
одним іменем, однак вони повинні вирізнятися кількістю, порядком або типом
параметрів. Очевидно, що не доцільно давати однакове ім'я методам, які
виконують зовсім різні задачі.
Якщо
базовий клас уже містить метод із заданим іменем, а кількість, тип і порядок
параметрів збігаються, то можливі два варіанта:
•
якщо метод віртуальний, його можна перекрити
(механізм
перекривання
розглянуто у розділі „Похідні класи");
•
використати модифікатор new в
оголошенні методу.
Розглянемо клас, який
реалізує функціональність роботи з масивом точок. Наведемо код початкового
варіанта такого класу:
public class Points {
private
readonly uint count; protected Point[] points; public uint Count {
get {
return count; }
}
public
override string ToString() { string Result = ""; for (int
i = 0; i < count; i++)
Result +=
points[i]
+ " "; return Result;
}
public
Points(uint count) { this.count = count; points =
new
Point[count]; for (int i = 0; i < count; i++)
points[i] = new Point();
}
public
Points(Points pts): this(pts.count) { for (int i = 0; i < count;
i++) points[i] = pts.points[i];
}
}
Клас Points містить
масив точок points (елементів типу Point). Розмірність
масиву задається параметром конструктора і встановлюється при утворенні об'
єкта класу:
points = new
Point[count];
Зверніть
увагу, що цей код виокремить пам'ять для розташування count вказівників
на об'єкти Point, а не власне об'єктів. Тим більше, що
самих об'єктів ще не існує. їх утворюють такі дві стрічки:
for (int
i = 0; i < count; i++) points[i] = new Point();
Властивість Count повертає
значення розмірності. Оскільки клас містить конструктори, то конструктор за
замовчуванням не утворюється. Отож для утворення об'єкта класу можна
використати або конструктор з параметром, який задає розмірність, або
конструктор копій.
Індексатори
дають змогу здійснювати доступ до об' єкта так, ніби він є масивом. Індексатори
означуються приблизно так, як властивості - з використанням функцій get та set. Однак
замість імені індексатора використовують ключове слово this.
Якщо ps - об'єкт типу Points, то
для доступу до точки з індексом 0 ми повинні
використовувати синтаксис:
ps.points [0]. Значно елегантніше було б застосувати ps [0], однак для цього потрібно додати індексатор.
Щоб оголосити індексатор
для класу Points, додамо до
його опису такий код:
public
Points this[int i] { get {
if (i >= 0
&& i < count)
return
points[i]; else
throw new
IndexOutOfRangeException( "Вихід за допустимий діапазон
індексів"+ i);
}
set {
if (i >= 0
&& i < count)
points[i]=value;
else
throw new
IndexOutOfRangeException( "Вихід за допустимий діапазон
індексів"+ i);
}
}
Тепер
для змінної ps типу Points ми
можемо використати
код:
string s = "x0="
+ ps[0].x + " y0=" + ps[0].y;
Індексатори не є обмежені
одномірними масивами та цілочисельними індексами. Наприклад, допустимим є такий
код:
public
bool this[int i, string s] { get {
switch
(i) {
case 0:
switch
(s) {
case
"AA": return true; default: return false;
}
break;
Для індексаторів
можна застосовувати цикли for, do та
while, однак
не можна написати цикл foreach, оскільки
він працює лише з колекціями, а не з масивами.
Інтерфейс - це список оголошень методів, властивостей, подій та
індексаторів. Оголошення інтерфейсу подібне до класу, однак не містить
модифікаторів доступу для членів і реалізацій. Інтерфейс не може мати
конструкторів. Отож об'єкт інтерфейсу не можна утворити.
Наприклад, інтерфейс IEnumerator із
простору імен System
.Collections оголошений так:
interface
IEnumerator { //властивість
object Current {get;} //методи
bool
MoveNext(); void Reset();
}
Кажуть,
що клас підтримує інтерфейс, якщо він містить реалізацію усіх оголошень
інтерфейсу. Зокрема, клас підтримує інтерфейс IEnumerator, якщо
він містить реалізацію властивості Current і методів MoveNext і Reset.
За
домовленістю назва інтерфейсу починається літерою I .
У
попередньому пункті ми оголосили індексатор для класу Points і зазначили,
що до нього не можна застосувати цикл foreach. Для того щоб
клас Points підтримував колекції, він повинен
виконати наперед оголошену домовленість: містити метод з назвою GetEnumerator, який
повертає об'єкт деякого класу з підтримкою інтерфейсу IEnumerator. Це
правило формалізується інтерфейсом IEnumerable, оголошеним
у просторі імен System. Collections так:
public
interface IEnumerable { IEnumerator GetEnumerator();
}
Перед
тим як додати підтримку цього інтерфейсу до класу Points, утворимо
допоміжний клас PointsEnum:
class
PointsEnum : IEnumerator { int location = -1; Points points; //конструктор класу public
PointsEnum(Points points) { this.points = points; location =
-1;
}
//реалізація членів інтерфейсу
IEnumerator
public object Current { get {
if (location < 0 || location >= points.Count) throw new
InvalidOperationException( "Некоректний
індекс"); return points[location];
}
}
public
bool MoveNext() { ++location;
return (location >= points.Count) ? false:true;
}
public
void Reset() { location = -1;
}
}
Код class
PointsEnum : IEnumerator вказує, що клас підтримує інтерфейс IEnumerator, тобто
містить реалізацію його членів. Якщо необхідно, щоб клас підтримував декілька
інтерфейсів, то після двокрапки треба перелічити назви цих інтерфейсів,
розділені комами. І, відповідно, реалізувати всі члени цих інтерфейсів.
Клас PointsEnum працює
з об'єктом типу Points, який
передається параметром конструктора.
Додамо тепер до
класу Points підтримку інтерфейсу IEnumerable:
public class Points : IEnumerable {
public
IEnumerator GetEnumerator() { return new PointsEnum(this);
}
}
Код new
PointsEnum(this) утворює об'єкт типу PointsEnum, а метод
повертає значення типу IEnumerator. Оскільки
клас PointsEnum підтримує
цей інтерфейс, то протиріччя тут не буде.
Тепер
компілятор не заперечуватиме проти використання foreach:
Points q = new
Points(5);
string s =
"";
foreach
(Point p in q) s += p.ToString();
Зауваження.
Компілятор C# для версії .NET Framework 2 роботу щодо утворення
допоміжного класу з інтерфейсом IEnumerator виконує
самостійно. Для підтримки колекції класом Points клас PointsEnum можна
не утворювати, а метод GetEnumerator реалізувати приблизно так:
public IEnumerator
GetEnumerator() {
for (int
i = 0; i < points.Count; i++) yield return points[i]
}
Інтерфейс може
успадковувати один або декілька інших. Наприклад: interface IInterface3 : IInterface1,
IInterface2 {
}
У цьому випадку
клас, який підтримує інтерфейс IInterface3, повинен
містити реалізацію всіх членів успадкованих інтерфейсів IInterface1 та IInterface2.
Клас
- це базовий інструмент об'єктно-орієнтованого програмування
(ООП). Ми розглянули поняття класу та деякі його елементи. Зокрема, інкапсуляцію
(об' єднання даних і методів їхньої обробки).
У цьому розділі
предметом розгляду будуть дві інші характеристики ООП:
успадкування та поліморфізм. Зазначимо,
що структури не підтримують успадкування та поліморфізм.
Побудуємо клас, який
представлятиме геометричний трикутник. Трикутник визначається трьома точками на
площині. Використаємо клас Points, який
дає змогу будувати множину точок і проводити деякі дії над ними.
public
class Triangle : Points {
public
Triangle(double x1, double y1, double x2,
double
y2, double x3, double y3)
:base(3)
{
points[0].Set(x1,
y1); points[1].Set(x2, y2); points[2].Set(x3, y3);
}
public Triangle
(Point p1, Point p2, Point p3) :
base (3)
{
points[0] = p1;
points [1] = p2; points[2] = p3;
}
public
override string ToString() {
return "трикутник " + base.ToString();
}
}
Код class
Triangle:Points оголошує, що клас Triangle успадковує
клас Points. Це означає,
що Triangle має всі компоненти класу Points: поля count,
points, метод ToString та
індексатор. Окрім того, оскільки клас Points успадковує
клас object (як і всі типи C#), то клас Triangle має
також усі компоненти класу object.
Класи object і Points є
класами-предками для класу Triangle.
Клас Points (клас,
який зазначено після двокрапки в оголошенні нового класу) називають базовим або батьківським класом для
класу Triangle.
Клас Triangle називають похідним або дочірнім для класу Points. А
для класів object і Points він
буде нащадком.
Похідний
клас може безпосередньо використовувати всі члени базового класу, якщо вони
означені з модифікаторами protected або public.
Однак
похідний клас не наслідує конструкторів базового класу (проте може використати,
як це зроблено в нашому прикладі). Єдиний виняток - це конструктор за
замовчуванням, який викликається конструктором за замовчуванням похідного
класу.
Якщо
похідний клас наслідує всі члени предків, то об'єкт цього класу містить
підмножину, яку можна розглядати як об' єкт деякого класу-предка. Наприклад:
Triangle
T = new Triangle(1,1,2,2,3,3); Points P = (Points)T;
object
obj = (object)T; IEnumerable ienum = (IEnumerable)T;
C# дає змогу
замінити члени базового класу в нащадках. Розглянемо такі два класи:
public class A {
public int x = 0; }
public
class B: A {
public
int x = 1; }
Клас B успадковує
клас A, отже - і поле x. Однак
у класі B оголошене нове поле з ідентичним
іменем. У цьому випадку компілятор не є упевненим, що він розуміє логіку
програміста, отож видасть повідомлення щодо своїх сумнівів. Проте код буде все
ж скомпільовано.
Надання новим членам
похідного класу імен, вже використаних у базовому - потенційна небезпека
помилок. Однак інколи така потреба виникає. У цьому випадку потрібно приховати
компонент x класу A, оголосивши
явно компонент x класу B новим
за допомогою ключового слова new:
public class B: A {
public new int x =
1; }
Для об'єкта класу B можемо
отримати значення обох компонентів x:
B b = new B ();
int bx = b.x; //bx набуде значення 1
int ax = ((A)b).x; //ax набуде
значення 0
Приховування
методів може стати необхідним у випадку конфлікту версій базового класу.
Припустимо, що програміст A розробив
базовий клас A, а програміст B на
основі класу A - клас B, у який
додав новий метод з назвою M. Через
деякий час A дописує в класі A новий
метод з назвою M та публікує нову версію. Після
перекомпілювання програми результат виконання програми може бути не таким, як
очікував B.
Оскільки компілятор C# відстежує
такі ситуації, то він видасть відповідне повідомлення. Програміст B має два варіанта дій. Якщо він контролює
усі класи, породжені від класу B, то
краще перейменувати свій метод M. Якщо
ж клас B опублікований для використання іншими
користувачами, то до оголошення методу M необхідно
додати модифікатор new.
Нехай
проектується деякий клас A. Вважають,
що всі його дочірні класи повинні мати деякий метод M. Однак
на рівні класу A недостатньо інформації для змістовного
означення цього методу. Якщо існує необхідність присутності методу M у
класі A, то цей метод можна оголосити з
модифікатором abstract без
реалізації. У цьому випадку метод M називатиметься абстрактним. Якщо клас містить абстрактні методи, то він
повинен також містити модифікатор abstract. Наприклад:
abstract
public class A { abstract public void M();
}
Зауважимо
також:
•
неможливо утворити об' єкт абстрактного класу;
•
неможливо оголосити конструктор абстрактним
методом;
•
абстрактні класи використовуються для породження
інших класів;
•
дочірні класи (якщо вони не є також
абстрактними) зобов' язані містити реалізацію усіх абстрактних методів,
успадкованих від базового класу.
Продовжимо
розгляд класу Points. Об'єкт цього
класу містить індексовану множину точок. Ці точки можна розглядати як вузли
ламаної лінії. Тоді можна ввести поняття довжини та оголосити метод GetLength, який
повертає цю довжину.
Похідний
від Points клас Triangle успадкує
цей метод GetLength. Однак
для трикутника довжина - це периметр. А успадкований GetLength не
враховує в довжині пряму, що з'єднує останню точку з першою.
Якщо може виникнути
потреба у зміні реалізації деякого методу в дочірніх класах, його потрібно
оголошувати віртуальним. З цією метою використовують
модифікатор virtual. Додамо метод GetLength до
класу Points:
public class Points :
IEnumerable
{
public
double GetDistance(Point p1, Point p2){ return Math.Sqrt(
(pl.x -
p2.x)
* (pl.x - p2.x) + (pl.y - p2.y) * (pl.y -
p2.y));
}
public
virtual double GetLength() { double length = 0;
for (int
i = 0; i < count - 1; i++) length +=
GetDistance
(Points[i],Points[i+1]); return length;
}
}
Метод GetLength тут
оголошено віртуальним, оскільки поняття довжини може змінюватися в
класах-нащадках. А от відстань між точками навряд чи потребуватиме
переозначення. Тому метод GetDistance не
описано як віртуальний.
Ми
вже розглядали перекривання віртуальних методів у класі Point, де
перекривається успадкований від object віртуальний
метод ToString:
public override
string ToString()
У
свою чергу в класі Points з таким самим синтаксисом
перекривається успадкований уже від Point віртуальний
метод
ToString.
Щоб
перекрити віртуальний метод, означений у базовому класі, необхідно в похідному
класі повторити оголошення методу, але модифікатор virtual замінити
на модифікатор override.
Очевидно, що потрібно
також написати нову реалізацію методу. Наприклад:
public class Triangle : Points {
public
override double GetLength() { return base.GetLength() + GetDistance(points[Count-1],
points[0]);
}
}
C# має
модифікатор sealed, який
використовують в парі з override і який дає вказівку заборонити перекривання
методу в дочірніх класах - запечатує.
Механізм віртуальних функцій реалізує концепцію
поліморфізму об' єктно-орієнтованого програмування. Розглянемо такі два класи:
public class A {
public
string Method() { return "A.Method"; } public
virtual string VirtualMethod() { return "A.VirtualMethod"; }
}
public class B: A {
public new string Method() { return
"B.Method"; } public
override string VirtualMethod() { return "B.VirtualMethod"; }
}
Клас B приховує
успадкований метод Method, оголосивши
новий з ідентичним іменем. А віртуальний метод VirtualMethod клас A перекриває.
Оголосимо змінні:
A a = new A ();
B b
= new B (); A x = b;
Змінні a та b містять
адреси утворених екземплярів, відповідно, класів A та B. Змінна x містить
адресу того ж об'єкта, що й b, але
має тип класу A. Наступний код демонструє особливості
віртуальних функцій:
string s;
s =
a.Method();
//"A.Method"
s = b.Method(); //"B.Method" s = x.Method();
//"A.Method"
s = a.VirtualMethod(); //"A.VirtualMethod" s = b.VirtualMethod();
//"B.VirtualMethod"
s = x.VirtualMethod(); //"B.VirtualMethod" s = ( (A)b)
.VirtualMethod() ;
//"B.VirtualMethod"
Якщо
метод не є віртуальним, компілятор використовує той тип, який змінна мала при
оголошенні. У нашому випадку x має
тип A. Отож код x.Method() викличе
метод класу A, хоча реально x є
посиланням на об' єкт класу B.
Якщо
метод є віртуальним, компілятор згенерує код, який під час виконання
перевірятиме, куди насправді вказує посилання, і використовуватиме методи
відповідного класу. Хоча x має
тип A, викликається метод VirtualMethod класу B. Окрім
того, навіть явне приведення типу до A ситуацію
не змінює.
Використаємо описану
властивість поліморфізму для означення функції, яка повертає довжину об'єкта
класу Points або Triangle.
public
double PointsLength(Points points) { return points.GetLength();
}
Оскільки метод GetLength є
віртуальним у класах Points та Triangle, то
функція PointsLength повертатиме
коректні значення довжини для об'єктів різних типів:
Points Ps
= new Points(3); Ps[0] .Set (0, 0) ; Ps[1] .Set (0,
3); Ps[2]
.Set
(4, 0) ;
Triangle
T = new Triangle(Ps[0], Ps[1],
Ps [2]);
double pl
= PointsLength(Ps); // pl = 8
double tl
= PointsLength(T); // tl = 12
можливість позики, запропонована містером Бенджаміном, яка рятує мою сім’ю від фінансової неволі {247officedept@gmail.com} привіт усім! Бенджамін, коли нас вигнали з дому, коли я вже не міг оплачувати рахунки, після того, як мене обманули різні компанії в Інтернеті та відмовили у позиці у моєму банку та іншій кредитній спілці, яку я відвідав. моїх дітей взяли в прийомні сім'ї, я був на вулиці один. день, коли я ганебно зайшов до старого шкільного товариша, який познайомив мене з маргариткою Морін. спочатку я сказав їй, що більше не готовий ризикувати запитом позики в Інтернеті, але вона запевнила мене, що отримаю свою позику від них. задумавшись, через свою безпритульність мені довелося взяти судовий розгляд та подати заявку на позику, на щастя для мене, я отримав позику у розмірі 80 000,00 доларів від пана Бенджаміна. Я щасливий, що ризикнув і подав заявку на позику. мої діти були повернені мені, і тепер я маю дім і власний бізнес. вся подяка та подяка йде на допомогу містеру Бенджаміну за те, що він дав мені сенс життя, коли я втратив будь-яку надію. якщо ви зараз шукаєте допомоги в позиці, ви можете зв’язатися з ними за адресою: {247officedept@gmail.com whatsapp + 1-989-394-3740.
ВідповістиВидалити