Por que o C # foi criado com as palavras-chave “new” e “virtual + override” ao contrário do Java?


61

Em Java não existam virtual, new, overridepalavras-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 newou virtual+deriveou nova + virtual override está funcionando.

Não consigo entender por que, no mundo, vou adicionar um método no meu DerivedClasscom o mesmo nome e a mesma assinatura BaseClasse definir um novo comportamento, mas no polimorfismo em tempo de execução, o BaseClassmétodo será chamado! (que não é obrigatório, mas logicamente deveria ser).

No virtual + overrideentanto, 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 + overridevez de newe também usar em newvez 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 SpaceShipderivada Vehicle. Então eu quero mudar tudo carpara um SpaceShipobjeto, 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 SpaceShipnão substituir a Vehicleclasse ' acceleratee usar new, a lógica do meu código será quebrada. Isso não é uma desvantagem?


77
Você está acostumado à maneira como o Java faz isso e simplesmente não gastou tempo para entender as palavras-chave C # em seus próprios termos. Eu trabalho com C #, entendi os termos imediatamente e acho o modo como o Java é estranho.
Robert Harvey

12
O IIRC, C # fez isso para "favorecer a clareza". Se você precisar dizer explicitamente "novo" ou "substituir", isso torna claro e imediatamente aparente o que está acontecendo, em vez de você ter que ficar tentando descobrir se o método está substituindo algum comportamento em uma classe base ou não. Também acho muito útil poder especificar quais métodos quero especificar como virtuais e quais não. (Java faz isso com final; é exatamente o contrário).
Robert Harvey

15
“Em Java, não há palavras-chave virtuais, novas e de substituição para definição de método.” Exceto que existe @Override.
svick


3
A anotação @svick e as palavras-chave não são a mesma coisa :)
Anirban Nag 'tintinmj'

Respostas:


86

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.

Existem várias razões. Um é o desempenho . Podemos observar que, quando as pessoas escrevem código em Java, elas esquecem de marcar seus métodos como finais. Portanto, esses métodos são virtuais. Por serem virtuais, não apresentam um desempenho tão bom. Há apenas uma sobrecarga de desempenho associada ao método virtual. Essa é uma questão.

Uma questão mais importante é a versão . Existem duas escolas de pensamento sobre métodos virtuais. A escola acadêmica de pensamento diz: "Tudo deve ser virtual, porque talvez eu queira substituí-lo um dia". A escola pragmática de pensamento, que vem da criação de aplicativos reais executados no mundo real, diz: "Temos que ter muito cuidado com o que tornamos virtuais".

Quando criamos algo virtual em uma plataforma, fazemos muitas promessas sobre como ela evolui no futuro. Para um método não virtual, prometemos que, quando você chamar esse método, x e y acontecerão. Quando publicamos um método virtual em uma API, não prometemos apenas que quando você chamar esse método, x e y acontecerão. Também prometemos que, quando você substituir esse método, o chamaremos nesta sequência específica em relação a esses outros e o estado estará nisto e naquele invariável.

Toda vez que você diz virtual em uma API, está criando um gancho de retorno de chamada. Como designer de estrutura de SO ou API, você precisa ter muito cuidado com isso. Você não deseja que os usuários substituam e conectem-se a qualquer ponto arbitrário de uma API, porque não podem necessariamente fazer essas promessas. E as pessoas podem não entender completamente as promessas que estão fazendo quando tornam algo virtual.

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:

Não consigo entender por que, no mundo, adicionarei um método na minha DerivedClass com o mesmo nome e a mesma assinatura da BaseClass e definir um novo comportamento, mas no polimorfismo em tempo de execução, o método BaseClass será chamado! (que não é obrigatório, mas logicamente deveria ser).

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 newe overrideem C #, consulte esta página do MSDN ).

Um cenário muito prático é o seguinte:

  • Você criou uma API, que tem uma classe chamada Vehicle.
  • Comecei a usar sua API e derivou Vehicle.
  • Sua Vehicleturma não tinha nenhum método PerformEngineCheck().
  • Na minha Carturma, adiciono um método PerformEngineCheck().
  • Você lançou uma nova versão da sua API e adicionou a PerformEngineCheck().
  • Não consigo renomear meu método porque meus clientes dependem da minha API e isso os quebraria.
  • Portanto, quando recompilar sua nova API, o C # me avisa sobre esse problema, por exemplo

    Se a base PerformEngineCheck()não fosse virtual:

    app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    Use the new keyword if hiding was intended.
    

    E se a base PerformEngineCheck()fosse 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.
    
  • 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.

  • Ao fazer isso new, eu não quebro meus clientes se a funcionalidade do método base for diferente do método derivado. Qualquer código referenciado Vehiclenão será Car.PerformEngineCheck()chamado, mas o código que tiver uma referência Carcontinuará a ver a mesma funcionalidade que eu ofereci PerformEngineCheck().

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 o PerformEngineCheck()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 da virtualpalavra - chave) quanto na classe derivada (por meio das palavras new- overridechave 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 newteria 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

Mas no caso de C #, se o SpaceShip não substituir a classe Vehicle 'acelerar e usar new, a lógica do meu código será quebrada. Isso não é uma desvantagem?

Quem decide se SpaceShipestá realmente substituindo o Vehicle.accelerate()ou se é diferente? Tem que ser o SpaceShipdesenvolvedor. Portanto, se o SpaceShipdesenvolvedor decidir que não está mantendo o contrato da classe base, sua chamada para Vehicle.accelerate()não deve ser cumprida SpaceShip.accelerate(), ou deveria? É quando eles marcarão como new. No entanto, se eles decidirem que realmente mantém o contrato, eles o marcarão de fato override. 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 se SpaceShip.accelerate()está realmente substituindo Vehicle.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 de Vehicle.accelerate(), a chamada do método continuaria SpaceShip.accelerate().


12
O ponto de desempenho está completamente obsoleto agora. Para uma prova, veja minha referência mostrando que acessar um campo por um método não final, mas nunca sobrecarregado, leva um único ciclo.
maaartinus

7
Claro, esse pode ser o caso. A questão era que, quando o C # decidiu fazê-lo, por que o fez naquele momento e, portanto, essa resposta é válida. Se a questão é se ainda faz sentido, essa é uma discussão diferente, IMHO.
Omer Iqbal

11
Eu concordo plenamente com você.
Maaartinus

2
IMHO, embora definitivamente haja usos para ter funções não virtuais, o risco de armadilhas inesperadas ocorre quando algo que não deve substituir um método de classe base ou implementar uma interface, o faz.
Supercat

3
@Nilzor: Daí o argumento sobre acadêmico versus pragmático. Pragmaticamente, a responsabilidade de quebrar algo está no último agente de mudança. Se você alterar sua classe base, e uma classe derivada existente depender da alteração, não será problema deles ("eles" podem não existir mais). Portanto, as classes base ficam bloqueadas no comportamento de suas classes derivadas, mesmo que essa classe nunca deva ter sido virtual . E como você diz, o RhinoMock existe. Sim, se você finalizar corretamente todos os métodos, tudo será equivalente. Aqui apontamos para todos os sistemas Java já construídos.
deworde

94

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.


7
Permitir que qualquer método arbitrário seja substituído é errado. Você só poderá substituir os métodos que o designer da superclasse designou como seguros para substituição.
Doval

21
Eric Lippert discute isso em detalhes em seu post, Virtual Methods e Brittle Base Classes .
Brian

12
Java tem finalmodificador, então qual é o problema?
Sarge Borsch

13
@corsiKa "os objetos devem estar abertos à extensão" - por quê? Se o desenvolvedor da classe base nunca pensou em herança, é quase garantido que existam dependências e suposições sutis no código que levarão a erros (lembra-se HashMap?). Crie um projeto para herança e esclareça o contrato ou não, e garanta que ninguém possa herdar a classe. @Overridefoi 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.
Voo

9
@jwenting "Opinião" é uma palavra engraçada; as pessoas gostam de anexá-lo aos fatos para que possam desconsiderá-los. Mas, para o que vale a pena, também é a "opinião" de Joshua Bloch (veja: Java efetivo , item 17 ), a "opinião" de James Gosling (veja esta entrevista ) e, como apontado na resposta de Omer Iqbal , também é a "opinião" de Anders Hejlsberg. (E embora ele nunca tenha abordado a opção de optar por não participar, Eric Lippert concorda claramente que a herança também é perigosa.) Então, a quem eles estão se referindo?
Doval

33

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, e private, mas Java só tem public, protectednada, e private. 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 newvs. virtual+override, é mais ou menos assim:

  • Se você deseja forçar as subclasses a implementar o método, use abstracte overridena subclasse.
  • Se você deseja fornecer funcionalidade, mas permitir que a subclasse a substitua, use virtuale overridena subclasse.
  • Se você deseja fornecer funcionalidades que as subclasses nunca precisam substituir, não use nada.
    • Se você tem então uma subclasse caso especial que não precisa se comportar de forma diferente, usar newna subclasse.
    • Se você quiser garantir que nenhum subclasse pode nunca substituir o comportamento, use sealedna 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 OrderProcessorque possuía a maior parte da lógica, com certos abstract/ virtualmé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) Adicionar virtualao método base e overridena criança; ou 2) Adicione newà 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.


