Por que C # implementa métodos como não virtuais por padrão?

105

Ao contrário do Java, por que o C # trata os métodos como funções não virtuais por padrão? É mais provável que seja um problema de desempenho do que outros resultados possíveis?

Lembro-me de ler um parágrafo de Anders Hejlsberg sobre várias vantagens que a arquitetura existente está trazendo. Mas e os efeitos colaterais? É realmente uma boa troca ter métodos não virtuais por padrão?

Burcu Dogan
fonte
1
As respostas que mencionam motivos de desempenho ignoram o fato de que o compilador C # compila principalmente chamadas de método para callvirt e não para chamar . É por isso que em C # não é possível ter um método que se comporte de maneira diferente se a thisreferência for nula. Veja aqui para mais informações.
Andy
Verdade! A instrução call IL é principalmente para chamadas feitas a métodos estáticos.
RBT
1
Os pensamentos do arquiteto Anders Hejlsberg do C # aqui e aqui .
RBT

Respostas:

98

As classes devem ser projetadas para herança para poder tirar proveito disso. Ter métodos virtualpor padrão significa que cada função da classe pode ser removida e substituída por outra, o que não é realmente uma coisa boa. Muitas pessoas até acreditam que as aulas deveriam ser o sealedpadrão.

virtualos métodos também podem ter uma leve implicação no desempenho. No entanto, não é provável que esse seja o motivo principal.

Mehrdad Afshari
fonte
7
Pessoalmente, duvido dessa parte sobre desempenho. Mesmo para funções virtuais, o compilador é muito capaz de descobrir onde substituir callvirtpor um simples callno código IL (ou ainda mais adiante durante o JITting). Java HotSpot faz o mesmo. O resto da resposta é exata.
Konrad Rudolph
5
Concordo que o desempenho não está na vanguarda, mas também pode ter implicações no desempenho. Provavelmente é muito pequeno. Certamente, não é esse o motivo dessa escolha, no entanto. Só queria mencionar isso.
Mehrdad Afshari
71
Aulas seladas me dão vontade de socar bebês.
mxmissile
6
@mxmissile: eu também, mas um bom design de API é difícil de qualquer maneira. Acho que lacrado por padrão faz todo o sentido para o código que você possui. Na maioria das vezes, o outro programador de sua equipe não considerou a herança quando implementou a classe de que você precisa e selado por padrão transmitiria essa mensagem muito bem.
Roman Starkov
49
Muitas pessoas também são psicopatas, não significa que devemos ouvi-las. Virtual deve ser o padrão em C #, o código deve ser extensível sem a necessidade de alterar as implementações ou reescrevê-lo completamente. Pessoas que superprotegem suas APIs geralmente acabam com APIs mortas ou parcialmente usadas, o que é muito pior do que o cenário de alguém fazendo uso indevido delas.
Chris Nicola
89

Estou surpreso que parece haver um consenso aqui de que não virtual por padrão é a maneira certa de fazer as coisas. Vou descer do outro lado - acho pragmático - da cerca.

A maioria das justificativas me parece o velho argumento "Se dermos a você o poder, você pode se machucar". De programadores ?!

Parece-me que o codificador que não sabia o suficiente (ou não tinha tempo) para projetar sua biblioteca para herança e / ou extensibilidade é o codificador que produziu exatamente a biblioteca que provavelmente terei de consertar ou ajustar - exatamente o biblioteca onde a capacidade de substituir seria mais útil.

O número de vezes que tive que escrever um código de solução feio e desesperado (ou abandonar o uso e lançar minha própria solução alternativa) porque não posso substituir muito, supera em muito o número de vezes que já fui mordido ( por exemplo, em Java) substituindo onde o designer pode não ter considerado eu.

Não virtual por padrão torna minha vida mais difícil.

ATUALIZAR: Foi apontado [muito corretamente] que eu realmente não respondi à pergunta. Então - e com desculpas pelo atraso ...

Eu meio que queria ser capaz de escrever algo incisivo como "C # implementa métodos como não virtuais por padrão porque uma má decisão foi tomada que valorizava mais os programas do que os programadores". (Acho que isso poderia ser um pouco justificado com base em algumas das outras respostas a esta pergunta - como desempenho (otimização prematura, alguém?), Ou garantia do comportamento das classes.)

No entanto, percebo que estaria apenas declarando minha opinião e não aquela resposta definitiva que Stack Overflow deseja. Certamente, pensei, no nível mais alto, a resposta definitiva (mas inútil) é:

Eles não são virtuais por padrão porque os designers da linguagem tiveram uma decisão a tomar e foi isso que escolheram.

Agora eu acho que a razão exata pela qual eles tomaram essa decisão nós nunca ... oh, espere! A transcrição de uma conversa!

Portanto, parece que as respostas e comentários aqui sobre os perigos de substituir APIs e a necessidade de projetar explicitamente para herança estão no caminho certo, mas estão todos perdendo um aspecto temporal importante: a principal preocupação de Anders era manter uma classe ou APIs implícitas contrato entre versões . E eu acho que ele está realmente mais preocupado em permitir que a plataforma .Net / C # mude no código, em vez de se preocupar com a mudança do código do usuário no topo da plataforma. (E seu ponto de vista "pragmático" é exatamente o oposto do meu porque ele está olhando do outro lado.)

