C#. Пізнє та раннє зв’язування. Поліморфізм. Основні поняття. Приклади. Передача в метод посилання на базовий клас




Пізнє та раннє зв’язування. Поліморфізм. Основні поняття. Приклади. Передача в метод посилання на базовий клас. Ключові слова virtual, override, new

Дана тема є продовженням теми:


Зміст


Пошук на інших ресурсах:

1. Поняття пізнього та раннього зв’язування. Ключові слова virtual, override

При вивченні теми поліморфізму важливо зрозуміти поняття пізнього та раннього зв’язування, яке використовується компілятором при побудові коду програми у випадку спадковості.

Якщо класи утворюють ієрархію успадкування, то при звертанні до елементів класу, компілятор може реалізовувати один з двох можливих способів зв’язування коду:

  1. Раннє зв’язування – пов’язане з формуванням коду на етапі компіляції. При ранньому зв’язуванні, програмний код формується на основі відомої інформації про тип (клас) посилання. Як правило, це посилання на базовий клас в ієрархії класів.
  2. Пізнє зв’язування – пов’язане з формуванням коду на етапі виконання. Якщо в ієрархії класів зустрічається ланцюг віртуальних методів (з допомогою слів virtual, override), то компілятор будує так зване пізнє зв’язування. При пізньому зв’язуванні виклик методу відбувається на основі типу об’єкту, а не типу посилання на базовий клас. Пізнє зв’язування використовується, коли потрібно реалізувати поліморфізм.

Вибір того чи іншого виду зв’язування для кожного окремого елементу (методу, властивості, індексатора тощо) визначається компілятором за такими правилами:

  • якщо в ієрархії успадкованих класів оголошується невіртуальний елемент, то реалізується раннє зв’язування;
  • якщо в ієрархії успадкованих класів оголошується віртуальний елемент, то виконується пізнє зв’язування (рисунки 1, 2). Віртуальний елемент в базовому класі позначається ключовим словом virtual, в усіх успадкованих класах ключовим словом override. У C# віртуальним елементом може бути метод, подія, індексатор чи властивість.

Необхідні умови для реалізації пізнього зв’язування:

  • класи повинні утворювати ієрархію успадкування;
  • у класах повинні бути методи з однаковою сигнатурою. Елементи (методи) похідних класів повинні перекривати (override) відповідні елементи (методи) базових класів;
  • елементи (методи) класу повинні бути віртуальними, тобто повинні бути позначені ключовим словами virtual, override.

На рисунку 1 наведено приклад, який відображає відмінність між пізнім та раннім зв’язуванням на прикладі двох класів A, B в яких реалізовано метод Print().

C#. Спадковість. Пізнє та раннє зв’язування. Відмінності

Рисунок 1. Пізнє та раннє зв’язування. Відмінності

У випадку раннього зв’язування, як тільки компілятор зустрічає рядок

A refA;

відбувається оголошення посилання refA, яке має тип базового класу A. Подальше присвоювання

refA = objB;

зв’язує посилання з об’єктом objB, однак тип посилання встановлюється A. Тому виклик

refA.Print();

викличе метод Print() класу A.

У випадку пізнього зв’язування, спочатку на основі опису класів A, B компілятор визначає, що метод Print() є віртуальним. Для віртуального методу компілятор будує таблицю віртуальних методів Print(), яка містить зміщення адрес кожного віртуального методу для кожного класу ієрархії (це окрема тема для дослідження).

Після рядка

A refA;

формується зв’язування посилання refA з типом A. Після присвоювання

refA = objB;

компілятор присвоює посиланню refA адресу екземпляру objB і визначає тип зв’язування як тип B (тому що метод Print() віртуальний). За основу береться тип об’єкту. У результаті посилання refA зв’язується з методом Print(), що реалізований у класі B (а не в класі A) – виконується так зване “пізнє зв’язування”.

Як наслідок, після виклику

refA.Print();

буде викликано метод Print() класу B.

