Почему C # был создан с ключевыми словами «new» и «virtual + override» в отличие от Java?

61

В Java есть нет virtual, new, overrideключевые слова для определения метода. Таким образом, работа метода легко понять. Потому что, если DerivedClass расширяет BaseClass и имеет метод с таким же именем и тем же подписью BaseClass тогда наиважнейшим будет проходить время выполнения полиморфизма ( при условии , что метод не является static).

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

Теперь перейдем к C #, может быть так много путаницы, и трудно понять, как работает виртуальное переопределение or newили virtual+deriveor + .

Я не могу понять, почему в мире я собираюсь добавить в мой метод такое DerivedClassже имя и ту же сигнатуру, что BaseClassи определить новое поведение, но при полиморфизме во время выполнения BaseClassметод будет вызван! (который не переопределяет, но логически так и должно быть).

В случае, virtual + overrideесли логическая реализация верна, но программист должен подумать, какой метод он должен дать пользователю разрешение на переопределение во время кодирования. Который имеет некоторый прокон (давайте не будем идти туда сейчас).

Так почему в C # есть так много места для ООН -logical рассуждений и путаницы. Итак, могу ли я перефразировать мой вопрос: в каком контексте реального мира я должен думать об использовании virtual + overrideвместо, newа также об использовании newвместо virtual + override?


После некоторых очень хороших ответов, особенно от Омара , я понял, что дизайнеры C # больше внимания уделяли программистам, прежде чем создавать метод, который хорош и обрабатывает некоторые ошибки новичка из Java.

Теперь у меня есть вопрос. Как в Java, если бы у меня был такой код

Vehicle vehicle = new Car();
vehicle.accelerate();

и позже я делаю новый класс, SpaceShipполученный из Vehicle. Затем я хочу изменить все carна SpaceShipобъект, я просто должен изменить одну строку кода

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

Это не нарушит мою логику в любой точке кода.

Но в случае C #, если SpaceShipне переопределить Vehicleкласс accelerateи использовать, newто логика моего кода будет нарушена. Разве это не недостаток?

Анирбан Наг 'tintinmj'
источник
77
Вы просто привыкли к тому, как это делает Java, и просто не нашли время, чтобы понять ключевые слова C # на их собственных терминах. Я работаю с C #, сразу понял термины и нахожу способ, которым Java делает это странным.
Роберт Харви
12
IIRC, C # сделал это, чтобы «внести ясность». Если вы должны явно сказать «новый» или «переопределить», это делает это ясно и сразу же очевидным, что происходит, вместо того, чтобы пытаться выяснить, переопределяет ли метод какое-либо поведение в базовом классе или нет. Я также считаю очень полезным иметь возможность указывать, какие методы я хочу указать как виртуальные, а какие нет. (Java делает это с помощью final; это как раз наоборот).
Роберт Харви
15
«В Java нет виртуальных, новых, переопределенных ключевых слов для определения метода». За исключением того, что есть @Override.
svick
3
@svick аннотация и ключевые слова не одно и то же :)
Anirban Nag 'tintinmj'

Ответы:

86

Так как вы спросили, почему 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().

Омер Икбал
источник
12
К настоящему моменту показатели производительности полностью устарели. Для доказательства см. Мой тест, показывающий, что доступ к полю через нефинальный, но никогда не перегруженный метод занимает один цикл.
Maaartinus
7
Конечно, это может быть так. Вопрос заключался в том, что когда C # решил сделать это, почему это произошло в то время, и, следовательно, этот ответ действителен. Если вопрос в том, имеет ли это смысл, это другое обсуждение, ИМХО.
Омер Икбал
1
Я полностью согласен с вами.
Maaartinus
2
ИМХО, хотя есть определенные способы использования не виртуальных функций, существует риск неожиданных ловушек, когда что-то, что не должно переопределять метод базового класса или реализовывать интерфейс, делает это.
суперкат
3
@ Нильзор: Отсюда аргумент относительно академического против прагматического. Прагматично, ответственность за нарушение чего-либо лежит на последнем изобретателе изменений. Если вы изменяете свой базовый класс, и существующий производный класс зависит от того, что он не меняется, это не их проблема («они» могут больше не существовать). Таким образом, базовые классы становятся привязанными к поведению своих производных классов, даже если этот класс никогда не должен был быть виртуальным . И, как вы говорите, RhinoMock существует. Да, если вы правильно завершите все методы, все равно. Здесь мы указываем на каждую когда-либо созданную систему Java.
deworde
94

