Наследование. Класс в С# может иметь произвольное количество потомков и только одного предка

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса

после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии 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, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компиля­тор учесть не может.

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


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: