Perché C # è stato realizzato con parole chiave "nuove" e "virtual + override" a differenza di Java?


61

In Java non ci sono virtual, new, overrideparole chiave per definizione di metodo. Quindi il funzionamento di un metodo è facile da capire. Perché se DerivedClass estende BaseClass e ha un metodo con lo stesso nome e la stessa firma del BaseClass poi il primario si svolgerà a run-time il polimorfismo (a condizione che il metodo non è static).

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

Ora vengono a C # non ci può essere tanta confusione e difficile da capire come il newo virtual+deriveo nuova + virtuale esclusione sta lavorando.

Non riesco a capire perché nel mondo aggiungerò un metodo DerivedClasscon il mio stesso nome e la stessa firma BaseClasse definirò un nuovo comportamento, ma al polimorfismo di runtime il BaseClassmetodo verrà invocato! (che non è prioritario ma logicamente dovrebbe essere).

Nel caso in cui virtual + overridel'implementazione logica sia corretta, ma il programmatore deve pensare a quale metodo dovrebbe autorizzare l'utente a sovrascrivere al momento della codifica. Che ha qualche pro-con (non andiamo lì adesso).

Quindi perché in C # ci sono così tanti spazi per ragionamenti e confusione non logici. Quindi posso riformulare la mia domanda come in quale contesto del mondo reale dovrei pensare di usare virtual + overrideinvece di newe anche usare newinvece di virtual + override?


Dopo alcune ottime risposte soprattutto da Omar , ho capito che i progettisti di C # hanno sottolineato di più sui programmatori che dovrebbero pensare prima di fare un metodo, che è buono e gestisce alcuni errori da principiante da Java.

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

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

e poi io faccio nuova classe SpaceShipderivata dalla Vehicle. Quindi voglio cambiare tutto carin un SpaceShipoggetto, devo solo cambiare una singola riga di codice

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

Ciò non interromperà la mia logica in nessun punto del codice.

Ma nel caso di C # se SpaceShipnon si sostituisce la Vehicleclasse ' acceleratee si usa, newallora la logica del mio codice sarà rotta. Non è uno svantaggio?


77
Sei solo abituato al modo in cui lo fa Java, e semplicemente non hai avuto il tempo di capire le parole chiave C # alle loro condizioni. Lavoro con C #, ho compreso immediatamente i termini e trovo strano il modo in cui Java lo fa.
Robert Harvey,

12
IIRC, C # ha fatto questo per "favorire la chiarezza". Se devi dire esplicitamente "nuovo" o "override", ciò rende chiaramente e immediatamente evidente ciò che sta accadendo, piuttosto che doverti cercare di capire se il metodo ha la precedenza su un comportamento in una classe base o meno. Trovo anche molto utile poter specificare quali metodi voglio specificare come virtuali e quali non lo faccio. (Java lo fa con final; è esattamente il contrario).
Robert Harvey,

15
"In Java non ci sono parole chiave virtuali, nuove, di sostituzione per la definizione del metodo." Tranne che esiste @Override.
svick,


3
Le annotazioni e le parole chiave @svick non sono la stessa cosa :)
Anirban Nag 'tintinmj'

Risposte:


86

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 newe overridein 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 Vehicleclasse non aveva alcun metodo PerformEngineCheck().
  • Nella mia Carclasse, 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 Vehiclenon vedrà Car.PerformEngineCheck()chiamato, ma il codice a cui si fa riferimento Carcontinuerà 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 virtualparola chiave), sia sulla classe derivata (tramite le parole chiave newe 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 newdovrebbe 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 SpaceShipsta effettivamente sostituendo Vehicle.accelerate()o se è diverso? Deve essere lo SpaceShipsviluppatore. Quindi se lo SpaceShipsviluppatore 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().


12
Il punto di prestazione è ormai completamente obsoleto. Per una prova vedi il mio benchmark che mostra che l'accesso a un campo tramite un metodo non definitivo ma mai sovraccarico richiede un solo ciclo.
maaartinus,

7
Certo, potrebbe essere così. La domanda era che quando C # decise di farlo, perché lo fece in quel momento, e quindi questa risposta è valida. Se la domanda è se ha ancora senso, questa è una discussione diversa, IMHO.
Omer Iqbal,

1
Sono pienamente d'accordo con te.
maaartinus,

2
IMHO, mentre ci sono sicuramente usi per avere funzioni non virtuali, il rischio di insidie ​​inattese si verifica quando qualcosa che non dovrebbe scavalcare un metodo di classe base o implementare un'interfaccia, lo fa.
supercat

3
@Nilzor: Da qui l'argomento accademico vs. pragmatico. Pragmaticamente, la responsabilità di rompere qualcosa risiede nell'ultimo changemaker. Se cambi la tua classe base e una classe derivata esistente dipende dal fatto che non cambia, non è questo il loro problema ("loro" potrebbero non esistere più). Quindi le classi di base vengono bloccate nel comportamento delle loro classi derivate, anche se quella classe non avrebbe mai dovuto essere virtuale . E come dici tu, RhinoMock esiste. Sì, se hai completato correttamente tutti i metodi, tutto è equivalente. Qui indichiamo ogni sistema Java mai costruito.
deworde,

94

È stato fatto perché è la cosa giusta da fare. Il fatto è che consentire di ignorare tutti i metodi è sbagliato; porta al fragile problema della classe base, in cui non hai modo di dire se una modifica alla classe base romperà le sottoclassi. Pertanto, è necessario inserire nella blacklist i metodi che non devono essere sostituiti o inserire nella whitelist quelli a cui è consentito eseguire l'override. Dei due, la whitelisting non è solo più sicura (poiché non è possibile creare accidentalmente una classe base fragile ), ma richiede anche meno lavoro poiché si dovrebbe evitare l'eredità a favore della composizione.


7
Permettere che qualsiasi metodo arbitrario venga ignorato è sbagliato. Dovresti essere in grado di ignorare solo quei metodi che il progettista della superclasse ha designato come sicuri per l'override.
Doval,

21
Eric Lippert ne discute in dettaglio nel suo post, Virtual Methods e Brittle Base Classes .
Brian,

12
Java ha un finalmodificatore, quindi qual è il problema?
Sarge Borsch,

13
@corsiKa "Gli oggetti dovrebbero essere aperti all'estensione" - perché? Se lo sviluppatore della classe base non ha mai pensato all'ereditarietà, è quasi garantito che ci siano sottili dipendenze e ipotesi nel codice che porteranno a bug (ricordi HashMap?). O progettare per eredità e rendere chiaro il contratto o no e assicurarsi che nessuno possa ereditare la classe. @Overrideè stato aggiunto come cerotto perché ormai era troppo tardi per cambiare il comportamento, ma anche gli sviluppatori Java originali (in particolare Josh Bloch) concordano sul fatto che questa è stata una decisione sbagliata.
Voo,

9
@jwenting "Opinione" è una parola divertente; alla gente piace collegarlo ai fatti in modo da poterli ignorare. Ma per qualunque cosa valga è anche l '"opinione" di Joshua Bloch (vedi: Java effettivo , punto 17 ), l' "opinione" di James Gosling (vedi questa intervista ), e come sottolineato nella risposta di Omer Iqbal , è anche l '"opinione" di Anders Hejlsberg. (E anche se non si è mai rivolto all'opt-in vs opt-out, Eric Lippert concorda chiaramente che anche l'eredità è pericolosa.) Quindi a chi si riferisce?
Doval,

33

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

Detto questo, perché questo in primo luogo? Per la stessa ragione per cui C # dispone public, internal(anche "niente"), protected, protected internal, e private, ma Java ha solo public, protectednulla, e private. Fornisce un controllo più preciso del comportamento di ciò che stai codificando, a scapito di avere più termini e parole chiave da tenere traccia.

Nel caso di newvs. virtual+override, va qualcosa del genere:

  • Se si desidera forzare le sottoclassi per implementare il metodo, utilizzare abstracte overridenella sottoclasse.
  • Se si desidera fornire funzionalità ma consentire alla sottoclasse di sostituirla, utilizzare virtuale overridenella sottoclasse.
  • Se si desidera fornire funzionalità che le sottoclassi non debbano mai avere la precedenza, non utilizzare nulla.
    • Se poi si dispone di un caso speciale sottoclasse che non ha bisogno di comportarsi in modo diverso, utilizzare newnella sottoclasse.
    • Se vuoi assicurarti che nessuna sottoclasse possa mai sovrascrivere il comportamento, usa sealednella classe base.

Per un esempio reale: un progetto su cui ho lavorato su ordini di e-commerce elaborati da molte fonti diverse. C'era una base OrderProcessorche aveva la maggior parte della logica, con alcuni abstract/ virtualmetodi per sovrascrivere la classe figlio di ogni sorgente. Questo ha funzionato bene, fino a quando non abbiamo ottenuto una nuova fonte che aveva un modo completamente diverso di elaborare gli ordini, in modo tale che abbiamo dovuto sostituire una funzione principale. Abbiamo avuto due scelte a questo punto: 1) Aggiungi virtualal metodo base e overridenel bambino; o 2) Aggiungi newal bambino.

Mentre uno dei due potrebbe funzionare, il primo renderebbe molto semplice l'override di quel particolare metodo in futuro. Ad esempio, verrebbe visualizzato in completamento automatico. Questo è stato un caso eccezionale, quindi abbiamo scelto di usarlo new. Ciò ha preservato lo standard di "questo metodo non ha bisogno di essere ignorato", consentendo nel contempo il caso speciale in cui lo ha fatto. È una differenza semantica che semplifica la vita.

Faccio notare, tuttavia, che v'è una differenza di comportamento associato con questo, non solo una differenza semantica. Vedi questo articolo per i dettagli. Tuttavia, non ho mai incontrato una situazione in cui dovevo approfittare di questo comportamento.


7
Penso che usare intenzionalmente in newquesto modo sia un bug in attesa di accadere. Se chiamo un metodo su qualche istanza, mi aspetto che faccia sempre la stessa cosa, anche se lancio l'istanza nella sua classe base. Ma non è così newche si comportano i metodi ed.
svick,

@svick - Potenzialmente, sì. Nel nostro scenario particolare, ciò non accadrebbe mai, ma è per questo che ho aggiunto l'avvertimento.
Bobson,

Un uso eccellente di newè in WebViewPage <TModel> nel framework MVC. Ma sono stato anche lanciato da un bug che coinvolge newper ore, quindi non penso che sia una domanda irragionevole.
pdr,

1
@ C.Champagne: qualsiasi strumento può essere usato male e questo è uno strumento particolarmente affilato: puoi tagliarti facilmente. Ma questo non è un motivo per rimuovere lo strumento dalla casella degli strumenti e rimuovere un'opzione da un designer API più talentuoso.
pdr,

1
@svick Correct, motivo per cui normalmente è un avviso del compilatore. Ma avere la capacità di "nuovo" copre le condizioni limite (come quella data), e ancora meglio, rende davvero ovvio che stai facendo qualcosa di strano quando vieni a diagnosticare l'inevitabile bug. "Perché questa classe e solo questa buggy di classe ... ah-ah, un" nuovo ", andiamo a provare dove viene usata la SuperClass".
deworde,

7

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

Ciò semplifica il runtime, ma può causare alcuni problemi sfavorevoli. Supponiamo che GrafBasenon si attui void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), ma lo GrafDerivedimplementa come un metodo pubblico che disegna un parallelogramma il cui quarto punto calcolato è opposto al primo. Supponiamo inoltre che una versione successiva di GrafBaseimplementi un metodo pubblico con la stessa firma, ma il suo quarto punto calcolato di fronte al secondo. I clienti che ricevono si aspettano un GrafBasema ricevono un riferimento a GrafDerivedsi aspettano DrawParallelogramdi calcolare il quarto punto nel modo del nuovo GrafBasemetodo, ma i clienti che hanno utilizzato GrafDerived.DrawParallelogramprima della modifica del metodo di base si aspettano il comportamento GrafDerivedimplementato originariamente.

