Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса
после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:
[ атрибуты ] [ спецификаторы ] class имя_класса [: предки ] тело класса
ПРИМЕЧАНИЕ
Обратите внимание на то, что слово «предки» присутствует в описании класса во множественном числе, хотя класс может иметь только одного предка. Причина в том, что класс наряду с единственным предком может наследовать от интерфейсов – специального вида классов, не имеющих реализации. Интерфейсы будут рассмотрены далее.
Рассмотрим наследование классов на примере. В разделе «Свойства» (см. с. 120) был описан класс Monster, моделирующий персонаж компьютерной игры. Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта Monster, а кроме того уметь думать. Будет логично сделать новый объект потомком объекта Monster (листинг 8.1).
|
|
Листинг 8.1. Класс Daemon, потомок класса Monster
using System;
namespace ConsoleApplication1
{
class Monster
{ }
class Daemon: Monster
{
public Daemon()
{
brain = 1;
}
public Daemon(string name, int brain)
: base(name) // 1
{
this.brain = brain;
}
public Daemon(int health, int ammo, string name, int brain)
: base(health, ammo, name) // 2
{
this.brain = brain;
}
new public void Passport() // 3
{
Console.WriteLine(
"Daemon {0} \t health = {1} ammo = {2} brain = {3}".
Name.Health, Ammo, brain);
}
public void Think() // 4
{
Console.Write(Name + " is");
for (int i = 0; i < brain; ++i)
Console.Write(" thinking"); Console.WriteLine("...");
}
int brain; // закрытое поле }
class Class1
{
static void Main()
{
Daemon Dima = new Daemon("Dima", 3); //5
Dima.Passport(); // 6
Dima.Think(); // 7
Dima.Health -= 10; // 8
Dima.Passport();
}
}
}
}
В классе Daemon введены закрытое поле brain и метод Think, определены собственные конструкторы, а также переопределен метод Passport. Все поля и свойства класса Monster наследуются в классе Daemon1. Результат работы программы:
Daemon Dima health = 100 ammo = 100 brain = 3
Dima is thinking thinking thinking...
Daemon Dima health = 90 ammo = 100 brain = 3
Как видите, экземпляр класса Daemon с одинаковой легкостью использует как собственные (операторы 5-7), так и унаследованные (оператор 8) элементы класса. Рассмотрим общие правила наследования, используя в качестве примера листинг 8.1.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными далее правилами:
Если бы в классе Monster были описаны методы, не переопределенные в Daemon, они бы также были унаследованы.
□ Если в конструкторе производного класса явный вызов конструктора базового
класса отсутствует, автоматически вызывается конструктор базового класса без
параметров. Это правило использовано в первом из конструкторов класса Daemon.
|
|
□ Для иерархии, состоящей из нескольких уровней, конструкторы базовых клас
сов вызываются, начиная с самого верхнего уровня. После этого выполняются
конструкторы тех элементов класса, которые являются объектами, в порядке
их объявления в классе, а затем исполняется конструктор класса. Таким об
разом, каждый конструктор инициализирует свою часть объекта.
□ Если конструктор базового класса требует указания параметров, он должен
быть явным образом вызван в конструкторе производного класса в списке ини
циализации (это продемонстрировано в конструкторах, вызываемых в опера
торах 1 и 2). Вызов выполняется с помощью ключевого слова base. Вызывается та версия конструктора, список параметров которой соответствует списку
аргументов, указанных после слова base.
Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new1. В листинге 8.1 таким образом переопределен метод вывода информации об объекте Passport. Другой способ переопределения методов рассматривается далее в разделе «Виртуальные методы».
Метод Passport класса Daemon замещает соответствующий метод базового класса, однако возможность доступа к методу базового класса из метода производного класса сохраняется. Для этого перед вызовом метода указывается все то же волшебное слово base, например:
base.Passport();
СОВЕТ
Вызов одноименного метода предка из метода потомка всегда позволяет сохранить функции предка и дополнить их, не повторяя фрагмент кода. Помимо уменьшения объема программы это облегчает ее модификацию, поскольку изменения, внесенные в метод предка, автоматически отражаются во всех его потомках. В конструкторах метод предка вызывается после списка параметров и двоеточия, а в остальных методах — в любом месте с помощью приведенного синтаксиса.
Вот, например, как выглядел бы метод Passport, если бы мы в классе Daemon хотели не полностью переопределить поведение его предка, а дополнить его:
new public void Passport() {
base.Passport();
Console.WriteLine(" brain = {1}". brain);
Можно этого не делать, но тогда компилятор выдаст предупреждение. Предупреждение (warning) — это не ошибка, оно не препятствует успешной компиляции, но тем не менее
Элементы базового класса, определенные как private, в производном классе недоступны. Поэтому в методе Passport для доступа к полям name, health и ammo пришлось использовать соответствующие свойства базового класса. Другое решение заключается в том, чтобы определить эти поля со спецификатором protected, в этом случае они будут доступны методам всех классов, производных от Monster. Оба решения имеют свои достоинства и недостатки.
ВНИМАНИЕ
Важно понимать, что на этапе выполнения программы объект представляет собой единое целое, не разделенное на части предка и потомка.
Во время выполнения программы объекты хранятся в отдельных переменных, массивах или других коллекциях.
Коллекция — объект, предназначенный для хранения данных и предоставляющий методы доступа к ним. Например, массив предоставляет прямой доступ к любому его элементу по индексу. Коллекции библиотеки.NET рассматриваются в главе 13. Еще раз напомню, что объекты относятся к ссылочному типу, следовательно, присваивается не сам объект, а ссылка на него.
Во многих случаях удобно оперировать объектами одной иерархии единообразно, то есть использовать один и тот же программный код для работы с экземплярами разных классов. Желательно иметь возможность описать:
□ объект, в который во время выполнения программы заносятся ссылки на объекты разных классов иерархии;
□ контейнер, в котором хранятся объекты разных классов, относящиеся к одной иерархии;
|
|
□ метод, в который могут передаваться объекты разных классов иерархии;
□ метод, из которого в зависимости от типа вызвавшего его объекта вызывают
ся соответствующие методы.
Все это возможно благодаря тому, что объекту базового класса можно присвоить объект производного класса2.
Давайте попробуем описать массив объектов базового класса и занести туда объекты производного класса. В листинге 8.2 в массиве типа Monster хранятся два объекта типа Monster и один — типа Daemon.
Листинг 8.2. Массив объектов разных типов
using System;
namespace ConsoleApplication1
{
class Monster
{
//...
}
class Daemon: Monster
{
//... //см. листинг 8.1
}
class Class1
{
static void Main()
{
const int n = 3;
Monster[] stado = new Monster[n];
stado[0] = new Monster("Monia");
stado[l] = new Monster("Monk");
stado[2] = new Daemon("Dimon", 3);
foreach (Monster elem in stado)
elem.Passport(); // 1
for (int i = 0; i < n; ++i)
stado[i].Ammo = 0; // 2
Console.WriteUne();
foreach (Monster elem in stado)
elem.Passport(); // 3
}
}
}
Результат работы программы:
Monster Monia health = 100 ammo = 100 Monster Monk health - 100 ammo - 100 Monster Dimon health = 100 ammo = 100
Monster Monia health = 100 ammo = 0 Monster Monk health = 100 ammo = 0 Monster Dimon health = 100 ammo - 0
Результат радует нас только частично: объект типа Daemon действительно можно поместить в массив, состоящий из элементов типа Monster, но для него вызываются только методы и свойства, унаследованные от предка. Это устраивает нас в операторе 2, а в операторах 1 и 3 хотелось бы, чтобы вызывался метод Passport, переопределенный в потомке.
Итак, присваивать объекту базового класса объект производного класса можно, но вызываются для него только методы и свойства, определенные в базовом классе. Иными словами, возможность доступа к элементам класса определяется типом ссылки, а не типом объекта, на который она указывает. Это и понятно: ведь компилятор должен еще до выполнения программы решить, какой метод вызывать, и вставить в код фрагмент, передающий управление на этот метод (этот процесс называется ранним связыванием). При этом компилятор может руководствоваться только типом переменной, для которой вызывается метод или свойство (например, stado[i].Ammo). To, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может.
|
|
Следовательно, если мы хотим, чтобы вызываемые методы соответствовали типу объекта, необходимо отложить процесс связывания до этапа выполнения программы, а точнее — до момента вызова метода, когда уже точно известно, на объект какого типа указывает ссылка. Такой механизм в С# есть — он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов, которые мы незамедлительно и рассмотрим.