Por que o C # foi criado com palavras-chave “new” e “virtual + override”, diferente do Java?

56

No Java, não há virtual , new , override palavras-chave para a definição do método. Portanto, o funcionamento de um método é fácil de entender. Porque se DerivedClass estender BaseClass e tiver um método com o mesmo nome e mesma assinatura de BaseClass , a substituição ocorrerá no polimorfismo de 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 vamos para C #, pode haver muita confusão e é difícil entender como o new ou virtual+derive ou new + 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 mesma assinatura que BaseClass e definir um novo comportamento, mas no polimorfismo de tempo de execução. o método BaseClass será invocado! (que não é importante, mas logicamente deveria ser).

No caso de virtual + override , a implementação lógica está correta, mas o programador deve pensar qual método ele deve permitir que o usuário substitua no momento da codificação. Que tem alguns pró-con (não vamos lá agora).

Então, por que em C # há muito espaço para un - raciocínio e confusão lógicos. Assim, posso reformular minha pergunta como em qual contexto do mundo real devo pensar em usar virtual + override em vez de new e também usar new em vez de virtual + override ?

Depois de algumas respostas muito boas, especialmente do Omar , percebi que os designers de C # deram mais ênfase aos programadores que deveriam pensar antes de fazer um método, o que é bom e lida com alguns erros de novatos do Java.

Agora tenho uma pergunta em mente. Como em Java, se eu tivesse um código como

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

e mais tarde eu faço a nova classe SpaceShip derivada de Vehicle . Então eu quero mudar todo o car para um objeto SpaceShip Eu só tenho que mudar uma única linha de código

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

Isso não quebrará minha lógica em nenhum ponto do código.

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

    
por Anirban Nag 'tintinmj' 18.06.2014 / 17:20
fonte

6 respostas

80

Como você perguntou por que o C # fez dessa maneira, é melhor perguntar aos criadores do C #. Anders Hejlsberg, o principal arquiteto do C #, respondeu por que eles escolheram não usar virtual por padrão (como em Java) em uma entrevista , os snippets pertinentes estão abaixo.

Lembre-se de que o Java é virtual por padrão com a palavra-chave final para marcar um método como não -virtual. Ainda existem dois conceitos para aprender, mas muitas pessoas não sabem sobre a palavra-chave final ou não usam proativamente. C # obriga a usar o virtual e o novo / substituir para conscientemente tomar essas decisões.

There are several reasons. One is performance. We can observe that as people write code in Java, they forget to mark their methods final. Therefore, those methods are virtual. Because they're virtual, they don't perform as well. There's just performance overhead associated with being a virtual method. That's one issue.

A more important issue is versioning. There are two schools of thought about virtual methods. The academic school of thought says, "Everything should be virtual, because I might want to override it someday." The pragmatic school of thought, which comes from building real applications that run in the real world, says, "We've got to be real careful about what we make virtual."

When we make something virtual in a platform, we're making an awful lot of promises about how it evolves in the future. For a non-virtual method, we promise that when you call this method, x and y will happen. When we publish a virtual method in an API, we not only promise that when you call this method, x and y will happen. We also promise that when you override this method, we will call it in this particular sequence with regard to these other ones and the state will be in this and that invariant.

Every time you say virtual in an API, you are creating a call back hook. As an OS or API framework designer, you've got to be real careful about that. You don't want users overriding and hooking at any arbitrary point in an API, because you cannot necessarily make those promises. And people may not fully understand the promises they are making when they make something virtual.

A entrevista tem mais discussões sobre como os desenvolvedores pensam sobre o design de herança de classes e como isso levou à sua decisão.

Agora, para a seguinte pergunta:

I'm not able to understand why in the world I'm going to add a method in my DerivedClass with same name and same signature as BaseClass and define a new behaviour but at the run-time polymorphism, the BaseClass method will be invoked! (which is not overriding but logically it should be).

