ООП. Полиморфизм

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 1 год, 3 мес. назад.

  • Автор
    Сообщения
  • #4026

    Полиморфизм используется в тех случаях, когда реальный тип объекта будет известен только на этапе выполнения и реализуется посредством механизма виртуальных функций и позднего связывания. Ему может быть дано следующее определение:

    Полиморфизм — центральное понятие в ООП, обозначающее способность объекта выбирать правильную функцию в зависимости от типа данных, полученных в сообщении (message). Благодаря полиморфизму объект выполняет нужные действия, даже если содержимое сообщения было неизвестно во время написания программы.

    Под «сообщением» тут имеется ввиду вызов функции-члена для объекта. Чтобы понять «на пальцах» и почувствовать полиморфизм — обратимся к примерам (исходный код на языках C# и С++, но подход справедлив для любых других языков).

    Пример 1 (на C#)

    Возьмем класс Transport, который реализует основную функциональность – передвижение (Move()). Его классы-наследники (Car, Airplane, Tram) по-своему переопределяют метод передвижения. Но везде, где мы будем применять их, мы можем рассчитывать на наличие общей функциональности, но по-своему определённой.

    class Transport {
       public virtual void Move() {
          //just implementing a moving ability
       }
    }
    class Airplane:Transport {
       public override void Move() {
          base.Move();
         //fly
       }   
    }
    class Car:Transport {
       public override void Move() {
          //ride
       }
    }
    class Tram:Transport {
       public override void Move() {
          //suffer (who knows what is Ukrainian trams, knows what I mean :) )
      }  
    }

    Обратим внимание на модификатор virtual, помечающий метод Move() в базовом классе Transport. Он означает, что мы в праве переопределить реализацию данного метода в любом наследнике, если она нас не устраивает. А также обратим внимание на модификатор override, помечающий метод Move() в классах – наследниках. Здесь он означает, что мы намерены дать своё собственное определение методу базового класса. Эти 2 модификатора используются в паре. Нельзя переопределить метод базового типа, если он не был помечен как virtual. Но, с другой стороны, мы и не обязаны переопределять виртуальный метод базового типа.

    Протестируем полиморфное использование всех типов наследников класса Transport. Экземпляры всех трёх классов мы помещаем в одну коллекцию, указывая их базовый тип. Затем циклически проходим по каждому объекту и вызываем у каждого метод Move(). Соответственно, подразумевалось, что у каждого объекта, находящегося в этой коллекции, должен быть такой метод, поскольку он есть у их базового типа.

    class Program {
       static void Main() {
           List<Transport> transport = new List<Transport>();
             // заполняем коллекцию
           transport.Add(new Airplane())
           transport.Add(new Car());
           transport.Add(new Tram());
     
          foreach (var t in transport) {
               t.Move();
           }
       }
    }

    В результате будет выполнен код метода Move, определенного в классах-наследниках.

    Подобная ситуация происходит всякий раз, когда определён некоторый метод, принимающий в качестве параметра ссылку на класс Transport. Тогда мы снова будем иметь возможность передавать как аргумент ссылку на любой класс, производный от него.

    напишем класс Tester, который включает единственный метод TestTransportUse() с параметром типа Transport. Создаём в точке входа экземпляр класса Car и передаём его при вызове метода.

    Всё прекрасно работает, поскольку функциональность, к которой мы обращаемся уже внутри метода, гарантированно присутствует в базовом типе – классе Transport. И вот получается, что мы используем неидентичные типы данных совершенно одинаково.

    class Tester
    {
       public static void TestTransportUse(Transport  t)
       {
          t.Move();
       }
    }
    class Program
    {
       static void Main(string[] args)
       {
          Car car = new Car();
          Tester.TestTransportUse(car);
       }
    }

    Перегрузку методов тоже можно отнести к проявлениям полиморфизма. Мы определяем набор одноимённых методов с разными параметрами и позволяем набором параметров при вызове определять, какой вариант реализации нам нужен. Получается, мы создаём разновидности одного и того же метода, а вызываемая версия определяется динамически.

    Пример 2 (на C#)

    Разработаем класс, выполняющий простые арифметические вычисления. Среди прочих, он включает метод для вычисления частного от деления двух параметров типа double. Всё бы ничего, но возвращаемое значение является неокруглённым результатом деления, и хорошо бы в каких-то случая явно задавать, до какого количества знаков после точки нужно округлить значение.

    Поэтому мы перегружаем данный метод так, чтобы можно было через параметры задавать точность округления результата.

    class Calculator
    {
       //другие методы вычислений
       //...
     
       public string Divide(double num1, double num2)
       {
          return (num1 / num2).ToString();
       }
       public string Divide(double num1, double num2, int precision)
       {
          return Math.Round(num1 / num2, precision, MidpointRounding.AwayFromZero).ToString();
       }
    }

    Анализируем результат: теперь в классе есть 2 варианта одного и того же метода, которые отличаются лишь набором принимаемых параметров. Протестируем их работу:

    class Program
    {
        static void Main(string[] args)
       {
          Calculator calc = new Calculator();
          Console.WriteLine(calc.Divide(5,7));
          Console.WriteLine(calc.Divide(5,7,2));
       }
    }

    Создав экземпляр класса Calculator, мы вызываем оба варианта метода Divide(). Выглядит, как будто это один и тот же метод, только количество параметров отличается.

    Итак, множественность форм, может проявляться по-разному, частный случай полиморфизма – перегрузка методов, переопределение методов в классах-наследниках и использование производных типов как базовых.

    Пример 3 (на С++)

    В качестве более реального примера, предлагаю посмотреть на исходный код простой игрушки для Android:

    cplusplus_polymorphism_example

    В этом проекте используется графическая сцена (QGraphicsScene), отображающая все игровые элементы (ежика, тучки и т.п.). На самом деле, все эти графические элементы являются наследниками класса QGraphicsPixmapItem, представляющего собой картинку на графической сцене. Стандартная графическая сцена ничего не знает о наших ежиках, но корректно их отображает за счет использования виртуальных функций. Например, в классе AnimatedGraphicsItem, отвечающего за отображение анимированного графического элемента, реализуется виртуальная функция boundingRect класса QGraphicsItem — за счет этого графическая сцена узнает размер элементов. Внутри графической сцены в каком-либо виде хранится набор указателей на QGraphicsItem (они добавляются функцией QGraphicsScene::addItem(QGraphicsItem * item)), при этом сцена может вызвать, например, код из класса WallCloud за счет использования механизма виртуальных функций и позднего связывания:

    QGraphicsScene scene;
    QGraphicsItem *item;
    if (condition) {
      item = new WallCloud();
    }
    else {
      item = new StrongCloud();
    }
    
    scene.addItem(item);

    Статический тип объекта (известный во время компиляции) — QGraphicsItem, а динамический (реальный тип, известный лишь во время выполнения) — WallCloud или StrongCloud. При вызове невиртуальной функции по указателю item будет выбрана функция, соответствующая статическому типу объекта (раннее связывание, на этапе компиляции) , но при вызове виртуальной функции — динамическому (позднее связывание, на этапе выполнения).

    Позднее связывание реализуется посредством таблицы виртуальных функций, которые хранятся внутри каждого объекта. Таблица заполняется во время создания объекта в конструкторе — в нее заносятся адреса виртуальных функций. Затем, при вызове функции, не просто переходит переход по адресу функции (как при раннем связывании), а нужный адрес сначала выбирается из таблицы функций.

    В этом же примере класс GameWidget содержит внутри себя двумерный массив тучек (vector<vector<CloudElement* >>). На самом деле внутри массива будут экземпляры классов WallCloud, Cloud и StrongCloud, т.е. опять используется полиморфизм. Что это дает?

    1. внутри одного массива мы можем хранить реальные (динамические) типы которых отличаются за счет того, что их статический тип одинаков (все они имеют один базовый класс);
    2. базовый класс задает интерфейс, через который мы можем работать с объектами. Заметьте, что мы могли бы хранить массив указателей на GameElement (т.к. он является базовым для всех CloudElement) или даже QGraphicsItem. Однако, наши тучки должны уметь, например, взрываться (виртуальная функция bang()), мы не сможем вызывать эту функцию, имея указатель на QGraphicsItem, т.к. он не содержит такой функции. Поэтому мы объявляем специальный класс, задающий интерфейс и порождаем от него конкретные виды объектов, реализующих интерфейс, а затем работаем с объектами через указатель на класс интерфейса.

Для ответа в этой теме необходимо авторизоваться.