Виртуальные функции и принцип полиморфизма

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

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

Основное условие отношения замещения заключается в том, что замещаемая функция базового класса должна при этом дополнительно специфицироваться спецификатором virtual. Отношение замещения между функциями базового и производного класса устанавливается, если соответствующая (одноименная функция с соответствующей сигнатурой) функция производного класса специфицируется дополнительным спецификатором override.

Введём следующие обозначения и построим схему наследования.

 

Спецификация и заголовок функции– члена Уровень наследования Обозначение
public void F(){ }
public virtual void F() { }
new public virtual void F() { } (npv) 1..N
new public void F() { } (np) 1..N
public override void F() { } (po) 1..N

 


Рис.4. Схема возможных вариантов объявления методов в трехуровневой схеме наследования.

Рассмотрим конкретный пример:

using System;

namespace Implementing

{

class A{ public virtual void F() { Console.WriteLine("A"); } }

class B:A { public override void F() { Console.WriteLine("B"); } }

class C:B{ new public virtual void F() { Console.WriteLine("C"); } }

class D:C { public override void F() { Console.WriteLine("D"); } }

class Starter

{

static void Main(string[] args)

{

D d = new D();

C c = d;

B b = c;

A a = b;

d.F(); // D::F(), переопределяющая аналогичную функцию в классе С

c.F(); // D::F(), переопределенная в дочернем классе D

b.F(); // B::F(), переопределяющая аналогичную функцию в классе A

a.F(); // B::F(), переопределенная в дочернем классе B

}

}

}

в котором становится все понятно, если подробно нарисовать схемы классов, структуру объекта–представителя класса D и посмотреть, какая ссылка за какое место удерживает этот самый объект.

В результате выполнения программы, получим следующий результат:

D

D

B

B

Абстрактные методы и их применение.

Иногда требуется создать базовый класс, в котором определяется лишь самая общая форма для всех производных классов, а наполнение ее деталями предоставляется каждому из этих классов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в производных классах, а не в самом базовом классе. Подобная ситуация возникает, например, в связи с невозможностью получить содержательную реализацию метода в базовом классе, и разрешается двумя способами. Один из них состоит в том, чтобы просто выдать предупреждающее сообщение. Такой способ может пригодиться в определенных ситуациях, например, при отладке, но в практике программирования он обычно не применяется. Ведь в базовом классе могут быть объявлены методы, которые должны быть переопределены в производных классах. В подобных случаях требуется какой-то способ, гарантирующий, что в производном классе действительно будут переопределены все необходимые методы. И такой способ в С# имеется. Он состоит в использовании абстрактного метода.

Абстрактный метод создается с помощью указываемого модификатора типа abstract и представляет собой метод, который объявляется в классе, но не определяется. Другими словами, у абстрактного метода отсутствует тело, и поэтому он не реализуется в базовом классе. Это означает, что он должен быть переопределен в производном классе, поскольку его вариант из базового класса просто непригоден для использования. Нетрудно догадаться, что абстрактный метод автоматически становится виртуальным и не требует указания модификатора virtual. В действительности совместное использование модификаторов virtual и abstract считается ошибкой. Для определения абстрактного метода служит следующая форма:

аbstract <тип> <имя>(список параметров);

Как видно, у абстрактного метода отсутствует тело. Модификатор abstract может применяться только в методах экземпляра, но не в статических методах (static). Абстрактными могут быть также свойства и индексаторы.

Класс, содержащий один или более абстрактных методов, должен быть также объявлен как абстрактный, и для этого перед его объявлением class указывается модификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект абстрактного класса с помощью оператора new приведет к ошибке во время компиляции. Когда производный класс наследует абстрактный класс, то в нем должны быть реализованы все абстрактные методы базового класса. В противном случае производный класс должен быть также определен как abstract. Таким образом, атрибут abstract наследуется до тех пор, пока не будет достигнута полная реализация класса.

Абстрактные классы

С наследованием тесно связан еще один важный механизм проектирования семейства классов - механизм абстрактных классов. Класс называется абстрактным, если он имеет, хотя бы один, абстрактный метод. Метод называется абстрактным, если при определении метода задана его сигнатура, но не задана реализация метода.

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

Абстрактные классы являются одним из важнейших инструментов объектно-ориентированного проектирования классов. К сожалению, я не могу входить в детали рассмотрения этой важной темы и ограничусь лишь рассмотрением самой идеи применения абстрактного класса. В основе любого класса лежит абстракция данных. Абстрактный класс описывает эту абстракцию, не входя в детали реализации, ограничиваясь описанием тех операций, которые можно выполнять над данными класса. Так, проектирование абстрактного класса Stack, описывающего стек, может состоять из рассмотрения основных операций над стеком и не определять, как будет реализован стек - списком или массивом. Два потомка абстрактного класса - ArrayStack и ListStack могут быть уже конкретными классами, основанными на различных представлениях стека.

Рассмотрим пример описания полностью абстрактного класса Stack:

public abstract class Stack

{

public Stack(){}

// втолкнуть элемент item в стек

public abstract void put(int item);

// удалить элемент в вершине стека

public abstract void remove();

// прочитать элемент в вершине стека

public abstract int item();

// определить, пуст ли стек

public abstract bool IsEmpty();

}

Описание содержит только сигнатуры методов класса и их спецификацию. Построим теперь одного из потомков этого класса, реализация которого основана на списковом представлении. Класс ListStack будет потомком абстрактного класса Stack и контейнером класса Linkable, задающего элементы списка.

public class ListStack: Stack

{

public ListStack(){ top = new Linkable(); }

Linkable top;

// втолкнуть элемент item в стек

public override void put(int item)

{

Linkable newitem = new Linkable();

newitem.info = item;

newitem.next = top;

top = newitem;

}
// удалить элемент в вершине стека

public override void remove(){ top = top.next; }

// прочитать элемент в вершине стека

public override int item(){ return(top.info); }

// определить, пуст ли стек

public override bool IsEmpty(){ return(top.next == null); }

}

Класс Linkable выглядит совсем просто: в нем - два поля и конструктор по умолчанию.

public class Linkable

{

public Linkable(){ }

public int info;

public Linkable next;

}

Класс имеет одно поле top класса Linkable и методы, наследованные от абстрактного класса Stack. Теперь, когда задано представление данных, нетрудно написать и реализацию операций. Реализация операций традиционна для стеков и, надеюсь, не требует пояснений.

Рассмотрим пример работы со стеком:

public void TestStack()

{

ListStack stack = new ListStack();

stack.put(7);

stack.put(9);

Console.WriteLine(stack.item());

stack.remove();

Console.WriteLine(stack.item());

stack.put(11);

stack.put(13);

Console.WriteLine(stack.item());

stack.remove();

Console.WriteLine(stack.item());

if(!stack.IsEmpty()) stack.remove();

Console.WriteLine(stack.item());

}

В результате работы этого теста будет напечатана следующая последовательность целых: 9, 7, 13, 11, 7.