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 new
e override
em 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
Vehicle
turma não tinha nenhum método PerformEngineCheck()
.
- Na minha
Car
turma, 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 Vehicle
não será Car.PerformEngineCheck()
chamado, mas o código que tiver uma referência Car
continuará 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 virtual
palavra - chave) quanto na classe derivada (por meio das palavras new
- 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
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 SpaceShip
está realmente substituindo o Vehicle.accelerate()
ou se é diferente? Tem que ser o SpaceShip
desenvolvedor. Portanto, se o SpaceShip
desenvolvedor 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()
.