JavaRush /Курсы /C# SELF /Создание иерархии классов

Создание иерархии классов

C# SELF
20 уровень , 4 лекция
Открыта

1. Введение

Представим мир без иерархий: тысячи классов Person, Animal, Vehicle и всё это абсолютно отдельно. Неудивительно, что программисты в таком мире не смогли бы дожить даже до обеда — запутались бы окончательно! В реальных проектах нам часто нужны объекты, которые умеют делать что-то общее (например, все животные могут двигаться), но при этом у каждого свои особенности (рыба плавает, а птица летает).

Именно иерархии классов позволяют выразить связи между сущностями, чтобы программирование стало творческой работой, а не бесконечной битвой с копипастой.

Рассмотрим пример. Допустим, у нас есть базовый класс Animal. Все животные могут издавать звук. Но только кошки мяукают, собаки лают, а попугаи могут даже пару анекдотов рассказать. Мы хотим это выразить в коде иерархически.

Базовый класс


public class Animal
{
    public string Name { get; set; }

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

    // Базовый метод: можно переопределить в потомках
    public virtual void Speak()
    {
        Console.WriteLine("Животное издаёт какой-то звук...");
    }
}

Здесь мы добавили слово virtual к методу Speak(). Это как пометка: "Эй, дочерние классы, если захотите — можете переопределить этот метод".

Создаём иерархию: производные классы

Теперь пусть у нас будет класс Cat, который будет наследоваться от Animal:


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

    // Переопределяем Speak — кошки же не могут просто рычать!
    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Мяу!");
    }
}

И класс Dog:

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

    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Гав!");
    }
}

А если у нас будет обычное животное, которое не умеет говорить? Тогда можно использовать базовый класс, ничего не переопределяя.

Визуализация — дерево иерархии


.               Animal
                /   \
             Cat    Dog
  • Animal — базовый класс
  • Cat, Dog — дочерние (производные) классы

2. Давайте напишем код, который использует такую иерархию

Продолжаем работу над нашим консольным приложением.

Допустим, у нас есть коллекция животных и мы хотим, чтобы каждое из них сказало что-то характерное:

Animal[] zoo = new Animal[]
{
    new Cat("Барсик"),
    new Dog("Рекс"),
    new Animal("Загадочное существо")
};

foreach (Animal animal in zoo)
{
    animal.Speak();
}

Ожидаемый вывод:

Барсик говорит: Мяу!
Рекс говорит: Гав!
Животное издаёт какой-то звук...

Вот так, благодаря иерархии и полиморфизму (мы его скоро подробно разберём, но суть — вызывается правильная версия метода в зависимости от реального типа объекта), ваше приложение становится гибким и расширяемым.

3. Добавляем новые методы и поля

Итак, с "озвучкой" разобрались. Но все животные — это слишком скучно. Например, у кошки может быть девять жизней, а собака умеет приносить палку.

Добавляем уникальное поведение

В классе-потомке можно добавлять свои собственные методы и поля:


public class Cat : Animal
{
    public int Lives { get; private set; } = 9;

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

    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Мяу! У меня {Lives} жизней.");
    }

    public void LoseLife()
    {
        if (Lives > 0)
        {
            Lives--;
            Console.WriteLine($"{Name} потерял одну жизнь. Осталось: {Lives}");
        }
        else
        {
            Console.WriteLine($"{Name} уже использовал все жизни!");
        }
    }
}

Применяем в коде:

var barsik = new Cat("Барсик");
barsik.Speak();      // Барсик говорит: Мяу! У меня 9 жизней.
barsik.LoseLife();   // Барсик потерял одну жизнь. Осталось: 8

Добавляем новые классы: расширяем "зоопарк"

Вы уже умеете создавать производные классы. Добавим, например, попугая:

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

    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Привет, человек!");
    }

    public void Repeat(string phrase)
    {
        Console.WriteLine($"{Name} повторяет: {phrase}");
    }
}

Теперь вы легко расширяете систему, не трогая старый код:

var keshka = new Parrot("Кеша");
keshka.Speak();                 // Кеша говорит: Привет, человек!
keshka.Repeat("Учись, студент!"); // Кеша повторяет: Учись, студент!

4. Сравнение поведения животных

