субота, 1 вересня 2012 р.

C#. Концепція та синтаксис. Частина 3


Класи та структури

Алгоритмічна мова 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             Метод переозначує успадкований віртуальний або абстрактний метод
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

1 коментар:

  1. можливість позики, запропонована містером Бенджаміном, яка рятує мою сім’ю від фінансової неволі {247officedept@gmail.com} привіт усім! Бенджамін, коли нас вигнали з дому, коли я вже не міг оплачувати рахунки, після того, як мене обманули різні компанії в Інтернеті та відмовили у позиці у моєму банку та іншій кредитній спілці, яку я відвідав. моїх дітей взяли в прийомні сім'ї, я був на вулиці один. день, коли я ганебно зайшов до старого шкільного товариша, який познайомив мене з маргариткою Морін. спочатку я сказав їй, що більше не готовий ризикувати запитом позики в Інтернеті, але вона запевнила мене, що отримаю свою позику від них. задумавшись, через свою безпритульність мені довелося взяти судовий розгляд та подати заявку на позику, на щастя для мене, я отримав позику у розмірі 80 000,00 доларів від пана Бенджаміна. Я щасливий, що ризикнув і подав заявку на позику. мої діти були повернені мені, і тепер я маю дім і власний бізнес. вся подяка та подяка йде на допомогу містеру Бенджаміну за те, що він дав мені сенс життя, коли я втратив будь-яку надію. якщо ви зараз шукаєте допомоги в позиці, ви можете зв’язатися з ними за адресою: {247officedept@gmail.com whatsapp + 1-989-394-3740.

    ВідповістиВидалити