Все ваши базы не принадлежат вам[1]

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

 class GrandBase
{
  public virtual void M() { Console.WriteLine("GB"); }
}
class Base : GrandBase
{
  public override void M() { Console.WriteLine("B"); }
}
class Derived : Base
{
  public override void M()
  {
    Console.WriteLine("D");
base.base.M(); // Не корректно!
  }
}

Автор последнего производного класса хочет вызвать реализацию метода M класса GrandBase, вместо вызова реализации метода M класса Base. Он хочет обойти «вызов» метода базового класса.

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

Тот факт, что класс Base наследует классу GrandBase, является открытой частью контактной зоны Base. Однако тот факт, что метод Base.M переопределяет метод GrandBase.M уже не является открытой частью контактной зоны Base; наследуя класс GrandParent, класс Base обещает предоставить реализацию метода M, однако переопределяет ли класс Base реализацию этого метода, или просто вызывает реализацию класса GrandBase, является деталью реализации класса Base. Класс Derived не должен знать или обращать внимание на то, как класс Base выполняет свой контракт; даже если это ему и хочется.

Более того, когда автор класса Derived наследовал его от класса Base, он делал это потому, что ему нравился класс Base и он хотел повторно использовать детали его реализации. Если же вам не нравятся детали реализации класса Base, тогда просто не наследуйтесь от него; наследуйте напрямую от класса GrandBase.

Также это плохая идея, и некорректна, поскольку может привести к ошибкам. Давайте рассмотрим более сложный сценарий. Давайте добавим protected virtual метод P в класс GrandBase. Предположим, из метода GrandBase.M вызывается P, и предположим, класс Base переопределил метод P. Если метод Base.M устанавливает некоторое состояние, на которое рассчитывает метод Base.P, тогда вызов GrandBase.M в обход метода Base.M из класса Derived приведет к тому, что будет вызван метод Base.P без установки необходимого ему состояния из метода Base.M.

 class GrandBase
{
  public virtual void M() { Console.WriteLine("GB"); P(); }
  protected virtual void P() { }
}
class Base : GrandBase
{
  public override void M()
  {
    Console.WriteLine("B");
    // Мы знаем, что должен быть вызов метода P.
    SetUpStateForP();
  }
  protected override void P()
  {
     // Использует состояние, проинициализированное методом M
     ...

Это может привести к проблемам с безопасностью или корректностью. В таком случае становится очень сложно спроектировать корректную, безопасную и надежную реализацию виртуальных методов; давайте еще усложним этот пример. Я обращаю внимание, что это правило относится только к C#, а не к CLR. CLR позволяет языку сделать невиртуальный вызов вирутального метода, пропустив любое количество методов вниз по иерархии наследования. CLR не обеспечивает выполнение этого правила языка C#. Однако CLR запрещает невиртуальные вызовы не из производного класса. Если вы создадите другой тип, например, C, который не является наследником класса GrandBase, тогда невиртуальный вызов метода GrandBase.M не будет проверяемым. Интересно, что это правило справедливо и для вложенных типов; верификатор CLR не позволяет невиртуальный вызов виртуального метода своего контейнерного базового класса.

Оригинал статьи


[1] Фраза “All your base are belong to us” является известным Интернет-мемом, ставшим популярным в середине 2000-х, который появился в результате быстрого и некачественного перевода текстов в игре “Zero Wings” с японского на английский. По-русски эта фраза может звучать примерно так: «Все ваши база принадлежит нам», хотя на самом деле должно быть что-то такое: “We have taken all your bases” – “мы захватили все ваши базы”. Подробности см. https://ru.wikipedia.org/wiki/All_your_base_are_belong_to_us. Однако в данном случае речь идет не о «базах», а о базовых классах, тогда наиболее близкий по смыслу перевод названия должен быть таким: «Нам не принадлежат все наши базовые классы», однако в таком случае мы потеряли бы исходную иронию автора. – Примеч. перев.