"Parent x = new Child ();" invece di "Child x = new Child ();" è una cattiva pratica se possiamo usare quest'ultimo?


32

Ad esempio, avevo visto alcuni codici che creano un frammento come questo:

Fragment myFragment=new MyFragment();

che dichiara una variabile come frammento anziché MyFragment, che MyFragment è una classe figlio di frammento. Non sono soddisfatto di questa linea di codici perché penso che questo codice dovrebbe essere:

MyFragment myFragment=new MyFragment();

che è più specifico, è vero?

O in generalizzazione della domanda, è una cattiva pratica usare:

Parent x=new Child();

invece di

Child x=new Child();

se riusciamo a cambiare il primo in quest'ultimo senza errori di compilazione?


6
Se facciamo quest'ultimo, non ha più senso l'astrazione / generalizzazione. Ovviamente ci sono delle eccezioni ...
Walfrat,

2
In un progetto a cui sto lavorando, le mie funzioni prevedono il tipo parent ed eseguono metodi di tipo parent. Posso accettare uno qualsiasi dei vari figli e il codice dietro quei metodi (ereditati e sovrascritti) è diverso, ma voglio eseguire quel codice indipendentemente da quale figlio sto usando.
Stephen S,

4
@Walfrat In effetti c'è poco senso nell'astrazione quando si sta già istanziando il tipo concreto, ma è ancora possibile restituire il tipo astratto in modo che il codice client sia beatamente ignorante.
Jacob Raihle,

4
Non usare Genitore / Figlio. Usa interfaccia / classe (per Java).
Thorbjørn Ravn Andersen,

4
Google "programmare su un'interfaccia" per una serie di spiegazioni sul perché l'utilizzo Parent x = new Child()è una buona idea.
Kevin Workman,

Risposte:


69

Dipende dal contesto, ma direi che dovresti dichiarare il tipo più astratto possibile. In questo modo il tuo codice sarà il più generale possibile e non dipenderà da dettagli irrilevanti.

Un esempio sarebbe avere un LinkedListe da ArrayListcui entrambi discendono List. Se il codice funzionasse ugualmente bene con qualsiasi tipo di elenco, non vi è motivo di limitarlo arbitrariamente a una delle sottoclassi.


24
Esattamente. Per aggiungere un esempio, pensate: al List list = new ArrayList()codice che utilizza l'elenco non interessa il tipo di elenco, solo che soddisfa il contratto dell'interfaccia Elenco. In futuro, potremmo facilmente passare a un'implementazione dell'elenco diversa e il codice sottostante non dovrebbe essere modificato o aggiornato.
Maybe_Factor

19
una buona risposta, ma dovrebbe ampliare il fatto che il tipo più astratto possibile significa che il codice nell'ambito non deve essere trasmesso al figlio per eseguire alcune operazioni specifiche del figlio.
Mike,

10
@Mike: spero che sia ovvio, altrimenti tutte le variabili, i parametri e i campi sarebbero dichiarati come Object!
JacquesB,

3
@JacquesB: poiché questa domanda e risposta ha ricevuto così tanta attenzione pensavo che essere espliciti sarebbe stato utile per le persone con tutti i diversi livelli di abilità.
Mike,

1
Dipende dal contesto non è una risposta.
Billal Begueradj,

11

La risposta di JacquesB è corretta, anche se un po 'astratta. Vorrei sottolineare alcuni aspetti più chiaramente:

Nel tuo frammento di codice, usi esplicitamente la newparola chiave, e forse vorresti continuare con ulteriori passaggi di inizializzazione che sono probabilmente specifici per il tipo concreto: quindi devi dichiarare la variabile con quel tipo specifico concreto.

La situazione è diversa quando si ottiene quell'elemento da qualche altra parte, ad es IFragment fragment = SomeFactory.GiveMeFragments(...);. Qui, la fabbrica dovrebbe normalmente restituire il tipo più astratto possibile e il codice verso il basso non dovrebbe dipendere dai dettagli di implementazione.


18
"sebbene un po 'astratto". Badumtish.
Rosuav,

1
non poteva essere stata una modifica?
beppe9000,

6

Ci sono potenzialmente differenze di prestazioni.

Se la classe derivata viene sigillata, il compilatore può incorporare, ottimizzare o eliminare quelle che altrimenti sarebbero chiamate virtuali. Quindi può esserci una differenza significativa nelle prestazioni.

Esempio: ToStringè una chiamata virtuale, ma Stringè sigillata. Sì String, ToStringè un no-op, quindi se dichiari objectche è una chiamata virtuale, se dichiari un Stringcompilatore sai che nessuna classe derivata ha ignorato il metodo perché la classe è sigillata, quindi è una no-op. Considerazioni analoghe valgono per ArrayListvs LinkedList.

Pertanto, se si conosce il tipo concreto dell'oggetto (e non esiste alcun motivo di incapsulamento per nasconderlo), è necessario dichiararlo come tale. Dato che hai appena creato l'oggetto, conosci il tipo concreto.


4
Nella maggior parte dei casi, le differenze di prestazioni sono minuscole. E nella maggior parte dei casi (ho sentito dire che questo è circa il 90% delle volte) dovremmo dimenticare quelle piccole differenze. Solo quando sappiamo (preferibilmente tramite la misurazione) che invecchiamo come sezione di codice critica per le prestazioni, dovremmo preoccuparci di tali ottimizzazioni.
Jules,

E il compilatore sa che "new Child ()" restituisce un Child, anche se assegnato a un Parent, e fintanto che lo sa, può usare quella conoscenza e chiamare il codice Child ().
gnasher729,

+1. Ho anche rubato il tuo esempio nella mia risposta. = P
Nat

1
Nota che ci sono ottimizzazioni delle prestazioni che rendono il tutto discutibile. HotSpot, ad esempio, noterà che un parametro passato a una funzione appartiene sempre a una classe specifica e si ottimizza di conseguenza. Se tale presupposto risulta essere falso, il metodo viene de-ottimizzato. Ma questo dipende fortemente dall'ambiente di compilazione / runtime. L'ultima volta che ho controllato il CLR non è nemmeno riuscito a fare l'ottimizzazione piuttosto banale di notare che p da Parent p = new Child()è sempre un bambino ..
Voo

4

La differenza fondamentale è il livello di accesso richiesto. E ogni buona risposta sull'astrazione coinvolge gli animali, o almeno così mi viene detto.

Supponiamo che tu abbia alcuni animali - Cat Birde Dog. Ora, questi animali hanno alcuni comportamenti comuni - move(), eat()e speak(). Mangiano tutti in modo diverso e parlano in modo diverso, ma se ho bisogno che il mio animale mangi o parli non mi interessa davvero come lo fanno.

Ma a volte lo faccio. Non ho mai provato un gatto da guardia o un uccello da guardia, ma ho un cane da guardia. Quindi, quando qualcuno entra in casa mia, non posso fare affidamento solo sul parlare per spaventare l'intruso. Ho davvero bisogno che il mio cane abbaia - questo è diverso dal suo parlare, che è solo una trama.

Quindi, nel codice che richiede un intruso, devo davvero farlo Dog fido = getDog(); fido.barkMenancingly();

Ma la maggior parte delle volte, posso tranquillamente fare in modo che qualsiasi animale faccia dei trucchi in cui tessono, miagolano o twittano per i dolcetti. Animal pet = getAnimal(); pet.speak();

Quindi, per essere più concreti, ArrayList ha alcuni metodi che Listnon lo fanno. In particolare, trimToSize(). Ora, nel 99% dei casi, non ci interessa. Non Listè quasi mai abbastanza grande per noi. Ma ci sono volte in cui lo è. Di tanto in tanto, dobbiamo specificamente chiedere l' ArrayListesecuzione trimToSize()e rendere più piccolo il suo array di supporto.

Si noti che questo non deve essere fatto durante la costruzione - potrebbe essere fatto tramite un metodo di ritorno o addirittura un cast se ne siamo assolutamente sicuri.


Confuso perché questo potrebbe ottenere un downvote. Sembra canonico, personalmente. Tranne l'idea di un cane da guardia programmabile ...
corsiKa

Perché ho dovuto leggere le pagine per ottenere la risposta migliore? Il voto fa schifo.
Basilevs,

Grazie per le gentili parole. Ci sono pro e contro nei sistemi di classificazione, e uno di questi è che le risposte successive a volte possono rimanere nella polvere. Succede!
corsiKa

2

In generale, dovresti usare

var x = new Child(); // C#

o

auto x = new Child(); // C++

per le variabili locali a meno che non vi sia una buona ragione per cui la variabile abbia un tipo diverso da quello con cui è inizializzata.

(Qui sto ignorando il fatto che il codice C ++ dovrebbe probabilmente utilizzare un puntatore intelligente. Ci sono casi in cui non è possibile conoscere e senza più di una riga.)

L'idea generale qui è con linguaggi che supportano il rilevamento automatico del tipo di variabili, usandolo rende il codice più facile da leggere e fa la cosa giusta (cioè, il sistema di tipi del compilatore può ottimizzare il più possibile e cambiando l'inizializzazione in una nuova funziona anche se fosse stata usata la maggior parte dell'estratto).


1
In C ++, le variabili sono per lo più creati senza la nuova parola chiave: stackoverflow.com/questions/6500313/...
Marian Spanik

1
La domanda non riguarda C # / C ++ ma un problema generale orientato agli oggetti in un linguaggio che abbia almeno una qualche forma di tipizzazione statica (cioè, sarebbe assurdo chiedere qualcosa come Ruby). Inoltre, anche in quelle due lingue, non vedo davvero una buona ragione per cui dovremmo farlo come dici tu.
AnoE

1

Buono perché è facile da capire e facile da modificare questo codice:

var x = new Child(); 
x.DoSomething();

Buono perché comunica l'intento:

Parent x = new Child(); 
x.DoSomething(); 

Ok, perché è comune e facile da capire:

Child x = new Child(); 
x.DoSomething(); 

L'unica opzione che è veramente male è di usare Parentse Childha solo un metodo DoSomething(). È male perché comunica intenzionalmente in modo errato:

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

Ora approfondiamo un po 'di più sul caso che la domanda specifica pone:

Parent x = new Child(); 
x.DoSomething(); 

Formattiamolo in modo diverso, effettuando una seconda chiamata di funzione:

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

Questo può essere abbreviato in:

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

Qui, suppongo che sia ampiamente accettato che WhichTypedovrebbe essere il tipo più semplice / astratto che consente alla funzione di funzionare (a meno che non ci siano problemi di prestazioni). In questo caso sarebbe il tipo corretto Parent, o qualcosa Parentda cui deriva.

Questo ragionamento spiega perché usare il tipo Parentsia una buona scelta, ma non va in malora le altre scelte sono cattive (non lo sono).


1

tl; dr - L' uso diChildoverParentè preferibile nell'ambito locale. Non solo aiuta con la leggibilità, ma è anche necessario garantire che la risoluzione del metodo sovraccarico funzioni correttamente e aiuti a consentire una compilazione efficiente.


Nell'ambito locale,

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

Concettualmente, si tratta di mantenere il maggior numero possibile di informazioni. Se eseguiamo il downgrade a Parent, stiamo essenzialmente eliminando le informazioni sul tipo che avrebbero potuto essere utili.

La conservazione delle informazioni complete sul tipo presenta quattro vantaggi principali:

  1. Fornisce ulteriori informazioni al compilatore.
  2. Fornisce ulteriori informazioni al lettore.
  3. Codice più pulito e più standardizzato.
  4. Rende la logica del programma più mutevole.

Vantaggio 1: maggiori informazioni al compilatore

Il tipo apparente viene utilizzato nella risoluzione del metodo sovraccaricata e nell'ottimizzazione.

Esempio: risoluzione del metodo sovraccaricata

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

Nell'esempio sopra, entrambe le foo()chiamate funzionano , ma in un caso otteniamo la migliore risoluzione del metodo sovraccarico.

Esempio: ottimizzazione del compilatore

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

Nell'esempio sopra, entrambe le .Foo()chiamate alla fine chiamano lo stesso overridemetodo che restituisce 2. Solo, nel primo caso, esiste una ricerca del metodo virtuale per trovare il metodo corretto; la ricerca di questo metodo virtuale non è necessaria nel secondo caso da quel metodo sealed.

Ringraziamo @Ben che ha fornito un esempio simile nella sua risposta .

Vantaggio 2: maggiori informazioni per il lettore

Conoscere il tipo esatto, ovvero Child, fornisce maggiori informazioni a chiunque stia leggendo il codice, rendendo più semplice vedere cosa sta facendo il programma.

Certo, forse non importa il codice reale poiché entrambi parent.Foo();e child.Foo();hanno senso, ma per qualcuno che vede il codice per la prima volta, ulteriori informazioni sono semplicemente utili.

Inoltre, a seconda dell'ambiente di sviluppo, l'IDE potrebbe essere in grado di fornire tooltip e metadati più utili Childrispetto a Parent.

Vantaggio 3: codice più pulito e più standardizzato

La maggior parte degli esempi di codice C # che ho visto di recente usano var, che è sostanzialmente una scorciatoia per Child.

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

Vedere una vardichiarazione di non dichiarazione sembra appena fuori; se c'è un motivo situazionale per questo, fantastico, ma per il resto sembra anti-schema.

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

Vantaggio 4: logica del programma più mutevole per la prototipazione

I primi tre vantaggi erano quelli grandi; questo è molto più situazionale.

Tuttavia, per le persone che usano il codice come gli altri usano Excel, cambiamo costantemente il nostro codice. Forse non è necessario chiamare un metodo univoco Childin questa versione del codice, ma potremmo riutilizzare o rielaborare il codice in un secondo momento.

Il vantaggio di un sistema di tipo forte è che ci fornisce alcuni metadati sulla logica del nostro programma, rendendo le possibilità più evidenti. Questo è incredibilmente utile nella prototipazione, quindi è meglio tenerlo dove possibile.

Sommario

L'utilizzo di Parentuna risoluzione del metodo sovraccarica compromette, inibisce alcune ottimizzazioni del compilatore, rimuove le informazioni dal lettore e rende il codice più brutto.

L'utilizzo varè davvero la strada da percorrere. È rapido, pulito, strutturato e aiuta il compilatore e l'IDE a svolgere correttamente il proprio lavoro.


Importante : questa risposta riguardaParentvs.Childnell'ambito locale di un metodo. Il problema diParentvs.Childè molto diverso per tipi di ritorno, argomenti e campi di classe.


2
Aggiungo che Parent list = new Child()non ha molto senso. Il codice che crea direttamente l'istanza concreta di un oggetto (in contrapposizione all'indirizzamento indiretto attraverso fabbriche, metodi di fabbrica ecc.) Contiene informazioni perfette sul tipo creato, potrebbe essere necessario interagire con l'interfaccia concreta per configurare l'oggetto appena creato ecc. L'ambito locale non è dove si ottiene la flessibilità. La flessibilità si ottiene quando si espone l'oggetto appena creato a un cliente della propria classe utilizzando un'interfaccia più astratta (fabbriche e metodi di fabbrica sono un esempio perfetto)
Crizzis

"tl; dr L'uso di Child over Parent è preferibile nell'ambito locale. Non solo aiuta con la leggibilità", - non necessariamente ... se non fai uso delle specifiche del bambino, aggiungerà solo più dettagli del necessario. "ma è anche necessario garantire che la risoluzione del metodo sovraccarico funzioni correttamente e che consenta una compilazione efficiente." - dipende molto dal linguaggio di programmazione. Ci sono molti che non hanno alcun problema derivante da questo.
AnoE

1
Avete una fonte che Parent p = new Child(); p.doSomething();e Child c = new Child; c.doSomething();avere un comportamento diverso? Forse è una stranezza in qualche lingua, ma si suppone che l'oggetto controlli il comportamento, non il riferimento. Questa è la bellezza dell'eredità! Finché ci si può fidare delle implementazioni secondarie per adempiere al contratto dell'implementazione padre, si è pronti per partire.
corsiKa

@corsiKa Sì, è possibile in alcuni modi. Due brevi esempi potrebbero essere la sovrascrittura delle firme dei metodi, ad esempio con la newparola chiave in C # e quando ci sono più definizioni, ad esempio con ereditarietà multipla in C ++ .
Nat

