Наследование и полиморфизм: Предсказать вывод
Условие
Дан следующий код:
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(); // ?
}
}
Задание:
- Предскажите вывод каждой строки
- Объясните разницу между virtual/override и new
- Когда следует использовать new вместо override?
Критерии оценки:
- Понимание полиморфизма
- Знание механизма виртуальных методов
- Понимание hiding vs overriding
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ и предсказание вывода
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/Override | New |
|---|---|---|
| Вызов при 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 когда:
-
Полиморфное поведение необходимо
abstract class Shape { public abstract double GetArea(); // Каждый shape по-своему } -
Подклассы предоставляют разные реализации одного контракта
class Circle : Shape { public override double GetArea() => Math.PI * r * r; } class Square : Shape { public override double GetArea() => side * side; } -
Используется interface/abstract class
interface IRepository { void Save(); } class UserRepository : IRepository { public void Save() { } }
✅ Используйте New когда:
-
Совершенно иное поведение, не вариация
class Base { public void Transform() => Console.WriteLine("Base"); } class Derived : Base { public new void Transform() => Console.WriteLine("Derived - no relation"); } -
Работа со статическими методами (они не могут быть virtual)
class Base { public static void Help() => Console.WriteLine("Base"); } class Derived : Base { public new static void Help() => Console.WriteLine("Derived"); } -
Изменение сигнатуры метода
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/Override | New |
|---|---|---|
| Связывание | Динамическое (late binding) | Статическое (early binding) |
| Вызов зависит от | Реального типа объекта | Типа переменной |
| Цель | Полиморфизм, вариация поведения | Скрытие, другое поведение |
| Рекомендация | Используйте чаще | Используйте осторожно |
Правило большого пальца:
- Если хотите полиморфизм →
virtual+override - Если нужно скрыть метод → редко, обычно означает плохой дизайн
- Когда сомневаетесь → выбирайте
virtual+override
Это база ООП! Понимание этого различия критично для карьеры backend-разработчика.