Практическое руководство. Управление версиями с помощью ключевых слов "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.