Isso ocorre quando uma classe derivada deseja declarar que não obedece ao contrato da classe base, mas possui um método com o mesmo nome. (Para quem não sabe a diferença entre new e override em C #, consulte este MSDN página ).

Um cenário muito prático é este:

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

    Se a base PerformEngineCheck() não for 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() for 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 sobre se minha turma está realmente estendendo o contrato da classe base ou se é um contrato diferente, mas é o mesmo nome.

  • Ao torná-lo new , não quebro meus clientes se a funcionalidade do método base for diferente do método derivado. Qualquer código que faça referência a Vehicle não verá Car.PerformEngineCheck() chamado, mas o código que tiver uma referência a Car continuará a ver a mesma funcionalidade que eu ofereci em PerformEngineCheck() .

Um exemplo similar é quando outro método na classe base pode estar chamando PerformEngineCheck() (esp. na versão mais recente), como é que se impede que ele chame o PerformEngineCheck() da classe derivada? Em Java, essa decisão ficaria na classe base, mas não sabe nada sobre a classe derivada. Em C #, essa decisão fica ambas na classe base (por meio da palavra-chave virtual ), e na classe derivada (por meio das palavras-chave new e override ).

Naturalmente, os erros que o compilador gera também fornecem uma ferramenta útil para que os programadores não cometam erros inesperados (isto é, substituam ou forneçam novas funcionalidades sem perceber isso).

Como Anders disse, o mundo real nos obriga a questões que, se começássemos do zero, nunca iríamos querer entrar.

EDIT: Adicionado um exemplo de onde new teria que ser usado para garantir a compatibilidade da interface.

EDIT: Enquanto passando pelos comentários, eu também me deparei com um write-up por Eric Lippert (então um dos membros do comitê de design do C #) em outros cenários de exemplo (mencionados por Brian).

PARTE 2: com base na pergunta atualizada

But in case of C# if SpaceShip does not override the Vehicle class' accelerate and use new then the logic of my code will be broken. Isn't that a disadvantage?

Quem decide se SpaceShip está realmente substituindo o Vehicle.accelerate() ou se é diferente? Tem que ser o desenvolvedor SpaceShip . Portanto, se SpaceShip desenvolvedor decidir que eles não estão mantendo o contrato da classe base, sua chamada para Vehicle.accelerate() não deve ir para SpaceShip.accelerate() ou deveria? É quando eles vão marcá-lo como new . No entanto, se eles decidirem que realmente mantêm o contrato, então, de fato, eles o marcarão override . Em ambos os 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 se SpaceShip.accelerate() não mantivesse o contrato de Vehicle.accelerate() , a chamada do método ainda iria para SpaceShip.accelerate() .

    
por 18.06.2014 / 21:47
fonte
92

Foi feito porque é a coisa certa a fazer. O fato é que permitir que os métodos all sejam substituídos está errado; isso leva ao frágil problema da classe base, onde você não tem como dizer se uma mudança na classe base irá quebrar subclasses. Portanto, você deve listar os métodos que não devem ser substituídos ou colocar na lista de permissões aqueles que podem ser 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 ), também requer menos trabalho, já que você deve evitar a herança em favor da composição.

    
por 18.06.2014 / 17:39
fonte
31

Como Robert Harvey disse, é tudo no que você está acostumado. Eu acho que a falta de Java dessa flexibilidade é estranha.

Dito isso, por que isso aconteceu em primeiro lugar? Pelo mesmo motivo que C # tem public , internal (também "nada"), protected , protected internal e private , mas Java tem apenas public , protected , nada e% código%. Ele fornece um melhor controle sobre o comportamento do que você está codificando, às custas de ter mais termos e palavras-chave para acompanhar.

No caso de private vs. new , é mais ou menos assim:

  • Se você quiser forçar subclasses a implementar o método, use virtual+override e abstract na subclasse.
  • Se você quiser fornecer funcionalidade, mas permitir que a subclasse a substitua, use override e virtual na subclasse.
  • Se você deseja fornecer funcionalidade que as subclasses nunca precisem substituir, não use nada.
    • Se você tiver uma subclasse de caso especial que faz precisa se comportar de maneira diferente, use override na subclasse.
    • Se você quiser garantir que nenhuma subclasse possa sempre substituir o comportamento, use new 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 sealed que tinha a maior parte da lógica, com determinados métodos OrderProcessor / abstract para substituir a classe filha de cada origem. Isso funcionou bem, até chegarmos a uma nova fonte que tinha uma maneira completamente diferente de processar pedidos, de modo que tivemos que substituir uma função principal. Neste momento, tivemos duas opções: 1) Adicionar virtual ao método base e virtual no filho; ou 2) Adicione override à criança.

Enquanto qualquer um poderia funcionar, o primeiro tornaria muito fácil substituir esse método específico novamente no futuro. Ele apareceria no preenchimento automático, por exemplo. Este foi um caso excepcional, no entanto, optamos por usar new . Isso preservou o padrão de "este método não precisa ser substituído", enquanto permite o caso especial em que ocorreu. É uma diferença semântica que torna a vida mais fácil.

Note, no entanto, que existe uma diferença de comportamento associada a isso, não apenas uma diferença semântica. Consulte este artigo para obter detalhes. No entanto, nunca encontrei uma situação em que precisei aproveitar esse comportamento.

    
por 18.06.2014 / 17:42
fonte
6

O design de Java é tal que, dada qualquer referência a um objeto, uma chamada para um determinado nome de método com determinados tipos de parâmetros, se for permitido, sempre invocará o mesmo método. É possível que conversões de tipo de parâmetro implícitas possam ser afetadas pelo tipo de uma referência, mas assim que todas essas conversões forem resolvidas, o tipo de referência é irrelevante.

