Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 11. Основные сведения о массивах параметров
Дальше: 13. Создание интерфейсов и определение абстрактных классов

12. Работа с наследованием

Прочитав эту главу, вы научитесь:

создавать производный класс, наследующий свойства базового класса;

управлять скрытием и перегрузкой методов с помощью ключевых слов new, virtual и override;

ограничивать доступность в иерархии наследования путем использования ключевого слова protected;

определять методы расширения в качестве механизма, альтернативного использованию наследования.

В мире объектно-ориентированного программирования наследование является ключевым приемом. Его можно использовать в качестве инструмента, позволяющего избегать дублирования кода при определении различных классов, имеющих ряд общих свойств и вполне конкретно связанных друг с другом. Возможно, это разные классы одного и того же типа, у каждого из которых имеются собственные отличительные признаки, например классы менеджеров, разнорабочих и всех заводских работников. Если создается приложение для моделирования предприятия, то как указать на то, что у этих менеджеров и разнорабочих имеется ряд одинаковых свойств, но имеются и свойства, отличающие их друг от друга? Например, у них у всех есть регистрационный номер работника, но обязанности и выполняемые задачи менеджеров отличаются от таковых у разнорабочих.

Именно здесь и пригодится наследование.

Что такое наследование?

Если спросить у нескольких опытных программистов, что такое наследование, они дадут весьма противоречивые ответы. Частично неразбериха возникает из-за того, что само слово «наследование» имеет несколько слегка отличающихся друг от друга значений. Если кто-нибудь что-нибудь вам завещает, то говорится, что вы это наследуете. Наряду с этим говорится, что вы наследуете половину своих генов от матери, а вторую половину — от отца. Оба значения этого слова не имеют практически ничего общего с тем, что подразумевается под наследованием в программировании.

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

Как создать в программе модель лошади и кита? Один из вариантов предполагает создание различных классов с именами Horse (лошадь) и Whale (кит). В каждом классе можно реализовать своего рода уникальное поведение, свойственное данному типу млекопитающего, например способность передвигаться рысью — Trot (для лошади) или плавать — Swim (для кита). Но как справиться с поведением, общим для лошади и кита, например дыханием — Breathe или питанием материнским молоком — SuckleYoung? К обоим классам можно добавить продублированные методы с этими именами, но это существенно затруднит сопровождение программы, особенно если вы решите создать модели и для других типов млекопитающих, например человека — Human и трубкозуба — Aardvark.

Чтобы справиться с этими задачами, в C# можно воспользоваться наследованием классов. Лошадь, кит, человек и трубкозуб являются разновидностями млекопитающих, поэтому можно создать класс по имени Mammal (млекопитающее), предоставляющий функциональные особенности, характерные для всех этих типов. Затем можно объявить, что классы Horse, Whale, Human и Aardvark наследуют свои свойства от класса Mammal. Эти классы автоматически включат в себя функциональные возможности класса Mammal (Breathe, SuckleYoung и т.д.), но можно дополнить каждый класс функциональными возможностями, уникальными для конкретного типа млекопитающего: методом Trot для класса Horse и методом Swim для класса Whale. Если потребуется изменить способ работы такого общего метода, как Breathe (дыхание), это нужно будет сделать только в одном месте — в классе Mammal.

Использование наследования

Объявление факта наследования, получаемого одним классом от другого класса, осуществляется с помощью следующего синтаксиса:

class ПроизводныйКласс : БазовыйКласс

{

    ...

}

Производный класс получает наследуемое от базового класса, и методы базового класса становятся частью производного класса. В C# классом, из которого разрешается создавать производный класс, является главным образом один базовый класс. Класс не разрешается создавать в качестве производного от двух и более классов. Но исключая те случаи, когда ПроизводныйКласс объявлен запечатанным, точно такой же синтаксис можно применять для создания других производных классов, наследующих свойства ПроизводногоКласса (запечатанные классы будут рассматриваться в главе 13 «Создание интерфейсов и определение абстрактных классов»):

class ПроизводныйПодКласс : ПроизводныйКласс

{

    ...

}

Продолжая работу с ранее рассмотренным примером, класс Mammal, в котором содержатся общие для всех млекопитающих методы Breathe и SuckleYoung, можно объявить следующим образом:

class Mammal

{

    public void Breathe()

    {

    ...

    }

    public void SuckleYoung()

    {

        ...

    }

    ...

}

Затем, как показано в следующем примере, можно определить классы для каждого отдельного вида млекопитающего, добавляя по мере необходимости дополнительные методы:

class Horse : Mammal

{

    ...

    public void Trot()

    {

        ...

    }

}

 

class Whale : Mammal

{

    ...

    public void Swim()

    {

        ...

    }

}

173754.png

