Основы программирования — второй семестр 08-09; Михалкович С.С.; VIII часть

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск

Полиморфизм и виртуальные методы

Вводные понятия

Для объектно-ориентированного программирования часто приводят следующую формулу:

ООП = инкапсуляция + наследование + полиморфизм

Рассмотрим последнюю часть этой формулы - полиморфизм.

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

Пример.
Есть класс

Студент
    Готовиться_к_экзамену()

И два его наследника:

Хороший_студент
    Готовиться_к_экзамену()

и

Плохой студент
    Готовиться_к_экзамену()

Действие Готовиться_к_экзамену() они выполняют по-разному.

Рассмотрим следующий код:

var p: Person;
p := new Student('Иванов', 17, 1, 11);
p.Print();

Вопрос: какой метод Print вызовется?
Ответ: В разных языках программирования вызовутся Print разных классов:

  • в таких языках, как Java, EiffelStudent.Print
  • а в C++, C#, PasacalABC.NETPerson.Print

Т.о. в PascalABC.NET вызовется метод Person.Print, но, хотелось бы, чтобы вызывался метод Student.Print.

Если решение о том, какой метод вызывать, принимается на этапе компиляции (рано), то связывание имени метода с конкретным кодом называется ранним связыванием.
Если же решение о том, какой метод вызывать, принимается на этапе выполнения программы (поздно), то связывание имени метода с конкретным кодом называется поздним связыванием.
Позднее связывание осущесвляется с методом того класса, на который ссылается переменная в процессе выполнения программы.

Итак, в PascalABC.NET по умолчанию реализовано раннее связывание.

Позднее связывание и виртуальные методы

Чтобы обеспечить позднее связывание в языке Pascal, соответствующие методы надо сделать виртуальными в базовом классе.

type
  Person = class
  ...
  public
    ...
    procedure Print; virtual;
    begin
      WriteFormat('Имя: {0}  Возраст: {1}  ', 
        fName, fAge);
    end;
  end;
  
  Student = class(Person)
  ...
  public
    procedure Print; override;
    begin
      inherited Print;
      WriteFormat('Курс: {0}  Группа: {1}  ', 
        fCourse, fGroup);
    end;
  end;

При переопределении виртуального метода в классе-потомке используется ключевое слово override.

Примечание. Переопределяющий виртуальный метод должен иметь те же параметры и тот же тип возвращаемого значения.

Вернемся к рассмотренному ранее коду:

var p: Person;
p := new Student('Иванов', 17, 1, 11);
p.Print();

Теперь решение о том, какой метод вызывать, будет отложено до этапа выполнения, и будет вызвано Print того класса, на объект которого ссылается переменная в текущий момент (в данном случае — для Student).

Вызов виртуального метода осуществляется немного медленнее обычного метода.

Полиморфизм в объектно-ориентированных программирования реализуется через механизм виртуальных методов.

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

Замечание. Обычная подпрограмма также может быть полиморфной.
Пример.

procedure Print(p: Person);
begin
  p.Print(); // обращение полиморфизма
end;
...
Print(new Student(...));
Обычная подпрограмма называется полиморфной, если она содержит хотя бы один полиморфный параметр.

Виртуальные методы как блоки замены кода

Рассмотрим следующий полиморфный метод:

procedure polymorph(p: Base);
begin
  p.Method1;
  p.Method2;
  p.Method3;
end;

Напишем класс StudentInSession, унаследовав его от Base:

procedure polymorph(p: Base);
type
  Base = class
  public 
    procedure Method1; virtual; begin end;
    procedure Method2; virtual; begin end;
    procedure Method3; virtual; begin end;
  end;
  
  StudentInSession = class(Base)
  public
    procedure Method1; override;
    begin
      writeln('Sleep');
    end;
    procedure Method2; override;
    begin
      writeln('Eat');
    end;
    procedure Method3; override;
    begin
      writeln('Think');
    end;
  end;

...

polymorph(new StudentInSession);

Места вызовов виртуальных методов в некотором коде могут быть заменены все одновременно с использованием следующего приема:

  • Определим от базового класса, объект которого вызывает эти методы, потомка, в котором переопределим данные виртуальные методы.
  • При конструировании вместо объекта базового класса создадим объект потомка и присвоим его указанной полиморфной переменной. При этом все вызовы виртуальных методов будут вызывать код для потомка.

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

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

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

В соответствии с этим, сделаем наш базовый класс Base абстрактным:

type Base = class
public 
  procedure Method1; virtual; abstract;
  procedure Method2; virtual; abstract;
  procedure Method3; virtual; abstract;
end;

Класс Object — неявный предок всех классов .NET

Все классы в PascalABC.NET, если не указан другой предок, наследуются от класса Object.

Т.е. и integer, и string — классы.

<xh4> Интерфейс класса Object </xh4>

Object = class
    function Equals(o: Object): boolean; virtual;
    function ToString: string; virtual;
    function GetType: System.Type;

Примечание. Как нам известно, использовать ключевые слова в качестве идентификаторов запрещено. Однако, можно использовать перед ними символ «&». Тогда можно, например, описать переменную &type.

Метод Equals

Сравнивает текущий объект с объектом o и, если они равны, возвращает true, иначе — false.
По умолчанию, все встроенные (а именно — размерные) типы и тип string сравниваются по значению, а все классы — по ссылке (две переменные считаются равными, если ссылаются на один объект).
Это можно изменить, переопределив метод Equals в потомке.

Метод ToString

Возвращает строковое представление объекта.
Если не переопределен, то возвращает имя типа.

Метод GetType

Возвращает объект типа System.Type, который характеризует тип данного объекта.

Пример 1.

var i: integer := 5;
begin
  var s: string := i.ToString;
end.

<xh4> Переопределение методов Equals и ToString в классах Person и Student </xh4>

type  Person = class 
...
public
...
function Equals(o: object): boolean;
begin
  if o = nil then
    Result := false
  else if GetType <> o.GetType then
    Result := false
  else
  begin
    var p := Person(o);
    Result := (Name = p.Name) and (Age = p.Age);
  end;
end;

function ToString: string;
begin
  Result := Format(
    'Имя: {0}  Возраст: {1}  ', 
    fName, fAge);
end;
type  Student = class(Person)
...
public
...
function Equals(o: object): boolean;
begin
  Result := inherited Equals(o);
  if Result then
  begin
    var s := Student(o);
    Result := (Course = s.Course) and (Group = s.Group);
  end;
end;

function ToString: string;
begin
  Result := inherited ToString + Format(
    'Курс: {0}  Группа: {1}  ', 
    fCourse, fGroup);
end;

Полный текст новой версии классов Person и Student

Пример 2. Родословная переменной.

uses University;

procedure PrintLineage(o: object);
begin
  var t := o.GetType;
  writeln(t.Name);
  
  repeat
    t := t.BaseType;
    if t = nil then
      exit;
    writeln(t.Name);
  until false;
end;

begin
  var s := new SeniorStudent('Иванов', 20, 4, 11, new Teacher('Петров'), Magister);
  PrintLineage(s);
end.

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

Цепочка виртуальности и её разрыв

Пусть у нас есть следующая иерархия наследования:

            A            <—             B             <—             C
procedure Print; virtual;   procedure Print; override;   procedure Print; override;
Говорят, что методы A.Print, B.Print и C.Print завязаны в цепочку виртуальности.

Унаследуем от класса C класс D следующим образом:

D (C)
  procedure Print;

Будет предупреждение: «Укажите override или reintroduce».

Ключевое слово reintroduce служит для разрыва цепочки виртуальности.
Если написать:

... reintroduce; virtual;

то это будет началом новой цепочки виртуальности.

Алгоритм поиска в цепочке виртуальности

Интерфейсы

Интерфейс - это специальным образом оформленный легковесный класс, задающий набор требований, которые должен реализовывать обычный класс.

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

Все методы в интерфейсах считаются публичными, уровень доступа указывать не надо.

Пример.

interface IPrintable
  procedure Print;
  procedure Println;
end;

interface ICoords<T>
  property X: T read;
  property Y: T read;
end;

interface ICloneable // стандартный, из пространства имен System
  procedure Clone;
end;

Интуитивно: интерфейсы - это роли, которые играют объекты классов в разных ситуациях.

Один и тот же класс может реализовывать несколько интерфейсов, один и тот же интерфейс может реализовываться совершенно различными классами.

Реализация интерфейсов

type Student = class(Person,ICloneable,IPrintable)
...
  function Clone: Object;
  begin
    Result := new Student(Name,Age,Course,Group);
  end;
  procedure Print;
  begin
    ...
  end;
  procedure Println;
  begin
    ...
  end;
end;
type List<T> = class(ICloneable,IPrintable)
...
end;

