Как запретить наследовать класс

Обновлено: 19.04.2024

В предыдущих уроках этой главы вы немного узнали о том, как работает базовое наследование. До сих пор во всех наших примерах мы использовали открытое (публичное) наследование. То есть наш производный класс открыто наследовал базовый класс.

В этом уроке мы более подробно рассмотрим открытое ( public ) наследование, а также два других вида наследования: закрытое (частное, private ) и защищенное ( protected ). Мы также рассмотрим, как различные виды наследования взаимодействуют со спецификаторами доступа, чтобы разрешить или ограничить доступ к членам.

К этому моменту вы уже видели спецификаторы доступа public и private , которые определяют, кто может получить доступ к членам класса. Напоминаем, что к открытым ( public ) членам может получить доступ кто угодно. Доступ к закрытым ( private ) членам могут получить только функции-члены этого же класса или друзей. Это означает, что производные классы не могут напрямую обращаться к закрытым членам базового класса!

Это довольно просто, и вы уже должны были к этому привыкнуть.

Спецификатор доступа protected

При работе с наследованными классами всё становится немного сложнее.

В C++ есть третий спецификатор доступа, о котором мы еще не говорили, потому что он полезен только в контексте наследования. Спецификатор доступа protected позволяет получить доступ к члену классу, к которому принадлежит член, друзьям и производным классам. Однако защищенные члены недоступны извне класса.

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

Итак, когда мне следует использовать спецификатор доступа protected ?

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

Следовательно, использование спецификатора доступа protected наиболее полезно, когда вы (или ваша команда) собираетесь наследоваться от ваших собственных классов, и количество производных классов не слишком велико. Таким образом, если вы вносите изменения в реализацию базового класса и в результате необходимы обновления производных классов, вы можете вносить эти обновления самостоятельно (и это не будет длиться вечно, поскольку количество производных классов ограничено).

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

Различные виды наследования и их влияние на доступ

Во-первых, классы могут наследоваться от других классов тремя разными способами: открытый ( public ), защищенный ( protected ) и закрытый ( private ).

Для этого при выборе класса для наследования просто укажите, какой тип доступа вам нужен:

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

Это дает нам 9 комбинаций: 3 спецификатора доступа к членам ( public , private и protected ) и 3 типа наследования ( public , private и protected ).

Так в чем разница между ними? Вкратце, когда члены наследуются, спецификатор доступа для унаследованного члена может быть изменен (только в производном классе) в зависимости от типа используемого наследования. Другими словами, члены, которые были открытыми или защищенными в базовом классе, могут изменять спецификаторы доступа в производном классе.

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

При рассмотрении примеров помните о следующих правилах:

  • Класс всегда может получить доступ к своим собственным (ненаследуемым) членам.
  • Сторонний код получает доступ к членам класса на основе спецификаторов доступа класса, к которому он обращается.
  • Производный класс обращается к унаследованным членам на основе спецификатора доступа, унаследованного от родительского класса. Это зависит от спецификатора доступа и типа наследования.

Открытое наследование

Открытое наследование – это, безусловно, наиболее часто используемый тип наследования. Фактически, вы очень редко увидите или будете использовать другие типы наследования, поэтому ваше основное внимание должно быть сосредоточено на понимании этого раздела. К счастью, открытое наследование также легче всего для понимания. Когда вы наследуете базовый класс открыто, унаследованные открытые члены остаются открытыми, а унаследованные защищенные члены остаются защищенными. Унаследованные закрытые члены, которые были недоступны, потому что они были закрытыми в базовом классе, остаются недоступными.

Спецификатор доступа в базовом классеСпецификатор доступа при открытом наследовании
public public
protected protected
private не доступен

Ниже приведен пример, показывающий, как всё работает:

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

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

Лучшая практика

Используйте открытое наследование, если у вас нет особых причин делать иначе.

Защищенное наследование

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

Поскольку эта форма наследования встречается очень редко, мы пропустим этот пример и подведем итоги в виде таблицы:

Спецификатор доступа в базовом классеСпецификатор доступа при защищенном наследовании
public protected
protected protected
private не доступен

Закрытое наследование

При закрытом наследовании все члены базового класса наследуются как закрытые. Это означает, что закрытые члены остаются закрытыми, а защищенные и открытые члены становятся закрытыми.

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