In Java, non ci sarebbe modo per l'autore di GrafDerivedfar coesistere quella classe con i client che usano il nuovo GrafBase.DrawParallelogrammetodo (e potrebbero non essere consapevoli del fatto che GrafDerivedesista) senza interrompere la compatibilità con il codice client esistente che GrafDerived.DrawParallelogramprima lo GrafBasedefiniva. Dal momento che DrawParallelogramnon può dire quale tipo di client lo sta invocando, deve comportarsi in modo identico quando viene invocato da tipi di codice client. Dal momento che i due tipi di codice client hanno aspettative diverse su come dovrebbe comportarsi, non c'è modo di GrafDerivedevitare di violare le aspettative legittime di uno di essi (ovvero infrangere il codice client legittimo).

In C #, se GrafDerivednon viene ricompilato, il runtime supporrà che il codice che invoca il DrawParallelogrammetodo su riferimenti di tipo GrafDerivedsi aspetti il ​​comportamento che ha GrafDerived.DrawParallelogram()avuto l'ultima volta che è stato compilato, ma che il codice che invoca il metodo su riferimenti di tipo GrafBasesarà in attesa GrafBase.DrawParallelogram(il comportamento che è stato aggiunto). Se in GrafDerivedseguito viene ricompilato in presenza del potenziato GrafBase, il compilatore squawk fino a quando il programmatore non specifica se il suo metodo è destinato a essere un sostituto valido per il membro ereditato GrafBaseo se il suo comportamento deve essere legato a riferimenti di tipo GrafDerived, ma non dovrebbe sostituire il comportamento dei riferimenti di tipo GrafBase.

