JavaRush /Курсы /C# SELF /Виртуальные методы в C#: полиморфизм и переопределение

Виртуальные методы в C#: полиморфизм и переопределение

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

1. Введение

Представьте, у вас есть базовый класс Animal с методом Speak(). Для всех животных по умолчанию он пишет "Some sound". Вы хотите создать классы-наследники, например, Dog и Cat, чтобы они говорили не "Some sound", а свои настоящие фразы ("Woof!" и "Meow!").

Можно было бы в каждом наследнике написать свой метод Speak(), но если вы работаете с коллекцией типа Animal[], вызов animal.Speak() всё равно будет обращаться к методу Animal, а не собаки или кошки — потому что C# по умолчанию ориентируется на тип переменной, а не реального объекта за ней.

Тут пример на пальцах:


Animal dog = new Dog();
dog.Speak(); // Опа, а что выведется?

По умолчанию — "Some sound", а не "Woof!", даже если в Dog есть свой метод Speak(). Как же быть? Нужно объявить метод как virtual (в базовом классе) и override (в производном).

Что такое виртуальный метод?

virtual-метод — это метод, который объявлен в базовом классе с модификатором virtual и предназначен для того, чтобы в наследниках его можно было переопределить ( override).

При вызове такого метода через ссылку на базовый класс будет вызван метод самого "реального" объекта — как в настоящей жизни: если вы приручили животное, и оно кот, то оно скажет "Meow!", а не "Some sound".

Виртуальные методы — это основа полиморфизма в C#.

2. Объявление виртуальных методов

Как объявить виртуальный метод

В базовом классе объявляем метод с ключевым словом virtual:


public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Some sound");
    }
}
Объявление виртуального метода в базовом классе

В наследнике используем ключевое слово override:


public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Meow!");
    }
}
Переопределение виртуального метода в наследниках

Пример: демонстрация полиморфизма


// Например, в файле Program.cs

// Базовый класс Animal с виртуальным методом Speak
public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Some sound");
    }
}

// Класс Dog, переопределяет поведение Speak
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

// Класс Cat, тоже переопределяет Speak
public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Meow!");
    }
}

public static class Program
{
    public static void Main()
    {
        Animal[] animals = new Animal[]
        {
            new Animal(),
            new Dog(),
            new Cat()
        };

        foreach (Animal animal in animals)
        {
            animal.Speak(); // Выведет: "Some sound", "Woof!", "Meow!"
        }
    }
}

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

3. Что происходит "под капотом": виртуальная таблица

Коротко о механизме

Когда вы объявляете метод как виртуальный, компилятор добавляет его в специальную "виртуальную таблицу" методов для каждого типа (почти как меню в столовой для каждого блюда). Когда вызывается такой метод, C# смотрит не на тип переменной, а на то, что реально лежит по ссылке — и "подсовывает" тот метод, который соответствует реальному типу объекта.

Можно сказать, что C# сам подставляет нужный рецепт вместо универсального, если у класса есть свой вариант.

Аналогия: договор с возможностью "дописать пункт мелким шрифтом"

Представьте, что у вас есть договор аренды (базовый класс), и в нём написано, что арендатор платит по умолчанию фиксированную сумму раз в месяц (метод PayRent()). Но вы — лукавый собственник — разрешаете в приложениях к договору указывать особые условия (виртуальный метод). Если кто-то из арендаторов (наследник) воспользуется этим правом, он напишет свой override, и теперь плата берётся по-особому.

4. Когда стоит использовать виртуальные методы?

  • Если метод в базовом классе должен типично работать для большинства наследников, но некоторые из них могут потребовать своего варианта поведения.
  • Когда строите иерархию классов, в которой предполагается расширение или изменение части логики.
  • Когда разрабатываете гибкую архитектуру, например, для бизнес-логики, где разные типы операций могут требовать индивидуального поведения.

В реальных проектах виртуальные методы — основа паттернов Шаблонный метод (Template Method), Стратегия (Strategy), да и вообще половины ООП.

Сравнение с обычными методами

Обычный метод Виртуальный метод
Можно переопределять Нет Да (override)
Как вызывается По типу переменной По "настоящему" типу объекта
Полиморфизм Нет Да

Часто задаваемые вопросы

  • Можно ли сделать конструктор виртуальным?
    Нет! Конструкторы никогда не бывают виртуальными — они всегда работают строго по типу объекта.
  • Могут ли виртуальные методы быть статическими?
    Нет, только экземплярные методы могут быть виртуальными. Статические методы не участвуют в полиморфизме, ибо нет объекта — а кто же тогда будет переопределять?
  • Могут ли поля быть виртуальными?
    Нет, только методы, свойства и события.

5. Практика: развиваем приложение "Животный мир"

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

Пример кода с комментариями


// В базовом классе задаём виртуальный метод Eat
public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Some sound");
    }

    public virtual void Eat()
    {
        Console.WriteLine("Eats generic food.");
    }
}

// В Dog переопределяем Eat
public class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public override void Eat() => Console.WriteLine("Eats dog food.");
}

// В Cat тоже своё поведение
public class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow!");
    public override void Eat() => Console.WriteLine("Eats fish.");
}

public static class Program
{
    public static void Main()
    {
        Animal[] animals = new Animal[]
        {
            new Animal(),
            new Dog(),
            new Cat()
        };
        foreach (var animal in animals)
        {
            animal.Speak();
            animal.Eat();
            Console.WriteLine("---");
        }
    }
}

Всё, теперь каждый котик и пёсик питаются по-своему! А базовый Animal продолжает есть что-то странное, как истинный функционал с "дефолтом".

6. Типичные ошибки и нюансы

  • Можно забыть override в наследнике — и тогда метод базового класса так и останется на месте.
  • Или, наоборот, случайно забыть virtual в базовом классе, тогда в наследнике нельзя будет написать override.

Нельзя написать override без virtual

Компилятор C# не позволит переопределить метод, который не был объявлен как виртуальный (или абстрактный) в базовом классе.

Зачем нужен override

override явно говорит компилятору и читателю кода: "Я не просто пишу новый метод — я осознанно меняю то, что задано в базовом классе". Это предотвращает ошибки, когда вы хотите "переопределить", но случайно просто пишете новый метод с похожей сигнатурой.

Если забыли override и написали новый метод

public class Dog : Animal
{
    public void Speak()
    {
        Console.WriteLine("Woof!");
    }
}
Это НЕ переопределение! Полиморфизм не работает.

Но это не переопределение! При обращении к Dog через переменную типа Animal, вызовется метод животного ("Some sound"), а не собаки. Ох уж эта неожиданная встреча с типами!

2
Задача
C# SELF, 20 уровень, 2 лекция
Недоступна
Создание класса с виртуальным методом
Создание класса с виртуальным методом
2
Задача
C# SELF, 20 уровень, 2 лекция
Недоступна
Виртуальный метод с дополнительной логикой
Виртуальный метод с дополнительной логикой
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