Это было сделано, потому что это правильно. Дело в том, что допускать переопределение всех методов неправильно; это приводит к хрупкой проблеме базового класса, когда вы не можете сказать, сломает ли изменение базового класса подклассы. Поэтому вы должны либо занести в черный список методы, которые не должны быть переопределены, либо внести в белый список те, которые разрешено переопределять. Из этих двух, белый список не только безопаснее (поскольку вы не можете случайно создать хрупкий базовый класс ), он также требует меньше работы, поскольку вам следует избегать наследования в пользу композиции.

Doval
источник
7
Допускать переопределение любого произвольного метода неправильно. Вы должны иметь возможность переопределять только те методы, которые разработчик суперкласса определил как безопасные для переопределения.
Довал
21
Эрик Липперт подробно обсуждает это в своем посте « Виртуальные методы и хрупкие базовые классы» .
Брайан
12
У Java есть finalмодификатор, так в чем же проблема?
Сардж Борщ
13
@corsiKa "объекты должны быть открыты для расширения" - почему? Если разработчик базового класса никогда не думал о наследовании, он почти гарантировал, что в коде есть тонкие зависимости и предположения, которые приведут к ошибкам (помните HashMap?). Либо спроектируйте наследование и сделайте договор понятным, либо не делайте этого и убедитесь, что никто не может наследовать класс. @Overrideбыл добавлен как бандит, потому что было слишком поздно, чтобы изменить поведение к тому времени, но даже оригинальные разработчики Java (особенно Джош Блох) соглашаются, что это было плохое решение.
Во
9
@jwenting "Мнение" это смешное слово; людям нравится привязывать это к фактам, чтобы они могли их игнорировать. Но что бы это ни стоило, это также «мнение» Джошуа Блоха (см. « Эффективная Ява» , пункт 17 ), «мнение» Джеймса Гослинга (см. Это интервью ), и, как указано в ответе Омера Икбала , это также «мнение» Андерса Хейлсберга. (И хотя он никогда не обращался к выбору против отказа, Эрик Липперт ясно соглашается, что наследование также опасно.) Так кто имеет в виду?
Довал
33

Как сказал Роберт Харви, это все к чему ты привык. Я нахожу отсутствие этой гибкости в Java странным.

Тем не менее, почему это во-первых? По той же причине , что C # имеет public, internal(также «ничего»), protected, protected internal, и private, а Java просто есть public, protectedничего, и private. Это обеспечивает более точный контроль над тем, что вы кодируете, за счет того, что нужно отслеживать больше терминов и ключевых слов.

В случае « newпротив» virtual+overrideэто выглядит примерно так:

  • Если вы хотите заставить подклассы реализовывать метод, используйте abstractи overrideв подклассе.
  • Если вы хотите предоставить функциональность, но позволить подклассу заменить ее, используйте virtualи overrideв подклассе.
  • Если вы хотите обеспечить функциональность, которую подклассам никогда не нужно переопределять, не используйте ничего.
    • Если вы после этого есть специальный случай подкласс , который действительно нужно вести себя по- другому, используйте newв подклассе.
    • Если вы хотите , чтобы убедиться , что ни один подкласс не может когда - либо переопределить поведение, использовать sealedв базовом классе.