ПРИМЕЧАНИЕ При наличии опыта программирования на C++ можно было заметить, что наследование тут не указывается явным образом, да и не может указываться открытым, закрытым или защищенным. В C# наследование всегда подразумевается открытым. Если вы знакомы с Java, обратите внимание на использование двоеточия и отсутствие ключевого слова extends.

При создании в приложении объекта Horse можно вызывать методы Trot, Breathe и SuckleYoung:

Horse myHorse = new Horse();

myHorse.Trot();

myHorse.Breathe();

myHorse.SuckleYoung();

Можно также создать объект Whale, но на этот раз можно будет вызвать методы Swim, Breathe и SuckleYoung. Метод Trot будет недоступен, потому что он определен только для класса Horse.

174883.png

ВНИМАНИЕ Наследование применяется только к классам, но не к структурам. Вы не можете определить свою собственную иерархию наследования, работая со структурами, и не можете определить структуру, являющуюся производной класса или другой структуры.

Все структуры фактически являются производными от абстрактного класса по имени System.ValueType. (Абстрактные классы рассматриваются в главе 13.) Это просто особенности реализации способа, с помощью которого среда Microsoft .NET Framework определяет общее поведение для типов значений, основанных на использовании стека. Непосредственное использование ValueType в ваших собственных приложениях маловероятно.

Повторное обращение к классу System.Object

Класс System.Object является корневым классом всех остальных классов. Подразумевается, что все классы являются производными от System.Object. Поэтому компилятор C# по умолчанию переписывает класс Mammal в следующий код (который, если в этом есть реальная потребность, можно записать явным образом):

class Mammal : System.Object

{

    ...

}

Любые методы в классе System.Object автоматически передаются вниз по цепочке наследования тем классам, которые являются производными от Mammal, например классам Horse и Whale. На практике это означает, что все определяемые вами классы автоматически наследуют все свойства класса System.Object. К ним относятся и такие методы, как ToString (эти методы рассматривались в главе 2 «Работа с переменными, операторами и выражениями»), используемые для преобразования объекта в строку, обычно с целью вывода информации на экран.

Вызов конструкторов базового класса

В дополнение к наследуемым методам производный класс автоматически содержит все поля из базового класса. Обычно при создании объекта эти поля требуют инициализации. Как правило, инициализация такого рода выполняется в конструкторе. Вспомним, что все классы имеют как минимум один конструктор. (Если вы не предоставляете конструктор сами, компилятор создает для вас пассивный конструктор.)

Полезно, чтобы в качестве составной части инициализации конструктор в производном классе вызывал конструктор своего базового класса, что позволит конструктору базового класса выполнить требующуюся ему дополнительную инициализацию. Для вызова конструктора базового класса можно при определении конструктора наследующего класса указать ключевое слово base:

class Mammal // базовый класс

{

    public Mammal(string name) // конструктор для базового класса

    {

        ...

    }

    ...

}

 

class Horse : Mammal // производный класс

{

    public Horse(string name)

        : base(name) // вызывает Mammal(name)

    {

        ...

    }

    ...

}

Если в конструкторе производного класса нет явного указания на вызов конструктора базового класса, компилятор перед выполнением кода в конструкторе производного класса пытается по умолчанию вставить вызов пассивного конструктора базового класса. Если взять ранее показанный пример, то компилятор переписывает код

class Horse : Mammal

{

    public Horse(string name)

    {

        ...

    }

    ...

}

в следующий код:

class Horse : Mammal

{

    public Horse(string name)

        : base()

    {

        ...

    }

    ...

}

Этот код работает в том случае, если у класса Mammal имеется открытый пассивный конструктор. Но такой конструктор есть не у всех классов (к примеру, стоит вспомнить, что компилятор создает пассивный конструктор, только если вами не созданы какие-либо активные конструкторы), и если в таком случае забыть вызвать правильный конструктор базового класса, это приведет к ошибке в ходе компиляции.

Присваивание классов

В предыдущих примерах этой книги было показано, как объявляется переменная путем использования типа класса и как для создания объекта используется ключевое слово new. Приводились также примеры того, как правила проверки типов C# не позволяли вам присваивать объект одного типа переменной, объявленной с указанием другого типа. Например, при показанных здесь определениях классов Mammal, Horse и Whale код, следующий за этими определениями, будет некорректным:

class Mammal

{

    ...

}

class Horse : Mammal

{

    ...

}

class Whale : Mammal

{

    ...

}

...

Horse myHorse = new Horse(...);

Whale myWhale = myHorse; // ошибка – разные типы

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

Horse myHorse = new Horse(...);

Mammal myMammal = myHorse; // допустимо, Mammal — базовый класс для Horse

