Em Java não existam virtual
, new
, override
palavras-chave para definição do método. Portanto, o funcionamento de um método é fácil de entender. Causa se DerivedClass estender BaseClass e tiver um método com o mesmo nome e a mesma assinatura de BaseClass , a substituição ocorrerá no polimorfismo em tempo de execução (desde que o método não seja static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Agora vêm para C # pode haver tanta confusão e difícil de entender como o new
ou virtual+derive
ou nova + virtual override está funcionando.
Não consigo entender por que, no mundo, vou adicionar um método no meu DerivedClass
com o mesmo nome e a mesma assinatura BaseClass
e definir um novo comportamento, mas no polimorfismo em tempo de execução, o BaseClass
método será chamado! (que não é obrigatório, mas logicamente deveria ser).
No virtual + override
entanto, a implementação lógica está correta, mas o programador precisa pensar em qual método ele deve dar permissão ao usuário para substituir no momento da codificação. Que tem alguns pró-contras (não vamos lá agora).
Então, por que em C # há tanto espaço para raciocínio e confusão não- lógicos? Então, posso reformular minha pergunta como em qual contexto do mundo real devo pensar em usar em virtual + override
vez de new
e também usar em new
vez de virtual + override
?
Depois de algumas respostas muito boas, especialmente do Omar , percebo que os designers de C # enfatizaram mais sobre os programadores que deveriam pensar antes de criar um método, o que é bom e lida com alguns erros de novato do Java.
Agora eu tenho uma pergunta em mente. Como em Java, se eu tivesse um código como
Vehicle vehicle = new Car();
vehicle.accelerate();
e depois eu faço nova classe SpaceShip
derivada Vehicle
. Então eu quero mudar tudo car
para um SpaceShip
objeto, só preciso alterar uma única linha de código
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
Isso não interromperá minha lógica em nenhum ponto do código.
Mas no caso de C # se SpaceShip
não substituir a Vehicle
classe ' accelerate
e usar new
, a lógica do meu código será quebrada. Isso não é uma desvantagem?
fonte
final
; é exatamente o contrário).@Override
.Respostas:
Como você perguntou por que o C # fez dessa maneira, é melhor perguntar aos criadores de C #. Anders Hejlsberg, o arquiteto principal do C #, respondeu por que eles escolheram não usar o virtual por padrão (como em Java) em uma entrevista , abaixo estão os trechos pertinentes.
Lembre-se de que Java possui virtual por padrão com a palavra-chave final para marcar um método como não virtual. Ainda há dois conceitos a serem aprendidos, mas muitas pessoas não sabem sobre a palavra-chave final ou não usam proativamente. O C # obriga a usar virtual e new / override para tomar essas decisões conscientemente.
A entrevista tem mais discussões sobre como os desenvolvedores pensam sobre o design de herança de classe e como isso levou à sua decisão.
Agora, para a seguinte pergunta:
Isso ocorre quando uma classe derivada deseja declarar que não cumpre o contrato da classe base, mas tem um método com o mesmo nome. (Para quem não sabe a diferença entre
new
eoverride
em C #, consulte esta página do MSDN ).Um cenário muito prático é o seguinte:
Vehicle
.Vehicle
.Vehicle
turma não tinha nenhum métodoPerformEngineCheck()
.Car
turma, adiciono um métodoPerformEngineCheck()
.PerformEngineCheck()
.Portanto, quando recompilar sua nova API, o C # me avisa sobre esse problema, por exemplo
Se a base
PerformEngineCheck()
não fossevirtual
:E se a base
PerformEngineCheck()
fossevirtual
:Agora, devo tomar uma decisão explícita se minha classe está realmente estendendo o contrato da classe base ou se é um contrato diferente, mas passa a ter o mesmo nome.
new
, eu não quebro meus clientes se a funcionalidade do método base for diferente do método derivado. Qualquer código referenciadoVehicle
não seráCar.PerformEngineCheck()
chamado, mas o código que tiver uma referênciaCar
continuará a ver a mesma funcionalidade que eu ofereciPerformEngineCheck()
.Um exemplo semelhante é quando outro método na classe base pode estar chamando
PerformEngineCheck()
(especialmente na versão mais recente), como impedir que ele chame oPerformEngineCheck()
da classe derivada? Em Java, essa decisão seria da classe base, mas não sabe nada sobre a classe derivada. Em C #, essa decisão repousa tanto na classe base (por meio davirtual
palavra - chave) quanto na classe derivada (por meio das palavrasnew
-override
chave e ).Obviamente, os erros que o compilador lança também fornecem uma ferramenta útil para os programadores não cometerem erros inesperadamente (ou seja, substituem ou fornecem novas funcionalidades sem perceber).
Como Anders disse, o mundo real nos obriga a enfrentar essas questões que, se começássemos do zero, nunca quereríamos entrar.
EDIT: Adicionado um exemplo de onde
new
teria que ser usado para garantir a compatibilidade da interface.Edição: Ao analisar os comentários, também deparei-me com um artigo de Eric Lippert (então um dos membros do comitê de design do C #) em outros exemplos de cenário (mencionados por Brian).
PARTE 2: Com base na pergunta atualizada
Quem decide se
SpaceShip
está realmente substituindo oVehicle.accelerate()
ou se é diferente? Tem que ser oSpaceShip
desenvolvedor. Portanto, se oSpaceShip
desenvolvedor decidir que não está mantendo o contrato da classe base, sua chamada paraVehicle.accelerate()
não deve ser cumpridaSpaceShip.accelerate()
, ou deveria? É quando eles marcarão comonew
. No entanto, se eles decidirem que realmente mantém o contrato, eles o marcarão de fatooverride
. Em qualquer um dos casos, seu código se comportará corretamente chamando o método correto com base no contrato . Como o seu código pode decidir seSpaceShip.accelerate()
está realmente substituindoVehicle.accelerate()
ou se é uma colisão de nomes? (Veja meu exemplo acima).No entanto, no caso de herança implícita, mesmo
SpaceShip.accelerate()
que não mantivesse o contrato deVehicle.accelerate()
, a chamada do método continuariaSpaceShip.accelerate()
.fonte
Foi feito porque é a coisa correta a fazer. O fato é que permitir que todos os métodos sejam substituídos está errado; isso leva ao frágil problema da classe base, onde você não tem como saber se uma alteração na classe base quebrará as subclasses. Portanto, você deve colocar na lista negra os métodos que não devem ser substituídos ou colocar na lista branca os que têm permissão para serem substituídos. Dos dois, a lista de permissões não é apenas mais segura (já que você não pode criar uma classe base frágil acidentalmente ), ela também requer menos trabalho, pois você deve evitar a herança em favor da composição.
fonte
final
modificador, então qual é o problema?HashMap
?). Crie um projeto para herança e esclareça o contrato ou não, e garanta que ninguém possa herdar a classe.@Override
foi adicionado como bandaid porque era tarde demais para mudar o comportamento até então, mas mesmo os desenvolvedores Java originais (particularmente Josh Bloch) concordam que essa foi uma decisão ruim.Como Robert Harvey disse, está tudo no que você está acostumado. Acho estranha a falta dessa flexibilidade em Java .
Dito isto, por que ter isso em primeiro lugar? Pela mesma razão que C # tem
public
,internal
(também "nada"),protected
,protected internal
, eprivate
, mas Java só tempublic
,protected
nada, eprivate
. Ele fornece controle mais refinado sobre o comportamento do que você está codificando, às custas de ter mais termos e palavras-chave para acompanhar.No caso de
new
vs.virtual+override
, é mais ou menos assim:abstract
eoverride
na subclasse.virtual
eoverride
na subclasse.new
na subclasse.sealed
na classe base.Para um exemplo do mundo real: um projeto em que trabalhei em pedidos de comércio eletrônico processados de várias fontes diferentes. Havia uma base
OrderProcessor
que possuía a maior parte da lógica, com certosabstract
/virtual
métodos para substituir a classe filho de cada fonte. Isso funcionou bem, até termos uma nova fonte com uma maneira completamente diferente de processar pedidos, de modo que tivemos que substituir uma função principal. Tivemos duas opções neste momento: 1) Adicionarvirtual
ao método base eoverride
na criança; ou 2) Adicionenew
à criança.Embora qualquer um deles possa funcionar, o primeiro tornaria muito fácil substituir esse método específico novamente no futuro. Apareceria no preenchimento automático, por exemplo. Porém, como foi um caso excepcional, optamos por usá-lo
new
. Isso preservou o padrão de "esse método não precisa ser substituído", além de permitir o caso especial em que ocorreu. É uma diferença semântica que facilita a vida.Note, porém, que não é uma diferença de comportamento associado a este, e não apenas uma diferença semântica. Veja este artigo para detalhes. No entanto, nunca tive uma situação em que precisava tirar proveito desse comportamento.
fonte
new
esse caminho é um bug que está esperando para acontecer. Se eu chamar um método em alguma instância, espero que ele sempre faça a mesma coisa, mesmo se eu converter a instância em sua classe base. Mas não é assim que osnew
métodos educacionais se comportam.new
está no WebViewPage <TModel> na estrutura MVC. Mas também fui atacada por um bug que envolvenew
horas, então não acho que seja uma pergunta irracional.O design do Java é tal que, dada qualquer referência a um objeto, uma chamada para um nome de método específico com tipos de parâmetros específicos, se for permitido, sempre invocará o mesmo método. É possível que as conversões implícitas do tipo de parâmetro possam ser afetadas pelo tipo de uma referência, mas depois que todas essas conversões forem resolvidas, o tipo da referência é irrelevante.
Isso simplifica o tempo de execução, mas pode causar alguns problemas infelizes. Suponha
GrafBase
que não implementevoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
, mas oGrafDerived
implemente como um método público que desenha um paralelogramo cujo quarto ponto computado é oposto ao primeiro. Suponha ainda que uma versão posterior deGrafBase
implemente um método público com a mesma assinatura, mas seu quarto ponto computado oposto ao segundo. Os clientes que recebem esperam a,GrafBase
mas recebem uma referência aGrafDerived
esperamDrawParallelogram
calcular o quarto ponto da maneira do novoGrafBase
método, mas os clientes que estiveram usandoGrafDerived.DrawParallelogram
antes da alteração do método base esperam o comportamentoGrafDerived
originalmente implementado.Em Java, o autor não teria como
GrafDerived
coexistir essa classe com clientes que usam o novoGrafBase.DrawParallelogram
método (e pode não perceber queGrafDerived
existe) sem quebrar a compatibilidade com o código de cliente existente usadoGrafDerived.DrawParallelogram
anteriormenteGrafBase
. ComoDrawParallelogram
não é possível dizer que tipo de cliente o está chamando, ele deve se comportar de forma idêntica quando chamado por tipos de código do cliente. Como os dois tipos de código do cliente têm expectativas diferentes sobre como ele deve se comportar, não há comoGrafDerived
evitar violar as expectativas legítimas de um deles (por exemplo, quebrar o código legítimo do cliente).Em C #, se
GrafDerived
não for recompilado, o tempo de execução assumirá que o código que chama oDrawParallelogram
método nas referências do tipoGrafDerived
estará esperando o comportamento queGrafDerived.DrawParallelogram()
tinha quando foi compilado pela última vez, mas o código que chama o método nas referências do tipoGrafBase
estará esperandoGrafBase.DrawParallelogram
(o comportamento que foi adicionado). SeGrafDerived
posteriormente for recompilado na presença do aprimoradoGrafBase
, o compilador gritará até que o programador especifique se seu método foi planejado para ser um substituto válido para um membro herdadoGrafBase
ou se seu comportamento precisa estar vinculado a referências do tipoGrafDerived
, mas não deve substitua o comportamento das referências do tipoGrafBase
.Alguém poderia argumentar razoavelmente que ter um método de
GrafDerived
fazer algo diferente de um membroGrafBase
com a mesma assinatura indicaria um design ruim e, como tal, não deveria ser suportado. Infelizmente, como o autor de um tipo de base não tem como saber quais métodos podem ser adicionados aos tipos derivados, nem vice-versa, a situação em que os clientes da classe base e da classe derivada têm expectativas diferentes para métodos com nomes semelhantes é essencialmente inevitável, a menos que ninguém tem permissão para adicionar qualquer nome que outra pessoa também possa adicionar. A questão não é se tal duplicação de nome deve ocorrer, mas como minimizar o dano quando isso ocorre.fonte
DerivedGraphics? A class using
GrafDerived`?GrafDerived
. Comecei a usarDerivedGraphics
, mas senti que era um pouco longo. EvenGrafDerived
ainda é um pouco longo, mas não sabia qual a melhor maneira de nomear tipos de renderizadores gráficos que deveriam ter uma clara relação base / derivada.Situação padrão:
Você é o proprietário de uma classe base que é usada por vários projetos. Você deseja fazer uma alteração na referida classe base que se dividirá entre 1 e inúmeras classes derivadas, que estão em projetos que fornecem valor no mundo real (uma estrutura fornece valor na melhor das hipóteses, um ser humano real não quer uma estrutura, ele deseja o que está sendo executado no Framework). Boa sorte declarando aos proprietários ocupados das classes derivadas; "bem, você precisa mudar, não deveria ter substituído esse método" sem obter um representante como "Estrutura: Delayer de projetos e causador de bugs" para as pessoas que precisam aprovar decisões.
Especialmente porque, ao não declarar que não pode ser substituído, você implicitamente declarou que eles estavam bem ao fazer o que agora impede sua alteração.
E se você não possui um número significativo de classes derivadas que fornecem valor no mundo real substituindo sua classe base, por que é uma classe base em primeiro lugar? A esperança é um motivador poderoso, mas também é uma maneira muito boa de acabar com um código não referenciado.
Resultado final: seu código de classe base da estrutura se torna incrivelmente frágil e estático, e você não pode realmente fazer as alterações necessárias para manter-se atualizado / eficiente. Como alternativa, sua estrutura obtém um representante por instabilidade (as classes derivadas continuam quebrando) e as pessoas não a usam, pois o principal motivo para usar uma estrutura é tornar a codificação mais rápida e confiável.
Simplificando, você não pode pedir aos proprietários ocupados do projeto que adiem o projeto para corrigir os erros que estão apresentando e esperem algo melhor do que um "ir embora", a menos que você esteja oferecendo benefícios significativos a eles, mesmo que a "falha" original era deles, o que é, na melhor das hipóteses, discutível.
É melhor não deixar que eles façam a coisa errada em primeiro lugar, que é onde "não virtual por padrão" entra. E quando alguém chega até você com uma razão muito clara pela qual eles precisam que esse método específico seja substituível e por que deve ser seguro, você pode "desbloqueá-lo" sem correr o risco de quebrar o código de outras pessoas.
fonte
A padronização para não virtual pressupõe que o desenvolvedor da classe base seja perfeito. Na minha experiência, os desenvolvedores não são perfeitos. Se o desenvolvedor de uma classe base não puder imaginar um caso de uso em que um método possa ser substituído ou se esqueça de adicionar virtual, não posso aproveitar o polimorfismo ao estender a classe base sem modificar a classe base. No mundo real, modificar a classe base geralmente não é uma opção.
Em C #, o desenvolvedor da classe base não confia no desenvolvedor da subclasse. Em Java, o desenvolvedor da subclasse não confia no desenvolvedor da classe base. O desenvolvedor da subclasse é responsável pela subclasse e deve (imho) ter o poder de estender a classe base como entenderem (exceto negação explícita e, em java, eles podem até errar).
É uma propriedade fundamental da definição de linguagem. Não está certo ou errado, é o que é e não pode mudar.
fonte