Так как вы спросили, почему C # сделал так, лучше спросить создателей C #. Андерс Хейлсберг, ведущий архитектор C #, ответил, почему они решили не использовать виртуальное по умолчанию (как в Java) в интервью , соответствующие фрагменты приведены ниже.
Имейте в виду, что по умолчанию в Java есть виртуальное ключевое слово final, чтобы пометить метод как не виртуальный. Еще две концепции для изучения, но многие люди не знают об окончательном ключевом слове или не используют активно. C # заставляет использовать виртуальное и новое / переопределение, чтобы сознательно принимать эти решения.
Есть несколько причин. Одним из них является производительность . Мы можем заметить, что когда люди пишут код на Java, они забывают пометить свои методы как окончательные. Таким образом, эти методы являются виртуальными. Потому что они виртуальные, они не так хорошо работают. Это просто издержки производительности, связанные с виртуальным методом. Это одна проблема.
Более важной проблемой является управление версиями . Есть две школы мысли о виртуальных методах. Академическая школа мысли говорит: «Все должно быть виртуальным, потому что я мог бы когда-нибудь переопределить это». Прагматическая школа мышления, основанная на создании реальных приложений, которые работают в реальном мире, говорит: «Мы должны быть очень осторожны с тем, что мы делаем виртуальными».
Когда мы делаем что-то виртуальное на платформе, мы даем много обещаний о том, как это будет развиваться в будущем. Для не виртуального метода мы обещаем, что когда вы вызовете этот метод, произойдут x и y. Когда мы публикуем виртуальный метод в API, мы не только обещаем, что при вызове этого метода произойдут x и y. Мы также обещаем, что когда вы переопределите этот метод, мы будем вызывать его в этой конкретной последовательности по отношению к этим другим, и состояние будет в том и инварианте.
Каждый раз, когда вы говорите виртуальный в API, вы создаете ловушку обратного вызова. Как разработчик фреймворка для ОС или API, вы должны быть очень осторожны с этим. Вы не хотите, чтобы пользователи переопределяли и перехватывали в любой произвольной точке API, потому что вы не можете обязательно давать такие обещания. И люди могут не полностью понимать обещания, которые они дают, когда делают что-то виртуальное.
В этом интервью обсуждается, как разработчики думают о дизайне наследования классов и как это привело к их решению.
Теперь к следующему вопросу:
Я не могу понять, почему в мире я собираюсь добавить метод в свой DerivedClass с тем же именем и той же сигнатурой, что и BaseClass, и определить новое поведение, но при полиморфизме во время выполнения будет вызван метод BaseClass! (который не переопределяет, но логически так и должно быть).
Это происходит, когда производный класс хочет объявить, что он не соблюдает контракт базового класса, но имеет метод с тем же именем. (Для тех, кто не знает разницы между new
и override
в C #, см. Эту страницу MSDN ).
Очень практичный сценарий таков:
- Вы создали API, класс которого называется
Vehicle
.
- Я начал использовать ваш API и получил
Vehicle
.
- У вашего
Vehicle
класса не было никакого метода PerformEngineCheck()
.
- В моем
Car
классе я добавляю метод PerformEngineCheck()
.
- Вы выпустили новую версию своего API и добавили
PerformEngineCheck()
.
- Я не могу переименовать мой метод, потому что мои клиенты зависят от моего API, и это нарушит их.
Поэтому, когда я перекомпилирую против вашего нового API, C # предупреждает меня об этой проблеме, например
Если основание PerformEngineCheck()
не было virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
И если база PerformEngineCheck()
была virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
Теперь я должен явно принять решение, действительно ли мой класс продлевает контракт базового класса, или это другой контракт, но с тем же именем.
- Делая это
new
, я не ломаю своих клиентов, если функциональность базового метода отличалась от производного метода. Любой код, на который есть ссылка Vehicle
, не увидит Car.PerformEngineCheck()
вызываемый, но код, на который была ссылка Car
, продолжит видеть ту же функциональность, что и я PerformEngineCheck()
.
Подобный пример - когда другой метод в базовом классе может вызываться PerformEngineCheck()
(особенно в более новой версии), как можно предотвратить вызов PerformEngineCheck()
метода из производного класса? В Java это решение будет зависеть от базового класса, но оно ничего не знает о производном классе. В C #, что решение лежит как на базовом классе (через virtual
ключевое слово), и на производный класс (через new
и override
ключевых слов).
Конечно, ошибки, которые выдает компилятор, также предоставляют программистам полезный инструмент, чтобы не делать неожиданных ошибок (т.е. либо переопределять, либо предоставлять новые функциональные возможности, не осознавая этого).
Как сказал Андерс, реальный мир заставляет нас сталкиваться с такими проблемами, которые, если бы мы начинали с нуля, мы бы никогда не хотели решать.
РЕДАКТИРОВАТЬ: Добавлен пример, где new
будет использоваться для обеспечения совместимости интерфейса.
РЕДАКТИРОВАТЬ: Проходя через комментарии, я также наткнулся на рецензию Эрика Липперта (тогда один из членов комитета по дизайну C #) на другие примеры сценариев (упомянутых Брайаном).
ЧАСТЬ 2: На основе обновленного вопроса
Но в случае C #, если SpaceShip не отменяет ускорение класса Vehicle и использует новый, логика моего кода будет нарушена. Разве это не недостаток?
Кто решает, SpaceShip
переопределяет Vehicle.accelerate()
ли он или нет? Это должен быть SpaceShip
разработчик. Так что, если SpaceShip
разработчик решит, что он не соблюдает контракт с базовым классом, то ваш призыв Vehicle.accelerate()
не должен идти SpaceShip.accelerate()
или не должен? Именно тогда они отметят это как new
. Однако, если они решат, что он действительно сохраняет контракт, то они фактически отметят его override
. В любом случае ваш код будет вести себя правильно, вызывая правильный метод на основе контракта . Как ваш код может решить, SpaceShip.accelerate()
является ли он на самом деле переопределенным Vehicle.accelerate()
или это коллизия имен? (См. Мой пример выше).
Однако, в случае неявного наследования, даже если SpaceShip.accelerate()
не держать контракт на Vehicle.accelerate()
вызов метода будет по- прежнему идти SpaceShip.accelerate()
.