Если мыслить логически, все лошади (Horse) являются млекопитающими (Mammal), поэтому можно вполне свободно присвоить объект типа Horse переменной типа Mammal. Иерархия наследования означает, что о Horse можно думать просто как о специализированном типе Mammal: у этого объекта есть все, что есть у Mammal, плюс к этому имеются и дополнения, определяемые любыми методами и полями, добавленными вами к классу Horse. Можно также сделать так, чтобы Mammal-переменная ссылалась на Whale-объект. Но есть одно существенное ограничение: при ссылке на Horse- или Whale-объект с использованием Mammal-переменной доступ можно получить только к методам и полям, определенным в классе Mammal. Любые дополнительные методы, определенные в классе Horse или Whale, увидеть из класса Mammal невозможно:

Horse myHorse = new Horse(...);

Mammal myMammal = myHorse;

myMammal.Breathe(); // все в порядке - Breathe является частью класса Mammal

myMammal.Trot();    // ошибка - Trot не является частью класса Mammal

173765.png

ПРИМЕЧАНИЕ Ранее уже объяснялось, почему object-переменной можно присвоить практически всё. Вспомним, что object является псевдонимом для System.Object и все классы наследуются из System.Object либо напрямую, либо опосредованно.

Следует понимать, что обратного действия это правило не имеет. Вы не можете просто так присвоить Mammal-объект Horse-переменной:

Mammal myMammal = newMammal(...);

Horse myHorse = myMammal; // ошибка

Это ограничение может показаться несколько странным, но вспомним, что не все Mammal-объекты, моделирующие млекопитающих, относятся к лошадям (Horse), некоторые могут относиться и к китам (Whale). Mammal-объект можно присвоить Horse-переменной только в том случае, если сначала с помощью оператора as или or удастся проверить, что млекопитающее действительно относится к лошади, или же после приведения типа с использованием ключевого слова cast (операторы as и or, а также приведение типов рассматриваются в главе 7 «Создание классов и объектов и управление ими»). В следующем примере оператор as используется для проверки того, что переменная myMammal ссылается на Horse-объект, и если проверка пройдет успешно, присваивание значения переменной myHorseAgain приведет к тому, что она будет ссылаться на тот же самый Horse-объект. Если же переменная myMammal ссылается на какой-либо другой тип млекопитающего (Mammal), оператор as вместо этого приведет к тому, что будет возвращено значение null:

Horse myHorse = new Horse(...);

Mammal myMammal = myHorse; // myMammal ссылается на Horse

...

Horse myHorseAgain = myMammal as Horse; // порядок: myMammal ссылалась на Horse

...

Whale myWhale = new Whale(...);

myMammal = myWhale;

...

myHorseAgain = myMammal as Horse; // возвращает null - myMammal ссылалась на Whale

Объявление новых методов

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

173773.png

ПРИМЕЧАНИЕ К сигнатуре метода относятся имя метода, а также количество и типы его параметров, но не тип его возвращаемого значения. Два метода с одинаковыми именами, получающие один и тот же список параметров, имеют одинаковую сигнатуру, даже если возвращают разные типы.

Метод в производном классе маскирует (или скрывает) метод в базовом классе, имеющий такую же сигнатуру. Например, если откомпилировать следующий код, компилятор выдаст предупреждающее сообщение о том, что Horse.Talk загораживает унаследованный метод Mammal.Talk:

class Mammal

{

    ...

    public void Talk() // предполагается, что все млекопитающие могут издавать

                       // звуки

    {

        ...

    }

}

 

class Horse : Mammal

{

    ...

    public void Talk() // лошади издают не такие звуки, как другие млекопитающие!

    {

        ...

    }

}

Хотя ваш код будет откомпилирован и запущен, к данному предупреждению нужно отнестись всерьез. Если другой класс станет производным от Horse и вызовет метод Talk, ожидания могут быть связаны с вызовом того метода, который реализован в классе Mammal. Но метод Talk в классе Horse загораживает метод Talk в классе Mammal, и вместо него будет вызван метод Horse.Talk. Такое совпадение в лучшем случае создает путаницу, и во избежание накладок лучше рассмотреть возможность переименования методов.

Но если есть уверенность в необходимости наличия двух методов с одинаковой сигнатурой, позволяющей загородить метод Mammal.Talk, предупреждение можно заглушить, воспользовавшись для этого ключевым словом new:

class Mammal

{

    ...

    public void Talk()

    {

        ...

    }

}

 

class Horse : Mammal

{

    ...

    new public void Talk()

    {

        ...

    }

}

Такое использование ключевого слова new не разобщает эти два метода, и загораживание по-прежнему происходит. Отключается только предупреждение. По сути, ключевое слово new как бы говорит следующее: «Я знаю, что делаю, поэтому прекращай показывать мне эти предупреждения».

Объявление виртуальных методов