(Mas eles não poderiam simplesmente ter escolhido o virtual por padrão e, em seguida, adicionado "final" por meio da base de código? Talvez não seja exatamente a mesma coisa ... e Anders é claramente mais inteligente do que eu, então vou deixar assim.)

Mwardm
fonte
2
Não poderia concordar mais. Muito frustrante ao consumir uma API de terceiros e querer ignorar algum comportamento e não conseguir.
Andy
3
Eu concordo com entusiasmo. Se você vai publicar uma API (seja internamente em uma empresa ou para o mundo externo), você realmente pode e deve certificar-se de que seu código foi projetado para herança. Você deve tornar a API boa e polida se estiver publicando para ser usada por muitas pessoas. Comparado ao esforço necessário para um bom design geral (bom conteúdo, casos de uso claros, testes, documentação), o design para herança realmente não é tão ruim. E se você não estiver publicando, o virtual por padrão consome menos tempo e você sempre pode corrigir a pequena parte dos casos em que é problemático.
Gravidade de
2
Agora, se houvesse apenas um recurso de editor no Visual Studio para marcar automaticamente todos os métodos / propriedades como virtual... Alguém suplemento do Visual Studio?
kevinarpe
1
Embora eu concorde totalmente com você, esta não é exatamente uma resposta.
Chris Morgan
2
Considerando as práticas de desenvolvimento modernas, como injeção de dependência, frameworks de simulação e ORMs, parece claro que nossos designers de C # erraram um pouco. É muito frustrante ter que testar uma dependência quando você não pode substituir uma propriedade por padrão.
Jeremy Holovacs,
17

Porque é muito fácil esquecer que um método pode ser substituído e não projetado para isso. C # faz você pensar antes de torná-lo virtual. Acho que é uma ótima decisão de design. Algumas pessoas (como Jon Skeet) até disseram que as classes deveriam ser seladas por padrão.

Zifre
fonte
12

Para resumir o que outros disseram, existem alguns motivos:

1- Em C #, há muitas coisas na sintaxe e na semântica que vêm direto do C ++. O fato de que os métodos não eram virtuais por padrão em C ++ influenciou C #.

2- Ter todos os métodos virtuais por default é uma preocupação de desempenho, pois toda chamada de método deve usar a Tabela Virtual do objeto. Além disso, isso limita fortemente a capacidade do compilador Just-In-Time de usar métodos sequenciais e realizar outros tipos de otimização.

3- Mais importante, se os métodos não são virtuais por padrão, você pode garantir o comportamento de suas classes. Quando eles são virtuais por padrão, como em Java, você não pode nem mesmo garantir que um método getter simples fará conforme o planejado porque ele pode ser substituído para fazer qualquer coisa em uma classe derivada (é claro que você pode, e deve, fazer o método e / ou final da aula).

Alguém pode se perguntar, como Zifre mencionou, por que a linguagem C # não deu um passo adiante e tornou as classes lacradas por padrão. Isso é parte de todo o debate sobre os problemas de herança de implementação, que é um tópico muito interessante.

Trillian
fonte
1
Eu também teria preferido classes seladas por padrão. Então seria consistente.
Andy
9

C # é influenciado por C ++ (e mais). C ++ não habilita o envio dinâmico (funções virtuais) por padrão. Um (bom?) Argumento para isso é a pergunta: "Com que freqüência você implementa classes que são membros de uma pesquisa de classe?". Outro motivo para evitar a ativação do despacho dinâmico por padrão é a área de cobertura da memória. Uma classe sem um ponteiro virtual (vpointer) apontando para uma tabela virtual , é obviamente menor do que a classe correspondente com a vinculação tardia habilitada.

O problema de desempenho não é tão fácil de dizer "sim" ou "não". A razão para isso é a compilação Just In Time (JIT), que é uma otimização de tempo de execução em C #.

Outra pergunta semelhante sobre " velocidade das chamadas virtuais .. "

Schildmeijer
fonte
Tenho algumas dúvidas de que os métodos virtuais tenham implicações de desempenho para C #, por causa do compilador JIT. Essa é uma das áreas em que o JIT pode ser melhor do que a compilação offline, porque eles podem fazer chamadas de função inline que são "desconhecidas" antes do tempo de execução
David Cournapeau
1
Na verdade, acho que é mais influenciado por Java do que C ++, que faz isso por padrão.
Mehrdad Afshari
5

A razão simples é o custo de design e manutenção, além dos custos de desempenho. Um método virtual tem custo adicional em comparação com um método não virtual porque o designer da classe deve planejar o que acontecerá quando o método for substituído por outra classe. Isso tem um grande impacto se você espera que um método específico atualize o estado interno ou tenha um comportamento específico. Agora você deve planejar o que acontece quando uma classe derivada altera esse comportamento. É muito mais difícil escrever código confiável nessa situação.