Для примера из реального мира: проект, над которым я работал, обрабатывал заказы электронной коммерции из разных источников. Существовала база, OrderProcessorкоторая имела большую часть логики, с определенными abstract/ virtualметодами для каждого дочернего класса каждого источника для переопределения. Это работало нормально, пока у нас не появился новый источник, у которого был совершенно другой способ обработки заказов, так что нам пришлось заменить основную функцию. У нас было два варианта на данный момент: 1) добавить virtualв базовый метод и overrideв дочерний; или 2) Добавить newк ребенку.

В то время как любой из них может работать, первый позволит очень легко переопределить этот конкретный метод в будущем. Например, он появится в автозаполнении. Однако это был исключительный случай, поэтому мы решили использовать его new. Это сохранило стандарт «этот метод не должен быть переопределен», хотя учитывал особый случай, где это было. Это семантическая разница, которая делает жизнь проще.

Обратите внимание, однако, что это различие поведение , связанное с этим, не только семантическое различие. Смотрите эту статью для деталей. Однако я никогда не сталкивался с ситуацией, когда мне нужно было воспользоваться этим поведением.

Bobson
источник
7
Я думаю, что намеренное использование newэтого пути - ошибка, ожидающая своего появления. Если я вызываю метод в каком-то экземпляре, я ожидаю, что он всегда будет делать одно и то же, даже если я приведу экземпляр к его базовому классу. Но это не то, как newведут себя методы.
svick
@svick - Потенциально, да. В нашем конкретном сценарии этого никогда не произойдет, но именно поэтому я добавил оговорку.
Бобсон
Одним из превосходных newпримеров использования является WebViewPage <TModel> в среде MVC. Но я также столкнулся с ошибкой в newтечение нескольких часов, поэтому я не думаю, что это необоснованный вопрос.
pdr
1
@ C.Champagne: любой инструмент может быть использован плохо, и этот инструмент особенно острый - вы можете легко порезаться. Но это не причина для удаления инструмента из панели инструментов и удаления опции из более талантливого дизайнера API.
pdr
1
@svick Правильно, поэтому обычно это предупреждение компилятора. Но способность «нового» охватывает граничные условия (например, заданные) и, что еще лучше, делает действительно очевидным, что вы делаете что-то странное, когда приходите диагностировать неизбежную ошибку. «Почему этот класс и только этот класс глючит ... а-а,« новый », давайте пойдем тестировать, где используется суперкласс».
deworde
7

Конструкция Java такова, что при любой ссылке на объект вызов определенного имени метода с определенными типами параметров, если он вообще разрешен, всегда будет вызывать один и тот же метод. Возможно, что неявные преобразования типа параметра могут зависеть от типа ссылки, но как только все такие преобразования будут разрешены, тип ссылки не имеет значения.

Это упрощает время выполнения, но может вызвать некоторые неприятные проблемы. Предположим GrafBase, не реализует void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), но GrafDerivedреализует его как открытый метод, который рисует параллелограмм, чья вычисленная четвертая точка противоположна первой. Предположим далее, что более поздняя версия GrafBaseреализует открытый метод с той же сигнатурой, но его вычисленная четвертая точка противоположна второй. Клиенты, которые получают ожидание, GrafBaseно получают ссылку на ожидание, GrafDerivedбудут рассчитывать DrawParallelogramчетвертую точку в моде нового GrafBaseметода, но клиенты, которые использовали GrafDerived.DrawParallelogramдо изменения базового метода, будут ожидать поведения, которое GrafDerivedизначально реализовано.

В Java у автора не было бы возможности GrafDerivedзаставить этот класс сосуществовать с клиентами, которые используют новый GrafBase.DrawParallelogramметод (и могут даже не подозревать, что он GrafDerivedсуществует), не нарушая совместимость с существующим клиентским кодом, который использовался GrafDerived.DrawParallelogramдо GrafBaseего определения. Поскольку DrawParallelogramневозможно определить, какой тип клиента его вызывает, он должен вести себя одинаково, когда его вызывают различные виды клиентского кода. Поскольку два вида клиентского кода имеют разные ожидания относительно того, как он должен вести себя, нет способа GrafDerivedизбежать нарушения законных ожиданий одного из них (то есть нарушения законного клиентского кода).