Иногда способ реализации метода в базовом классе нужно скрыть. Рассмотрим в качестве примера метод ToString в System.Object. Назначение метода ToString заключается в преобразовании объекта в его строковое представление. Наличие метода в классе System.Object объясняется высокой востребованностью этого метода, поэтому класс автоматически предоставляет его всем классам. Но откуда версия метода ToString, реализованная в классе System.Object, знает, как преобразовать в строку экземпляр производного класса? В производном классе может быть любое количество полей с интересующими вас значениями, которые могут стать частью строки. Ответ заключается в простоте реализации ToString в System.Object. Все, на что он способен, — это преобразовать объект в строку, содержащую имя его типа, например «Mammal» или «Horse». Но пользы от этого мало. Тогда зачем предоставлять почти бесполезный метод? Ответ на этот вопрос требует некоторых размышлений.

Очевидно, что, в принципе, идея наличия метода ToString неплоха и все классы должны предоставлять метод, который можно было бы использовать для преобразования объектов в строки для их отображения на экране или выполнения отладки. Внимания требует лишь сама реализация. Фактически вы не рассчитываете на вызов метода ToString, определенного классом System.Object, и он является всего лишь прототипом. Скорее всего, вам придется предоставить собственную версию метода ToString в каждом определяемом вами классе, перегружая его исходную реализацию, имеющуюся в System.Object. Версия в System.Object является всего лишь своеобразной страховкой на случай, если в классе не реализована своя собственная, особая версия метода ToString или если она ему и вовсе не требуется.

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

Метод можно пометить как виртуальный, воспользовавшись для этого ключевым словом virtual. Например, метод ToString в классе System.Object определен следующим образом:

namespace System

{

    class Object

    {

        public virtual string ToString()

        {

            ...

        }

        ...

    }

    ...

}

173783.png

ПРИМЕЧАНИЕ Если вам приходилось программировать на Java, то вы могли заметить, что изначально методы в C# не являются виртуальными.

Объявление методов с помощью ключевого слова override

Если в базовом классе метод объявлен виртуальным, то в производном классе для объявления другой реализации метода можно воспользоваться ключевым словом override:

class Horse : Mammal

{

    ...

    public override string ToString()

    {

        ...

    }

}

Новая реализация метода в производном классе может вызвать исходную реализацию метода в базовом классе, для чего нужно воспользоваться ключевым словом base:

public override string ToString()

{

    string temp = base.ToString();

    ...

}

При объявлении полиморфных методов, рассматриваемых во врезке «Виртуальные методы и полиморфизм», нужно следовать нескольким важным правилам использования ключевых слов virtual и override.

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

• Сигнатуры виртуальных и переопределенных методов должны быть идентичными: у них должны быть одинаковые имена, а также количество и типы параметров.

• Перегрузить можно только виртуальный метод. Если метод базового класса не является виртуальным и вы попытаетесь его перегрузить, в ходе компиляции будет выдана ошибка. Это вполне резонно, поскольку решать, какие из методов могут быть перегружены, — прерогатива проектировщика базового класса.

• Если в производном классе метод объявлен без использования ключевого слова override, метод базового класса не перегружается, а скрывается. Иными словами, получается реализация совершенно другого метода с точно таким же именем. Как и ранее, в результате этого во время компиляции будет выдано предупреждение, от которого можно избавиться, воспользовавшись, как уже было показано, ключевым словом new.

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

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

Используя виртуальные методы, можно вызывать различные версии одного и того же метода, основываясь на типе объекта, который определяется динамически в ходе выполнения программы. Рассмотрим следующие примеры классов, определяющих один из вариантов ранее рассмотренной иерархии Mammal:

class Mammal

{

    ...

    public virtual string GetTypeName()

    {

        return "This is a mammal" ;

    }

}

 

class Horse : Mammal

{

    ...

    public override string GetTypeName()

    {

        return "This is a horse";

    }

}

class Whale : Mammal

{

    ...

    public override string GetTypeName()

    {

        return "This is a whale";

    }

}

class Aardvark : Mammal

{

    ...

}

Здесь следует отметить две особенности: во-первых, ключевое слово override, используемое при определении метода GetTypeName в классах Horse и Whale, и во-вторых, тот факт, что в классе Aardvark нет метода GetTypeName.

А теперь изучим следующий блок кода:

Mammal myMammal;

Horse myHorse = new Horse(...);

Whale myWhale = new Whale(...);

Aardvark myAardvark = new Aardvark(...);

myMammal = myHorse;

Console.WriteLine(myMammal.GetTypeName()); // Horse

myMammal = myWhale;

Console.WriteLine(myMammal.GetTypeName()); // Whale

myMammal = myAardvark;

Console.WriteLine(myMammal.GetTypeName()); // Aardvark