7
Eu acho que usar intencionalmente newesse 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 os newmétodos educacionais se comportam.
svick

@svick - Potencialmente, sim. Em nosso cenário específico, isso nunca ocorreria, mas foi por isso que acrescentei a ressalva.
Bobson

Um excelente uso newestá no WebViewPage <TModel> na estrutura MVC. Mas também fui atacada por um bug que envolve newhoras, então não acho que seja uma pergunta irracional.
PDR

11
@ C.Champagne: Qualquer ferramenta pode ser mal usada e esta é uma ferramenta particularmente afiada - você pode se cortar facilmente. Mas esse não é um motivo para remover a ferramenta da caixa de ferramentas e remover uma opção de um designer de API mais talentoso.
PDR

11
@svick Correto, e é por isso que normalmente é um aviso do compilador. Mas ter a capacidade de "novo" abrange as condições de borda (como a mencionada) e, melhor ainda, torna realmente óbvio que você está fazendo algo estranho quando chega para diagnosticar o bug inevitável. "Por que essa classe e apenas essa classe são de buggy ... ah-hah, um" novo ", vamos testar onde a SuperClasse é usada".
deworde

7

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 GrafBaseque não implemente void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), mas o GrafDerivedimplemente como um método público que desenha um paralelogramo cujo quarto ponto computado é oposto ao primeiro. Suponha ainda que uma versão posterior de GrafBaseimplemente um método público com a mesma assinatura, mas seu quarto ponto computado oposto ao segundo. Os clientes que recebem esperam a, GrafBasemas recebem uma referência a GrafDerivedesperam DrawParallelogramcalcular o quarto ponto da maneira do novo GrafBasemétodo, mas os clientes que estiveram usando GrafDerived.DrawParallelogramantes da alteração do método base esperam o comportamento GrafDerivedoriginalmente implementado.

Em Java, o autor não teria como GrafDerivedcoexistir essa classe com clientes que usam o novo GrafBase.DrawParallelogrammétodo (e pode não perceber que GrafDerivedexiste) sem quebrar a compatibilidade com o código de cliente existente usado GrafDerived.DrawParallelogramanteriormente GrafBase. Como DrawParallelogramnã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á como GrafDerivedevitar violar as expectativas legítimas de um deles (por exemplo, quebrar o código legítimo do cliente).

Em C #, se GrafDerivednão for recompilado, o tempo de execução assumirá que o código que chama o DrawParallelogrammétodo nas referências do tipo GrafDerivedestará esperando o comportamento que GrafDerived.DrawParallelogram()tinha quando foi compilado pela última vez, mas o código que chama o método nas referências do tipo GrafBaseestará esperando GrafBase.DrawParallelogram(o comportamento que foi adicionado). Se GrafDerivedposteriormente for recompilado na presença do aprimorado GrafBase, o compilador gritará até que o programador especifique se seu método foi planejado para ser um substituto válido para um membro herdado GrafBaseou se seu comportamento precisa estar vinculado a referências do tipo GrafDerived, mas não deve substitua o comportamento das referências do tipo GrafBase.

Alguém poderia argumentar razoavelmente que ter um método de GrafDerivedfazer algo diferente de um membro GrafBasecom 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.


2
Acho que há uma boa resposta aqui, mas está sendo perdida na parede de texto. Por favor, divida isso em mais alguns parágrafos.
Bobson

O que é DerivedGraphics? A class using GrafDerived`?
C.Champagne

@ C.Champagne: eu quis dizer GrafDerived. Comecei a usar DerivedGraphics, mas senti que era um pouco longo. Even GrafDerivedainda é 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.
Supercat

11
@ Bobson: Melhor?
precisa

6

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.


0

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.


2
este não parece oferecer nada substancial sobre os pontos feitos e explicado na prévia 5 respostas
mosquito
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.