На рисунку 2 наведено відмінність між пізнім та раннім зв’язуваннями на прикладі трьох класів A, B, C, в кожному з яких оголошено метод Print().

C#. Спадковість. Раннє та пізнє зв’язування на прикладі трьох класів

Рисунок 2. Раннє та пізнє зв’язування для методу Print() на прикладі трьох класів A, B, C.

Виклик методу Print() з посилання на об’єкт класу C

 

2. Що таке поліморфізм? Динамічний поліморфізм

Поліморфізм – це властивість програмного коду змінюватись в залежності від ситуації, що виникає в момент виконання програми.

Головний принцип поліморфізму – один інтерфейс, багато реалізацій (методів). У термінах мови програмування, поліморфізм – це можливість, з допомогою посилання на базовий клас, звертатись до елементів (методів) екземплярів успадкованих класів єдиним уніфікованим способом.

Використання переваг поліморфізму можливе у ситуаціях:

  • коли класи утворюють ієрархію з допомогою концепції успадкування;
  • коли в класах, що утворюють ієрархію, є елементи (методи, властивості тощо) з однаковою сигнатурою. У таких випадках виникає поняття “перевизначення методу” (method override).

У мові програмування C# поліморфізм забезпечується з допомогою ключових слів virtual та override. Завдяки використанню цих ключових слів забезпечується динамічний поліморфізм. Термін “динамічний” означає, що виклик віртуального елементу здійснюється динамічно під час виконання програми в залежності від типу об’єкту, для якого цей елемент викликається.

 

3. Для яких елементів класу можна реалізувати поліморфізм?

Поліморфізм можна застосувати для наступних елементів:

  • методів;
  • властивостей;
  • індексаторів;
  • подій.


 

4. Схематичне пояснення поліморфізму

На рисунку 3 демонструється застосування поліморфізму на прикладі двох класів.

C#. Спадковість. Реалізація поліморфізму на прикладі двох класів A, B

Рисунок 3. Реалізація поліморфізму на прикладі двох класів A, B

 

5. Поліморфізм у випадку передачі в метод посилання на базовий клас. Пізнє зв’язування

У будь-який метод може бути передане посилання на базовий клас. З допомогою цього посилання також можна викликати методи, властивості які підтримують поліморфізм.

Приклад.

Задано 2 класи з іменами Base та Derived. Клас Derived успадковує клас Base. В обох класах є віртуальний метод Info(), який виводить інформацію про клас. Виклик віртуального методу Info() здійснюється зі статичного методу ShowInfo() класу Program. Метод ShowInfo() отримує вхідним параметром посилання на базовий клас і за цим посиланням звертається до методу Info() класу. В залежності від того, екземпляр якого класу передається в метод ShowInfo(), відповідний метод Info() викликається.

using System;

namespace ConsoleApp5
{
  // Передача в метод посилання на базовий клас
  class Base
  {
    // Віртуальний метод Info()
    public virtual void Info()
    {
      Console.WriteLine("Base.Info()");
    }
  }

  class Derived : Base
  {
    // віртуальний метод Info() успадкованого класу
    public override void Info()
    {
      Console.WriteLine("Derived.Info()");
    }
  }

  class Program
  {
    // Статичний метод ShowInfo() - отримує посилання
    // на базовий клас Base в якості параметру
    static void ShowInfo(Base r)
    {
      // Виклик методу Info() за його посиланням.
      // У цьому методі невідомо, на екземпляр якого класу
      // вказує r: на Base чи на Derived?
      // Компілятор згенерує код на етапі виконання - цей процес
      // називається пізнім зв'язуванням.
      r.Info(); // єдиний інтерфейс (виклик) для усіх реалізацій екземплярів
    }