Подведем итоги в виде таблицы:

Спецификатор доступа в базовом классеСпецификатор доступа при закрытом наследовании
public private
protected private
private не доступен

Закрытое наследование может быть полезно, когда производный класс не имеет очевидной связи с базовым классом, но использует базовый класс для внутренней реализации. В таком случае мы, вероятно, не хотим, чтобы открытый интерфейс базового класса предоставлялся через объекты производного класса (как это было бы, если бы мы наследовали открыто).

На практике закрытое наследование используется редко.

Последний пример

Base может получить доступ к своим членам без ограничений. Внешний код может получить доступ только к m_public . Производные классы могут обращаться к m_public и m_protected .

D2 может получить доступ к своим членам без ограничений. D2 может получить доступ к членам Base m_public и m_protected , но не к m_private . Поскольку D2 наследуется от Base закрыто, при доступе через D2 члены m_public и m_protected теперь считаются закрытыми. Это означает, что внешний код не может получить доступ к этим переменным при использовании объекта D2 , равно как и никакие классы, производные от D2 .

D3 может получить доступ к своим собственным членам без ограничений. D3 может получить доступ к членам m_public2 и m_protected2 класса D2 , но не к m_private2 . Поскольку D3 наследуется от D2 открыто, при доступе через D3 члены m_public2 и m_protected2 сохраняют свои спецификаторы доступа. D3 не имеет доступа к m_private класса Base , который в Base уже был закрытым. У него также нет доступа к членам m_protected или m_public класса Base , которые стали закрытыми, когда D2 унаследовал их.

Резюме

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

Во-первых, класс (и его друзья) всегда могут получить доступ к своим собственным ненаследуемым членам. Спецификаторы доступа влияют только на то, могут ли посторонние и производные классы получить доступ к этим членам.

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

Вот таблица всех комбинаций спецификаторов доступа и типов наследования:

Спецификатор доступа в базовом классеСпецификатор доступа при открытом наследованииСпецификатор доступа при закрытом наследованииСпецификатор доступа при защищенном наследовании
public public private protected
protected protected private protected
private не доступенне доступенне доступен

В заключение, хотя в приведенных выше примерах мы показали только примеры с использованием переменных-членов, эти правила доступа верны для всех членов (например, функций-членов и типов, объявленных внутри класса).

Объект представляет собой некую абстрактную сущность. Допустим мы хотим создать класс "Animal" (Животное). Но животных же существует великое множество и вряд ли мы одним классом сможем описать их всех. В классе "Animal" мы можем создать поля и методы, присущие всем животным. Например, полями общими для всех животных могут быть "Вес", "Средняя продолжительность жизни", "Имеется хвост или нет". Методом может быть "eat()" (кушать), ведь все же животные питаются. От такого общего класса мы можем создать дочерний класс, который расширяет родительский класс. Например, класс "Dog" (собака) может расширять класс "Animal" уже конкретными полями и методами, которые соответствуют именно собакам.

Отношение наследования — это отношение перехода от более общей абстракции к более конкретной.

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

Абстрактный класс — это класс, объекты которого нельзя создавать, т.е. нельзя будет использовать ключевое слово "new". Абстрактные классы используются при наследовании и используются как прародители к другим классам (реальным, не абстрактным). Т.е. в нашем примере можно класс "Animal" пометить как "abstract". Абстрактным может быть также и метод (это метод без реализации). Если в классе присутствует хотя бы один абстрактный метод, то и сам класс обязан быть абстрактным. В обратную сторону правило не действует.

С помощью ключевого слова "sealed" можно запретить создавать наследников от класса. Пример: класс "String". От этого класса мы не сможем создать наследников. Применение ключевого слова "sealed" к методу означает, что мы запрещаем переопределение этого метода в классах-наследниках.

Ключевое слово "virtual" применяется только к методам, и используется для того, чтобы превратить метод в виртуальный. Это делается для того, чтобы метод можно было переопределить в классах-наследниках. Переопределение метода означает, что мы внутри класса-наследника создаём метод, у которого заголовок полностью совпадает с заголовком метода класса-родителя. При этом в классе-наследнике нужно указать ключевое слово "override", чтобы явно указать, что мы переопределяем метод.

