Принудительный полиморфизм: абстрактные методы

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

Рис. 3.14. Иерархия классов геометрических фигур

 

Скорее всего, вы спросите: «А зачем это нужно?» Для ответа на этот вопрос мы вернемся к иерархии геометрических фигур, с которой мы уже имели дело в этой главе (рис. 3.14).

Как и в случае с классом Employee, желательно явным образом запретить создание объектов класса Shape. Это можно сделать следующим образом:

namespace Shapes

{

public abstract class Shape

{

// Пусть каждый объект-геометрическая фигура получит у нас дружеское прозвище:

protected string petName;

 

// Конструкторы

public Shape( ) {petName = "NoName";}

public Shape(string s) {petName = s; }

 

// Метод Draw( ) объявлен как виртуальный и может быть замещен

public virtualvoid Draw( )

{

Console.WriteLine("Shape.Draw()”);

}

public string PetName

{

get {return petName; }

set {petName = value;}

}

}

 

// В классе Circle метод Draw( ) HE ЗАМЕЩЕН

public class Circle : Shape

{

public Circle( ) {}

public Circle(string name): base(name) {}

}

 

// В классе Hexagon метод Draw( ) ЗАМЕЩЕН

public class Hexagon : Shape

{

public Hexagon( ) {}

public Hexagon(string name) : base(name) {}

public overrnde void Draw( )

{

Console.WriteLine("Drawing {0} the Hexagon", PetName);

}

}

 

Обратите внимание, что в классе Shape определен виртуальный метод Draw( ). Как мы видим, виртуальные методы можно замещать в производных классах при помощи ключевого слова override (как это сделано в определении класса Hexagon). Однако в С# виртуальные методы можно и не замещать в производных классах (например, в определении класса Circle виртуальный метод Draw( ) базового класса остался незамещенным). При этом в случае вызова метода Draw( ) для объекта класса Hexagon будет вызван уникальный вариант этого метода для класса Hexagon, а если мы вызовем тот же метод для объекта класса Circle, этот метод будет выполнен в соответствии со своим определением в базовом классе:

 

// В объекте Circle реализация базового класса для Draw( ) не замещена

public static int Main(string[ ] args)

{

// Создаем и рисуем шестиугольник

Hexagon hex = new Hexagon('Beth");

hex.Draw( );

Circle cir = new Circle("Cindy");

// М-мм! Придется использовать реализацию Draw( ) базового класса

cir.Draw( );

}

 

 

Если нам необходимо гарантировать, что каждый производный класс обязательно заместит метод Draw( ), то мы должны объявить метод Draw( ) в базовом классе Shape абстрактным. Абстрактные методы в С# работают так же, как чистые виртуальные функции в C++ — для них даже не надо указывать реализацию по умолчанию:

 

// Каждая геометрическая фигура теперь ОБЯЗАНА самостоятельно определять

// метод Draw( )

public abstractclass Shape

{

. . . .

// Метод Draw( ) теперь определен как абстрактный (обратите внимание

// на точку с запятой)

public abstractvoid Draw( );

public string PetName

{

get {return petName;}

set {petName = value;}

}

}

 

Теперь мы обязаны определить метод Draw( ) в классе Сirclе:

 

// Если мы не заместим в классе Circle абстрактный метод Draw( ). класс Circle будет

// также считаться абстрактным и мы не сможем создавать объекты этого класса!

public class Circle : Shape

{

public Circle( ) {}

public Circle(string name): base(name) {}

 

// Теперь метод Draw( ) придется замещать в любом производном непосредственно

//от Shape классе

public overridevoid Draw( )

{

Console.WriteLine(“Drawing {0} the Cricle", PetName);

}

}

 

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

 

// Создаем массив объектов разных геометрических фигур

public static int Main(string[ ] args)

{

// Массив фигур

Shaped s = {new Hexagon( ), new Hexagon("Freda"), new Circle( ), new Circle(“JoJo")};

// Проходим с помощью цикла по всем элементам массива и просим нарисовать

// каждый объект

for(int i = 0; i < s.Length; i++)

s[i].Draw( );

. . . .

}

 

 

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

Приведение типов в С#

К настоящему моменту мы создали уже не одну развитую иерархию типов. При этом С# позволяет приводить (cast) один тип к другому, то есть осуществлять преобразование объектов одного типа в объекты другого типа. Рассмотрению правил приведения типов в С# и посвящен этот раздел.

Рассмотрим приведение типов на простом примере. Вспомним нашу иерархию классов сотрудников. Конечно же, на самой вершине этой иерархии стоит класс System Object — в С# все типы производятся от этого класса. Можно сказать, используя терминологию классического наследования, что все типы являются is-a-объектами. Кроме того, в нашей иерархии существуют и другие отношения классического наследования. Например, PTSalesPerson (продавец на неполный рабочий день) является is-a-продавцом Salesperson и т. д.

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

 

public class TheMachine

{

public static void FireThisPerson(Employee e)

{

// Удаляем сотрудника из базы данных

// Отбираем у него ключ и точилку для карандашей

}

}

 

В соответствии с правилами приведения типов мы можем передавать методу FireThisPerson( ) объект как самого типа Employee, так и любого производного от Employ ее типа:

 

// Производим сокращение персонала

TheMachine.FireThisPerson(e);

TheMachine.FireThisPerson(sp);

 

Этот код будет выполнен без ошибок, поскольку здесь производится неявное приведение от базового класса (Employee) к производному. Однако что, если вы также хотите уволить объект класса Manager (который в настоящее время хранится через ссылку на объект базового класса)? Если вы попробуете передать ссылку на объект (типа System.Object) нашему методу FireThisPerson( ), то вы получите сообщение об ошибке компилятора:

 

// Класс Manager - производный от System Object поэтому мы имеем право провести

// следующую операцию приведения

object о = new Manager("Frank Zappa", 9, 40000, “111-11-1111", 5);

 

TheMachine.FireThisPerson(o); // Ошибка компилятора!

 

Причина ошибки кроется в определении метода FireThisPerson( ), который принимает объект типа Employee. Чтобы этой ошибки не возникало, нам необходимо явно привести объект базового класса System.Object к производному типу Employee (учитывая происхождение нашего объекта о, это вполне возможно):

 

// Здесь будет ошибка - вначале нужно провести явное приведение типов:

// FireThisPersonO

// А вот так проблем не возникнет:

FireThisPerson((Employee)o);