    static void Main(string[] args)
    {
      // Демонстрація поліморфізму та пізнього зв'язування
      // 1. Оголосити посилання на базовий клас
      Base rB;

      // 2. Створити екземпляри класів Base та Derived
      Base oB = new Base();
      Derived oD = new Derived();

      // 3. Виклик методу ShowInfo() з передачею параметру rB,
      //    який вказує на екземпляр oB класу Base
      rB = oB;
      ShowInfo(rB); // викликається Base.Info()

      rB = oD;
      ShowInfo(rB); // викликаєтсья Derived.Info()

      // 4. Виклик методу ShowInfo(), у метод передаються
      //    об'єкти oB та oD

      ShowInfo(oB); // викликається Base.Info()
      ShowInfo(oD); // викликається Derived.Info()

      // Висновок: метод ShowInfo() на етапі компіляції не знає,
      // екземпляр якого класу йому буде передано як параметр.
      // Метод ShowInfo() реалізує єдиноподібний виклик методу
      // Info: r.Info() для усіх екземплярів, що йому будуть передані -
      // це називається "один інтерфейс".
    }
  }
}

 

6. Які вимоги накладаються на елемент класу для того, щоб він підтримував поліморфізм?

Для того, щоб елемент класу (наприклад метод) підтримував поліморфізм, його потрібно зробити віртуальним. Щоб елемент класу був віртуальним, потрібно виконати наступні вимоги:

  • у базовому класі цей елемент (метод, властивість) повинен бути позначений як virtual або abstract. Ключове слово abstract також робить елемент віртуальним. Це слово використовується, коли елемент класу є абстрактним. Більш детально про абстрактні класи описується тут;
  • у похідних класах однойменні елементи повинні бути позначені як override. Якщо у похідному класі потрібно реалізувати невіртуальний метод, ім’я якого співпадає з віртуальним методом базового класу, то цей метод позначається ключовим словом new (дивіться пункт 7).

 

7. Використання ключового слова new в ланцюгу віртуальних методів. Приклад

Як відомо, елемент класу, який оголошений віртуальним (virtual), передає можливість реалізувати поліморфізм в однойменних елементах успадкованих класів. Таким чином, віртуальні елементи утворюють ланцюг вниз по ієрархії.

Для того, щоб елемент класу, який перевизначає (override) віртувальний елемент базового класу, не підтримував поліморфізм потрібно вказати ключове слово new. Якщо в ланцюгу однойменних віртуальних методів зустрічаєтья один невіртуальний метод (з ключовим словом new) то цей метод розриває ланцюг.

Приклад. Задано класи з іменами A1, A2, A3, A4. Класи утворюють ієрархію успадкування. У класі A3 оголошується метод Print() з ключовим словом new, який розриває ланцюг віртуальних методів.

Текст демонстраційної програми наступний

using static System.Console;

namespace ConsoleApp5
{
  // Базовий клас в ієрархії
  class A1
  {
    // Віртуальний метод Print()
    virtual public void Print()
    {
      WriteLine("A1.Print()");
    }
  }

  // Клас, похідний від класу A1
  class A2 : A1
  {
    // віртуальний метод Print()
    override public void Print()
    {
      WriteLine("A2.Print()");
    }
  }

  // Клас, похідний від класу A2
  class A3 : A2
  {
    // Невіртуальний метод Print() - даний метод
    // розриває ланцюг віртуальних методів,
    // метод позначений ключовим словом new
    new public void Print()
    {
      WriteLine("A3.Print()");
    }
  }

  // Клас, похідний від класу A3
  class A4 : A3
  {
    // Знову невіртуальний метод Print().
    // Встановити тут ключове слово override не вийде,
    // тому що ланцюг віртуальних методів розірвано.
    new public void Print() // тільки new можна використовувати
    {
      WriteLine("A4.Print()");
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Посилання на базовий клас
      A1 refA1;

      // 2. Екземпляри класів
      A1 objA1 = new A1();
      A2 objA2 = new A2();
      A3 objA3 = new A3();
      A4 objA4 = new A4();

      // 3. Виклик методу Print() екземплярів
      //   A1, A2, A3, A4 через посилання refA1
      refA1 = objA1;
      refA1.Print(); // A1.Print - метод базового класу

      // objA2
      refA1 = objA2;
      refA1.Print(); // A2.Print - поліморфізм

      // objA3
      refA1 = objA3;
      refA1.Print(); // A2.Print - немає поліморфізму
      (refA1 as A3).Print(); // A3.Print - статичний поліморфізм

      // objA4
      refA1 = objA4;
      refA1.Print(); // A2.Print - немає поліморфізму
      (refA1 as A4).Print(); // A4.Print - статичний поліморфізм
    }
  }
}

