Perché a differenza di Java, C # è stato creato con parole chiave "nuove" e "virtuali + override"?

56

In Java non ci sono virtual , new , override parole chiave per la definizione del metodo. Quindi il funzionamento di un metodo è facile da capire. Causa se DerivedClass estende BaseClass e ha un metodo con lo stesso nome e la stessa firma di BaseClass , allora l'override avrà luogo al polimorfismo run-time ( a condizione che il metodo non sia static ).

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

Ora vieni in C # ci può essere tanta confusione e difficile capire come new o virtual+derive o nuovo + sovrascrittura virtuale funziona.

Non sono in grado di capire perché nel mondo aggiungerò un metodo nel mio 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 è eccessivo ma logicamente dovrebbe essere).

In caso di virtual + override sebbene l'implementazione logica sia corretta, ma il programmatore deve pensare a quale metodo deve dare il permesso all'utente di eseguire l'override al momento della codifica. Che ha alcuni pro-con (non andiamo lì ora).

Quindi perché in C # ci sono così tanti spazi per un - ragionamento logico e confusione. Posso quindi riformulare la mia domanda come in quale contesto del mondo reale dovrei pensare di usare virtual + override invece di new e anche usare new invece di virtual + override ?

Dopo alcune ottime risposte, specialmente da Omar , ho notato che i progettisti di C # davano più stress sui programmatori dovrebbero pensare prima di fare un metodo, che è buono e gestisce alcuni errori da rookie da Java.

Ora ho una domanda in mente. Come in Java se avessi un codice come

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

e più tardi renderò la nuova classe SpaceShip derivata da Vehicle . Quindi voglio cambiare tutto car in un oggetto SpaceShip Devo solo cambiare una riga di codice

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

Questo non infrangerà la mia logica in nessun punto del codice.

Ma in caso di C # se SpaceShip non sovrascrive la Vehicle class ' accelerate e usa new allora la logica del mio codice sarà interrotta. Non è uno svantaggio?

    
posta Anirban Nag 'tintinmj' 18.06.2014 - 17:20
fonte

6 risposte

80

Dato che hai chiesto perché C # è stato così, è meglio chiedere ai creatori di C #. Anders Hejlsberg, l'architetto principale di C #, ha risposto perché ha scelto di non utilizzare il virtual di default (come in Java) in un colloquio , i frammenti pertinenti sono riportati di seguito.

Ricorda che Java è 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 la usano in modo proattivo. C # obbliga a usare virtuale e nuovo / override per prendere consapevolmente queste decisioni.

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.

L'intervista ha più discussioni su come gli sviluppatori pensano alla progettazione dell'ereditarietà delle classi e su come ciò abbia portato alla loro decisione.

Ora alla seguente domanda:

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).

