Урок 10. Наследование и интерфейсы
В прошлом уроке каждый класс описывался с нуля. Но классы часто похожи: у собаки и кошки есть имя, обе умеют подавать голос, только по-разному. Повторять общее в каждом классе не нужно — для этого есть наследование, а для общих «умений» разных классов — интерфейсы.
Наследование
Класс может наследовать другой: получить все его поля, свойства и методы и добавить свои. Исходный класс называют базовым, новый — производным. Наследование записывают через двоеточие: class Dog : Animal.
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}";
}Шарик: Гав
Мурка: Мяу
Рекс: Гав
Кошка МуркаЧто здесь происходит:
DogиCatполучили отAnimalсвойствоName— писать его заново не пришлось.: base(name)в конструкторе передаёт имя конструктору базового класса: он уAnimalтребует параметр, и производный класс обязан его вызвать.virtualу метода в базовом классе разрешает производным его заменить,override— заменяет.- Список
List<Animal>хранит и собак, и кошек: объект производного класса можно положить туда, где ждут базовый.
Полиморфизм
Обратите внимание на цикл: у каждого элемента вызывается animal.Speak(), а отвечают они по-разному — собака лает, кошка мяукает. Какой метод выполнится, решает настоящий тип объекта, а не тип переменной. Это называется полиморфизм. Код цикла не знает, какие животные бывают, — завтра можно добавить класс Cow, и цикл менять не придётся.
ToString
У любого класса в C# есть метод ToString(): его вызывают Console.WriteLine(объект) и интерполяция строк. Все классы наследуют его от общего предка — класса object, а там он возвращает имя класса. Поэтому Console.WriteLine для объекта без своего ToString печатает что-то вроде Dog. Переопределите его, как у Cat выше, — и объект будет печататься понятно.
base и protected
Через base производный класс может вызвать версию метода из базового и дополнить её:
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, без тела:
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. Класс может наследовать только один класс, но реализовать сколько угодно интерфейсов:
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,57Rectangle и Circle не связаны наследованием, но оба реализуют IShape, и с ними можно работать одинаково. readonly у поля значит, что задать его можно только в конструкторе. Интерфейсы есть и в стандартной библиотеке: например, IComparable<T> (объекты можно сравнивать и сортировать) и IEnumerable<T> (по объекту можно пройти foreach).
Абстрактный класс или интерфейс? Если у классов есть общие данные и код — абстрактный класс. Если общего только «что умеет» — интерфейс.
Проверка типа: is
Узнать настоящий тип объекта и сразу получить его в переменной нужного типа помогает is:
foreach (Animal animal in animals)
{
if (animal is Dog dog)
{
Console.WriteLine($"{dog.Name} — собака");
}
}Частые ошибки
- Забыт
override— компилятор предупреждает: метод «скрывает» базовый (hides inherited member, CS0114). При вызове через переменную базового типа сработает базовый метод, и полиморфизм не работает. - Забыт
virtualу метода базового класса —overrideк нему не применить, ошибка компиляции. - Класс не реализовал метод интерфейса —
does not implement interface member(CS0535). - В базовом классе конструктор с параметрами, а наследник не вызывает
: base(...)— ошибка CS7036.
Коротко
class Dog : Animal— наследование: производный класс получает всё от базового; базовый класс — только один.virtualв базовом классе иoverrideв производном — замена метода;base.Метод()— вызов исходного.- Полиморфизм: метод выбирается по настоящему типу объекта, а не по типу переменной.
abstract— класс без своих объектов и методы, которые обязаны написать наследники.- Интерфейс (
interface IShape) — список обещаний; класс может реализовать несколько интерфейсов.