Perché C # implementa i metodi come non virtuali per impostazione predefinita?


105

A differenza di Java, perché C # tratta i metodi come funzioni non virtuali per impostazione predefinita? È più probabile che si tratti di un problema di prestazioni piuttosto che di altri possibili risultati?

Mi viene in mente di aver letto un paragrafo di Anders Hejlsberg sui numerosi vantaggi che l'architettura esistente sta portando fuori. Ma per quanto riguarda gli effetti collaterali? È davvero un buon compromesso avere metodi non virtuali per impostazione predefinita?


1
Le risposte che menzionano i motivi delle prestazioni trascurano il fatto che il compilatore C # compila principalmente chiamate di metodo a callvirt e non call . Ecco perché in C # non è possibile avere un metodo che si comporti in modo diverso se il thisriferimento è null. Vedi qui per maggiori informazioni.
Andy

Vero! call L' istruzione IL è principalmente per le chiamate effettuate a metodi statici.
RBT

1
I pensieri dell'architetto di C # Anders Hejlsberg qui e qui .
RBT

Risposte:


98

Le classi dovrebbero essere progettate in modo che l'ereditarietà possa trarne vantaggio. Avere metodi virtualper impostazione predefinita significa che ogni funzione nella classe può essere scollegata e sostituita da un'altra, il che non è davvero una buona cosa. Molte persone credono addirittura che le classi avrebbero dovuto essere sealedpredefinite.

virtuali metodi possono anche avere una leggera implicazione sulle prestazioni. Tuttavia, è improbabile che questo sia il motivo principale.


7
Personalmente, dubito che la parte riguardi le prestazioni. Anche per le funzioni virtuali, il compilatore è molto in grado di capire dove sostituire callvirtcon un semplice callcodice IL (o anche più a valle durante il JITting). Java HotSpot fa lo stesso. Il resto della risposta è azzeccato.
Konrad Rudolph

5
Sono d'accordo che le prestazioni non siano in prima linea, ma possono anche avere implicazioni sulle prestazioni. Probabilmente è molto piccolo. Certamente, però, non è il motivo di questa scelta. Volevo solo menzionarlo.
Mehrdad Afshari

71
Le lezioni sigillate mi fanno venire voglia di prendere a pugni i bambini.
mxmissile

6
@mxmissile: anche a me, ma un buon design delle API è comunque difficile. Penso che sigillato per impostazione predefinita abbia perfettamente senso per il codice che possiedi. Il più delle volte, l'altro programmatore del tuo team non ha preso in considerazione l'ereditarietà quando ha implementato quella classe di cui hai bisogno, e sealed-by-default trasmetterebbe quel messaggio abbastanza bene.
Roman Starkov

49
Molte persone sono anche psicopatiche, non significa che dovremmo ascoltarle. Virtual dovrebbe essere l'impostazione predefinita in C #, il codice dovrebbe essere estensibile senza dover modificare le implementazioni o riscriverlo completamente. Le persone che proteggono eccessivamente le proprie API spesso finiscono con API morte o utilizzate a metà, il che è molto peggio dello scenario di qualcuno che ne fa un uso improprio.
Chris Nicola

89

Sono sorpreso che sembra esserci un tale consenso qui che il non virtuale per impostazione predefinita è il modo giusto di fare le cose. Scenderò dall'altra parte - penso pragmatica - del recinto.

La maggior parte delle giustificazioni mi si legge come il vecchio argomento "Se ti diamo il potere potresti farti del male". Dai programmatori ?!

Mi sembra che il programmatore che non sapeva abbastanza (o ha avuto abbastanza tempo) per progettare la propria libreria per ereditarietà e / o estensibilità sia il programmatore che ha prodotto esattamente la libreria che probabilmente dovrò aggiustare o modificare - esattamente il libreria in cui la possibilità di sovrascrivere sarebbe più utile.

