Презентацию к данной лекции Вы можете скачать здесь.
Управлять большим количеством разрозненных классов достаточно сложно. С этой проблемой можно справиться путем упорядочивания и ранжирования классов, то есть объединяя общие для нескольких классов свойства в одном классе и используя его в качестве базового.
Эту возможность предоставляет механизм наследования, который является мощнейшим инструментом ООП. Он позволяет строить иерархии, в которых классы-потомки получают свойства классов-предков и могут дополнять их или изменять. Таким образом, наследование обеспечивает важную возможность многократного использования кода.
Классы, расположенные ближе к началу иерархии, объединяют в себе общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных особенностей.
Класс в C# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:
[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]
тело класса
Примечание
Обратите внимание, что слово "предки" присутствует в описании класса во множественном числе, хотя класс может иметь только одного предка. Причина в том, что класс наряду с единственным предком может наследовать от интерфейсов — специального вида классов, не имеющих реализации. Интерфейсы рассматриваются на следующей лекции.
Рассмотрим наследование классов на примере. Ранее был описан класс Monster, моделирующий персонаж компьютерной игры. Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта Monster, а кроме того, уметь думать. Будет логично сделать новый объект потомком объекта Monster (листинг 8.11).
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 наследуются в классе Daemon.
Результат работы программы:
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) элементы класса. Рассмотрим общие правила наследования.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными ниже правилами.
base. Вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new. В листинге 8.1 таким образом переопределен метод вывода информации об объекте Passport. Метод Passport класса Daemon замещает соответствующий метод базового класса, однако возможность доступа к методу базового класса из метода производного класса сохраняется. Для этого перед вызовом метода указывается все то же волшебное слово base, например:
base.Passport();
Элементы базового класса, определенные как private, в производном классе недоступны. Поэтому в методе Passport для доступа к полям name, health и ammo пришлось использовать соответствующие свойства базового класса. Другое решение заключается в том, чтобы определить эти поля со спецификатором protected, в этом случае они будут доступны методам всех классов, производных от Monster. Оба решения имеют свои достоинства и недостатки.
Во время выполнения программы объекты хранятся в отдельных переменных, массивах или других коллекциях. Во многих случаях удобно оперировать объектами одной иерархии единообразно, то есть использовать один и тот же программный код для работы с экземплярами разных классов. Это возможно благодаря тому, что объекту базового класса можно присвоить объект производного класса.
Попробуем описать массив объектов базового класса и занести туда объекты производного класса. В листинге 8.2 в массиве типа Monster хранятся два объекта типа Monster и один — типа Daemon.
using System;
namespace ConsoleApplication1
{
class Monster
{
...
}
class Daemon : Monster
{
... //
}
class Class1
{ static void Main()
{
const int n = 3;
Monster[] stado = new Monster[n];
stado[0] = new Monster( "Monia" );
stado[1] = 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.WriteLine();
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 ). То, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может.
Следовательно, если мы хотим, чтобы вызываемые методы соответствовали типу объекта, необходимо отложить процесс связывания до этапа выполнения программы, а точнее — до момента вызова метода, когда уже точно известно, на объект какого типа указывает ссылка. Такой механизм в C# есть — он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов, которые мы незамедлительно и рассмотрим.
При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то заранее жестко связывать эти методы с остальной частью кода нельзя.
Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в C# существует ключевое слово virtual. Оно записывается в заголовке метода базового класса, например:
virtual public void Passport() ...
Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться в момент его вызова во время выполнения программы. Этот механизм называется поздним связыванием.
Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table,
Каждый объект во время выполнения должен иметь доступ к
Если в производном классе требуется переопределить виртуальный метод, используется ключевое слово override, например:
override public void Passport() ...
Переопределенный виртуальный метод должен обладать таким же набором параметров, как и одноименный метод базового класса.
Добавим в листинге 8.2 два волшебных слова — virtual и override — в описания методов Passport соответственно базового и производного классов (листинг 8.3).
using System;
namespace ConsoleApplication1
{
class Monster
{
...
virtual public void Passport()
{
Console.WriteLine( "Monster {0} \t health = {1} ammo = {2}",
name, health, ammo );
}
...
}
class Daemon : Monster
{
...
override public void Passport()
{
Console.WriteLine(
"Daemon {0} \t health = {1} ammo = {2} brain = {3}",
Name, Health, Ammo, brain );
}
...
}
class Class1
{ static void Main()
{
const int n = 3;
Monster[] stado = new Monster[n];
stado[0] = new Monster( "Monia" );
stado[1] = new Monster( "Monk" );
stado[2] = new Daemon ( "Dimon", 3 );
foreach ( Monster elem in stado ) elem.Passport();
for ( int i = 0; i < n; ++i ) stado[i].Ammo = 0;
Console.WriteLine();
foreach ( Monster elem in stado ) elem.Passport();
}
}
}
Результат работы программы:
Monster Monia health = 100 ammo = 100 Monster Monk health = 100 ammo = 100 Daemon Dimon health = 100 ammo = 100 brain = 3 Monster Monia health = 100 ammo = 0 Monster Monk health = 100 ammo = 0 Daemon Dimon health = 100 ammo = 0 brain = 3
Теперь в циклах 1 и 3 вызывается метод Passport, соответствующий типу объекта, помещенного в массив.
Виртуальные методы базового класса определяют интерфейс всей иерархии. Этот интерфейс может расширяться в потомках за счет добавления новых виртуальных методов. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, метод наследуется.
С помощью виртуальных методов реализуется один из основных принципов объектно-ориентированного программирования — полиморфизм. Это слово в переводе с греческого означает "много форм", что в данном случае означает "один вызов — много методов".
Виртуальные методы незаменимы и при передаче объектов в методы в качестве параметров. В параметрах метода описывается объект базового типа, а при вызове в нее передается объект производного класса. Виртуальные методы, вызываемые для объекта из метода, будут соответствовать типу аргумента, а не параметра.
При создании иерархии объектов для исключения повторяющегося кода часто бывает логично выделить их общие свойства в один родительский класс. При этом может оказаться, что создавать экземпляры такого класса не имеет смысла, потому что никакие реальные объекты им не соответствуют. Такие классы называют абстрактными.
Абстрактный класс служит только для порождения потомков. Как правило, в нем задается набор методов, которые каждый из потомков будет реализовывать по-своему. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах.
Абстрактный класс задает интерфейс для всей иерархии, при этом методам класса может не соответствовать никаких конкретных действий. В этом случае методы имеют пустое тело и объявляются со спецификатором abstract.
Если в классе есть хотя бы один абстрактный метод, весь класс также должен быть описан как абстрактный, например:
abstract class Spirit
{
public abstract void Passport();
}
class Monster : Spirit
{
...
override public void Passport()
{
Console.WriteLine( "Monster {0} \t health = {1} ammo = {2}",
name, health, ammo );
}
...
}
class Daemon : Monster
{
...
override public void Passport()
{
Console.WriteLine(
"Daemon {0} \t health = {1} ammo = {2} brain = {3}",
Name, Health, Ammo, brain );
}
... // полный текст этих классов приведен в главе 12
}
Абстрактные классы используются при работе со структурами данных, предназначенными для хранения объектов одной иерархии, и в качестве параметров методов.
В C# есть ключевое слово sealed, позволяющее описать класс, от которого, в противоположность абстрактному, наследовать запрещается:
sealed class Spirit
{
...
}
// class Monster : Spirit { ... } ошибка!
Большинство встроенных типов данных описано как sealed. Если необходимо использовать функциональность бесплодного класса, применяется не наследование, а вложение, или включение: в классе описывается поле соответствующего типа.
Вложение классов, когда один класс включает в себя поля, являющиеся классами, является альтернативой наследованию при проектировании. Например, если есть объект "двигатель", а требуется описать объект "самолет", логично сделать двигатель полем этого объекта, а не его предком.
Механизм наследования классов предоставляет программисту богатейшие возможности организации кода и его многократного использования. Выбор наиболее подходящих средств для целей конкретного проекта основывается на знании механизма их работы и взаимодействия.
Тимоти Бадд [1] приводит интересную классификацию форм наследования. Форма наследования определяет, с какой целью оно используется. Бадд считает, что порождение дочернего класса может быть выполнено по следующим причинам.
Альтернативой наследованию при проектировании классов является вложение, когда один класс включает в себя поля, являющиеся классами. Например, если есть класс "двигатель", а требуется описать класс "самолет", логично сделать двигатель полем этого класса, а не его предком. Вложение представляет отношения классов "Y содержит X" или "Y реализуется посредством Х" и обычно реализуется с помощью модели "включение-делегирование", которая иллюстрируется в листинге 8.4.
using System;
namespace ConsoleApplication1
{
class Двигатель
{ public void Запуск()
{
Console.WriteLine( "вжжжж!!" );
}
}
class Самолет
{ public Самолет()
{
левый = new Двигатель();
правый = new Двигатель();
}
public void Запустить_двигатели()
{
левый.Запуск();
правый.Запуск();
}
Двигатель левый, правый;
}
class Class1
{ static void Main()
{
Самолет АН24_1 = new Самолет();
АН24_1.Запустить_двигатели();
}
}
}
Результат работы программы:
вжжжж!! вжжжж!!
В методе Запустить_двигатели запрос на запуск двигателей передается, или, как принято говорить, делегируется вложенному классу.
В процессе проектирования объектно-ориентированных программ для описания различного рода взаимоотношений классов и объектов часто используется UML.
UML — Unified Modeling Language — является языком для специфицирования, визуализации, конструирования и документирования программных продуктов, а также используется в бизнес-моделировании и моделировании любых иных (не программных) систем.
UML позволяет задавать различные аспекты представления системы, например, поведение системы с точки зрения ее внешних пользователей, внутреннюю структуру и различные виды взаимодействия элементов системы. Взаимоотношения классов отображают на диаграмме классов.
Отношение наследования изображается на диаграмме классов в виде незакрашенного треугольника, направленного к базовому классу (предку). Пример изображения наследования с помощью диаграммы классов приведен на рис 8.1.
(рис 8.1) Пример диаграммы UML для наследованияВложение классов изображается в виде закрашенного или незакрашенного ромба, направленного к владельцу части. Другой конец линии может быть снабжен цифрой, указывающей количество вложенных частей, или звездочкой, если таких частей может быть произвольное количество. Примеры приведены на рис 8.2.
(рис 8.2) Примеры диаграмм UML для вложения классовПри проектировании классов следует выбирать модель, наиболее точно отражающую смысл взаимоотношений классов, например, моделируемых объектов предметной области.
Корневой класс System.Object всей иерархии объектов .NET, называемый в C# object, обеспечивает всех наследников несколькими важными методами. Производные классы могут использовать эти методы непосредственно или переопределять их.
Класс object часто используется и непосредственно при описании типа параметров методов для придания им общности, а также для хранения ссылок на объекты различного типа — таким образом реализуется полиморфизм.
Открытые методы класса System.Object перечислены ниже.
Equals с одним параметром возвращает значение true, если параметр и вызывающий объект ссылаются на одну и ту же область памяти. Синтаксис:public virtual bool Equals(object obj);
Equals с двумя параметрами возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:public static bool Equals(object ob1, object ob2);
GetHashCode формирует хэш-код объекта и возвращает число, однозначно идентифицирующее объект. Это число используется в различных структурах и алгоритмах библиотеки. Если переопределяется метод Equals, необходимо перегрузить и метод GetHashCode. Синтаксис:public virtual int GetHashCode();
GetType возвращает текущий полиморфный тип объекта, то есть не тип ссылки, а тип объекта, на который она в данный момент указывает. Возвращаемое значение имеет тип Type. Это абстрактный базовый класс иерархии, использующийся для получения информации о типах во время выполнения. Синтаксис:public Type GetType();
ReferenceEquals возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:public static bool ReferenceEquals(object ob1, object ob2);
ToString по умолчанию возвращает для ссылочных типов полное имя класса в виде строки, а для значимых — значение величины, преобразованное в строку. Этот метод переопределяют для того, чтобы можно было выводить информацию о состоянии объекта. Синтаксис:public virtual string ToString()
В Equals для того, чтобы задать собственные критерии сравнения объектов.
Пример применения и переопределения методов класса object для класса Monster приведен в листинге 8.5.
using System;
namespace ConsoleApplication1
{
class Monster
{
public Monster( int health, int ammo, string name )
{
this.health = health;
this.ammo = ammo;
this.name = name;
}
public override bool Equals( object obj )
{
if ( obj == null || GetType() != obj.GetType() ) return false;
Monster temp = (Monster) obj;
return health == temp.health
ammo == temp.ammo
name == temp.name;
}
public override int GetHashCode()
{
return name.GetHashCode();
}
public override string ToString()
{
return string.Format( "Monster {0} \t health = {1} ammo = {2}",
name, health, ammo );
}
string name;
int health, ammo;
}
class Class1
{ static void Main()
{
Monster X = new Monster( 80, 80, "Вася" );
Monster Y = new Monster( 80, 80, "Вася" );
Monster Z = X;
if ( X == Y ) Console.WriteLine(" X == Y ");
else Console.WriteLine(" X != Y ");
if ( X == Z ) Console.WriteLine(" X == Z ");
else Console.WriteLine(" X != Z ");
if ( X.Equals(Y) ) Console.WriteLine( " X Equals Y " );
else Console.WriteLine( " X not Equals Y " );
Console.WriteLine(X.GetType());
}
}
}
Результат работы программы:
X != Y X == Z X Equals Y ConsoleApplication1.Monster
Анализируя результат работы программы, можно увидеть, что в операции сравнения на равенство сравниваются ссылки, а в перегруженном методе Equals — значения. Для концептуального единства можно переопределить и операции отношения.
В программах требуется описать базовый класс (возможно, абстрактный), в котором с помощью виртуальных или абстрактных методов и свойств задается интерфейс для производных классов. Целью лабораторной работы является максимальное использование наследования, даже если для конкретной задачи оно не дает выигрыша в объеме программы. Во всех классах следует переопределить метод Equals, чтобы обеспечить сравнение значений, а не ссылок.
Функция Main должна содержать массив из элементов базового класса, заполненный ссылками на производные классы. В этой функции должно демонстрироваться использование всех разработанных элементов классов.
Создать абстрактный класс Pair (пара значений) с виртуальными арифметическими операциями и методом вывода на экран. На его основе реализовать классы Money (деньги) и Complex (комплексное число).
В классе Money денежная сумма представляется в виде двух целых, в которых хранятся рубли и копейки соответственно. При выводе части числа снабжаются словами "руб." и "коп.". В классе Complex предусмотреть при выводе символ мнимой части (i).
Создать класс Series (набор), содержащий список (или массив) объектов этих классов в динамической памяти. Предусмотреть возможность вывода объектов списка. Написать демонстрационную программу, в которой будут использоваться все методы классов.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.