Почему С# поддерживает абстрактные переопределения абстрактных членов?

Во время просмотра какого-либо старого кода я был удивлен, увидев абстрактное переопределение элемента , которое было абстрактным. В основном, что-то вроде этого:

public abstract class A
{
    public abstract void DoStuff();    
}

public abstract class B : A
{
    public override abstract void DoStuff();  // <--- Why is this supported?
}

public abstract class C : B
{
    public override void DoStuff() => Console.WriteLine("!");    
}

Разве различие между виртуальным или абстрактным элементом всегда доступно компилятору? Почему С# поддерживает это?

(Этот вопрос не является дубликатом Что такое "абстрактное переопределение" в С#?, поскольку метод DoStuff в классе A не виртуальный, а абстрактный.)

Ответ 1

Чтобы прояснить вопрос: вопрос не в том, "почему абстрактное переопределение легально"? Существующий вопрос. (См. Также мой пост в блоге по этому вопросу.)

Скорее, возникает вопрос: "Почему абстрактное переопределение законно, когда переопределенный метод также является абстрактным?"

Существует несколько аргументов в пользу того, что здесь не указывается ошибка.

  • Это безвредно.

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

  • Всякий раз, когда что-то выглядит странно в С#, спросите себя , что, если базовый класс принадлежал кому-то, но не в моей команде, и им нравится его редактировать?

Рассмотрим, например, вариант вашего сценария:

public abstract class Z 
{
  public abstract void DoStuff();
}

public class A : Z
{
    public override void DoStuff() 
    { 
      throw new NotImplementedException(); 
    }
}

public abstract class B : A
{
    public override abstract void DoStuff();
}

Авторы класса А понимают, что они допустили ошибку, и A и DoStuff должны быть абстрактными. Создание абстрактного класса является нарушением, потому что любой, кто говорит "новый A()", теперь ошибается, но они проверяют, что ни одна из их групп клиентов в своей организации не создала новый A. Аналогично, вызов base.DoStuff теперь неверен. Но опять же, их нет.

Итак, они меняют свой код на вашу версию класса A. Если класс B теперь имеет ошибку времени компиляции? Зачем? Нет ничего плохого в B.

Многие функции С#, которые кажутся странными, существуют потому, что дизайнеры считают хрупкий базовый класс важным сценарием.

  1. Наконец, мы должны рассмотреть, каковы предпосылки для переопределения метода. Переопределенный метод должен быть доступен с помощью переопределяющего кода, переопределяющий код должен быть в производном типе, а переопределенный метод должен быть виртуальным методом в первую очередь. Для этих требований есть очевидные веские причины. Представляя другое требование - переопределения требуют, чтобы не более чем один из переопределяющего и переопределенного метода был абстрактным - не имеет принципиальной причины, лежащей в основе этого.