Что будут выводить на экран консоли три различные инструкции Console.WriteLine? На первый взгляд от всех них следует ожидать вывода на экран строки «This is a mammal», поскольку в каждой инструкции метод GetTypeName вызывается в отношении переменной myMammal, имеющей тип Mammal. Но в первом случае можно увидеть, что myMammal фактически ссылается на Horse. (Вспомним, что Horse-переменную можно присвоить Mammal-переменной, потому что класс Horse наследуется из класса Mammal.) Поскольку метод GetTypeName определен в качестве виртуального, среда выполнения определяет, что нужно вызвать метод Horse.GetTypeName, поэтому инструкция выведет на экран сообщение «This is a horse». Точно такая же логика применяется ко второй инструкции Console.WriteLine, которая выводит сообщение «This is a whale». Третья инструкция вызывает метод Console.WriteLine в отношении объекта Aardvark. Но в классе Aardvark нет метода GetTypeName, поэтому вызывается исходный метод, находящийся в классе Mammal и возвращающий строку «This is a mammal».

Этот механизм вызова с помощью одной и той же инструкции различных методов в зависимости от имеющегося контекста называется полиморфизмом, что буквально означает «множество форм».

Основные сведения о защищенном доступе

С помощью ключевых слов public и private создаются две крайние степени доступности: открытые поля и методы класса доступны отовсюду, а закрытые поля и методы доступны только внутри самого класса.

С точки зрения изолированности классов этих двух крайностей вполне достаточно. Но как известно всем опытным программистам, пользующимся объектно-ориентированными технологиями, решать сложные проблемы с помощью изолированных классов невозможно. Эффективным способом связи классов является наследование, позволяющее понятным образом получить специализированные и тесные взаимоотношения между производным классом и его базовым классом. Зачастую базовому классу полезно будет разрешить производным классам получать доступ к некоторым своим компонентам, одновременно закрывая к ним доступ со стороны классов, не являющихся частью иерархии наследования. В такой ситуации эти компоненты можно пометить ключевым словом protected. Данный механизм работает следующим образом.

• Если класс А является производным другого класса Б, он может получать доступ к защищенным составляющим класса Б. Иными словами, внутри производного класса А защищенная составляющая класса Б фактически является открытой.

• Если класс А не является производным класса Б, он не может получать доступ к защищенным составляющим класса Б. Таким образом, внутри класса А защищенная составляющая класса Б фактически является закрытой.

Язык C# дает программистам полную свободу в объявлении методов и полей защищенными. Но в большинстве руководств по объектно-ориентированному программированию рекомендуется содержать поля в строго закрытом состоянии везде, где это возможно, и ослаблять это ограничение, только когда в этом возникнет абсолютная необходимость. Открытые поля нарушают инкапсуляцию, поскольку все пользователи класса получают непосредственный неограниченный доступ к полям. Защищенные поля сохраняют инкапсуляцию для тех пользователей класса, которым эти поля недоступны. Но защищенные поля все же позволяют нарушать инкапсуляцию другими классами, наследуемыми из базового класса.

173789.png

ПРИМЕЧАНИЕ Доступ к защищенной составляющей базового класса можно получить не только из производного класса, но и из классов, являющихся производными по отношению к данному производному классу.

В следующем упражнении будет определена простая иерархия классов для моделирования различных типов транспортных средств. Будут определены базовый класс по имени Vehicle и производные классы с именами Airplane и Car. В классе Vehicle будут также определены общие методы с именами StartEngine и StopEngine, означающими запуск и остановку двигателя соответственно, а в оба производных класса будут добавлены методы, специфичные для этих классов. И наконец, к классу Vehicle будет добавлен виртуальный метод по имени Drive, а в оба производных класса будут добавлены методы, переопределяющие исходную реализацию этого метода.

Создание иерархии классов

Откройте в среде Microsoft Visual Studio 2015 проект Vehicles, который находится в папке \Microsoft Press\VCSBS\Chapter 12\Vehicles вашей папки документов. В проекте Vehicles содержится файл Program.cs, в котором определяется класс Program с методами Main и doWork, уже встречавшимися в предыдущих упражнениях.

Щелкните правой кнопкой мыши в обозревателе решений на проекте Vehicles, укажите на пункт Добавить, а затем щелкните на пункте Класс. Откроется диалоговое окно Добавить новый элемент, в котором нужно убедиться в выделении шаблона Класс. Наберите в поле Имя строку Vehicle.cs и щелкните на кнопке Добавить. Будет создан и добавлен к проекту файл Vehicle.cs, код которого появится в окне редактора. В файле содержится определение пустого класса по имени Vehicle.

Добавьте к классу Vehicle методы StartEngine и StopEngine, выделенные далее жирным шрифтом:

class Vehicle

{

    public void StartEngine(string noiseToMakeWhenStarting)

    {

        Console.WriteLine($"Starting engine: {noiseToMakeWhenStarting}");

    }

 

