Когда несколько классов отличаются деталями, а не сутью, описывать каждый с нуля бессмысленно. Наследование позволяет взять готовый класс и уточнить его.
Наследование
public class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name} спит");
}
}
public class Dog : Animal
{
public void Fetch()
{
Console.WriteLine($"{Name} принёс палку");
}
}
Двоеточие означает «наследует». Dog получает всё публичное содержимое Animal – и добавляет
своё:
var dog = new Dog { Name = "Рекс" };
dog.Sleep(); // унаследовано
dog.Fetch(); // своё
Animal называют базовым классом, Dog – производным. Наследование описывает отношение
«является»: собака является животным.
Базовый класс в C# может быть только один. Множественного наследования, как в C++, здесь нет: слишком много неоднозначностей возникает, когда два предка предлагают разные версии одного метода. Реализовать несколько интерфейсов при этом можно – об этом следующий урок.
Конструктор базового класса
Наследник обязан позаботиться о том, чтобы базовая часть объекта была построена:
public class Animal
{
public string Name { get; }
public Animal(string name)
{
Name = name;
}
}
public class Dog : Animal
{
public string Breed { get; }
public Dog(string name, string breed) : base(name)
{
Breed = breed;
}
}
Запись : base(name) вызывает конструктор базового класса. Он выполняется первым, до тела
конструктора наследника: сначала собирается общая часть, потом уточнение.
Полиморфизм
Самое интересное начинается, когда наследники по-разному делают одно и то же.
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Животное издаёт звук");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Гав");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Мяу");
}
}
virtual в базовом классе разрешает переопределение, override в наследнике его выполняет.
Теперь объекты разных классов можно обрабатывать одинаково:
var animals = new List<Animal> { new Dog(), new Cat(), new Animal() };
foreach (var animal in animals)
{
animal.Speak(); // Гав, Мяу, Животное издаёт звук
}
Переменная объявлена как Animal, но вызывается метод того класса, объект которого лежит внутри.
Решение принимается во время работы, а не при компиляции – это и называется полиморфизмом.
Практическая ценность здесь в том, что цикл не знает про Dog и Cat. Добавите класс Parrot – цикл не изменится ни на строчку. Именно ради этого свойства полиморфизм и применяют, а не ради красивого примера с животными.
Почему virtual обязателен
В Java любой метод виртуален по умолчанию, в C# – нет. Разница осмысленная: автор базового класса решает, что разрешено менять наследникам, а что является частью его гарантий.
Если написать в наследнике метод с тем же именем без override, компилятор предупредит о
сокрытии. Сокрытый метод ведёт себя коварно: через переменную типа Animal вызовется базовая
версия, а через переменную типа Dog – наследника. Одно и то же поведение зависит от типа
переменной, а не от объекта. Так почти никогда не нужно.
base: дополнить, а не заменить
Переопределённый метод может вызвать базовую версию:
public override void Speak()
{
base.Speak(); // сначала общая часть
Console.WriteLine("...и виляет хвостом");
}
Это частый приём: наследник не отменяет поведение предка, а расширяет его.
Абстрактные классы
Иногда базовый класс не имеет смысла сам по себе: «животное вообще» не существует, существуют
собаки и кошки. Такой класс помечают abstract:
public abstract class Animal
{
public string Name { get; set; }
public abstract void Speak(); // без тела: наследник обязан реализовать
public void Sleep() // обычный метод с реализацией
{
Console.WriteLine($"{Name} спит");
}
}
Два следствия:
new Animal()не соберётся – абстрактный класс нельзя создать;- каждый наследник обязан реализовать
Speak, иначе не соберётся уже он.
Абстрактный класс удобен, когда часть поведения общая (Sleep написан один раз), а часть обязана
различаться (Speak).
Все классы – наследники object
Любой класс в C# неявно наследует object, даже если это не написано. Отсюда методы, которые есть
у всего:
| Метод | Что делает |
|---|---|
ToString() |
текстовое представление; по умолчанию – имя типа |
Equals(object) |
сравнение с другим объектом |
GetHashCode() |
число для словарей и множеств |
ToString переопределяют чаще всего – он вызывается автоматически при выводе и в интерполяции:
public override string ToString() => $"{Name} ({Breed})";
После этого Console.WriteLine(dog) напечатает «Рекс (овчарка)» вместо имени типа.
Наследование – не единственный инструмент
Наследование связывает классы жёстко: изменение базового задевает всех потомков. Часто вместо «собака является животным» правильнее сказать «у машины есть двигатель» – и просто держать объект внутри поля. Это называется композицией.
Когда выбирать наследование, когда композицию, чем опасны глубокие иерархии и как это связано с принципами проектирования – тема курса «Объектно ориентированное программирование». Здесь мы разобрали механику языка; там разбирают, что с ней делать.