Andriy Shyrokoryadov

.Net developer, data scientist

Наследование, ключевые слова - вопрос №5 на собеседование C# / .NET

Текст к видео "Наследование, ключевые слова" на канале YouTube

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

Преимущества рассмотрим на примере типичных сценариев использования наследования.

Сценарий 1.

Класс «В» наследует реальный класс «А». В этом случае класс «В» обладает всеми членами (полями, свойствами, методами) класса «А», которые обозначены модификаторами доступа public, protected, protected internal. Дополнительно новые члены могут быть добавлены в класс «В». Также существует возможность добавить новые члены в класс «В», которые будут такими же, как существующие члены класса «А». Под словосочетанием «такими же» имеется в виду члены с одинаковым типом и названием, а в случае методов – методы с одинаковым названием и сигнатурой. Что такое сигнатура метода? Сигнатура – от латинского „signatura”, ручная подпись или просто подпись - набор аргументов метода по типу и порядку, а также тип возвращаемого значения метода. В этом случае для такого члена в классе «В» используется ключевое слово „new”. Это даёт указание компилятору использовать реализацию определенного члена в классе «В», не используя такой же по сигнатуре член в базовом классе (классе-родителе). Однако следует помнить, что если программист обратится к классу «В», как будто бы это был класс «А» (полиморфизм), то будет использована реализация члена в классе «А». Даже несмотря на то, что в классе «В» есть другая реализации этого члена с ключевым словом «new».

Сценарий 2.

Класс «В» наследует реальный класс «А», однако в классе «А» некоторые методы обозначены ключевым словом virtual. В данной ситуации, как и в сценарии 1, класс «В» обладает всеми членами (полями, свойствами, методами) класса «А», которые обозначены модификаторами доступа public, protected, protected internal. Дополнительно новые члены могут быть добавлены в класс «В». А также у нас есть возможность переопределить в классе «В» методы с ключевым словом „virtual”. Естественно, сигнатуры методов должны совпадать. Также перед данным методом в классе «В» необходимо использовать ключевое слово «override». Обоснованный вопрос – чем отличается использований ключевых слов “new” и „override”. Как работает ключевое слово “new” описано в сценарии 1. Что касается ключевого слова „override”, то независимо от того, как программист будет обращаться к классу «В» - как к классу «В» или как к классу «А» - всегда будет использована самая низкая (или последняя) версия метода в иерархии наследования, то есть будет использована версия с ключевым словом „override”, а если таковой не будет – то будет использована версия с ключевым словом „virtual”. Важное замечание – если будет создан класс «С», наследующий класс «В», то в классе «С» также можно переопределить методы, обозначенные словами „virtual” и „override” в родительских классах «А» и «В». Действие этих ключевых слов не ограничивается уровнями наследования.

Сценарий 3.

Класс «В» наследует абстрактный класс «А». В классе «А» некоторые методы обозначены ключевым словом „virtual”, а некоторые методы обозначены ключевым словом „abstract”. Здесь необходимо сразу определиться с понятиями: абстрактный класс, абстрактный метод и его отличие от виртуального метода.

Абстрактный класс – это класс, который обозначен ключевым слово „abstract” и содержит абстрактные методы. Второе условие необязательно, но без него создание абстрактного класса не имеет смысла.

Абстрактный метод – метод, который не содержит реальной реализации, а только определяет сигнатуру метода и обозначен ключевым слово „abstract”. Предполагается, что конкретные реализации (имплементации) абстрактного метода будут представлены в классах наследниках абстрактного класса с абстрактными методами.

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

Возвращаемся к сценарию 3. В данной ситуации, как и в сценарии 1, класс «В» обладает всеми членами (полями, свойствами, методами) класса «А», которые обозначены модификаторами доступа public, protected, protected internal. Методы класса «А», которые обозначены словом „virtual” мы можем переопределить в классе «В», однако не обязаны это делать. Иначе обстоит дело с методами, обозначенными ключевым словом „abstract”. Данные методы мы обязаны переопределить в классе-наследнике, однако мы можем это не делать если класс - наследник также является абстрактным – переопределение будет выполнено в конкретных, не абстрактных, классах – наследниках.

Вопрос – зачем использовать абстрактные классы? Абстрактные классы служат для определения какой-то схемы, которая может иметь разные варианты реализации. Например, класс «Горячий напиток». Процесс приготовления горячего напитка может состоять из нескольких этапов:

  • нагреть воду;
  • добавить основной компонент напитка;
  • добавить дополнительные компоненты напитка;
  • добавить сахар;
  • подать напиток;

В этом случае класс «Горячий напиток» может быть абстрактным, а методы могут быть виртуальными или абстрактными. Подумайте сами какие методы могут быть виртуальными, а какие абстрактными. Ключевым словом „virtual” могут быть обозначены методы: нагреть воду, добавить сахар, подать напиток. Для большей части горячих напитков эти действия одинаковы и можно написать для них имплементацию по умолчанию. Методы «добавить основной компонент напитка» и «добавить дополнительные компоненты напитка» можно обозначить как абстрактные, так как для каждого напитка будет свой компонент. Есть еще одно улучшение данного примера – название «добавить сахар» можно заменить на «добавить подсластитель»: всегда лучше использовать названия виртуальных и абстрактных методов, которые носят обобщенный характер. Конкретная имплементация метода «добавить подсластитель» класса «Горячий напиток» может уже не иметь общего характера: например «добавить сахар» или «добавить аспартам». Конкретными наследниками абстрактного класса «Горячий напиток» могут быть классы «Чай» и «Кофе». Подумайте, как можно реализовать 5 вышеперечисленных методов в этих классах.

Сценарий 4. Есть конкретный класс «А» и есть необходимость ограничить возможность его наследования, то есть надо запретить наследование класса «А». Это легко можно сделать, добавляя в декларации класса ключевое слово „sealed”. В сценарии указано, что класс «А» должен быть конкретным. Почему? Давайте подумаем, что было бы если бы мы задекларировали класс как abstract sealed class. Такой класс было бы невозможно использовать. Невозможно создать объект абстрактного класса – это всегда должен быть объект конкретного класса, которые наследует данный абстрактный класс. Но в нашем случае мы не можем наследовать абстрактный класс, как как он обозначен ключевым словом „sealed”. В этом случае декларация класса есть, но объектов этого класса не будет.

Подведем итоги:

  • классы могут быть абстрактными и конкретными;
  • можно наследовать как абстрактные, так и конкретные классы;
  • ключевое слово „sealed” перед декларацией класса запрещает наследование класса;
  • методы могут быть обычными, виртуальными (virtual) и абстрактными (abstract);
  • обычный метод можно переопределить в классе-наследнике используя ключевое слово „new”;
  • виртуальные и абстрактные методы можно переопределять в классе-наследнике используя ключевое слово „override”;
  • мы можем, но не обязаны, переопределять виртуальные методы в классе-наследнике;
  • мы обязаны переопределять абстрактные методы в классе-наследнике, однако если класс-наследник абстрактный, мы не обязаны это делать;
  • следует помнить о разнице между ключевыми словами „new” и „override”;