Dato che hai chiesto perché C # lo ha fatto in questo modo, è meglio chiedere ai creatori di C #. Anders Hejlsberg, l'architetto capo di C #, ha risposto al motivo per cui hanno scelto di non usare il virtual di default (come in Java) in un'intervista , i frammenti pertinenti sono di seguito.
Tieni presente che Java ha virtuale per impostazione predefinita con la parola chiave finale per contrassegnare un metodo come non virtuale. Ancora due concetti da imparare, ma molte persone non conoscono la parola chiave finale o non usano in modo proattivo. C # costringe a usare virtuale e nuovo / override per prendere consapevolmente quelle decisioni.
Ci sono diverse ragioni. Uno è la prestazione . Possiamo osservare che quando le persone scrivono codice in Java, dimenticano di contrassegnare i loro metodi come finali. Pertanto, tali metodi sono virtuali. Perché sono virtuali, non si comportano altrettanto bene. C'è solo un sovraccarico di prestazioni associato all'essere un metodo virtuale. Questo è un problema.
Un problema più importante è il controllo delle versioni . Esistono due scuole di pensiero sui metodi virtuali. La scuola di pensiero accademica dice: "Tutto dovrebbe essere virtuale, perché un giorno potrei voler scavalcarlo". La pragmatica scuola di pensiero, che deriva dalla costruzione di applicazioni reali che girano nel mondo reale, dice: "Dobbiamo stare molto attenti a ciò che rendiamo virtuale".
Quando realizziamo qualcosa di virtuale in una piattaforma, facciamo moltissime promesse su come si evolverà in futuro. Per un metodo non virtuale, promettiamo che quando si chiama questo metodo, accadranno xey. Quando pubblichiamo un metodo virtuale in un'API, non promettiamo solo che quando chiami questo metodo, accadranno xey. Promettiamo anche che quando si sostituisce questo metodo, lo chiameremo in questa sequenza particolare rispetto a questi altri e lo stato sarà in questo e quell'invariante.
Ogni volta che dici virtuale in un'API, stai creando un hook di call back. Come progettista di framework di sistemi operativi o API, devi fare molta attenzione a questo. Non vuoi che gli utenti prevalgano e si colleghino in qualsiasi punto arbitrario di un'API, perché non puoi necessariamente fare queste promesse. E le persone potrebbero non comprendere appieno le promesse che stanno facendo quando realizzano qualcosa di virtuale.
L'intervista ha più discussioni su come gli sviluppatori pensano al design dell'eredità di classe e su come ciò abbia portato alla loro decisione.
Ora alla seguente domanda:
Non riesco a capire perché nel mondo aggiungerò un metodo nella mia DerivedClass con lo stesso nome e la stessa firma di BaseClass e definirò un nuovo comportamento, ma al polimorfismo di runtime verrà invocato il metodo BaseClass! (che non è prioritario ma logicamente dovrebbe essere).
Questo sarebbe quando una classe derivata vuole dichiarare che non rispetta il contratto della classe base, ma ha un metodo con lo stesso nome. (Per chiunque non conosca la differenza tra new
e override
in C #, vedere questa pagina MSDN ).
Uno scenario molto pratico è questo:
- Hai creato un'API, che ha una classe chiamata
Vehicle
.
- Ho iniziato a utilizzare la tua API e ho derivato
Vehicle
.
- La tua
Vehicle
classe non aveva alcun metodo PerformEngineCheck()
.
- Nella mia
Car
classe, aggiungo un metodo PerformEngineCheck()
.
- Hai rilasciato una nuova versione della tua API e aggiunto un
PerformEngineCheck()
.
- Non riesco a rinominare il mio metodo perché i miei clienti dipendono dalla mia API e li romperebbe.
Quindi, quando mi ricompilo contro la tua nuova API, C # mi avvisa di questo problema, ad es
Se la base PerformEngineCheck()
non 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 la 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.
Ora, devo prendere esplicitamente una decisione se la mia classe sta effettivamente estendendo il contratto della classe base, o se si tratta di un contratto diverso ma sembra avere lo stesso nome.
- Facendolo
new
, non rompo i miei clienti se la funzionalità del metodo di base era diversa dal metodo derivato. Qualsiasi codice a cui si fa riferimento Vehicle
non vedrà Car.PerformEngineCheck()
chiamato, ma il codice a cui si fa riferimento Car
continuerà a vedere la stessa funzionalità che avevo offerto PerformEngineCheck()
.
Un esempio simile è quando un altro metodo nella classe base potrebbe chiamare PerformEngineCheck()
(specialmente nella versione più recente), come si può impedire che chiama la PerformEngineCheck()
classe derivata? In Java, quella decisione ricade sulla classe base, ma non sa nulla della classe derivata. In C #, tale decisione si basa sia sulla classe base (tramite la virtual
parola chiave), sia sulla classe derivata (tramite le parole chiave new
e override
).
Ovviamente, gli errori generati dal compilatore forniscono anche uno strumento utile per i programmatori per non commettere errori inaspettatamente (ovvero sovrascrivere o fornire nuove funzionalità senza accorgersene).
Come ha detto Anders, il mondo reale ci obbliga a tali problemi che, se dovessimo iniziare da zero, non vorremmo mai entrare.
EDIT: aggiunto un esempio di dove new
dovrebbe essere usato per garantire la compatibilità dell'interfaccia.
EDIT: Durante la lettura dei commenti, mi sono anche imbattuto in una recensione di Eric Lippert (allora uno dei membri del comitato di progettazione C #) su altri scenari di esempio (menzionati da Brian).
PARTE 2: in base alla domanda aggiornata
Ma in caso di C # se SpaceShip non ha la precedenza sulla classe Vehicle 'accelerare e usare new, la logica del mio codice verrà interrotta. Non è uno svantaggio?
Chi decide se SpaceShip
sta effettivamente sostituendo Vehicle.accelerate()
o se è diverso? Deve essere lo SpaceShip
sviluppatore. Quindi se lo SpaceShip
sviluppatore decide che non sta mantenendo il contratto della classe base, allora la tua chiamata a Vehicle.accelerate()
non dovrebbe andare SpaceShip.accelerate()
o dovrebbe farlo? Questo è quando lo contrassegneranno come new
. Tuttavia, se decidono che mantiene effettivamente il contratto, allora lo contrassegneranno override
. In entrambi i casi, il codice si comporterà correttamente chiamando il metodo corretto basato sul contratto . Come può il tuo codice decidere se SpaceShip.accelerate()
è effettivamente prioritario Vehicle.accelerate()
o se si tratta di una collisione di nomi? (Vedi il mio esempio sopra).
Tuttavia, nel caso di eredità implicita, anche se SpaceShip.accelerate()
non a mantenere il contratto di Vehicle.accelerate()
, la chiamata al metodo sarebbe ancora andare SpaceShip.accelerate()
.