Com um método não virtual, você tem controle total. Tudo o que der errado é culpa do autor original. O código é muito mais fácil de raciocinar.

JaredPar
fonte
Este é um post realmente antigo, mas para um novato que está escrevendo seu primeiro projeto, eu constantemente me preocupo com consequências indesejadas / desconhecidas do código que escrevo. É muito reconfortante saber que os métodos não virtuais são totalmente MINHA culpa.
trevorc
2

Se todos os métodos C # fossem virtuais, o vtbl seria muito maior.

Os objetos C # só têm métodos virtuais se a classe tiver métodos virtuais definidos. É verdade que todos os objetos têm informações de tipo que incluem um equivalente vtbl, mas se nenhum método virtual for definido, apenas os métodos de base do objeto estarão presentes.

@Tom Hawtin: Provavelmente é mais correto dizer que C ++, C # e Java são todos da família de linguagens C :)

Thomas Bratt
fonte
1
Por que seria um problema para o vtable ser muito maior? Há apenas 1 vtable por classe (e não por instância), então seu tamanho não faz muita diferença.
Gravidade de
1

Vindo de um background de perl, acho que o C # selou o destino de todo desenvolvedor que quisesse estender e modificar o comportamento de uma classe base por meio de um método não virtual, sem forçar todos os usuários da nova classe a estarem cientes dos possíveis bastidores detalhes.

Considere o método Add da classe List. E se um desenvolvedor quisesse atualizar um dos vários bancos de dados em potencial sempre que uma lista específica fosse 'Adicionada'? Se 'Adicionar' fosse virtual por padrão, o desenvolvedor poderia desenvolver uma classe 'BackedList' que substituísse o método 'Adicionar' sem forçar todo o código do cliente a saber que era um 'BackedList' em vez de uma 'Lista' normal. Para todos os efeitos práticos, a 'BackedList' pode ser vista apenas como outra 'Lista' do código do cliente.

Isso faz sentido do ponto de vista de uma grande classe principal que pode fornecer acesso a um ou mais componentes de lista que são apoiados por um ou mais esquemas em um banco de dados. Dado que os métodos C # não são virtuais por padrão, a lista fornecida pela classe principal não pode ser um IEnumerable ou ICollection simples ou mesmo uma instância de List, mas deve ser anunciada ao cliente como um 'BackedList' para garantir que a nova versão da operação 'Adicionar' é chamado para atualizar o esquema correto.

Travis
fonte
É verdade que os métodos virtuais por padrão tornam o trabalho mais fácil, mas isso faz sentido? Há muitas coisas que você pode empregar para tornar sua vida mais fácil. Por que não apenas campos públicos nas aulas? Alterar seu comportamento é muito fácil. Na minha opinião, tudo na linguagem deve ser estritamente encapsulado, rígido e resiliente a mudanças por padrão. Altere-o apenas se necessário. Apenas sendo um pouco filosófico sobre decisões de design. Continuação ..
nawfal
... Para falar sobre o tópico atual, o modelo de herança deve ser usado apenas se Bfor A. Se Brequer algo diferente do que Anão é A. Acredito que substituir como um recurso na própria linguagem é uma falha de design. Se você precisar de um Addmétodo diferente , sua classe de coleção não será a List. Tentar dizer que é isso é fingimento. A abordagem certa aqui é a composição (e não a falsificação). É verdade que toda a estrutura é construída sobre recursos de substituição, mas eu simplesmente não gosto disso.
nawfal
Acho que entendi seu ponto, mas no exemplo concreto fornecido: 'BackedList' poderia apenas implementar a interface 'IList' e o cliente só conhece a interface. certo? estou esquecendo de algo? no entanto, entendo o ponto mais amplo que você está tentando apresentar.
Vetras de
0

Certamente não é um problema de desempenho. O interpretador Java da Sun usa o mesmo código para despachar ( invokevirtualbytecode) e o HotSpot gera exatamente o mesmo código, seja finalou não. Eu acredito que todos os objetos C # (mas não structs) têm métodos virtuais, então você sempre vai precisar da vtblidentificação de classe / runtime. C # é um dialeto de "linguagens semelhantes a Java". Sugerir que vem do C ++ não é totalmente honesto.

Existe uma ideia de que você deve "projetar para herança ou então proibi-lo". O que parece uma ótima ideia até o momento em que você tem um caso sério de negócios para resolver rapidamente. Talvez herdando de código que você não controla.

Tom Hawtin - tackline
fonte
O ponto de acesso é forçado a ir muito longe para fazer essa otimização, precisamente porque todos os métodos são virtuais por padrão, o que tem um grande impacto no desempenho. CoreCLR é capaz de alcançar um desempenho semelhante, embora seja muito mais simples
Yair Halberstadt
O @YairHalberstadt Hotspot precisa ser capaz de restaurar o código compilado por uma variedade de outros motivos. Já se passaram anos desde que olhei para a fonte, mas a diferença entre métodos finale finalmétodos eficazes é trivial. Também é importante notar que ele pode fazer inlining bimórfico, ou seja, métodos inline com duas implementações diferentes.
Tom Hawtin - tackline