Давайте на примере разберем всё то, что мы только что узнали. Для простоты все поля и методы сделаем публичными (в реальных программах так, конечно, делать не нужно):

Конструкторы при наследовании.

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

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

С помощью "base" также можно вызывать родительский метод.

Разберем на эту тему вот такой пример: пусть имеется родительский класс "Degree", имеющий одной поле "degrees" и один метод, который возвращает значение данного поля. Создадим дочерний класс "Radiance", который, используя метод родительского класса, возвращает градусы, переведенные в радианы:

Напоследок, обобщу особенности наследования:

  • Ключевые слова "sealed" и "static" (статический класс, про него поговорим в отдельной статье) запрещают наследование
  • Если в базовом классе определен какой-то метод "abstract", то базовый класс тоже должен быть абстрактным. В классе-наследнике такой абстрактный метод нужно переопределить. Абстрактный метод, по умолчанию, является виртуальным.
  • При проектировании программы важным является понимание того, что от чего можно унаследовать, а что нельзя. Для проверки условия наследования используется слово "является". В нашем примере: "Собака является животным? — является", "Питбуль является собакой? — является". А вот наоборот лучше не делать (технически конечно можно, но программы лучше сразу проектировать правильно), "Животное является собакой? — не является". Поэтому класс "Animal" от класса "Dog" наследовать нельзя.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.

Назначение множественного наследования

Предположим, что нам нужно нарисовать на форме логотип, который состоит из квадрата и круга. Сначала посмотрим, как это сделать на Си. Предположим, что у нас уже есть функции рисования квадрата и круга.

Далее везде, где нужно рисование логотипа, вызываем функцию Logo.

Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?

Можно, конечно, сделать так:

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

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

Для поддержки множественного наследования нужно при определении класса перечислить через запятую базовые классы. В примере для простоты вместо рисования фигур методы Draw просто выводят их названия.

Отличия множественного наследования

В случае единичного наследования производный класс получает доступ к публичным и защищенным элементам базового класса.


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

Кроме того, в момент создания экземпляра производного класса выполняться все конструкторы базовых классов, а при удалении объекта выполнятся все деструкторы базовых классов.


Ошибка неоднозначности

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

то компилятор выдаст ошибку, так непонятно метод какого класса нужно использовать. Ведь метод Draw есть в обоих классах.

Алмаз смерти

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


В рассмотренном примере добавим суперкласс Shape.

Мы сразу получаем множество проблем неоднозначности вызова. Так, например, если мы видим в классе Logo координаты (x, y) , то какие именно координаты мы имеем в виду?

Проблемы множественного наследования в крупных проектах

В конце концов если классы пишет один человек, то он сам все-таки представляет, что делать в случае неоднозначности вызова. Но если это используется в группе, то ситуация резко усложняется.

Лучше всего эту картину иллюстрирует карточный домик.


Представьте себе, что каждая карта — это класс, который разрабатывает отдельный программист. Нижние карты — это базовые классы, а все остальные — производные классы. И теперь представьте, что вам достался самый верхний этаж.

Запрет наследования

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

В этот момент к вам подходит коллега и говорит:

— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.

Как вы понимаете ответить этому программисту хочется только одно:

— А может ты не будешь наследовать мой класс?!

Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.

А как запретить, например, только функцию test(float i), а функцию test(BYTE i) оставить доступной?

22 ответа

class MyB : public MyA
private:
void test(float i)
MyA::test(i);
>
public:
void test(BYTE i)
MyA::test(i);
>
>;