@corsiKa Solo un rapido terzo esempio (perché penso che sia un netto contrasto tra C ++ e C #), C # ha un problema come quello di C ++, in cui puoi anche ottenere questo comportamento da metodi a interfaccia esplicita . È divertente perché linguaggi come C # usano interfacce invece dell'ereditarietà multipla in parte per evitare questi problemi, ma adottando le interfacce, hanno ancora ottenuto parte del problema - ma, non è così male perché, in quello scenario, si applica solo a quando Parentè un interface.
Nat

0

Dato parentType foo = new childType1();, il codice sarà limitato all'uso di metodi di tipo genitore su foo, ma sarà in grado di memorizzare in fooun riferimento a qualsiasi oggetto il cui tipo deriva da - parentnon solo istanze di childType1. Se la nuova childType1istanza è l'unica cosa a cui foopotrà mai essere associato un riferimento, allora potrebbe essere meglio dichiarare fooil tipo come childType1. D'altra parte, se foopotrebbe essere necessario contenere riferimenti ad altri tipi, averlo dichiarato come parentTypelo renderà possibile.


0

Suggerisco di usare

Child x = new Child();

invece di

Parent x = new Child();

Quest'ultimo perde informazioni mentre il primo no.

Puoi fare tutto con il primo che puoi fare con il secondo ma non viceversa.


Per elaborare ulteriormente il puntatore, abbiamo più livelli di ereditarietà.

Base-> DerivedL1->DerivedL2

E vuoi inizializzare il risultato di new DerivedL2()una variabile. L'uso di un tipo di base consente di usare Baseo DerivedL1.

Puoi usare

Base x = new DerivedL2();

o

DerivedL1 x = new DerivedL2();

Non vedo alcun modo logico di preferire l'uno all'altro.

Se usi

DerivedL2 x = new DerivedL2();

non c'è nulla di cui discutere.


3
Essere in grado di fare di meno se non è necessario fare di più è una buona cosa, no?
Peter,

1
@Peter, la capacità di fare di meno non è limitata. È sempre possibile. Se la capacità di fare di più è limitata, potrebbe essere un punto dolente.
R Sahu,

Ma perdere informazioni a volte è una buona cosa. Per quanto riguarda la tua gerarchia, molte persone probabilmente voterebbero Base(supponendo che funzioni). Preferisci il più specifico, AFAIK la maggior parte preferisce il meno specifico. Direi che per le variabili locali non importa.
maaartinus,

@maaartinus, sono sorpreso che la maggior parte degli utenti preferisca perdere informazioni. Non vedo i benefici della perdita di informazioni con la stessa chiarezza degli altri.
R Sahu,

Quando dichiari Base x = new DerivedL2();di essere vincolato di più, questo ti consente di scambiare un altro (nipote) bambino con il minimo sforzo. Inoltre, vedi immediatamente che è possibile. +++ Siamo d'accordo che void f(Base)sia meglio di void f(DerivedL2)?
maaartinus,

0

Suggerirei sempre di restituire o archiviare le variabili come il tipo più specifico, ma di accettare i parametri come i tipi più ampi.

Per esempio

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

I parametri sono List<T>, un tipo molto generale. ArrayList<T>, LinkedList<T>E altri potrebbero tutti essere accettate da questo metodo.

È importante sottolineare che il tipo restituito è LinkedHashMap<K, V>no Map<K, V>. Se qualcuno vuole assegnare il risultato di questo metodo a un Map<K, V>, può farlo. Comunica chiaramente che questo risultato è più di una semplice mappa, ma una mappa con un ordine definito.

Supponiamo che questo metodo sia stato restituito Map<K, V>. Se il chiamante voleva un LinkedHashMap<K, V>, dovrebbe fare un controllo del tipo, cast e gestione degli errori, il che è davvero ingombrante.


La stessa logica che ti obbliga ad accettare un tipo generico come parametro dovrebbe applicarsi anche al tipo restituito. Se l'obiettivo della funzione è mappare le chiavi sui valori, dovrebbe restituire un Mapnon a LinkedHashMap- si vorrebbe solo restituire un LinkedHashMapper una funzione simile keyValuePairsToFifoMapo qualcosa del genere. Più ampio è meglio fino a quando non hai un motivo specifico per non farlo.
corsiKa

@corsiKa Hmm Sono d'accordo che sia un nome migliore, ma questo è solo un esempio. Non sono d'accordo sul fatto che (in generale) sia meglio ampliare il tipo di reso. Rimuovere artificialmente le informazioni sul tipo, senza alcuna giustificazione. Se ritieni che il tuo utente dovrà ragionevolmente ridimensionare il tipo generico che hai fornito, allora hai fatto loro un disservizio.
Alexander - Ripristina Monica il

"senza alcuna giustificazione" - ma esiste una giustificazione! Se riesco a cavarmela con la restituzione di un tipo più ampio, questo facilita il refactoring lungo la strada. Se qualcuno HA BISOGNO del tipo specifico a causa di un comportamento su di esso, sono tutto per restituire il tipo appropriato. Ma non voglio essere bloccato in un tipo specifico se non devo con me. Mi piacerebbe essere libero di modificare le specifiche del mio metodo senza danneggiare quelli che consumano, e se cambio da, diciamo LinkedHashMapa TreeMapper qualsiasi motivo, ora ho un sacco di cose da cambiare.
corsiKa

In realtà, sono d'accordo con quello fino all'ultima frase. Sei passato da LinkedHashMapa TreeMapnon è un cambiamento banale, come passare da ArrayLista LinkedList. È un cambiamento di importanza semantica. In tal caso, si desidera che il compilatore generi un errore, per forzare gli utenti del valore di ritorno a notare la modifica e intraprendere le azioni appropriate.
Alexander - Ripristina Monica il

Ma può essere un cambiamento banale se tutto ciò che ti interessa è il comportamento della mappa (che, se puoi, dovresti.) Se il contratto è "otterrai una mappa del valore chiave" e l'ordine non ha importanza (il che è vero per maggior parte delle mappe), non importa se si tratta di un albero o di una mappa hash. Ad esempio, Thread#getAllStackTraces()- restituisce un Map<Thread,StackTraceElement[]>invece di uno HashMapspecificamente così lungo la strada che possono cambiarlo in qualunque tipo di Maploro vogliono.
corsiKa
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.