1. Введение
В этой лекции мы разберём, как в C# работает синтаксис наследования: научимся объявлять производные классы, поймём, что такое ключевое слово base, и как с его помощью обращаться к родительскому классу. Это не только упростит вашу жизнь при использовании кода, но и сделает программу куда более элегантной (а иногда даже позволит лишний раз поспать — потому что исправлять ошибки копипасты будет не нужно).
Вот простой пример из жизни: у нас есть класс Vehicle (транспортное средство) — базовый, универсальный чертёж. Все транспортные средства могут ехать, у них есть цвет, производитель и т.д.:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public void Drive()
{
Console.WriteLine("Поехали!");
}
}
А теперь мы хотим добавить новый вид транспорта — автомобиль. Автомобиль всё ещё транспортное средство, но у него есть особенности (например, количество дверей). Было бы странно снова писать всё про цвет и производителя, ведь это уже определено!
Наследование позволяет нам сказать: "Автомобиль — это транспортное средство, только с кучей дополнительных фишек."
2. Как объявить дочерний класс
В C# для создания класса-наследника используется двоеточие :. После имени класса мы указываем, от какого класса наследуемся.
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}
Читай так: класс Car наследует все поля и методы класса Vehicle и добавляет к ним свои собственные фишки — например, количество дверей.
Обратите внимание: в C# поддерживается только одиночное наследование классов (т.е. класс может наследовать только ОДИН родительский класс). Но вот интерфейсов — сколько душе угодно! (о них поговорим позже).
Как это работает?
Когда вы объявляете:
Car myCar = new Car();
...то ваш объект myCar имеет доступ и ко всем полям/методам Vehicle, и к своим собственным свойствам. Пример:
myCar.Brand = "Toyota";
myCar.Color = "Синий";
myCar.NumberOfDoors = 4;
myCar.Drive(); // Наследованный метод
Реальный сценарий:
В нашем "развивающемся" приложении можно было бы сделать меню транспортных средств, где пользователь выбирает тип транспорта, а затем программа отображает все его свойства. При добавлении нового типа транспорта вы будете писать минимальное количество кода!
3. Наследование конструкторов
Когда мы создаём объекты производного класса, иногда хочется инициализировать свойства базового класса через конструктор. Например, чтобы уж точно не забыть указать марку и цвет:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
}
Теперь в конструкторе класса-наследника можно и нужно вызывать конструктор базового класса! Для этого используют ключевое слово base:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color) // вызываем конструктор Vehicle!
{
NumberOfDoors = numberOfDoors;
}
}
Как это выглядит на практике?
Car bmw = new Car("BMW", "Чёрный", 4);
Console.WriteLine($"{bmw.Brand}, {bmw.Color}, дверей: {bmw.NumberOfDoors}");
bmw.Drive(); // Всё ещё работает!
Получается, что bmw — это и автомобиль, и транспортное средство одновременно. Он помнит о своих "родителях".
4. Отличие base от this
В C# есть два ключевых слова для работы с членами класса: this и base.
- this — обращение к текущему объекту (например, его собственному полю или методу).
- base — обращение к члену базового класса (то есть к родителю).
Когда нужно использовать base? Представьте, что вы хотите расширить логику метода из базового класса, или просто инициализировать базовый класс из производного. Например:
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int gears)
: base(brand, color) // вызываем конструктор базового класса
{
NumberOfGears = gears;
}
public void ShowInfo()
{
// Используем свойства из базового класса!
Console.WriteLine($"{Brand} ({Color}), передач: {NumberOfGears}");
}
}
5. Ключевое слово base в методах
base — это не только про конструкторы! Иногда вы хотите изменить поведение метода базового класса, но при этом использовать часть его логики.
Допустим, мы хотим, чтобы метод Drive в классе Car ещё и выводил, что за машину мы ведём:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void DriveCar()
{
base.Drive(); // вызываем базовую реализацию
Console.WriteLine($"Ведём {Brand} с {NumberOfDoors} дверями!");
}
}
Пояснение:
Вызов base.Drive(); — это как сказать: "Папа, ты сначала расскажи свою часть истории, а я добавлю подробностей!". В результате при вызове DriveCar() будет напечатано:
Поехали!
Ведём BMW с 4 дверями!
Про переопределение методов (override) мы поговорим в следующих лекциях :P
6. Схема наследования
Давайте визуализируем отношения между классами. Вот простая схема (а то UML — страшная штука, особенно по утрам!):
Vehicle (базовый)
/ \
Car Bicycle
- Все стрелки идут ОТ производного К базовому.
- Все дочерние классы имеют доступ к public/protected членам базового класса.
Различия между public, protected и private
Небольшое напоминание, чтобы не было путаницы.
В производном классе видны:
| Модификатор | Видимость в наследнике |
|---|---|
| public | Да |
| protected | Да |
| private | Нет |
Частая ошибка:
Попытаться обратиться к private-полю из наследника:
public class Vehicle
{
private string secret = "Никто не узнает!";
}
public class Car : Vehicle
{
public void RevealSecret()
{
Console.WriteLine(secret); // Ошибка! Не видно.
}
}
Сравнение: базовый vs. производный класс
| Vehicle | Car (наследник) | |
|---|---|---|
| Brand | есть | наследует |
| Color | есть | наследует |
| Drive() | есть | наследует (или расширяет) |
| NumberOfDoors | нет | своё (уникальное свойство) |
7. Пример
Если мы продолжаем развивать простое приложение «Учёт транспорта», то можем построить его так:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
public void Drive()
{
Console.WriteLine($"{Brand} {Color} поехал!");
}
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void ShowCar()
{
Console.WriteLine($"Марка: {Brand}, Цвет: {Color}, Дверей: {NumberOfDoors}");
}
}
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int numberOfGears)
: base(brand, color)
{
NumberOfGears = numberOfGears;
}
}
В главном методе можно создать список транспорта и по-разному его описывать:
Car ford = new Car("Ford", "Красный", 4);
ford.ShowCar();
ford.Drive();
Bicycle trek = new Bicycle("Trek", "Зелёный", 21);
trek.Drive();
8. Ошибки, особенности, и немного юмора
Очень частая ошибка — забывать вызывать базовый конструктор в наследнике. Если у Vehicle нет конструктора без параметров, а вы не вызвали base(...), получите ошибку компиляции: компилятор будет ругаться, что не может инициализировать базу. Это как пытаться построить второй этаж без первого. Не получается. Придётся передать данные наверх.
Вторая ошибка: забывать, что поля/методы базового класса, объявленные с модификатором private, в наследниках НЕ ВИДНЫ. Если хочется что-то оставить для "детей", используйте protected (об этом подробнее — в следующих лекциях).
И главное: наследование — штука удобная, но не стоит строить слишком запутанные иерархии. В реальной жизни, если у вашей цепочки наследований больше двух-трёх уровней, что-то, скорее всего, пошло не так. Очень легко запутаться, кто от кого унаследован, и зачем вообще всё это было нужно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