Isso simplifica o tempo de execução, mas pode causar alguns problemas infelizes. Suponha que GrafBase não implemente void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3) , mas GrafDerived o implemente como um método público que desenhe um paralelogramo cujo quarto ponto calculado seja o oposto do primeiro. Suponha ainda que uma versão posterior de GrafBase implemente um método público com a mesma assinatura, mas seu quarto ponto calculado oposto ao segundo. Os clientes que recebem esperam um GrafBase mas recebem uma referência a um GrafDerived esperam que DrawParallelogram calcule o quarto ponto na moda do novo método GrafBase , mas clientes que usaram GrafDerived.DrawParallelogram antes da base método foi alterado irá esperar o comportamento que GrafDerived originalmente implementado.

Em Java, não haveria como o autor de GrafDerived fazer essa classe coexistir com clientes que usam o novo método GrafBase.DrawParallelogram (e podem não saber que GrafDerived existe mesmo) sem quebrar a compatibilidade com os existentes. código do cliente que usou GrafDerived.DrawParallelogram antes de GrafBase defini-lo. Como o DrawParallelogram não pode dizer qual tipo de cliente está invocando, ele deve se comportar de maneira idêntica quando invocado por tipos de código do cliente. Como os dois tipos de código do cliente têm diferentes expectativas sobre como ele deve se comportar, não há como GrafDerived evitar a violação das expectativas legítimas de um deles (ou seja, 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 invoca o método DrawParallelogram nas referências do tipo GrafDerived estará esperando o comportamento GrafDerived.DrawParallelogram() quando foi compilado pela última vez, mas o código que invoca o método em referências do tipo GrafBase estará esperando GrafBase.DrawParallelogram (o comportamento que foi adicionado). Se GrafDerived for mais tarde recompilado na presença do GrafBase aprimorado, o compilador irá grasnar até que o programador especifique se seu método foi planejado para ser um substituto válido para o membro herdado GrafBase , ou se seu comportamento precisa ser vinculado a referências do tipo GrafDerived , mas não deve substituir o comportamento de referências do tipo GrafBase .

Alguém poderia razoavelmente argumentar que ter um método de GrafDerived fazer algo diferente de um membro de GrafBase que tenha a mesma assinatura indicaria design incorreto e, como tal, não deveria ser suportado. Infelizmente, como o autor de um tipo base não tem como saber quais métodos podem ser adicionados a tipos derivados, nem vice-versa, a situação em que clientes de classe base e classe derivada têm expectativas diferentes para métodos nomeados é essencialmente inevitável a menos que ninguém pode adicionar nenhum nome que outra pessoa possa adicionar. A questão não é se tal duplicação de nome deve acontecer, mas sim como minimizar o dano quando isso acontece.

    
por 18.06.2014 / 18:25
fonte
5

Situação padrão:

Você é o proprietário de uma classe base usada por vários projetos. Você quer fazer uma mudança na dita classe base que irá quebrar entre 1 e inúmeras classes derivadas, que estão em projetos fornecendo valor do mundo real (Um framework fornece valor na melhor das hipóteses a um remover, nenhum Ser Humano quer um Framework, eles querem a coisa rodando no Framework). Boa sorte declarando aos ocupados proprietários das classes derivadas; "bem, você tem que mudar, você não deveria ter substituído esse método" sem ganhar um representante como "Framework: Delayer of Projects e Cause of Bugs" para as pessoas que têm que aprovar decisões.

Especialmente porque, ao não declarar que não pode ser sobreposto, você implicitamente declarou que estava tudo bem em fazer o que agora impede sua mudança.

E se você não tiver um número significativo de classes derivadas fornecendo valor real substituindo sua classe base, por que ela é uma classe base em primeiro lugar? A esperança é um motivador poderoso, mas também uma maneira muito boa de acabar com o código não referenciado.

Resultado final: o código da classe base do seu framework torna-se incrivelmente frágil e estático, e você não pode realmente fazer as alterações necessárias para permanecer atual / eficiente. Alternativamente, sua estrutura obtém um representante para instabilidade (as classes derivadas continuam sendo interrompidas) e as pessoas não a usarão, já que o principal motivo para usar uma estrutura é tornar a codificação mais rápida e confiável.

Simplificando, você não pode pedir a proprietários de projetos ocupados que atrasem seus projetos para corrigir erros que você está introduzindo, e esperar algo melhor do que um "ir embora", a menos que você esteja fornecendo benefícios significativos para eles, mesmo que o original "culpa" era deles, que é na melhor das hipóteses discutíveis.

Melhor não deixá-los fazer a coisa errada em primeiro lugar, que é onde entra o "não-virtual por padrão". E quando alguém chega até você com uma razão muito clara porque eles precisam desse método específico para ser superestimavel , e por que deve ser seguro, você pode "desbloquear" sem arriscar quebrar o código de qualquer outra pessoa.

    
por 20.06.2014 / 14:16
fonte
0

O padrão para não virtual pressupõe que o desenvolvedor da classe base é 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 esquecer de incluir virtual, não será possível 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 a negação explícita, e no java eles podem até entender errado).

É uma propriedade fundamental da definição da linguagem. Não é certo ou errado, é o que é e não pode mudar.

    
por 09.11.2014 / 06:32
fonte