    public void StopEngine(string noiseToMakeWhenStopping)

    {

        Console.WriteLine($"Stopping engine: {noiseToMakeWhenStopping}");

    }

}

Эти методы будут унаследованы всеми классами, являющимися производными от класса Vehicle. Значения для параметров noiseToMakeWhenStarting (шум при запуске) и noiseToMakeWhenStopping (шум при глушении) для каждого типа транспортного средства будут разными и помогут вам позже идентифицировать, какое транспортное средство запускается или глушится.

Щелкните в меню Проект на пункте Добавить класс. На экране снова откроется диалоговое окно Добавить новый элемент. Наберите в поле Имя строку Airplane.cs и щелкните на кнопке Добавить. К проекту будет добавлен новый файл, содержащий класс по имени Airplane, код которого появится в окне редактора. Внесите в определение класса Airplane изменение, выделенное здесь жирным шрифтом и показывающее, что он наследуется из класса Vehicle:

class Airplane : Vehicle

{

}

Добавьте к классу Airplane методы TakeOff (взлет) и Land (приземление), выделенные здесь жирным шрифтом:

class Airplane : Vehicle

{

    public void TakeOff()

    {

        Console.WriteLine("Taking off");

    }

 

    public void Land()

    {

        Console.WriteLine("Landing");

    }

}

Щелкните в меню Project на пункте Добавить класс. На экране снова появится диалоговое окно Добавить новый элемент. Наберите в поле Имя строку Car.cs и щелкните на кнопке Добавить. К проекту будет добавлен новый файл, содержащий класс по имени Car, код которого появится в окне редактора. Внесите в определение класса Car изменение, выделенное здесь жирным шрифтом и показывающее, что он наследуется из класса Vehicle:

class Car : Vehicle

{

}

Добавьте к классу Car методы Accelerate (газ) и Brake (тормоз), выделенные жирным шрифтом:

class Car : Vehicle

{

    public void Accelerate()

    {

        Console.WriteLine("Accelerating");

    }

 

    public void Brake()

    {

        Console.WriteLine("Braking");

    }

}

Выведите в окно редактора файл Vehicle.cs. Добавьте к классу Vehicle виртуальный метод Drive, выделенный здесь жирным шрифтом:

class Vehicle

{

    ...

    public virtual void Drive()

    {

        Console.WriteLine("Default implementation of the Drive method");

    }

}

Выведите в окно редактора файл Program.cs. Удалите в методе doWork комментарий // TODO: и добавьте код, который создает экземпляр класса Airplane и тестирует работу его методов, имитируя быстрое путешествие на самолете:

static void doWork()

{

    Console.WriteLine("Journey by airplane:");

    Airplane myPlane = new Airplane();

    myPlane.StartEngine("Contact");

    myPlane.TakeOff();

    myPlane.Drive();

    myPlane.Land();

    myPlane.StopEngine("Whirr");

}

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

static void doWork()

{

    ...

    Console.WriteLine("\nJourney by car:");

    Car myCar = new Car();

    myCar.StartEngine("Brm brm");

    myCar.Accelerate();

    myCar.Drive();

    myCar.Brake();

    myCar.StopEngine("Phut phut");

}

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь, что в окне консоли эта программа выводит сообщения, имитирующие различные стадии путешествия на самолете и на машине (рис. 12.1).

Обратите внимание на то, что оба транспортных средства вызывают исходную реализацию виртуального метода Drive, поскольку ни один из классов пока этот метод не перегружает.

12_01.tif 

Рис. 12.1

Нажмите Ввод, чтобы закрыть приложение и вернуться в среду Visual Stu­dio 2015.

Выведите в окно редактора класс Airplane. Создайте в классе Airplane переопределенный метод Drive, выделенный здесь жирным шрифтом:

class Airplane : Vehicle

{

    ...

    public override void Drive()

    {

        Console.WriteLine("Flying");

    }

}

173796.png

ПРИМЕЧАНИЕ Система IntelliSense выведет список доступных виртуальных методов. Если выбрать из списка IntelliSense метод Drive, Visual Studio автоматически вставит в ваш код инструкцию, вызывающую метод base.Drive. В таком случае удалите инструкцию, поскольку в данном упражнении она не требуется.

Выведите в окно редактора класс Car. Создайте в классе Car переопределенный метод Drive, выделенный здесь жирным шрифтом:

class Car : Vehicle

{

    ...

    public override void Drive()

    {

        Console.WriteLine("Motoring");

    }

}

Щелкните в меню Отладка на пункте Запуск без отладки. Обратите внимание на то, что теперь при вызове приложения объект Airplane выводит в окне консоли сообщение Flying, а объект Car выводит сообщение Motoring (рис. 12.2).

12_02.tif 

Рис. 12.2

