Практическое руководство. Управление версиями с помощью ключевых слов "Override" и "New" (Руководство по программированию в C#)

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

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

  • Если методу в производном классе не предшествуют ключевые слова new или override, компилятор выдаст предупреждение, а метод будет вести себя так, как если бы имелось ключевое слово new.

  • Если методу в производном классе предшествует ключевое слово new, он определяется как независимый от метода в базовом классе.

  • Если методу в производном классе предшествует ключевое слово override, объекты производного класса вызывают этот метод вместо метода базового класса.

  • Чтобы применить ключевое слово override к методу в производном классе, необходимо фактически определить метод базового класса.

  • Метод базового класса можно вызывать из производного класса с помощью ключевого слова base.

  • Ключевые слова override, virtual иnew можно также применять к свойствам, индексаторам и событиям.

По умолчанию методы C# не являются виртуальными. Если метод объявлен как виртуальный, любой наследующий его класс может реализовать свою собственную версию. Чтобы сделать метод виртуальным, в объявление метода базового класса добавляется модификатор virtual. После этого производный класс может переопределить базовый виртуальный метод с помощью ключевого слова override или скрыть виртуальный метод в базовом классе с помощью ключевого слова new. Если ключевое слово override или new не указано, компилятор выдает предупреждение, а метод в производном классе скрывает метод в базовом классе.

Чтобы продемонстрировать это на практике, предположим, что компания А создала класс с именем GraphicsClass, который использует ваша программа. Ниже показан GraphicsClass:

class GraphicsClass
{
    public virtual void DrawLine() { }
    public virtual void DrawPoint() { }
}

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

class YourDerivedGraphicsClass : GraphicsClass
{
    public void DrawRectangle() { }
}

Ваше приложение работает, пока компания А не выпускает новую версию GraphicsClass, напоминающую следующий код:

class GraphicsClass
{
    public virtual void DrawLine() { }
    public virtual void DrawPoint() { }
    public virtual void DrawRectangle() { }
}

Новая версия GraphicsClass содержит метод с именем DrawRectangle. Сначала ничего не происходит. Новая версия по-прежнему совместима со старой версией на уровне двоичного кода. Любая развернутая вами программа будет работать как раньше, даже если в системе соответствующего компьютера установлен новый класс. Существующие вызовы метода DrawRectangle будут и дальше ссылаться на вашу версию в производном классе.

Однако, как только вы выполните повторную компиляцию приложения с новой версией GraphicsClass, компилятор выдаст предупреждение CS0108. В предупреждении будет сказано, что вам необходимо указать, каким образом метод DrawRectangle будет вести себя в приложении.

Если метод должен переопределять новый метод базового класса, используйте ключевое слово override:

class YourDerivedGraphicsClass : GraphicsClass
{
    public override void DrawRectangle() { }
}

Ключевое слово override гарантирует, что все объекты, производные от YourDerivedGraphicsClass, будут использовать версию производного класса DrawRectangle. Объекты, производные от YourDerivedGraphicsClass, сохраняют доступ к версии базового класса DrawRectangle за счет ключевого слова base:

base.DrawRectangle();

Если вы не хотите, чтобы ваш метод переопределял новый метод базового класса, воспользуйтесь следующими рекомендациями. Чтобы избежать путаницы между двумя методами, вы можете переименовать свой метод. Эта работа требует времени и подвержена ошибкам, а в некоторых случаях непрактична. В то же время, если проект относительно небольшой, метод можно переименовать, используя параметры рефакторинга в Visual Studio. Дополнительные сведения см. в разделе Рефакторинг классов и типов (конструктор классов).

Кроме того, предупреждения можно избежать с помощью ключевого слова new в определении производного класса:

class YourDerivedGraphicsClass : GraphicsClass
{
    public new void DrawRectangle() { }
}

Ключевое слово new сообщает компилятору о том, что ваше определение скрывает определение, которое содержится в базовом классе. Это поведение принимается по умолчанию.

Переопределение и выбор метода

Если метод в классе именован, с вызовом совместимы сразу несколько методов (например, если существуют два метода с одинаковыми именами), а параметры совместимы с переданным параметром, компилятор C# выбирает метод, наиболее подходящий для вызова. Пример совместимых методов:

public class Derived : Base
{
    public override void DoWork(int param) { }
    public void DoWork(double param) { }
}

При вызове DoWork к экземпляру Derived компилятор C#, в первую очередь, пытается сделать вызов совместимым с версиями DoWork, изначально объявленными в Derived. Методы переопределения не считаются объявленными в классе, они представляют собой новые реализации метода, объявленного в базовом классе. Только в том случае, если компилятор C# не сможет сопоставить вызов метода с исходным методом в Derived, он попытается сопоставить вызов с переопределенным методом, имеющим то же имя и совместимые параметры. Например:

int val = 5;
Derived d = new Derived();
d.DoWork(val);  // Calls DoWork(double).

Поскольку переменная val может быть преобразована в значение double неявно, компилятор C# вызывает DoWork(double), а не DoWork(int). Избежать этого можно двумя способами. Во-первых, избегайте объявления новых методов, имена которых совпадают с виртуальными методами. Во-вторых, можно настроить компилятор C# на вызов виртуального метода, заставив его выполнить поиск метода базового класса путем приведения экземпляра Derived к Base. Поскольку метод виртуальный, будет вызвана реализация DoWork(int) в Derived. Например:

((Base)d).DoWork(val);  // Calls DoWork(int) on Derived.

Дополнительные примеры new и override см. в разделе Использование ключевых слов Override и New.

См. также