Понятно, спасибо. Правда есть минус - если одинаковых ф-ий много, то переписывать придётся их все(

Ещё вопрос: Есть ли возможность запретить использование ф-ии (т.б. сказать компилятору, чтобы он в наследуемый класс не помещал такую-то ф-ию)? Мб директива есть для этого?

Примерно как-нибудь так:


Ещё вопрос: Есть ли возможность запретить использование ф-ии (т.б. сказать компилятору, чтобы он в наследуемый класс не помещал такую-то ф-ию)? Мб директива есть для этого?


А что значит "не помещал такую-то ф-ию в класс" ?


В классе A ф-ия есть, а в классе B (наследуемого от класса А) небыло её (ф-ии) вообще.

А почему просто не ограничить к ней доступ посредством private?


Значит иерархия классов неправильная. Надо наследовать A и B от общего базового класса, в котором есть все необходимое обоим.

Можно сделать еще так:

struct A
void func1()<>
void func2()<>
void func3()<>
void func4()<>
>;

В принципе, при наследовании происходит примерно то же.


Реализация всегда наследуется полностью.
Можно ограничивать доступ. Можно реализовать примеси (стратегии), в т.ч. применив множественное наследование.

И все равно твой вопрос не понятен.
Что значит "не было вообще"?
Какая разница между "нет вообще" и "запрещена" (оба определения в нотации автора топика) ?
В каком контексте (для чего) задан вопрос?


Тем не мение такие грабли встречаются. К примеру в Java есть возможность создать неизменяемую колекцыю данных с оригинальной. По сути создается оболочка над изначальной келекцыей в которой метод add не делегирует действие агрегату а генерирует есключение.

Кстати сопутствующий вопрос (С++ давно забыл) как при таком припрятывании метода розруливается приведение к родительскому типу ? Ибо во многих языках такой фокус невозможен и для скрытия метода приходится создавать новый интерфейс что тоже не всегда удобно.


Кстати сопутствующий вопрос (С++ давно забыл) как при таком припрятывании метода розруливается приведение к родительскому типу ? Ибо во многих языках такой фокус невозможен и для скрытия метода приходится создавать новый интерфейс что тоже не всегда удобно.

По большому счету никак. Если класс привести к базовому, метод все равно можно будет вызвать.

Rebbit, пока попытаюсь поэкспериментировать. Мои компиляторы (gcc, и тот, что в Экспресс Студии) не ругаются на такое:

class MyA
public :
virtual void test(int i)
std::cout test(20);
aPtr->test(20.1);
delete aPtr;
return 0;
>

Помнится у Саттера в книжке такая глава была: как "нелегально" получить доступ к закрытым членам :)

Можно еще защищенное наследование использовать. Но это вообще будет близкий аналог агрегирования, которое я привел в первом ответе.

Kogrom
Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.

Kogrom
Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.


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

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

Ключевое слово virtual в наследнике в этом случае используется ( или не используется:) ) только для повышения читабельности.

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


Я не говорил слова "должен". Я говорил слово "значит". Остальное твои фантазии.
Виртуальная функция может быть переопределена в наследнике, иначе она будет та же.

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

Ключевое слово virtual в наследнике в этом случае используется ( или не используется:) ) только для повышения читабельности.


Прочитай ещё раз и скажи где я говорил про ключевое слово virtual в наследнике ?

Не называй меня на "вы". Я буду обращптся к тебе как угодно.

[QUOTE=Airhand;]
Kogrom
Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.
[/QUOTE]

К чему этот ответ? Как уже заметил Green (а вы это проигнорировали, тупо придравшись к словам), virtual отнюдь не выставляет подобных требований.

[QUOTE=Airhand;]
Виртуальная функция может быть переопределена в наследнике, иначе она будет та же.
[/QUOTE]

Сами себе противоречите ;)

Kogrom
Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.


мне кажется или чувак путает virtual с abstract?

Я не говорил слова "должен". Я говорил слово "значит". Остальное твои фантазии.
Виртуальная функция может быть переопределена в наследнике, иначе она будет та же.


Что-то с памятью или руки пишут не то что голова думает?
Перепрочти СВОЙ пост внимательно:


Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.

Green
Да я сказал "должны". Я ошибся, на самом деле "может".


Почему, я же сказал, что буду обращаться к вам как вам будет угодно. А ко мне не надо обращаться на "вы".

К чему этот ответ? Как уже заметил Green (а вы это проигнорировали, тупо придравшись к словам), virtual отнюдь не выставляет подобных требований.


Вы хоть поняли, ЧТО написали ? Наследный класс это тот же базовый, которому добавили свойства и методы. Поэтому функция, определённая в базовом классе, будет и в наследном. Это легко проверить: выведете в функции что-нибудь на экран, а потом вызовите наследный класс. Только убедитесь, что функция в базовом классе вызывается в базовом классе.

Kogrom
Ничего ты не обманул: virtual значит, что функции должны быть переопределены в наследнике. Когда ты убираешь слово "virtual" ты снимашь это требование.

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

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

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

