← Назад к вопросам

Наследование и полиморфизм: Предсказать вывод

2.0 Middle🔥 131 комментариев
#ООП и паттерны проектирования

Условие

Дан следующий код:

class Animal { public virtual void Speak() => Console.WriteLine("Animal speaks"); public void Walk() => Console.WriteLine("Animal walks"); }

class Dog : Animal { public override void Speak() => Console.WriteLine("Dog barks"); public new void Walk() => Console.WriteLine("Dog runs"); }

class Program { static void Main() { Animal a1 = new Animal(); Animal a2 = new Dog(); Dog d1 = new Dog();

    a1.Speak();  // ?
    a1.Walk();   // ?
    a2.Speak();  // ?
    a2.Walk();   // ?
    d1.Speak();  // ?
    d1.Walk();   // ?
}

}

Задание:

  1. Предскажите вывод каждой строки
  2. Объясните разницу между virtual/override и new
  3. Когда следует использовать new вместо override?

Критерии оценки:

  • Понимание полиморфизма
  • Знание механизма виртуальных методов
  • Понимание hiding vs overriding

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Анализ и предсказание вывода

a1.Speak();   // "Animal speaks"
a1.Walk();    // "Animal walks"
a2.Speak();   // "Dog barks"
a2.Walk();    // "Animal walks"
d1.Speak();   // "Dog barks"
d1.Walk();    // "Dog runs"

Почему именно такой вывод? Это критический момент для понимания разницы между virtual/override и new.


Детальный разбор каждой строки

1. a1.Speak() → "Animal speaks"

Тип переменной: Animal
Объект: Animal (не Dog)
Метод: virtual, но не переопределён
Результат: вызов базовой реализации

2. a1.Walk() → "Animal walks"

Тип переменной: Animal
Объект: Animal
Метод: обычный (НЕ virtual)
Результат: вызов Animal.Walk()

3. a2.Speak() → "Dog barks"

Тип переменной: Animal (полиморфно!)
Объект: Dog
Метод: virtual + override
Результат: вызывается РЕАЛЬНО вызывается DOG.Speak()

Это полиморфизм в действии! Несмотря на то что a2 типа Animal, на runtime вызывается Dog.Speak().

4. a2.Walk() → "Animal walks"

Тип переменной: Animal
Объект: Dog
Метод: обычный (НЕ virtual) + new (скрывает базовый)
Результат: вызывается Animal.Walk()

Важно: new НЕ переопределяет метод, а СКРЫВАЕТ его. Поскольку переменная типа Animal, вызывается Animal.Walk(), даже если объект на самом деле Dog.

5. d1.Speak() → "Dog barks"

Тип переменной: Dog
Объект: Dog
Метод: override
Результат: вызывается Dog.Speak()

6. d1.Walk() → "Dog runs"

Тип переменной: Dog (статический тип!)
Объект: Dog
Метод: new (скрытие метода базового класса)
Результат: вызывается Dog.Walk()

Разница: Когда мы вызываем через переменную типа Dog, компилятор знает о Dog.Walk() и вызывает его. Когда через Animal, компилятор знает только о Animal.Walk().


Virtual vs Override vs New

┌─────────────────────────────────────────────┐
│ VIRTUAL + OVERRIDE (Полиморфизм)            │
├─────────────────────────────────────────────┤
│ Базовый класс: public virtual void Method() │
│ Наследник:     public override void Method()│
│                                             │
│ Вызов зависит от РЕАЛЬНОГО типа объекта    │
│ Animal a = new Dog();                       │
│ a.Method();  // Dog.Method() - полиморфно! │
│                                             │
│ Используется для:                           │
│ - Реализация абстрактных методов            │
│ - Переопределение поведения в подклассах    │
│ - Достижение полиморфного поведения        │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│ NEW (Скрытие методов - Hiding)              │
├─────────────────────────────────────────────┤
│ Базовый класс: public void Method()         │
│ Наследник:     public new void Method()     │
│                                             │
│ Вызов зависит от ТИПА переменной            │
│ Animal a = new Dog();                       │
│ a.Method();  // Animal.Method() - НЕ Dog!  │
│ Dog d = new Dog();                          │
│ d.Method();  // Dog.Method()                │
│                                             │
│ Используется для:                           │
│ - Совершенно другое поведение               │
│ - Разные сигнатуры методов                  │
│ - Работа со статическими методами           │
└─────────────────────────────────────────────┘

Сравнительная таблица

АспектVirtual/OverrideNew
Вызов при Animal a = new Dog()Dog.Method()Animal.Method()
Вызов при Dog d = new Dog()Dog.Method()Dog.Method()
Зависит отРеального типа объектаТипа переменной
ПолиморфизмДаНет
УсложнениеМожет вызвать неожиданное поведениеЯвное скрытие, проще отследить
Когда использоватьВариация поведения в иерархииПолностью другой метод
СигнатураДолжна совпадатьМожет быть иной

Практические примеры

Пример 1: Virtual/Override — правильный полиморфизм

abstract class Payment
{
    public abstract decimal Calculate(decimal amount);
}

class CreditCardPayment : Payment
{
    public override decimal Calculate(decimal amount)
        => amount * 1.02m;  // +2% комиссия
}