Нажмите Ввод, чтобы закрыть приложение и вернуться в среду Visual Stu­dio 2015.

Выведите в окно редактора файл Program.cs. Добавьте к методу doWork инструкции, выделенные здесь жирным шрифтом:

static void doWork()

{

    ...

    Console.WriteLine("\nTesting polymorphism");

    Vehicle v = myCar;

    v.Drive();

    v = myPlane;

    v.Drive();

}

Этот код тестирует полиморфность, возникающую при использовании виртуального метода Drive. В нем путем использования Vehicle-переменной создается ссылка на объект Car (что вполне допустимо, поскольку все Car-объекты являются Vehicle-объектами), а затем с использованием Vehicle-переменной вызывается метод Drive. Последние две инструкции создают ссылку Vehicle-переменной на объект Airplane и совершают еще один вызов, похожий на очередной вызов того же самого метода Drive.

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что в окне консоли появились те же самые сообщения, что и прежде, а за ними был выведен следующий текст (рис. 12.3):

Testing polymorphism

Motoring

Flying

12_03.tif 

Рис. 12.3

Метод Drive является виртуальным, поэтому среда выполнения (но не компилятор) определяет, какую из версий метода Drive нужно вызвать в отношении ссылающейся на него Vehicle-переменной, на основе реального типа объекта, на который ссылается эта переменная. В первом случае Vehicle-объект ссылается на Car, поэтому приложение вызывает метод Car.Drive. Во втором случае Vehicle-объект ссылается на Airplane, поэтому приложение вызывает метод Airplane.Drive.

Нажмите Ввод, чтобы закрыть приложение и вернуться в среду Visual Stu­dio 2015.

Основные сведения о методах расширения

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

Представим, к примеру, что вам нужно добавить новую функцию к типу int путем создания метода по имени Negate, возвращающего эквивалент значения, уже имеющегося у целочисленной переменной, но с отрицательным знаком. (Понятно, что для выполнения подобной задачи можно просто воспользоваться оператором унарного минуса (), но потерпите меня еще немного.) Один из способов достижения поставленной цели заключается в определении нового типа по имени NegInt32, наследуемого из System.Int32 (int является псевдонимом для System.Int32), и добавлении метода Negate:

class NegInt32 : System.Int32 // не пытайтесь сделать это!

{

    public int Negate()

    {

        ...

    }

}

Теоретически в дополнение к методу Negate класс NegInt32 унаследует все функциональные возможности, связанные с типом System.Int32. Но отказаться от применения этого подхода можно по двум причинам.

• Этот метод применяется только к типу NegInt32, и если потребуется воспользоваться им с существующими в вашем коде int-переменными, придется изменить определение каждой int-переменной на тип NegInt32.

• Тип System.Int32 фактически является структурой, а не классом, а применить наследование в отношении структуры невозможно.

Именно здесь и пригодятся методы расширения.

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

Методы расширения определяются в статическом классе и в качестве первого параметра метода вместе с ключевым словом this определяют тип, к которому применяется метод. Следующий пример показывает, как можно реализовать метод расширения Negate для типа int:

static class Util

{

    public static int Negate(this int i)

    {

        return -i;

    }

}

Синтаксис выглядит немного странно, но на его принадлежность к методу расширения указывает ключевое слово this, стоящее перед параметром метода Negate, а принадлежность параметра, перед которым стоит this, к типу int означает, что расширению подвергается тип int.

Чтобы воспользоваться методом расширения, нужно ввести класс Util в область видимости. (Если необходимо, добавьте инструкцию using, указывающую на то пространство имен, которому принадлежит класс Util, или инструкцию using static, указывающую непосредственно на класс Util.) Затем можно будет для ссылки на метод воспользоваться обычной формой записи с использованием символа точки (.):

int x = 591;

Console.WriteLine($"x.Negate {x.Negate()}");

Обратите внимание на то, что ссылаться на класс Util где-либо в инструкции, вызывающей метод Negate, не нужно. Компилятор C# автоматически обнаружит все методы расширения для заданного типа из всех статических классов, находящихся в области видимости. Можно также вызвать метод Util.Negate, передав ему в качестве параметра int-значение и воспользовавшись уже встречавшимся ранее обычным синтаксисом, но тогда было бы незачем определять этот метод в качестве метода расширения:

int x = 591;

Console.WriteLine($"x.Negate {Util.Negate(x)}");

В следующем упражнении метод расширения будет добавлен к типу int. С помощью этого метода расширения можно будет преобразовать значение int-переменной с основанием 10 в его представление в системе счисления с другим основанием.

Создание метода расширения

Откройте в среде Visual Studio 2015 проект ExtensionMethod, который находится в папке \Microsoft Press\VCSBS\Chapter 12\ExtensionMethod вашей папки документов.