В C #, если GrafDerivedне перекомпилируется, среда выполнения будет предполагать, что код, который вызывает DrawParallelogramметод при ссылках типа, GrafDerivedбудет ожидать поведение, которое GrafDerived.DrawParallelogram()было при последней компиляции, но код, который вызывает метод при ссылках типа, GrafBaseбудет ожидать GrafBase.DrawParallelogram(поведение, которое был добавлен). Если GrafDerivedпозже перекомпилируется в присутствии расширенного GrafBase, компилятор будет кричать, пока программист не укажет, должен ли его метод быть допустимой заменой унаследованного члена GrafBase, или его поведение должно быть привязано к ссылкам типа GrafDerived, но не должно заменить поведение ссылок типа GrafBase.

Можно разумно утверждать, что наличие метода, GrafDerivedвыполняющего что-то отличное от члена, GrafBaseкоторый имеет одинаковую подпись, будет указывать на плохой дизайн, и поэтому не должен поддерживаться. К сожалению, поскольку у автора базового типа нет возможности узнать, какие методы могут быть добавлены к производным типам, и наоборот, ситуация, когда клиенты базового класса и производного класса имеют разные ожидания для одноименных методов, по существу неизбежна, если только никому не разрешается добавлять какие-либо имена, которые кто-то еще может добавить. Вопрос не в том, должно ли произойти такое дублирование имен, а в том, как минимизировать вред, когда это происходит.

Supercat
источник
2
Я думаю, что здесь есть хороший ответ, но он теряется в стене текста. Пожалуйста, разбейте это на несколько пунктов.
Бобсон
Что такое DerivedGraphics? A class using GrafDerived`?
C.Champagne
@ C.Champagne: я имел в виду GrafDerived. Я начал использовать DerivedGraphics, но чувствовал, что это было немного долго. Даже GrafDerivedнемного длиннее, но не знал, как лучше назвать типы графического рендерера, которые должны иметь четкое отношение база / производное.
суперкат
1
@ Бобсон: лучше?
суперкат
6

Стандартная ситуация:

Вы являетесь владельцем базового класса, который используется несколькими проектами. Вы хотите внести изменения в указанный базовый класс, который будет разбиваться между 1 и бесчисленным производными классами, которые находятся в проектах, обеспечивающих реальную ценность (фреймворк обеспечивает ценность в лучшем случае за один раз, ни один реальный человек не хочет фреймворка, он хочет вещь, работающая на платформе). Удачи, заявляя занятым владельцам производных классов; «ну, вы должны измениться, вам не следовало бы переопределять этот метод», не получив репутацию «Framework: задержка проектов и причина ошибок» для людей, которые должны утверждать решения.

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

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

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

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

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

deworde
источник
0

По умолчанию не виртуальный предполагает, что разработчик базового класса идеально подходит. По моему опыту разработчики не идеальны. Если разработчик базового класса не может представить вариант использования, в котором метод может быть переопределен, или забывает добавить виртуальный, то я не могу воспользоваться преимуществом полиморфизма при расширении базового класса без изменения базового класса. В реальном мире изменение базового класса часто не вариант.

В C # разработчик базового класса не доверяет разработчику подкласса. В Java разработчик подкласса не доверяет разработчику базового класса. Разработчик подкласса отвечает за подкласс и должен (imho) иметь возможность расширять базовый класс по своему усмотрению (за исключением явного отказа, а в java они могут даже ошибиться).

Это фундаментальное свойство определения языка. Это не правильно или неправильно, это то, что есть и не может измениться.

clish
источник
2
кажется, это не дает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 5 ответах
комнат