Где-то еще надо было важно сказать про срезку, но я не придумал где.


Вы хоть поняли, ЧТО написали ? Наследный класс это тот же базовый, которому добавили свойства и методы. Поэтому функция, определённая в базовом классе, будет и в наследном. Это легко проверить: выведете в функции что-нибудь на экран, а потом вызовите наследный класс. Только убедитесь, что функция в базовом классе вызывается в базовом классе.


Ты сам то понял, что написал?
Уже утомляет этот детский непрофессиональный лепет.
Прочитай хотя бы одну книгу по ООП, чтоб понять что такое наследование, какие виды наследования бывают, что именно наследуется при разных видах наследования.
У тебя проблемы как с пониманием предмета, так и с терминологией (в каждом предложении!).

Но мы хотим, чтобы Rabbit расширял Animal . Другими словами, кролики должны происходить от животных, т.е. иметь доступ к методам Animal и расширять функциональность Animal своими методами.

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

Ниже Rabbit наследует от Animal :

Теперь код Rabbit стал короче, так как используется конструктор класса Animal по умолчанию и кролик может использовать метод run как и все животные.

Ключевое слово extends работает, используя прототипы. Оно устанавливает Rabbit.prototype.[[Prototype]] в Animal.prototype . Так что если метод не найден в Rabbit.prototype , JavaScript берёт его из Animal.prototype .

Как мы помним из главы Встроенные прототипы, в JavaScript используется наследование на прототипах для встроенных объектов. Например Date.prototype.[[Prototype]] это Object.prototype , поэтому у дат есть универсальные методы объекта.

Синтаксис создания класса допускает указывать после extends не только класс, но любое выражение.

Пример вызова функции, которая генерирует родительский класс:

Здесь class User наследует от результата вызова f("Привет") .

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

Переопределение методов

Давайте пойдём дальше и переопределим метод. Сейчас Rabbit наследует от Animal метод stop , который устанавливает this.speed = 0 .

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

…Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.

У классов есть ключевое слово "super" для таких случаев.

  • super.method(. ) вызывает родительский метод.
  • super(. ) вызывает родительский конструктор (работает только внутри нашего конструктора).

Пусть наш кролик автоматически прячется при остановке:

Теперь у класса Rabbit есть метод stop , который вызывает родительский super.stop() в процессе выполнения.

Как упоминалось в главе Повторяем стрелочные функции, стрелочные функции не имеют super .

При обращении к super стрелочной функции он берётся из внешней функции:

Переопределение конструктора

С конструкторами немного сложнее.

До сих пор у Rabbit не было своего конструктора.

Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим собственный конструктор.

Давайте добавим конструктор для Rabbit . Он будет устанавливать earLength в дополнение к name :

Упс! При создании кролика – ошибка! Что не так?

Если коротко, то в классах-потомках конструктор обязан вызывать super(. ) , и (!) делать это перед использованием this .

…Но почему? Что происходит? Это требование кажется довольно странным.

Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.

Разница в следующем:

  • Когда выполняется обычный конструктор, он создаёт пустой объект и присваивает его this .
  • Когда запускается конструктор унаследованного класса, он этого не делает. Вместо этого он ждёт, что это сделает конструктор родительского класса.

Поэтому, если мы создаём собственный конструктор, мы должны вызвать super , в противном случае объект для this не будет создан, и мы получим ошибку.

Чтобы конструктор Rabbit работал, он должен вызвать super() до того, как использовать this , чтобы не было ошибки:

Устройство super, [[HomeObject]]

Если вы читаете учебник первый раз – эту секцию можно пропустить.

Она рассказывает о внутреннем устройстве наследования и вызов super .

Вообще, исходя из наших знаний до этого момента, super вообще не может работать!

Ну правда, давайте спросим себя – как он должен работать, чисто технически? Когда метод объекта выполняется, он получает текущий объект как this . Если мы вызываем super.method() , то движку необходимо получить method из прототипа текущего объекта. И как ему это сделать?

Продемонстрируем проблему. Без классов, используя простые объекты для наглядности.

Вы можете пропустить эту часть и перейти ниже к подсекции [[HomeObject]] , если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.

В примере ниже rabbit.__proto__ = animal . Попробуем в rabbit.eat() вызвать animal.eat() , используя this.__proto__ :