Класс наследуется от одного класса, но может реализовывать несколько интерфейсов.

Все методы и свойства интерфейса, реализуемые классом, должны быть объявлены публичными

Совершенно разнородные классы могут реализовывать одинаковые интерфейсы.

Совместимость по := и операции is as для интерфейсов

Для интерфейсов работают те же правила совместимости по присваиванию, что и для обычных классов:

  1. Если класс реализует интерфейс, то переменной типа интерфейс можно присвоить объект этого класса
  2. Можно использовать конструкции вида p is MyClass, p as MyClass, где p - переменная типа интерфейс

Интерфейсы и полиморфизм

Все методы интерфейсов, реализуемые классами, являются виртуальными - для этого необязательно использовать virtual

Если virtual не используется, то цепочка виртуальности прерывается, если используется - продолжается:

type IShape = interface(ICoords<integer>)
  procedure Draw;
  procedeure Hide;
  procedeure MoveTo(x,y: integer);
  procedeure MoveOn(dx,dy: integer);
end;
Shape = class(IShape)
...
  procedure Draw; virtual;
  procedeure Hide; virtual;
end;

Иерархия графических фигур - классический пример использования полиморфизма

Абстрактные методы и классы

Сделать Shape абстрактным

Полиморфные контейнеры

List<Shape> Цикл по полиморфному контейнеру

  1. с вызовом виртуального метода
  2. с вызовом индивидуального метода и использованием as
  3. с вызовом индивидуального метода и использованием GetType

Интерфейсы и наследование

Реализация интерфейсов является одной из форм наследования. В этой форме всегда выполняется принцип подстановки. Недостатки: все методы необходимо реализовывать с нуля Достоинства: нет "груза прошлого" - тяжеловесной реализации базовых классов, которая уже не нужна в производных. Пример с наследованием с ограничениями (круг от эллипса): реализация эллипса тяжеловесна, при реализации круга мы используем лишь часть этих возможностей.

Стандартные интерфейсы .NET

Пример 1. Интерфейс IComparable и его использование.

uses System;

type Student = class(Person,IComparable<Student>)
...
public
  function CompareTo(o: Student): integer;
  begin
    var s := Student(o);
    if (Course < s.Course) or (Course = s.Course) and (Group < s.Group) then
      Result := -1
    else if (Course = s.Course) and (Group = s.Group) then
      Result := 0
    else Result := 1;   
  end;
end;

Пример 2. Интерфейс IComparer и его использование.

Пример 3. Написание MinElem<T>(a: array of T); where T: IComparable<T>

Методы расширения

integer.Print

 write(Self)

Методы расширения и интерфейсы

uses System.Collections.Generic;

function Identity(i: integer): integer;
begin
  Result := i;
end;

procedure Print<T>(ie: IEnumerable<T>);
begin
  foreach i: integer in ie do
    write(i,' ');
  writeln;  
end;

begin
  var a: array of integer := (1,5,3,10,7,4,8,3,11,2,2,3,4);
  var a1: array of integer := (2,5,7,4,11,9,23,34);
  writeln(a.All(Odd));
  writeln(a.Any(Odd));
  writeln(a.Average());
  writeln(a.Contains(10));  
  writeln(a.Count());
  writeln(a.Distinct().Count());
  writeln(a.ElementAt(2));
  writeln(a.ElementAtOrDefault(100));

  Print(a.Concat(a1));
  Print(a.Distinct());
  Print(a.Except(a1));
  Print(a.Intersect(a1));
  Print(a.Union(a1));
  Print(a.Skip(5));
  Print(a.Take(5));
  Print(a.TakeWhile(Odd));
  Print(a.Where(Odd));
  writeln(a.Where(Odd).Average());

  writeln(a.Last());
  writeln(a.Max());
  writeln(a.Min());
  var ff: System.Func<integer,integer> := f;
  a.Select(Identity); // !!
  writeln(a.SequenceEqual(a));
  writeln(a.Sum());
  a.OrderBy(Identity);
end.

Лямбды

uses Arrays;

var a: array of integer := (1,2,3,5,7);

begin
  a := a.Select((x: integer)->x*x).ToArray(); 
  a.Writeln;
end.
uses Core,Arrays;

var a: array of string := ('Hello','Abracadabra','Hi','Good','Bye');

begin
  var res := a.OrderBy((x: string) -> x.Length);
  res.ToArray().Writeln();
end.
  writeln(a.First((x: integer) -> x>5));