Выведите в окно редактора файл Util.cs. В этом файле содержится статический класс по имени Util, находящийся в пространстве имен Extensions. Не забудьте, что методы расширения нужно определять в статическом классе. В этом классе не содержится ничего, кроме комментария // TODO:.

Удалите комментарий и объявите в классе Util открытый статический метод по имени ConvertToBase. Этот метод должен принимать два параметра: int-параметр по имени i, перед которым будет указано ключевое слово this, свидетельствующее о том, что это метод расширения для типа int, и еще один обычный int-параметр по имени baseToConvertTo (основание счисления для преобразования).

Этот метод будет заниматься преобразованием значения, содержащегося в i, в число по основанию, которое указано в параметре baseToConvertTo. Метод должен возвратить int-значение с конвертированной величиной.

Метод ConvertToBase должен иметь следующий вид:

static class Util

{

    public static int ConvertToBase(this int i, int baseToConvertTo)

    {

    }

}

Добавьте к методу ConvertToBase инструкцию if, проверяющую, что значение параметра baseToConvertTo находится между 2 и 10.

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

Метод ConvertToBase должен приобрести следующий вид:

public static int ConvertToBase(this int i, int baseToConvertTo)

{

    if (baseToConvertTo < 2 || baseToConvertTo > 10)

    {

        throw new ArgumentException("Value cannot be converted to base " +

                                      baseToConvertTo.ToString());

    }

}

Добавьте к методу ConvertToBase после инструкции, выдающей исключение ArgumentException, инструкции, выделенные далее жирным шрифтом.

В этом коде реализуется широко известный алгоритм, преобразующий число по основанию 10 в число с другим основанием системы счисления. (Версия этого алгоритма для преобразования десятичного числа в восьмеричное уже была представлена в главе 5 «Использование инструкций составного присваивания и итераций».)

public static int ConvertToBase(this int i, int baseToConvertTo)

{

    ...

    int result = 0;

    int iterations = 0;

    do

    {

        int nextDigit = i % baseToConvertTo;

        i /= baseToConvertTo;

        result += nextDigit * (int)Math.Pow(10, iterations);

        iterations++;

    }

    while (i != 0);

 

    return result;

}

Выведите в окно редактора файл Program.cs. После инструкции using System;, находящейся в начале файла, добавьте инструкцию using:

using Extensions;

Эта инструкция вводит пространство имен, содержащее класс UTIL, в область видимости. Если этого не сделать, то в файле Program.cs метод расширения ConvertToBase виден не будет.

Добавьте к методу doWork класса Program инструкции, выделенные здесь жирным шрифтом, заменив ими комментарий // TODO::

static void doWork()

{

    int x = 591;

    for (int i = 2; i <= 10; i++)

    {

        Console.WriteLine($"{x} in base {i} is {x.ConvertToBase(i)}");

    }

}

Этот код создает int-переменную по имени x и присваивает ей значение 591. (Можете выбрать любое другое понравившееся вам целочисленное значение.) Затем в коде используется цикл для вывода на экран значения 591 во всех видах с основанием систем счисления между 2 и 10 (рис. 12.4). Обратите внимание на то, что ConvertToBase появляется в качестве метода расширения в системе IntelliSense, как только вы в инструкции Console.WriteLine наберете после x символ точки (.).

12_04.tif 

Рис. 12.4

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь, что программа выводит на консоль сообщения, показывающие значение 591 в различных системах счисления (рис. 12.5).

12_05.tif 

Рис. 12.5

Нажмите Ввод, чтобы закрыть программу и вернуться в среду Visual Studio 2015.

Выводы

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

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 13.

Если вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Создать производный класс из базового класса

Объявите новое имя класса, после него поставьте двоеточие и укажите имя базового класса, например:

class DerivedClass : BaseClass

{

   ...

}

Вызвать конструктор базового класса в качестве части конструктора для производного класса

Укажите в определении конструктора перед телом конструктора производного класса суффикс в виде вызова base и предоставьте базовому конструктору любые необходимые ему параметры, например:

class DerivedClass : BaseClass

{

   ...

   public DerivedClass(int x) : base(x)

   {

      ...

   }

   ...

}

Объявить виртуальный метод

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

class Mammal

{

   public virtual void Breathe()

   {

      ...

   }

   ...

}

Реализовать в производном классе метод, переопределяющий унаследованный виртуальный метод

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

class Whale : Mammal

{

  public override void Breathe()

   {

      ...

   }

   ...

}

Определить для типа метод расширения

Добавьте к статическому классу статический открытый метод. Первым параметром должен быть расширяемый тип, перед которым ставится ключевое слово this, например:

static class Util

{

   public static int Negate(this int i)

   {

      return -i;

   }

}

Назад: 11. Основные сведения о массивах параметров
Дальше: 13. Создание интерфейсов и определение абстрактных классов

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com