Найти в Дзене
"Мы"-Прогер

Изучаем C# - Наследование и полиморфизм в ООП

Это одна из самых больших и важных тем. В прошлых статьях мы начали делать забег животных. Мы создали класс Animal, который реализует логику бегуна - за каждый вызов метода Move() координата _x увеличивается на _speed: Теперь добавим новые виды бегунов - новые классы. Пусть черепаха бегает по тем же правилам, но дополнительно может прятаться в панцирь. По вызову метода Hide() она прячется и не может двигаться до вызова метода Show(). Пусть собака двигается так же, как и все животные, но дополнительно гавкает при каждом передвижении. Пусть гепард всегда бегает со скоростью 30. Черепаха и все остальные бегуны бегают по схожим правилам - их координата увеличивается на скорость. Мы могли бы написать в каждом из этих классов всю логику бега, но тогда код будет дублироваться. Дублирование кода плохо - это лишние усилия по дописыванию и по чтению кода, это риски забыть поменять что-нибудь в одном из мест и так далее. Чтобы избежать дублирования в данном случае, используют наследование. Когда
Оглавление

Это одна из самых больших и важных тем.

В прошлых статьях мы начали делать забег животных. Мы создали класс Animal, который реализует логику бегуна - за каждый вызов метода Move() координата _x увеличивается на _speed:

Файл Animal.cs
Файл Animal.cs
Файл Program.cs
Файл Program.cs

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

Теперь добавим новые виды бегунов - новые классы. Пусть черепаха бегает по тем же правилам, но дополнительно может прятаться в панцирь. По вызову метода Hide() она прячется и не может двигаться до вызова метода Show(). Пусть собака двигается так же, как и все животные, но дополнительно гавкает при каждом передвижении. Пусть гепард всегда бегает со скоростью 30.

Черепаха и все остальные бегуны бегают по схожим правилам - их координата увеличивается на скорость. Мы могли бы написать в каждом из этих классов всю логику бега, но тогда код будет дублироваться. Дублирование кода плохо - это лишние усилия по дописыванию и по чтению кода, это риски забыть поменять что-нибудь в одном из мест и так далее. Чтобы избежать дублирования в данном случае, используют наследование. Когда один класс наследуется от другого, то все поля, методы и свойства базового класса автоматически оказываются в классе-наследнике и писать их вручную не нужно. В нашем случае класс Черепаха наследуется от Животного:

-3

Наследование нужно применять всегда, когда у нас есть вид объектов (животные) и подвиды (черепаха, собака, гепард).

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

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

Ошибка C#: Base class ... does not contain parameterless constructor
Ошибка C#: Base class ... does not contain parameterless constructor

"Базовый класс 'HelloWorld.Animal' не содержит конструктора без параметров". Дело вот в чём. Сейчас у класса Turtle мы не написали никакого конструктора. Значит, для него генерируется конструктор по умолчанию - без входных параметров и без каких-либо действий внутри. Но поскольку логика черепахи включает в себя всю логику животного, то создать черепаху, не создавая каркас животного, нельзя. Иными словами, нам обязательно нужно вызвать конструктор базового класса, чтобы проинициализировать его логику. Сделать это можно с помощью ключевого слова base:

-5

Из конструктора черепахи вызывается конструктор базового животного. Туда передаётся кличка и скорость. Сейчас скорость всех черепах будет равна 1, но мы можем получать её на вход черепашьего конструктора и прокидывать её в животное так же, как делаем это с кличкой.

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

Переопределение методов. Вызов метода базового класса

Напишем логику черепахи. При вызове метода Hide() она должна прятаться и больше не двигаться до вызова метода Show(). Очевидно, что состояние черепахи - спрятана она или нет - это флажок, то есть переменная логического типа данных bool. Работа методов Hide() и Show() тоже очевидна - они изменяют этот флажок. Но как сделать метод Move()? Изменение работы метода базового класса называется переопределением (не путать с перегрузкой - когда есть несколько методов с одинаковым названием, но разными входными аргументами). Чтобы метод можно было переопределить, нужно объявить его с ключевым словом virtual, а переопределения - со словом override:

Метод базового класса
Метод базового класса
Метод класса-наследника
Метод класса-наследника

Логика передвижения остаётся в базовом классе животного; черепаха лишь вызывает или не вызывает этот метод в зависимости от того, спрятана она или нет. Чтобы обратиться к базовому классу, используется base. Оно содержит в себе поля и методы базового класса подобно тому, как использовалось this для текущего класса. Вызов метода базового класса необязателен (в отличие от конструктора) - в случае, если нам не надо двигать черепаху, вызов base.Move() не происходит.

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

Черепаха готова. Вернёмся к забегу:

-8

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

Чтобы вызывать у черепахи методы Hide() и Show(), мы должны уметь определять, является ли животное черепахой. В C#, чтобы проверить, является ли объект объектом определённого класса, можно использовать is: animal is Turtle. Отметим, что animal is Animal также будет истинно для черепах, потому что черепаха является животным. Убедившись, что перед нами черепаха, мы включаем случайные числа, чтобы заставить черепаху спрятаться/высунуться с вероятностью 30%:

-9

Если черепаха спрятана, то она должна высунуться, а если высунута, то должна спрятаться. Чтобы можно было узнать, в каком состоянии сейчас находится черепаха, мы должны иметь доступ к флажку _isHidden. Но сейчас он объявлен как private-поле. Если же мы сделаем его public, то мы нарушим инкапсуляцию логики черепахи (то есть, можно будет вмешаться в её логику и сломать её). Поэтому правильный выход - сделать это поле свойством с { get; private set; }, как было в предыдущей статье https://dzen.ru/a/aaAKjosrTz0TTFNA?share_to=link

Но мы всё равно не имеем доступа к IsHidden, потому что переменная animal объявлена как хранящая объект класса Animal, а у базового животного нет IsHidden. Однако, если мы проверили, что в animal лежит черепаха, мы можем привести хранящийся там объект к типу Turtle, написав (Turtle) animal. Результатом этого будет тот же самый объект, который лежит в animal, но по синтаксису это будет уже черепаха и мы сможем вызывать черепашьи методы. Приводить типы можно от любого базового класса к наследнику. Если по факту в переменной этого наследника не будет (не черепаха, а собака или просто базовое животное), то будет ошибка, поэтому предварительная проверка с помощью is обязательна.

-10

Мы можем сделать приведение типов, заведя переменную для черепахи одновременно с проверкой is: animal is Turtle turtle:

-11

Обычно всегда так и пишут.

Итак, в переменной turtle лежит черепаха. Осталось вызвать черепашьи методы:

-12

Добавьте в черепашьи методы печать сообщений "Черепаха {кличка} спряталась", "Черепаха {кличка} высунулась".

Переименуйте класс Animal ("животное") на BaseAnimal ("базовое животное") с помощью Refactor - Rename.

Запускаем забег.

Что такое полиморфизм в ООП?

Полиморфизм в объектно-ориентированном программировании - это то, что наследник класса всегда может использоваться вместо базового класса. У нас черепаха всегда может использоваться вместо базового животного. В список животных можно добавить черепах, и всё будет работать. При этом вызываемые методы будут автоматически выбираться по факту, то есть, когда мы вызываем метод Move(), то будет вызван черепаший Move() / собачий Move() / Move() базового животного в зависимости от того, кто по факту находится в переменной. Ключевое слово virtual у метода в C# включает применение такой логики.

Задачи

Разрешите задавать черепахам любую скорость при их создании, но не более 5.

Добавьте класс "собака" и сделайте, что при каждом передвижении собака лает в консоль.

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

Добавьте класс "буст-собака" (BoostedDog), у которой метод SetBoost(int koefficient) меняет буст. Буст - это сколько обычных тиков передвижения применяется за один раз. Например, после SetBoost(3) каждый вызов метода Move() должен работать как три. Меняйте буст с 30% вероятностью, выбирайте значения буста 1, 2 или 3 с равной вероятностью. Буст-собака является собакой.

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

Далее

abstract protected-методы - https://dzen.ru/a/aaWBJp2cOUSTyLtR?share_to=link

Оглавление - https://dzen.ru/a/aXisxwt_Mnz2qTjs?share_to=link