Тип Метод Speak() Собственное поле Дополнительное поведение
Animal Да (virtual) Name
Cat Да (override) Lives LoseLife()
Dog Да (override)
Parrot Да (override) Repeat(string)

Как выглядит иерархия классов в памяти (блок-схема)

Animal (Name)
 ├── Cat (Lives)
 ├── Dog
 └── Parrot (Repeat)

5. Практика в нашем приложении

Давайте свяжем идею иерархии с приложением — например, у нас есть задачи разного типа:

  1. Task (базовый класс): Любая задача — у неё есть название и статус выполнения.
  2. WorkTask (рабочая): Помимо всего прочего, имеет дедлайн.
  3. HomeTask (домашняя): Может иметь приоритет ("Очень важно", "Так себе").

Начнем с базового класса:

public class Task
{
    public string Title { get; set; }
    public bool IsCompleted { get; private set; }

    public Task(string title)
    {
        Title = title;
    }

    public virtual void Complete()
    {
        IsCompleted = true;
        Console.WriteLine($"Задача \"{Title}\" выполнена!");
    }
}

Теперь добавляем рабочую задачу:

public class WorkTask : Task
{
    public DateTime Deadline { get; set; }

    public WorkTask(string title, DateTime deadline)
        : base(title)
    {
        Deadline = deadline;
    }

    public override void Complete()
    {
        base.Complete();
        Console.WriteLine($"Срок исполнения: {Deadline:d}");
    }
}

И домашнюю задачу:

public class HomeTask : Task
{
    public string Priority { get; set; }

    public HomeTask(string title, string priority)
        : base(title)
    {
        Priority = priority;
    }

    // Можно не переопределять Complete, если поведения достаточно из базового класса
}

Заводим список задач в программе:

List<Task> tasks = new List<Task>
{
    new WorkTask("Отправить отчёт", DateTime.Today.AddDays(2)),
    new HomeTask("Помыть посуду", "Очень важно"),
    new Task("Прочитать лекцию по наследованию")
};

foreach (Task task in tasks)
{
    Console.WriteLine($"Задача: {task.Title}");
    task.Complete();
}

Ожидаемый вывод:

Задача: Отправить отчёт
Задача "Отправить отчёт" выполнена!
Срок исполнения: 13.07.2025
Задача: Помыть посуду
Задача "Помыть посуду" выполнена!
Задача: Прочитать лекцию по наследованию
Задача "Прочитать лекцию по наследованию" выполнена!

Видите, насколько удобно: все задачи хранятся вместе, мы их обрабатываем единообразно, а специфика проявляется там, где нужно.

6. Типичные ошибки при использовании наследования

Ошибка №1: попытка переопределить метод, который не объявлен как virtual.
Если метод в базовом классе не помечен как virtual, его нельзя переопределить в производных классах. В результате вся гибкость полиморфизма теряется, и иерархия становится бесполезной.

Ошибка №2: наследование без логической связи между сущностями.
Не стоит использовать наследование, если объекты не связаны по смыслу. Например, Круг действительно является Фигурой, но Конь как Транспортное средство — это уже натяжка. Исключение — специфические контексты (например, средневековая игра), где такая связь может быть оправдана.

Ошибка №3: чрезмерно глубокие иерархии.
Когда структура классов уходит глубоко (5–6 и более уровней), код становится сложно читать, сопровождать и тестировать. Это сигнал, что стоит рассмотреть композицию как альтернативу наследованию.

Ошибка №4: забыли вызвать базовый конструктор.
При добавлении новых свойств в производном классе легко забыть явно вызвать base(...) в конструкторе. Это может привести к неполной или неправильной инициализации базовой части объекта и к трудноуловимым багам.

2
Задача
C# SELF, 20 уровень, 4 лекция
Недоступна
Создание базового и производного класса
Создание базового и производного класса
2
Задача
C# SELF, 20 уровень, 4 лекция
Недоступна
Расширение иерархии классов
Расширение иерархии классов
1
Опрос
Понятие наследования, 20 уровень, 4 лекция
Недоступен
Понятие наследования
Наследование и иерархия классов
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Slevin Уровень 59
3 февраля 2026
В обоих задачах перепутаны термины "Поле" / "Свойство", что сбивает.
Александр Уровень 62
11 января 2026
задачи как всегда детский сад...