Questo sarebbe quando una classe derivata vuole dichiarare che non si attiene al contratto della classe base, ma ha un metodo con lo stesso nome. (Per chi non conosce la differenza tra new e override in C #, vedi questo MSDN pagina ).

Uno scenario molto pratico è questo:

  • Hai creato un'API, che ha una classe chiamata Vehicle .
  • Ho iniziato a utilizzare la tua API e ho ricavato Vehicle .
  • La tua classe Vehicle non ha avuto alcun metodo PerformEngineCheck() .
  • Nella mia classe Car , 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 client dipendono dalla mia API e li romperà.
  • Quindi quando ricompilato la tua nuova API, C # mi avvisa di questo problema, ad es.

    Se la base PerformEngineCheck() non era 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() era 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 una decisione esplicita se la mia classe stia effettivamente estendendo il contratto della classe base, o se si tratta di un contratto diverso ma è lo stesso nome.

  • Rendendolo new , non interrompo i miei clienti se la funzionalità del metodo base era diversa dal metodo derivato. Qualsiasi codice che fa riferimento a Vehicle non vedrà chiamato Car.PerformEngineCheck() , ma il codice con un riferimento a Car continuerà a visualizzare la stessa funzionalità che avevo offerto in PerformEngineCheck() .

Un esempio simile è quando un altro metodo nella classe base potrebbe chiamare PerformEngineCheck() (specialmente nella versione più recente), come si impedisce di chiamare il PerformEngineCheck() della classe derivata? In Java, tale decisione si baserà sulla classe base, ma non conosce nulla sulla classe derivata. In C #, tale decisione riposa entrambi sulla classe base (tramite la parola chiave virtual ), e sulla classe derivata (tramite le parole chiave new e override ).

Ovviamente, gli errori che genera il compilatore forniscono anche uno strumento utile affinché i programmatori non commettano errori in modo imprevisto (ovvero eseguono l'override o forniscono nuove funzionalità senza rendersene conto.)

Come ha detto Anders, il mondo reale ci costringe a risolvere tali problemi che, se dovessimo ripartire da zero, non vorremmo mai entrare.

EDIT: Aggiunto un esempio di dove new dovrebbe essere usato per garantire la compatibilità dell'interfaccia.

MODIFICA: passando attraverso i commenti, ho trovato anche un write-up di Eric Lippert (quindi uno dei membri del comitato di progettazione C #) su altri scenari di esempio (citato da Brian).

PARTE 2: basata sulla domanda aggiornata

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?

Chi decide se SpaceShip sta effettivamente sovrascrivendo Vehicle.accelerate() o se è diverso? Deve essere lo sviluppatore SpaceShip . Quindi se lo sviluppatore SpaceShip decide che non mantengono il contratto della classe base, la tua chiamata a Vehicle.accelerate() non dovrebbe andare a SpaceShip.accelerate() , o dovrebbe? Questo è il momento in cui lo contrassegneranno come new . Tuttavia, se decidono che effettivamente mantiene il contratto, allora lo contrassegneranno infatti override . In entrambi i casi, il tuo codice si comporterà correttamente chiamando il metodo corretto in base al contratto . Come può il tuo codice decidere se SpaceShip.accelerate() stia effettivamente sovrascrivendo Vehicle.accelerate() o se si tratta di una collisione di nomi? (Vedi il mio esempio sopra).

Tuttavia, nel caso dell'ereditarietà implicita, anche se SpaceShip.accelerate() non ha mantenuto il contratto di Vehicle.accelerate() , la chiamata al metodo andrebbe comunque a SpaceShip.accelerate() .

    
risposta data 18.06.2014 - 21:47
fonte
92

È stato fatto perché è la cosa giusta da fare. Il fatto è che permettere di sovrascrivere i metodi tutti è sbagliato; porta al fragile problema della classe base, in cui non si ha modo di dire se una modifica alla classe base interromperà le sottoclassi. Pertanto è necessario inserire nella blacklist i metodi che non dovrebbero essere sovrascritti o inserire nella whitelist quelli che possono essere sovrascritti. Tra i due, la whitelisting non è solo più sicura (dato che non è possibile creare una classe base fragile accidentalmente ), richiede anche meno lavoro dal momento che si dovrebbe evitare l'ereditarietà a favore della composizione.

    
risposta data 18.06.2014 - 17:39
fonte
31

Come ha detto Robert Harvey, è tutto ciò a cui sei abituato. Trovo la mancanza di Java di questa flessibilità dispari.

Detto questo, perché avere questo in primo luogo? Per lo stesso motivo per cui C # ha public , internal (anche "nulla"), protected , protected internal e private , ma Java ha appena public , protected , niente e% codice%. Fornisce un controllo dei grani più fine sul comportamento di ciò che stai codificando, a scapito di avere più termini e parole chiave da seguire.

Nel caso di private rispetto a new , va qualcosa del tipo:

  • Se vuoi forzare le sottoclassi per implementare il metodo, usa virtual+override e abstract nella sottoclasse.
  • Se vuoi fornire funzionalità ma consenti alla sottoclasse di sostituirla, usa override e virtual nella sottoclasse.
  • Se vuoi fornire funzionalità che le sottoclassi non dovrebbero mai sovrascrivere, non usare nulla.
    • Se hai una sottoclasse di casi speciali che fa devono comportarsi diversamente, usa override nella sottoclasse.
    • Se vuoi assicurarti che nessuna sottoclasse possa mai sovrascrivere il comportamento, usa new nella classe base.

Per un esempio del mondo reale: un progetto ho lavorato su ordini di commercio elettronico elaborati da molte fonti diverse. C'era una base sealed che aveva la maggior parte della logica, con alcuni metodi OrderProcessor / abstract per la classe secondaria di ogni sorgente da sovrascrivere. Questo ha funzionato bene, fino a quando non abbiamo ottenuto una nuova fonte che avesse un completamente modo diverso di elaborare gli ordini, così che abbiamo dovuto sostituire una funzione principale. Avevamo due scelte a questo punto: 1) Aggiungi virtual al metodo base e virtual nel bambino; oppure 2) Aggiungi override al bambino.

Mentre uno potrebbe funzionare, il primo renderebbe molto semplice sovrascrivere quel particolare metodo in futuro. Ad esempio, verrebbe visualizzato nel completamento automatico. Questo è stato un caso eccezionale, tuttavia, quindi abbiamo scelto di utilizzare new . Questo ha preservato lo standard di "questo metodo non ha bisogno di essere ignorato", pur tenendo conto del caso speciale in cui è stato fatto. È una differenza semantica che semplifica la vita.

Notate, tuttavia, che è una differenza di comportamento associata a questo, non solo una differenza semantica. Vedi questo articolo per i dettagli. Tuttavia, non mi sono mai imbattuto in una situazione in cui avevo bisogno di sfruttare questo comportamento.

    
risposta data 18.06.2014 - 17:42
fonte
6

La progettazione di Java è tale che, dato ogni riferimento a un oggetto, una chiamata a un particolare nome di metodo con particolari tipi di parametri, se è consentita, invocherà sempre lo stesso metodo. È possibile che le conversioni implicite del tipo di parametro possano essere influenzate dal tipo di riferimento, ma una volta che tutte queste conversioni sono state risolte, il tipo di riferimento è irrilevante.

Questo semplifica il runtime, ma può causare alcuni problemi sfortunati. Supponiamo che GrafBase non implementi void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3) , ma GrafDerived lo implementa come metodo pubblico che disegna un parallelogramma il cui quarto punto calcolato è opposto al primo. Supponiamo inoltre che una versione successiva di GrafBase implementa un metodo pubblico con la stessa firma, ma il suo quarto punto calcolato opposto al secondo. I client che ricevono si aspettano GrafBase ma ricevono un riferimento a GrafDerived aspettano DrawParallelogram per calcolare il quarto punto nel modo del nuovo metodo GrafBase , ma i clienti che hanno utilizzato GrafDerived.DrawParallelogram prima della base il metodo è stato modificato prevediamo il comportamento che GrafDerived ha originariamente implementato.

In Java, non ci sarebbe alcun modo per l'autore di GrafDerived di far coesistere quella classe con i client che usano il nuovo metodo GrafBase.DrawParallelogram (e potrebbe non sapere che GrafDerived esiste anche) senza rompere la compatibilità con esistente codice cliente che ha utilizzato GrafDerived.DrawParallelogram prima di GrafBase definito. Poiché DrawParallelogram non è in grado di stabilire quale tipo di client lo sta invocando, deve comportarsi in modo identico quando invocato da tipi di codice client. Poiché i due tipi di codice cliente hanno aspettative diverse su come dovrebbe comportarsi, non c'è modo in cui GrafDerived possa evitare di violare le legittime aspettative di uno di essi (cioè di violare il codice cliente legittimo).

In C #, se GrafDerived non viene ricompilata, il runtime presuppone che il codice che richiama il metodo DrawParallelogram sui riferimenti di tipo GrafDerived si aspetta il comportamento GrafDerived.DrawParallelogram() avvenuto quando è stato compilato l'ultima volta, ma il codice che invoca il metodo su riferimenti di tipo GrafBase si aspetta GrafBase.DrawParallelogram (il comportamento che è stato aggiunto). Se GrafDerived viene in seguito ricompilato in presenza del GrafBase migliorato, il compilatore squigherà fino a quando il programmatore non specificherà se il suo metodo è destinato a essere un sostituto valido per il membro ereditato GrafBase , o se il suo comportamento deve essere legato ai riferimenti di tipo GrafDerived , ma non dovrebbe sostituire il comportamento dei riferimenti di tipo GrafBase .

Si potrebbe ragionevolmente sostenere che avere un metodo di GrafDerived fare qualcosa di diverso da un membro di GrafBase che ha la stessa firma indicherebbe un cattivo design, e come tale non dovrebbe essere supportato. Sfortunatamente, poiché l'autore di un tipo di base non ha modo di sapere quali metodi potrebbero essere aggiunti ai tipi derivati, né viceversa, la situazione in cui i client di classe base e classe derivata hanno aspettative diverse per i metodi con nome simile è essenzialmente inevitabile a meno nessuno ha il permesso di aggiungere un nome che qualcun altro potrebbe aggiungere. La questione non è se una tale duplicazione di nomi debba accadere, ma piuttosto come minimizzare il danno quando lo fa.

    
risposta data 18.06.2014 - 18:25
fonte
5

Situazione standard:

Sei il proprietario di una classe base che viene utilizzata da più progetti. Si desidera apportare una modifica alla suddetta classe di base che si interromperà tra 1 e innumerevoli classi derivate, che sono in progetti che forniscono valore reale (un framework fornisce il valore nel migliore dei casi a una rimozione, nessun Essere Umano Reale vuole un Framework, vogliono la cosa che corre sul Framework). Buona fortuna che dichiara ai proprietari occupati delle classi derivate; "beh, devi cambiare, non dovresti aver scavalcato quel metodo" senza ottenere un rappresentante come "Framework: Delayer of Projects and Causer of Bugs" alle persone che devono approvare le decisioni.

Tanto più che, non dichiarandolo non superabile, hai implicitamente dichiarato che erano a posto per fare ciò che ora impedisce il tuo cambiamento.

E se non hai un numero significativo di classi derivate che forniscono valore reale superando la tua classe base, perché è una classe base in primo luogo? La speranza è un potente motivatore, ma anche un ottimo modo per finire con il codice senza riferimento.

Risultato finale: il codice classe del framework base diventa incredibilmente fragile e statico e non è possibile apportare le modifiche necessarie per rimanere aggiornati / efficienti. In alternativa, il tuo framework riceve un rep per l'instabilità (le classi derivate continuano a rompersi) e la gente non la usa affatto, poiché la ragione principale per usare un framework è rendere la codifica più veloce e più affidabile.

In parole povere, non puoi chiedere a proprietari di progetti indaffarati di ritardare il loro progetto per correggere bug che stai introducendo, e aspettarti qualcosa di meglio di un "andare via" a meno che tu non stia fornendo vantaggi significativi a loro, anche se l'originale "colpa" era loro, che è al massimo discutibile.

Meglio non lasciare che facciano la cosa sbagliata in primo luogo, che è dove entra in gioco "non-virtuale di default". E quando qualcuno viene da te con una ragione molto chiara per cui hanno bisogno che questo particolare metodo sia superabile e perché dovrebbe essere sicuro, puoi "sbloccarlo" senza rischiare di infrangere il codice di qualcun altro.

    
risposta data 20.06.2014 - 14:16
fonte
0

L'impostazione predefinita su non virtuale presuppone che lo sviluppatore della classe base sia perfetto. Nella mia esperienza gli sviluppatori non sono perfetti. Se lo sviluppatore di una classe base non può immaginare un caso d'uso in cui un metodo può essere sovrascritto o si dimentica di aggiungere virtual, non posso sfruttare il polimorfismo quando si estende la classe base senza modificare la classe base. Nel mondo reale la modifica della classe base spesso non è un'opzione.

In C # lo sviluppatore della classe base non si fida dello sviluppatore della sottoclasse. In Java lo sviluppatore della sottoclasse non si fida dello sviluppatore della classe base. Lo sviluppatore della sottoclasse è responsabile della sottoclasse e dovrebbe (imho) avere il potere di estendere la classe base come meglio crede (escludendo la negazione esplicita, e in java possono anche sbagliare).

È una proprietà fondamentale della definizione del linguaggio. Non è giusto o sbagliato, è quello che è e non può cambiare.

    
risposta data 09.11.2014 - 06:32
fonte

Leggi altre domande sui tag