На рисунку 4 схематично зображено виклик методу Print() у випадку використання ключового слова new.

C#. Спадковість. Ієрархія класів. Розривання ланцюга віртуальних методів

Рисунок 4. Ієрархія класів A1, A2, A3, A4. Розривання ланцюга віртуальних методів Print() у класі A3

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

A1.Print()
A2.Print()
A2.Print()
A3.Print()
A2.Print()
A4.Print()

 

8. Приклад класів Figure->Rectangle->RectangleColor, які демонструють поліморфізм

Оголосити клас Figure, який містить поле name, яке визначає назву фігури. У класі Figure оголосити наступні методи:

  • конструктор з 1 параметром;
  • метод Display(), який відображає назву фігури.

З класу Figure успадкувати клас Rectangle (прямокутник), який містить наступні поля:

  • координату лівого верхнього кута (x1; y1);
  • координату правого нижнього кута (x2; y2).

У класі Rectangle реалізувати наступні методи та функції:

  • конструктор з 5 параметрами, який викликає конструктор базового класу Figure;
  • конструктор без параметрів, який реалізує встановлення координат кутів (0; 0), (1; 1) та викликає конструктор з 5 параметрами з допомогою засобу this;
  • метод Display(), який відображає назву фігури та значення внутрішніх полів. Даний метод звертається до однойменного методу базового класу;
  • метод Area(), який повертає площу прямокутника.

З класу Rectangle успадкувати клас RectangleColor. У класі RectangleColor реалізувати поле color (колір) та наступні методи;

  • конструктор з 6 параметрами, який викликає конструктор базового класу Rectangle;
  • конструктор без параметрів, який встановлює координати (0; 0), (1; 1) та викликає конструктор з 6 параметрами з допомогою засобу this;
  • метод Display(), який відображає назву фігури та значення внутрішніх полів. Даний метод звертається до однойменного методу базового класу;
  • метод Area(), який повертає площу прямокутника. У методі викликається метод Area() базового класу.

У функції main() виконати наступні дії:

  • оголосити посилання на базовий клас Figure;
  • створити екземпляри класів Rectangle та RectangleColor;
  • продемонструвати використання динамічного поліморфізму для доступу до методів похідних класів з допомогою посилання на клас Figure.

Текст програми наступний:

using System;
using static System.Console;

namespace ConsoleApp1
{
  // Клас Figure
  class Figure
  {
    // Приховане поле класу
    protected string name;

    // Конструктор з 1 параметром
    public Figure(string name) { this.name = name; }

    // Метод Display() - віртуальний - з ключовим словом virtual
    virtual public void Display()
    {
      WriteLine("Figure.name = {0}", name);
    }
  }

  // Клас Rectangle - успадковує (розширює) можливості класу Figure
  class Rectangle : Figure
  {
    // Приховані поля - координати точок
    protected double x1, y1, x2, y2;

    // Конструктор з 5 параметрами
    public Rectangle(string name, double x1, double y1, double x2, double y2) :
        base(name) // викликати конструктор базового класу
    {
      this.x1 = x1; this.y1 = y1;
      this.x2 = x2; this.y2 = y2;
    }

    // Конструктор без параметрів, викликає конструктор з 5 параметрами
    public Rectangle() : this("Rectangle", 0, 0, 1, 1) { }