class CashPayment : Payment
{
    public override decimal Calculate(decimal amount)
        => amount;  // Без комиссии
}

class Program
{
    static decimal ProcessPayment(Payment payment, decimal amount)
    {
        // Вызывается нужный метод в зависимости от реального типа
        return payment.Calculate(amount);
    }

    static void Main()
    {
        Payment p1 = new CreditCardPayment();
        Payment p2 = new CashPayment();

        Console.WriteLine(ProcessPayment(p1, 100));  // 102 (Card)
        Console.WriteLine(ProcessPayment(p2, 100));  // 100 (Cash)
    }
}

Пример 2: New — скрытие методов (плохая практика)

class Base
{
    public virtual void Log()
    {
        Console.WriteLine("Base logging");
    }
}

class Derived : Base
{
    public new void Log()  // ❌ Плохая идея!
    {
        Console.WriteLine("Derived logging");
    }
}

var obj = (Base)(new Derived());
obj.Log();  // "Base logging" — неожиданный результат!

Пример 3: New с разными сигнатурами (допустимо)

class GenericList
{
    public void Add(object item) { }
}

class GenericList<T> : GenericList
{
    public new void Add(T item)  // Другая сигнатура — OK
    {
        // Типобезопасная версия
    }
}

Сигнатура и правила компилятора

class Animal
{
    public void Walk() { }  // НЕ virtual
}

class Dog : Animal
{
    // ❌ Компилятор выдаст WARNING
    public void Walk() { }  // Неявное скрытие! Нужен new или override

    // ✅ Правильно
    public new void Walk() { }  // Явное скрытие

    // ❌ Ошибка компилятора — нельзя override немного virtual метод
    // public override void Walk() { }
}

Late Binding vs Early Binding

Virtual/Override — Late Binding (динамическое связывание):

var animal = (Animal)(new Dog());
animal.Speak();  // Связывание происходит во время выполнения
                 // Ищется виртуальная таблица методов Dog
                 // Вызывается Dog.Speak()

New — Early Binding (статическое связывание):

var animal = (Animal)(new Dog());
animal.Walk();   // Связывание происходит во время компиляции
                 // Компилятор знает только о Animal.Walk()
                 // Вызывается Animal.Walk()

Когда использовать что?

✅ Используйте Virtual/Override когда:

  1. Полиморфное поведение необходимо

    abstract class Shape
    {
        public abstract double GetArea();  // Каждый shape по-своему
    }
    
  2. Подклассы предоставляют разные реализации одного контракта

    class Circle : Shape { public override double GetArea() => Math.PI * r * r; }
    class Square : Shape { public override double GetArea() => side * side; }
    
  3. Используется interface/abstract class

    interface IRepository { void Save(); }
    class UserRepository : IRepository { public void Save() { } }
    

✅ Используйте New когда:

  1. Совершенно иное поведение, не вариация

    class Base { public void Transform() => Console.WriteLine("Base"); }
    class Derived : Base { public new void Transform() => Console.WriteLine("Derived - no relation"); }
    
  2. Работа со статическими методами (они не могут быть virtual)

    class Base { public static void Help() => Console.WriteLine("Base"); }
    class Derived : Base { public new static void Help() => Console.WriteLine("Derived"); }
    
  3. Изменение сигнатуры метода

    class Base { public void Process(string input) { } }
    class Derived : Base { public new void Process(int input) { } }  // Другой параметр
    

❌ Избегайте New для:

  • Переопределения поведения в иерархии классов (используйте override)
  • Скрытия важных методов базового класса (нарушает принцип Liskov Substitution)
  • Создания путаницы — разработчик может не понять, какой метод вызовется

Расширенный пример: Правильная иерархия

// ✅ ПРАВИЛЬНО — используем виртуальные методы
abstract class Vehicle
{
    public abstract void Start();
    public abstract void Stop();
}

class Car : Vehicle
{
    public override void Start() => Console.WriteLine("Car engine starts");
    public override void Stop() => Console.WriteLine("Car stops");
}

class Bicycle : Vehicle
{
    public override void Start() => Console.WriteLine("Bicycle ready");
    public override void Stop() => Console.WriteLine("Bicycle stops");
}

static void DriveVehicle(Vehicle v)
{
    v.Start();   // Правильный полиморфизм
    v.Stop();
}

// Использование
DriveVehicle(new Car());      // Car engine starts, Car stops
DriveVehicle(new Bicycle());  // Bicycle ready, Bicycle stops

Выводы

Ключевые различия:

ХарактеристикаVirtual/OverrideNew
СвязываниеДинамическое (late binding)Статическое (early binding)
Вызов зависит отРеального типа объектаТипа переменной
ЦельПолиморфизм, вариация поведенияСкрытие, другое поведение
РекомендацияИспользуйте чащеИспользуйте осторожно

Правило большого пальца:

  • Если хотите полиморфизм → virtual + override
  • Если нужно скрыть метод → редко, обычно означает плохой дизайн
  • Когда сомневаетесь → выбирайте virtual + override

Это база ООП! Понимание этого различия критично для карьеры backend-разработчика.