Si potrebbe ragionevolmente sostenere che avere un metodo per GrafDerivedfare qualcosa di diverso da un membro GrafBasecon la stessa firma significherebbe 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 metodi con nomi simili è essenzialmente inevitabile a meno che a nessuno è permesso aggiungere alcun nome che qualcun altro potrebbe aggiungere. La domanda non è se tale duplicazione del nome debba avvenire, ma piuttosto come minimizzare il danno quando lo fa.


2
Penso che ci sia una buona risposta qui, ma si sta perdendo nel muro di testo. Per favore, suddividilo in qualche altro paragrafo.
Bobson,

Che cos'è DerivedGraphics? A class using GrafDerived`?
C.Champagne il

@ C.Champagne: intendevo GrafDerived. Ho iniziato a usare DerivedGraphics, ma ho pensato che fosse un po 'lungo. Anche GrafDerivedè ancora un po 'lungo, ma non sapeva come nominare meglio i tipi di renderer grafici che dovrebbero avere una chiara relazione base / derivata.
supercat

1
@Bobson: meglio?
supercat

6

Situazione standard:

Sei il proprietario di una classe base utilizzata da più progetti. Volete apportare una modifica a detta classe base che si romperà tra 1 e innumerevoli classi derivate, che sono in progetti che forniscono valore nel mondo reale (Un framework fornisce il valore nella migliore delle ipotesi in una rimozione, nessun Essere Umano Reale vuole un Framework, loro vogliono la cosa in esecuzione sul Framework). Buona fortuna affermando ai proprietari occupati delle classi derivate; "beh, devi cambiare, non avresti dovuto scavalcare quel metodo" senza ottenere un rappresentante come "Framework: Delayer of Projects and Causer of Bugs" per le persone che devono approvare le decisioni.

Soprattutto perché, non dichiarandolo non sostituibile, hai implicitamente dichiarato che stavano bene fare ciò che ora impedisce il tuo cambiamento.

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

Risultato finale: il codice della classe base del tuo framework diventa incredibilmente fragile e statico e non puoi davvero apportare le modifiche necessarie per rimanere aggiornato / efficiente. In alternativa, il tuo framework ottiene una reputazione per l'instabilità (le classi derivate continuano a rompersi) e le persone non lo useranno affatto, poiché il motivo principale per usare un framework è rendere la codifica più veloce e più affidabile.

In poche parole, non puoi chiedere ai proprietari di progetti impegnati di ritardare il loro progetto al fine di correggere i bug che stai introducendo e aspettarti qualcosa di meglio di un "andare via" a meno che tu non stia fornendo loro vantaggi significativi, anche se l'originale "errore" era loro, il che è discutibile nella migliore delle ipotesi.

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


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 potrebbe essere ignorato o dimentica di aggiungere virtuale, allora non posso sfruttare il polimorfismo quando estendo 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.


2
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 5 risposte
moscerino del
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.