Глиф Учебники

Урок 10. Наследование и интерфейсы

Курс «C#» · урок 10 из 10 · 4 минуты чтения

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

Наследование

Класс может наследовать другой: получить все его поля, свойства и методы и добавить свои. Исходный класс называют базовым, новый — производным. Наследование записывают через двоеточие: class Dog : Animal.

C#
List<Animal> animals = new List<Animal>
{
    new Dog("Шарик"),
    new Cat("Мурка"),
    new Dog("Рекс")
};

foreach (Animal animal in animals)
{
    Console.WriteLine($"{animal.Name}: {animal.Speak()}");
}
Console.WriteLine(animals[1]);

class Animal
{
    public string Name { get; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual string Speak()
    {
        return "...";
    }
}

class Dog : Animal
{
    public Dog(string name) : base(name)
    {
    }

    public override string Speak()
    {
        return "Гав";
    }
}

class Cat : Animal
{
    public Cat(string name) : base(name)
    {
    }

    public override string Speak() => "Мяу";

    public override string ToString() => $"Кошка {Name}";
}
Вывод
Шарик: Гав
Мурка: Мяу
Рекс: Гав
Кошка Мурка

Что здесь происходит:

object ToString() → имя класса общий предок Animal Name virtual Speak() "..." базовый класс Dog : Animal Name — от Animal override Speak() "Гав" Cat : Animal Name — от Animal override Speak() "Мяу" override ToString() производные классы
Dog и Cat наследуют от Animal свойство Name и заменяют его virtual-метод Speak своим override, а все классы, включая Animal, происходят от object с методом ToString.

Полиморфизм

Обратите внимание на цикл: у каждого элемента вызывается animal.Speak(), а отвечают они по-разному — собака лает, кошка мяукает. Какой метод выполнится, решает настоящий тип объекта, а не тип переменной. Это называется полиморфизм. Код цикла не знает, какие животные бывают, — завтра можно добавить класс Cow, и цикл менять не придётся.

foreach (Animal animal in animals) animal.Speak() шаг 1 animal Dog "Шарик" Гав из Dog шаг 2 animal Cat "Мурка" Мяу из Cat шаг 3 animal Dog "Рекс" Гав из Dog
Переменная animal типа Animal на каждом шаге цикла указывает на другой объект, и вызов Speak выполняет метод того класса, к которому объект относится на самом деле.

ToString

У любого класса в C# есть метод ToString(): его вызывают Console.WriteLine(объект) и интерполяция строк. Все классы наследуют его от общего предка — класса object, а там он возвращает имя класса. Поэтому Console.WriteLine для объекта без своего ToString печатает что-то вроде Dog. Переопределите его, как у Cat выше, — и объект будет печататься понятно.

base и protected

Через base производный класс может вызвать версию метода из базового и дополнить её:

C#
class Puppy : Dog
{
    public Puppy(string name) : base(name)
    {
    }

    public override string Speak() => base.Speak() + "-тяв";
}

Здесь new Puppy("Бим").Speak() вернёт Гав-тяв. Член с модификатором protected доступен внутри класса и его наследников, но не снаружи. А класс со словом sealed наследовать запрещено.

В C# у класса только один базовый класс: class A : B, C, где B и C — классы, не скомпилируется.

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

Иногда базовый класс — только общее описание, и создавать его объекты бессмысленно: «просто фигура» не имеет площади. Такой класс помечают abstract, а методы, которые обязаны написать наследники, — тоже abstract, без тела:

C#
abstract class Shape
{
    public abstract double GetArea();
}

class Square : Shape
{
    private readonly double side;

    public Square(double side)
    {
        this.side = side;
    }

    public override double GetArea() => side * side;
}

new Shape() — ошибка компиляции (CS0144), а new Square(3) — пожалуйста. Если наследник не напишет GetArea, программа тоже не соберётся.

Интерфейсы

Интерфейс — список того, что класс обещает уметь, без реализации (кода методов в нём нет, его пишет класс). Имена интерфейсов по традиции начинаются с I. Класс может наследовать только один класс, но реализовать сколько угодно интерфейсов:

C#
IShape[] shapes = { new Rectangle(3, 4), new Circle(2) };
double total = 0;
foreach (IShape shape in shapes)
{
    Console.WriteLine($"{shape.Name}: {shape.GetArea():F2}");
    total += shape.GetArea();
}
Console.WriteLine($"Всего: {total:F2}");

interface IShape
{
    string Name { get; }
    double GetArea();
}

class Rectangle : IShape
{
    private readonly double width;
    private readonly double height;

    public Rectangle(double width, double height)
    {
        this.width = width;
        this.height = height;
    }

    public string Name => "Прямоугольник";

    public double GetArea() => width * height;
}

class Circle : IShape
{
    private readonly double radius;

    public Circle(double radius)
    {
        this.radius = radius;
    }

    public string Name => "Круг";

    public double GetArea() => Math.PI * radius * radius;
}
Вывод
Прямоугольник: 12,00
Круг: 12,57
Всего: 24,57

Rectangle и Circle не связаны наследованием, но оба реализуют IShape, и с ними можно работать одинаково. readonly у поля значит, что задать его можно только в конструкторе. Интерфейсы есть и в стандартной библиотеке: например, IComparable<T> (объекты можно сравнивать и сортировать) и IEnumerable<T> (по объекту можно пройти foreach).

interface IShape string Name { get; } double GetArea(); что класс обещает уметь, без кода реализуют Rectangle : IShape Name "Прямоугольник" GetArea() ширина × высота Circle : IShape Name "Круг" GetArea() π × радиус² new Rectangle(3, 4) Прямоугольник: 12,00 new Circle(2) Круг: 12,57
Интерфейс IShape только обещает свойство Name и метод GetArea, а Rectangle и Circle выполняют обещание каждый по-своему, поэтому с ними можно работать одинаково.

Абстрактный класс или интерфейс? Если у классов есть общие данные и код — абстрактный класс. Если общего только «что умеет» — интерфейс.

Проверка типа: is

Узнать настоящий тип объекта и сразу получить его в переменной нужного типа помогает is:

C#
foreach (Animal animal in animals)
{
    if (animal is Dog dog)
    {
        Console.WriteLine($"{dog.Name} — собака");
    }
}

Частые ошибки

Коротко

Проверьте себя

1. Что нужно написать в производном классе, чтобы заменить virtual-метод базового?
2. Сколько базовых классов может быть у класса в C#?
3. Почему нельзя написать new Shape(), если Shape — abstract?
4. Что напечатает Console.WriteLine(obj), если в классе объекта не переопределён ToString?