    // Метод Display() - перевизначає однойменний метод базового класу,
    // тому, для забезпечення поліморфізму, потрібно задати
    // ключове слово override.
    public override void Display()
    {
      base.Display(); // викликати метод Display базового класу

      Write("Rectangle: x1 = {0:f2}, y1 = {1:f2}, ", x1, y2);
      WriteLine("x2 = {0:f2}, y2 = {1:f2}", x2, y2);
    }

    // Метод, що повертає площу прямокутника
    public double Area()
    {
      return Math.Abs(x1 - x2) * Math.Abs(y1 - y2);
    }
  }

  // Клас RectangleColor - додає до прямокутника колір,
  // успадковує можливості класу Rectangle
  class RectangleColor : Rectangle
  {
    // Приховане поле класу
    protected int color = 0;

    // Конструктор з 6 параметрами,
    // викликає конструктор базового класу Rectangle
    public RectangleColor(string name, double x1, double x2,
        double y1, double y2, int color) : base(name, x1, y1, x2, y2)
    {
      this.color = color;
    }

    // Конструктор без параметрів,
    // викликає конструктор з 6 параметрами
    public RectangleColor() : this("RectangleColor", 0, 0, 1, 1, 0) { }

    // Метод Display() - викликає однойменний метод базового класу,
    // щоб забезпечити поліморфізм потрібно вказати override
    public override void Display()
    {
      base.Display();
      WriteLine("RectangleColor.color = {0}", color);
    }

    // Метод обчислення площі
    public new double Area()
    {
      return base.Area(); // виклик методу Area() базового класу
    }
  }

  class Program
  {
    // Метод, що демонструє поліморфізм.
    // У метод передається посилання на базовий клас Figure
    static void DemoPolymorphism(Figure refFg)
    {
      // Виклик методу Display()
      refFg.Display(); // Один інтерфейс - різні реалізації
    }

    static void Main(string[] args)
    {
      // 1. Оголосити посилання на базовий клас
      Figure refFg;

      // 2. Створити екземпляри класів Figure, Rectangle, RectangleColor
      Figure objFg = new Figure("Figure");
      Rectangle objRect = new Rectangle("Rectangle", 1, 2, 5, -4);
      RectangleColor objRectCol = new RectangleColor("RectangleColor", 1, 8, -1, 3, 2);

      // 3. Демонстрація поліморфізму з допомогою
      //    використання посилання на базовий клас
      // 3.1. Присвоїти посиланню на базовий клас
      //     значення посилання на клас Figure
      refFg = objFg;
      refFg.Display(); // Викликається Figure.Display()

      // 3.2. Присвоїти посиланню значення посилання на Rectangle
      refFg = objRect;
      refFg.Display(); // Викликається Rectangle.Display()

      // 3.3. Присвоїти посиланню refFg значення objRectCol
      refFg = objRectCol;
      refFg.Display(); // Викликається RectangleColor.Display()

      // 4. Демонстрація поліморфізму через передачу параметру у функцію
      // 4.1. Передача у DemoPolymorphism посилання на екземпляр класу Rectangle
      refFg = objRect;
      Program.DemoPolymorphism(refFg); // виклик Rectangle.Display()

      // 4.2. Передача у DemoPolymorphism посилання на екземпляр класу RectangleColor
      refFg = objRectCol;
      Program.DemoPolymorphism(refFg); // виклик RectangleColor.Display()
    }
  }
}

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

Figure.name = Figure
Figure.name = Rectangle
Rectangle: x1 = 1.00, y1 = -4.00, x2 = 5.00, y2 = -4.00
Figure.name = RectangleColor
Rectangle: x1 = 1.00, y1 = 3.00, x2 = 8.00, y2 = 3.00
RectangleColor.color = 2
Figure.name = Rectangle
Rectangle: x1 = 1.00, y1 = -4.00, x2 = 5.00, y2 = -4.00
Figure.name = RectangleColor
Rectangle: x1 = 1.00, y1 = 3.00, x2 = 8.00, y2 = 3.00
RectangleColor.color = 2

 


Зв’язані теми