В Java есть нет virtual
, new
, override
ключевые слова для определения метода. Таким образом, работа метода легко понять. Потому что, если DerivedClass расширяет BaseClass и имеет метод с таким же именем и тем же подписью BaseClass тогда наиважнейшим будет проходить время выполнения полиморфизма ( при условии , что метод не является static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Теперь перейдем к C #, может быть так много путаницы, и трудно понять, как работает виртуальное переопределение or new
или virtual+derive
or + .
Я не могу понять, почему в мире я собираюсь добавить в мой метод такое 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
то логика моего кода будет нарушена. Разве это не недостаток?
источник
final
; это как раз наоборот).@Override
.Ответы:
Так как вы спросили, почему C # сделал так, лучше спросить создателей C #. Андерс Хейлсберг, ведущий архитектор C #, ответил, почему они решили не использовать виртуальное по умолчанию (как в Java) в интервью , соответствующие фрагменты приведены ниже.
Имейте в виду, что по умолчанию в Java есть виртуальное ключевое слово final, чтобы пометить метод как не виртуальный. Еще две концепции для изучения, но многие люди не знают об окончательном ключевом слове или не используют активно. C # заставляет использовать виртуальное и новое / переопределение, чтобы сознательно принимать эти решения.
В этом интервью обсуждается, как разработчики думают о дизайне наследования классов и как это привело к их решению.
Теперь к следующему вопросу:
Это происходит, когда производный класс хочет объявить, что он не соблюдает контракт базового класса, но имеет метод с тем же именем. (Для тех, кто не знает разницы между
new
иoverride
в C #, см. Эту страницу MSDN ).Очень практичный сценарий таков:
Vehicle
.Vehicle
.Vehicle
класса не было никакого методаPerformEngineCheck()
.Car
классе я добавляю методPerformEngineCheck()
.PerformEngineCheck()
.Поэтому, когда я перекомпилирую против вашего нового API, C # предупреждает меня об этой проблеме, например
Если основание
PerformEngineCheck()
не былоvirtual
:И если база
PerformEngineCheck()
былаvirtual
:Теперь я должен явно принять решение, действительно ли мой класс продлевает контракт базового класса, или это другой контракт, но с тем же именем.
new
, я не ломаю своих клиентов, если функциональность базового метода отличалась от производного метода. Любой код, на который есть ссылкаVehicle
, не увидитCar.PerformEngineCheck()
вызываемый, но код, на который была ссылкаCar
, продолжит видеть ту же функциональность, что и яPerformEngineCheck()
.Подобный пример - когда другой метод в базовом классе может вызываться
PerformEngineCheck()
(особенно в более новой версии), как можно предотвратить вызовPerformEngineCheck()
метода из производного класса? В Java это решение будет зависеть от базового класса, но оно ничего не знает о производном классе. В C #, что решение лежит как на базовом классе (черезvirtual
ключевое слово), и на производный класс (черезnew
иoverride
ключевых слов).Конечно, ошибки, которые выдает компилятор, также предоставляют программистам полезный инструмент, чтобы не делать неожиданных ошибок (т.е. либо переопределять, либо предоставлять новые функциональные возможности, не осознавая этого).
Как сказал Андерс, реальный мир заставляет нас сталкиваться с такими проблемами, которые, если бы мы начинали с нуля, мы бы никогда не хотели решать.
РЕДАКТИРОВАТЬ: Добавлен пример, где
new
будет использоваться для обеспечения совместимости интерфейса.РЕДАКТИРОВАТЬ: Проходя через комментарии, я также наткнулся на рецензию Эрика Липперта (тогда один из членов комитета по дизайну C #) на другие примеры сценариев (упомянутых Брайаном).
ЧАСТЬ 2: На основе обновленного вопроса
Кто решает,
SpaceShip
переопределяетVehicle.accelerate()
ли он или нет? Это должен бытьSpaceShip
разработчик. Так что, еслиSpaceShip
разработчик решит, что он не соблюдает контракт с базовым классом, то ваш призывVehicle.accelerate()
не должен идтиSpaceShip.accelerate()
или не должен? Именно тогда они отметят это какnew
. Однако, если они решат, что он действительно сохраняет контракт, то они фактически отметят егоoverride
. В любом случае ваш код будет вести себя правильно, вызывая правильный метод на основе контракта . Как ваш код может решить,SpaceShip.accelerate()
является ли он на самом деле переопределеннымVehicle.accelerate()
или это коллизия имен? (См. Мой пример выше).Однако, в случае неявного наследования, даже если
SpaceShip.accelerate()
не держать контракт наVehicle.accelerate()
вызов метода будет по- прежнему идтиSpaceShip.accelerate()
.источник
Это было сделано, потому что это правильно. Дело в том, что допускать переопределение всех методов неправильно; это приводит к хрупкой проблеме базового класса, когда вы не можете сказать, сломает ли изменение базового класса подклассы. Поэтому вы должны либо занести в черный список методы, которые не должны быть переопределены, либо внести в белый список те, которые разрешено переопределять. Из этих двух, белый список не только безопаснее (поскольку вы не можете случайно создать хрупкий базовый класс ), он также требует меньше работы, поскольку вам следует избегать наследования в пользу композиции.
источник
final
модификатор, так в чем же проблема?HashMap
?). Либо спроектируйте наследование и сделайте договор понятным, либо не делайте этого и убедитесь, что никто не может наследовать класс.@Override
был добавлен как бандит, потому что было слишком поздно, чтобы изменить поведение к тому времени, но даже оригинальные разработчики Java (особенно Джош Блох) соглашаются, что это было плохое решение.Как сказал Роберт Харви, это все к чему ты привык. Я нахожу отсутствие этой гибкости в 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
. Это сохранило стандарт «этот метод не должен быть переопределен», хотя учитывал особый случай, где это было. Это семантическая разница, которая делает жизнь проще.Обратите внимание, однако, что это различие поведение , связанное с этим, не только семантическое различие. Смотрите эту статью для деталей. Однако я никогда не сталкивался с ситуацией, когда мне нужно было воспользоваться этим поведением.
источник
new
этого пути - ошибка, ожидающая своего появления. Если я вызываю метод в каком-то экземпляре, я ожидаю, что он всегда будет делать одно и то же, даже если я приведу экземпляр к его базовому классу. Но это не то, какnew
ведут себя методы.new
примеров использования является WebViewPage <TModel> в среде MVC. Но я также столкнулся с ошибкой вnew
течение нескольких часов, поэтому я не думаю, что это необоснованный вопрос.Конструкция 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
который имеет одинаковую подпись, будет указывать на плохой дизайн, и поэтому не должен поддерживаться. К сожалению, поскольку у автора базового типа нет возможности узнать, какие методы могут быть добавлены к производным типам, и наоборот, ситуация, когда клиенты базового класса и производного класса имеют разные ожидания для одноименных методов, по существу неизбежна, если только никому не разрешается добавлять какие-либо имена, которые кто-то еще может добавить. Вопрос не в том, должно ли произойти такое дублирование имен, а в том, как минимизировать вред, когда это происходит.источник
DerivedGraphics? A class using
GrafDerived`?GrafDerived
. Я начал использоватьDerivedGraphics
, но чувствовал, что это было немного долго. ДажеGrafDerived
немного длиннее, но не знал, как лучше назвать типы графического рендерера, которые должны иметь четкое отношение база / производное.Стандартная ситуация:
Вы являетесь владельцем базового класса, который используется несколькими проектами. Вы хотите внести изменения в указанный базовый класс, который будет разбиваться между 1 и бесчисленным производными классами, которые находятся в проектах, обеспечивающих реальную ценность (фреймворк обеспечивает ценность в лучшем случае за один раз, ни один реальный человек не хочет фреймворка, он хочет вещь, работающая на платформе). Удачи, заявляя занятым владельцам производных классов; «ну, вы должны измениться, вам не следовало бы переопределять этот метод», не получив репутацию «Framework: задержка проектов и причина ошибок» для людей, которые должны утверждать решения.
Тем более, что, не объявляя его не переопределенным, вы неявно заявили, что они могут делать то, что теперь предотвращает ваши изменения.
И если у вас нет значительного числа производных классов, предоставляющих реальную ценность путем переопределения вашего базового класса, почему он вообще является базовым классом? Надежда - это мощный мотиватор, но также очень хороший способ получить не имеющий ссылки код.
Конечный результат: код базового класса фреймворка становится невероятно хрупким и статичным, и вы не можете действительно внести необходимые изменения, чтобы оставаться актуальными / эффективными. Кроме того, ваш фреймворк получает репутацию за нестабильность (производные классы продолжают ломаться), и люди вообще не будут его использовать, поскольку основная причина использования фреймворка - сделать кодирование более быстрым и надежным.
Проще говоря, вы не можете просить занятых владельцев проектов отложить свой проект, чтобы исправить ошибки, которые вы вносите, и ожидать чего-то лучшего, чем «уйти», если вы не предоставите им значительную выгоду, даже если первоначальная «ошибка» принадлежит им, что в лучшем случае спорно.
Лучше не позволять им делать неправильные вещи, во-первых, именно здесь появляется «не виртуальный по умолчанию». И когда кто-то приходит к вам с очень ясной причиной, почему ему нужен этот конкретный метод, который можно переопределить, и почему это должно быть безопасно, вы можете «разблокировать» его, не рискуя нарушить чужой код.
источник
По умолчанию не виртуальный предполагает, что разработчик базового класса идеально подходит. По моему опыту разработчики не идеальны. Если разработчик базового класса не может представить вариант использования, в котором метод может быть переопределен, или забывает добавить виртуальный, то я не могу воспользоваться преимуществом полиморфизма при расширении базового класса без изменения базового класса. В реальном мире изменение базового класса часто не вариант.
В C # разработчик базового класса не доверяет разработчику подкласса. В Java разработчик подкласса не доверяет разработчику базового класса. Разработчик подкласса отвечает за подкласс и должен (imho) иметь возможность расширять базовый класс по своему усмотрению (за исключением явного отказа, а в java они могут даже ошибиться).
Это фундаментальное свойство определения языка. Это не правильно или неправильно, это то, что есть и не может измениться.
источник