Il numero di volte in cui ho dovuto scrivere codice brutto e disperato (o abbandonare l'uso e lanciare la mia soluzione alternativa) perché non posso scavalcare di gran lunga, supera di gran lunga il numero di volte in cui sono stato morso ( ad esempio in Java) sovrascrivendo dove il progettista potrebbe non aver considerato che avrei potuto.

Non virtuale per impostazione predefinita mi rende la vita più difficile.

AGGIORNAMENTO: È stato sottolineato [abbastanza correttamente] che in realtà non ho risposto alla domanda. Quindi, e scusandomi per il ritardo ...

Volevo essere in grado di scrivere qualcosa di conciso come "C # implementa metodi come non virtuali per impostazione predefinita perché è stata presa una decisione sbagliata che apprezzava i programmi più dei programmatori". (Penso che potrebbe essere in qualche modo giustificato in base ad alcune delle altre risposte a questa domanda, come le prestazioni (ottimizzazione prematura, chiunque?) O la garanzia del comportamento delle classi.)

Tuttavia, mi rendo conto che sto solo affermando la mia opinione e non quella risposta definitiva che Stack Overflow desidera. Sicuramente, ho pensato, al livello più alto la risposta definitiva (ma inutile) è:

Per impostazione predefinita, non sono virtuali perché i progettisti del linguaggio avevano una decisione da prendere ed è quello che hanno scelto.

Ora immagino che il motivo esatto per cui hanno preso quella decisione non lo faremo mai ... oh, aspetta! La trascrizione di una conversazione!

Quindi sembrerebbe che le risposte e i commenti qui sui pericoli dell'override delle API e sulla necessità di progettare esplicitamente per l'ereditarietà siano sulla strada giusta, ma mancano tutti di un importante aspetto temporale: la preoccupazione principale di Anders era mantenere l'implicito di una classe o API contratto tra le versioni . E penso che in realtà sia più preoccupato di consentire alla piattaforma .Net / C # di cambiare sotto il codice piuttosto che preoccuparsi della modifica del codice utente sulla piattaforma. (E il suo punto di vista "pragmatico" è l'esatto opposto del mio perché guarda dall'altra parte.)

(Ma non potevano semplicemente aver scelto virtual-by-default e poi pepato "finale" attraverso il codice base? Forse non è proprio la stessa cosa .. e Anders è chiaramente più intelligente di me, quindi lascerò mentire.)


2
Non potrei essere più d'accordo. Molto frustrante quando si consuma un'API di terze parti e si desidera sovrascrivere alcuni comportamenti e non essere in grado di farlo.
Andy

3
Sono d'accordo con entusiasmo. Se hai intenzione di pubblicare un'API (sia internamente a un'azienda che per il mondo esterno), puoi e dovresti davvero assicurarti che il tuo codice sia progettato per l'ereditarietà. Dovresti rendere l'API buona e raffinata se la pubblichi per essere utilizzata da molte persone. Rispetto allo sforzo necessario per una buona progettazione complessiva (buon contenuto, casi d'uso chiari, test, documentazione), la progettazione per l'ereditarietà non è davvero male. E se non stai pubblicando, virtuale per impostazione predefinita richiede meno tempo e puoi sempre risolvere la piccola porzione di casi in cui è problematico.
Gravity

2
Ora, se in Visual Studio fosse presente solo una funzionalità di editor per contrassegnare automaticamente tutti i metodi / proprietà come virtual... Componente aggiuntivo di Visual Studio chiunque?
kevinarpe

1
Anche se sono assolutamente d'accordo con te, questa non è esattamente una risposta.
Chris Morgan

2
Considerando le pratiche di sviluppo moderne come l'inserimento di dipendenze, i framework di mock e gli ORM, sembra chiaro che i nostri progettisti C # abbiano mancato un po 'il bersaglio. È molto frustrante dover testare una dipendenza quando non è possibile sovrascrivere una proprietà per impostazione predefinita.
Jeremy Holovacs

17

Perché è troppo facile dimenticare che un metodo può essere sovrascritto e non progettato per questo. C # ti fa pensare prima di renderlo virtuale. Penso che questa sia un'ottima decisione di design. Alcune persone (come Jon Skeet) hanno persino affermato che le classi dovrebbero essere sigillate per impostazione predefinita.


12

Per riassumere ciò che hanno detto gli altri, ci sono alcuni motivi:

1- In C #, ci sono molte cose nella sintassi e nella semantica che provengono direttamente da C ++. Il fatto che i metodi non fossero virtuali per impostazione predefinita in C ++ ha influenzato C #.

2- Avere ogni metodo virtuale per impostazione predefinita è un problema di prestazioni perché ogni chiamata di metodo deve utilizzare la tabella virtuale dell'oggetto. Inoltre, questo limita fortemente la capacità del compilatore Just-In-Time di incorporare metodi ed eseguire altri tipi di ottimizzazione.

3- Ancora più importante, se i metodi non sono virtuali per impostazione predefinita, puoi garantire il comportamento delle tue classi. Quando sono virtuali per impostazione predefinita, come in Java, non puoi nemmeno garantire che un semplice metodo getter funzioni come previsto perché potrebbe essere sovrascritto per fare qualsiasi cosa in una classe derivata (ovviamente puoi, e dovresti, fare il metodo e / o la classe finale).

Ci si potrebbe chiedere, come ha detto Zifre, perché il linguaggio C # non sia andato oltre e abbia reso le classi sigillate per impostazione predefinita. Questo fa parte dell'intero dibattito sui problemi dell'ereditarietà dell'implementazione, che è un argomento molto interessante.


1
Avrei preferito anche le classi sigillate per impostazione predefinita. Allora sarebbe coerente.
Andy

9

C # è influenzato da C ++ (e altro). C ++ non abilita l'invio dinamico (funzioni virtuali) per impostazione predefinita. Un argomento (valido?) Per questo è la domanda: "Con che frequenza implementate classi che sono membri di una classe hiearchy?". Un altro motivo per evitare di abilitare l'invio dinamico per impostazione predefinita è l'impronta di memoria. Una classe senza un puntatore virtuale (vpointer) che punta a un file tabella virtuale , è ovviamente più piccola della classe corrispondente con l'associazione tardiva abilitata.

Non è così facile dire "sì" o "no" al problema delle prestazioni. La ragione di ciò è la compilazione Just In Time (JIT) che è un'ottimizzazione del tempo di esecuzione in C #.

Un'altra domanda simile sulla " velocità delle chiamate virtuali .. "


Sono un po 'dubbioso che i metodi virtuali abbiano implicazioni sulle prestazioni per C #, a causa del compilatore JIT. Questa è una delle aree in cui JIT può essere migliore della compilazione offline, perché possono chiamare funzioni inline "sconosciute" prima del runtime
David Cournapeau,

1
In realtà, penso che sia più influenzato da Java che da C ++ che lo fa per impostazione predefinita.
Mehrdad Afshari,

5

Il semplice motivo è il costo di progettazione e manutenzione oltre ai costi di prestazione. Un metodo virtuale ha un costo aggiuntivo rispetto a un metodo non virtuale perché il progettista della classe deve pianificare ciò che accade quando il metodo viene sovrascritto da un'altra classe. Ciò ha un grande impatto se ti aspetti che un particolare metodo aggiorni lo stato interno o abbia un comportamento particolare. Ora devi pianificare cosa succede quando una classe derivata cambia quel comportamento. È molto più difficile scrivere codice affidabile in quella situazione.

Con un metodo non virtuale hai il controllo totale. Tutto ciò che va storto è colpa dell'autore originale. Il codice è molto più facile da ragionare.


Questo è un post davvero vecchio ma per un principiante che sta scrivendo il suo primo progetto mi preoccupo costantemente per le conseguenze non intenzionali / sconosciute del codice che scrivo. È molto confortante sapere che i metodi non virtuali sono totalmente colpa MIA.
trevorc

2

Se tutti i metodi C # fossero virtuali, vtbl sarebbe molto più grande.

Gli oggetti C # hanno metodi virtuali solo se la classe dispone di metodi virtuali definiti. È vero che tutti gli oggetti hanno informazioni sul tipo che includono un equivalente vtbl, ma se non vengono definiti metodi virtuali, saranno presenti solo i metodi Object di base.

@ Tom Hawtin: Probabilmente è più accurato dire che C ++, C # e Java appartengono tutti alla famiglia di linguaggi C :)


1
Perché sarebbe un problema che vtable fosse molto più grande? C'è solo 1 vtable per classe (e non per istanza), quindi le sue dimensioni non fanno molta differenza.
Gravity

1

Provenendo da uno sfondo Perl, penso che C # abbia sigillato il destino di ogni sviluppatore che avrebbe potuto voler estendere e modificare il comportamento di una classe base attraverso un metodo non virtuale senza costringere tutti gli utenti della nuova classe a essere consapevoli di potenzialmente dietro le quinte dettagli.

Considera il metodo Add della classe List. Cosa succede se uno sviluppatore desidera aggiornare uno dei numerosi potenziali database ogni volta che viene "aggiunto" un determinato elenco? Se "Add" fosse stato virtuale per impostazione predefinita, lo sviluppatore potrebbe sviluppare una classe "BackedList" che sovrascrisse il metodo "Add" senza forzare tutto il codice client a sapere che era un "BackedList" invece di un normale "List". Per tutti gli scopi pratici, "BackedList" può essere visto come un altro "List" dal codice client.

Ciò ha senso dal punto di vista di una grande classe principale che potrebbe fornire accesso a uno o più componenti di elenco che a loro volta sono supportati da uno o più schemi in un database. Dato che i metodi C # non sono virtuali per impostazione predefinita, l'elenco fornito dalla classe principale non può essere un semplice IEnumerable o ICollection o anche un'istanza di List, ma deve invece essere annunciato al client come "BackedList" per garantire che la nuova versione dell'operazione "Aggiungi" viene chiamata per aggiornare lo schema corretto.


È vero, i metodi virtuali per impostazione predefinita facilitano il lavoro, ma ha senso? Ci sono molte cose che potresti impiegare per semplificarti la vita. Perché non solo campi pubblici nelle classi? Modificare il suo comportamento è troppo facile. A mio parere tutto nella lingua dovrebbe essere strettamente incapsulato, rigido e resiliente al cambiamento per impostazione predefinita. Modificalo solo se necessario. Essere solo un po 'filosofico sulle decisioni di design. Continua ..
nawfal

... Per parlare dell'argomento corrente, il modello di ereditarietà dovrebbe essere utilizzato solo se lo Bè A. Se Brichiede qualcosa di diverso da Aallora non lo è A. Credo che prevalere come capacità nel linguaggio stesso sia un difetto di progettazione. Se hai bisogno di un Addmetodo diverso , la tua classe di raccolta non è un file List. Cercare di dirlo è fingere. L'approccio giusto qui è la composizione (e non la finzione). È vero che l'intero framework è costruito su capacità di override, ma semplicemente non mi piace.
nawfal

Penso di aver capito il tuo punto, ma sull'esempio concreto fornito: "BackedList" potrebbe semplicemente implementare l'interfaccia "IList" e il client conosce solo l'interfaccia. giusto? mi sto perdendo qualcosa? tuttavia capisco il punto più ampio che stai cercando di fare.
Vetras

0

Non è certamente un problema di prestazioni. L'interprete Java di Sun utilizza lo stesso codice per l'invio ( invokevirtualbytecode) e HotSpot genera esattamente lo stesso codice, indipendentemente dal fatto che lo stesso final. Credo che tutti gli oggetti C # (ma non le strutture) abbiano metodi virtuali, quindi avrai sempre bisogno divtbl dell'identificazione della classe / runtime. C # è un dialetto di "linguaggi simili a Java". Suggerire che provenga da C ++ non è del tutto onesto.

C'è l'idea che dovresti "progettare per l'eredità o altrimenti proibirla". Il che suona come un'ottima idea fino al momento in cui hai un caso aziendale serio da mettere in una soluzione rapida. Forse ereditando da codice che non controlli.


Hotspot è costretto a fare di tutto per fare tale ottimizzazione, proprio perché tutti i metodi sono virtuali per impostazione predefinita, il che ha un enorme impatto sulle prestazioni. CoreCLR è in grado di ottenere prestazioni simili pur essendo molto più semplice
Yair Halberstadt

@YairHalberstadt Hotspot deve essere in grado di ritirare il codice compilato per una serie di altri motivi. Sono passati anni da quando ho esaminato la fonte, ma la differenza tra i metodi finale in modo efficace finalè banale. Vale anche la pena notare che può eseguire l'inlining bimorfico, ovvero metodi inline con due diverse implementazioni.
Tom Hawtin - tackline
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.