Перейти к содержанию
Educora
Средний20 мин7 / 16

Наследование, интерфейсы и полиморфизм

Создавай новые классы на основе существующих, переопределяй методы с помощью `virtual` и `override` и используй абстрактные классы и интерфейсы, чтобы один код работал с разными объектами.

Проверь себя
В этом уроке ты узнаешь
  • Создавать производный класс и использовать ключевые слова virtual, override и base
  • Объяснять, зачем нужны абстрактные классы и абстрактные методы
  • Определять интерфейс и реализовывать его в нескольких классах
  • Объяснять полиморфизм на примерах

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

Наследование: virtual и override

Запись class Dog : Animal означает, что Dog наследуется от Animal: он получает все public-члены родителя. Метод, помеченный в базовом классе как virtual, производный класс может переопределить с помощью override. Ключевое слово base позволяет вызвать версию родителя. При первичных конструкторах аргумент передаётся конструктору родителя напрямую: : Animal(name).

C#
List<Animal> pets = [new Dog("Rex"), new Cat("Mestan"), new Animal("Nemo")];

foreach (Animal pet in pets)
    Console.WriteLine(pet.Introduce());

class Animal(string name)
{
    public string Name => name;
    public virtual string Speak() => "...";
    public virtual string Introduce() => $"I am {Name} and I say {Speak()}";
}

class Dog(string name) : Animal(name)
{
    public override string Speak() => "Woof!";
}

class Cat(string name) : Animal(name)
{
    public override string Speak() => "Meow!";
    public override string Introduce() => base.Introduce() + " (and ignores you)";
}
Ожидаемый результат
I am Rex and I say Woof!
I am Mestan and I say Meow! (and ignores you)
I am Nemo and I say ...

Обрати внимание: тип списка — List<Animal>, но при вызове pet.Introduce() каждый объект использует свой метод Speak. То, что один и тот же вызов даёт разный результат в зависимости от настоящего типа объекта, называется полиморфизмом. Цикл ничего не знает о собаках, кошках или попугае, которого добавят позже, — и знать не должен.

Абстрактные классы

Иногда базовый класс сам по себе неполон: площадь «фигуры вообще» не вычислить, её можно найти только для конкретного прямоугольника или круга. Такой класс объявляют abstract. Создать из него объект нельзя (new Shape() даёт ошибку CS0144), а его abstract-методы без тела каждый производный класс обязан реализовать — иначе возникнет ошибка CS0534.

C#
using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Shape[] shapes = [new Rectangle(4, 3), new Square(5), new Circle(1)];
foreach (Shape s in shapes)
    Console.WriteLine($"{s.GetType().Name}: area {s.Area():F2}");

abstract class Shape
{
    public abstract double Area();
}

class Rectangle(double width, double height) : Shape
{
    public override double Area() => width * height;
}

class Square(double side) : Rectangle(side, side);

class Circle(double radius) : Shape
{
    public override double Area() => Math.PI * radius * radius;
}
Ожидаемый результат
Rectangle: area 12.00
Square: area 25.00
Circle: area 3.14
Square наследуется от Rectangle и берёт его метод Area в готовом виде — ему даже не понадобилось тело.

Интерфейсы

Интерфейс — это контракт: он говорит, какие методы и свойства должны быть у класса, но не говорит, как они работают. Имена интерфейсов по традиции начинаются с буквы I: INotifier, IComparable. Класс принимает контракт, написав : INotifier, и обязан реализовать все его члены. Один и тот же интерфейс могут реализовывать даже совсем не связанные классы.

C#
List<INotifier> notifiers =
[
    new EmailNotifier("aysel@example.com"),
    new SmsNotifier("+994 50 000 00 00")
];

foreach (INotifier n in notifiers)
    n.Send("Your order has shipped!");

interface INotifier
{
    void Send(string message);
}

class EmailNotifier(string address) : INotifier
{
    public void Send(string message) => Console.WriteLine($"Email to {address}: {message}");
}

class SmsNotifier(string phone) : INotifier
{
    public void Send(string message) => Console.WriteLine($"SMS to {phone}: {message}");
}
Ожидаемый результат
Email to aysel@example.com: Your order has shipped!
SMS to +994 50 000 00 00: Your order has shipped!

Если завтра понадобятся уведомления в Telegram, достаточно написать новый класс TelegramNotifier, реализующий INotifier, — цикл менять не придётся. Сам .NET полон интерфейсов: IComparable<T> позволяет сортировать объекты, а IEnumerable<T> — перебирать их в foreach.

КритерийАбстрактный классИнтерфейс
Сколько можно унаследоватьтолько одинсколько угодно
Поля и состояниемогут бытьне бывает
Когда выбиратьу близких классов есть общий кодразные классы разделяют одну способность

Главное

  • class Dog : Animal — Dog наследуется от Animal и получает его члены; базовый класс может быть только один.
  • Производный класс переопределяет virtual-метод через override; base вызывает версию родителя.
  • Из abstract-класса нельзя создать объект; его абстрактные методы реализуют производные классы.
  • Интерфейс — это контракт; класс может реализовать несколько интерфейсов.
  • Полиморфизм: один и тот же вызов работает по-разному в зависимости от настоящего типа объекта.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какое ключевое слово используют в производном классе, чтобы переопределить метод родителя?