В строке (*) мы берём eat из прототипа ( animal ) и вызываем его в контексте текущего объекта. Обратите внимание, что .call(this) здесь неспроста: простой вызов this.__proto__.eat() будет выполнять родительский eat в контексте прототипа, а не текущего объекта.

Приведённый выше код работает так, как задумано: выполняется нужный alert .

Теперь давайте добавим ещё один объект в цепочку наследования и увидим, как все сломается:

Теперь код не работает! Ошибка возникает при попытке вызова longEar.eat() .

На первый взгляд все не так очевидно, но если мы проследим вызов longEar.eat() , то сможем понять причину ошибки. В обеих строках (*) и (**) значение this – это текущий объект ( longEar ). Это важно: для всех методов объекта this указывает на текущий объект, а не на прототип или что-то ещё.

Итак, в обеих линиях (*) и (**) значение this.__proto__ одно и то же: rabbit . В обоих случаях метод rabbit.eat вызывается в бесконечном цикле не поднимаясь по цепочке вызовов.

Картина того, что происходит:

Внутри longEar.eat() строка (**) вызывает rabbit.eat со значением this=longEar .

В строке (*) в rabbit.eat мы хотим передать вызов выше по цепочке, но this=longEar , поэтому this.__proto__.eat снова равен rabbit.eat !

… rabbit.eat вызывает себя в бесконечном цикле, потому что не может подняться дальше по цепочке.

Проблема не может быть решена с помощью одного только this .

[[HomeObject]]

Для решения этой проблемы в JavaScript было добавлено специальное внутреннее свойство для функций: [[HomeObject]] .

Когда функция объявлена как метод внутри класса или объекта, её свойство [[HomeObject]] становится равно этому объекту.

Затем super использует его, чтобы получить прототип родителя и его методы.

Давайте посмотрим, как это работает – опять же, используя простые объекты:

Это работает как задумано благодаря [[HomeObject]] . Метод, такой как longEar.eat , знает свой [[HomeObject]] и получает метод родителя из его прототипа. Вообще без использования this .

Но само существование [[HomeObject]] нарушает этот принцип, так как методы запоминают свои объекты. [[HomeObject]] нельзя изменить, эта связь – навсегда.

Единственное место в языке, где используется [[HomeObject]] – это super . Поэтому если метод не использует super , то мы все ещё можем считать его свободным и копировать между объектами. А вот если super в коде есть, то возможны побочные эффекты.

Вот пример неверного результата super после копирования:

  • В строке (*) , метод tree.sayHi скопирован из rabbit . Возможно, мы хотели избежать дублирования кода?
  • Его [[HomeObject]] – это rabbit , ведь он был создан в rabbit . Свойство [[HomeObject]] никогда не меняется.
  • В коде tree.sayHi() есть вызов super.sayHi() . Он идёт вверх от rabbit и берёт метод из animal .

Вот диаграмма происходящего:

Методы, а не свойства-функции

Свойство [[HomeObject]] определено для методов как классов, так и обычных объектов. Но для объектов методы должны быть объявлены именно как method() , а не "method: function()" .

Для нас различий нет, но они есть для JavaScript.

В приведённом ниже примере используется синтаксис не метода, свойства-функции. Поэтому у него нет [[HomeObject]] , и наследование не работает:

Итого

  1. Чтобы унаследовать от класса: class Child extends Parent :
    • При этом Child.prototype.__proto__ будет равен Parent.prototype , так что методы будут унаследованы.
  2. При переопределении конструктора:
    • Обязателен вызов конструктора родителя super() в конструкторе Child до обращения к this .
  3. При переопределении другого метода:
    • Мы можем вызвать super.method() в методе Child для обращения к методу родителя Parent .
  4. Внутренние детали:
    • Методы запоминают свой объект во внутреннем свойстве [[HomeObject]] . Благодаря этому работает super , он в его прототипе ищет родительские методы.
    • Поэтому копировать метод, использующий super , между разными объектами небезопасно.

Задачи

Ошибка создания экземпляра класса

В коде ниже класс Rabbit наследует Animal .

К сожалению, объект класса Rabbit не создаётся. Что не так? Исправьте ошибку.

Читайте также: