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 ref;

происходит объявление ссылки ref, которая имеет тип базового класса A. Дальнейшее присваивание

refA = objB;

связывает ссылку с объектом objB, однако тип ссылки устанавливается A. Поэтому вызов

ref.Print();

вызовет метод Print() класса A.

В случае позднего связывания, сначала на основе описания классов A, B компилятор определяет, что метод Print() есть виртуальным. Для виртуального метода компилятор строит таблицу виртуальных методов Print(), которая содержит смещение адресов каждого виртуального метода для каждого класса иерархии (это отдельная тема для исследования).

После строки

A ref;

формируется связывание ссылки ref с типом A. После присваивания

ref = objB;

компилятор присваивает ссылке ref адрес экземпляра objB и определяет тип связывания как тип B (поскольку метод Print() виртуальный). За основу берется тип объекта. В результате ссылка ref связывается с методом Print(), который реализован в классе B (а не в классе A) – выполняется так называемое «позднее связывание».

Как следствие, после вызова

ref.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, Rectangle
      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

 


Связанные темы