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

32

Ad esempio, ho 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 figlia di Fragment. Non sono soddisfatto di questa linea di codici perché penso che questo codice dovrebbe essere:

MyFragment myFragment=new MyFragment();

che è più specifico, è vero?

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

Parent x=new Child();

invece di

Child x=new Child();

se possiamo modificare il precedente in quest'ultimo senza errore di compilazione?

    
posta ggrr 01.06.2017 - 05:35
fonte

10 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 potrebbe avere un LinkedList e ArrayList che entrambi discendono da List . Se il codice funzionasse ugualmente bene con qualsiasi tipo di elenco, non c'è motivo di restringerlo arbitrariamente a una delle sottoclassi.

    
risposta data 01.06.2017 - 07:51
fonte
11

La risposta di JacquesB è corretta, anche se un po 'astratta. Permettetemi di enfatizzare alcuni aspetti in modo più chiaro:

Nel tuo snippet di codice, utilizzi esplicitamente la parola chiave new e forse desideri continuare con ulteriori passaggi di inizializzazione che sono probabilmente specifici per il tipo concreto: devi quindi dichiarare la variabile con quel tipo concreto specifico.

La situazione è diversa quando ottieni quell'elemento da qualche altra parte, ad es. %codice%. Qui, la factory dovrebbe normalmente restituire il tipo più astratto possibile, e il codice in discesa non dovrebbe dipendere dai dettagli di implementazione.

    
risposta data 01.06.2017 - 10:15
fonte
6

Ci sono potenzialmente differenze di prestazioni.

Se la classe derivata è sigillata, il compilatore può in linea e ottimizzare o eliminare quelle che altrimenti sarebbero chiamate virtuali. Quindi ci può essere una significativa differenza di prestazioni.

Esempio: ToString è una chiamata virtuale, ma String è chiusa. In String , ToString è un no-op, quindi se dichiari object che è una chiamata virtuale, se dichiari un String il compilatore sa che nessuna classe derivata ha scavalcato il metodo perché la classe è sigillata, quindi è un no-op. Considerazioni simili si applicano a ArrayList rispetto a LinkedList .

Quindi, se conosci il tipo concreto dell'oggetto, (e non c'è alcun motivo di incapsulamento per nasconderlo), devi dichiararlo come quel tipo. Dato che hai appena creato l'oggetto, conosci il tipo concreto.

    
risposta data 01.06.2017 - 12:02
fonte
4

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

Supponiamo che tu abbia pochi animali - Cat Bird e 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 avuto un gatto da guardia o da guardia, ma ho un cane da guardia. Quindi, quando qualcuno irrompe in casa mia, non posso fare affidamento su parlare per spaventare l'intruso. Ho davvero bisogno che il mio cane abbaia - questo è diverso dal suo parlare, che è solo un esempio.

Quindi nel codice che richiede un intruso ho davvero bisogno di fare Dog fido = getDog(); fido.barkMenancingly();

Ma la maggior parte delle volte, posso fare felicemente che alcuni animali facciano trucchetti in cui gettano, miagolano o twittano per le leccornie. Animal pet = getAnimal(); pet.speak();

Quindi per essere più concreti, ArrayList ha alcuni metodi che List non ha. In particolare, trimToSize() . Ora, nel 99% dei casi, non ci interessa questo. Il List non è quasi mai abbastanza grande da farci preoccupare. Ma ci sono momenti in cui lo è. Di tanto in tanto, dobbiamo chiedere specificamente un ArrayList per eseguire trimToSize() e rendere più piccolo il suo array di supporto.

Nota che questo non deve essere fatto al momento della costruzione - potrebbe essere fatto tramite un metodo di ritorno o persino un cast se ne siamo assolutamente sicuri.

    
risposta data 01.06.2017 - 18:22
fonte
2

In generale, dovresti usare

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

o

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

per le variabili locali a meno che non ci sia una buona ragione per la variabile di avere un tipo diverso come con cui è inizializzato.

(Qui sto ignorando il fatto che il codice C ++ dovrebbe molto probabilmente usare un puntatore intelligente. Ci sono casi in cui non si può e non c'è più di una riga che non posso conoscere.)

L'idea generale qui è con linguaggi che supportano il rilevamento automatico del tipo di variabili, il suo uso 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 cambiare l'inizializzazione in essere un nuovo tipo funziona anche se la maggior parte dell'abstract è stata usata).

    
risposta data 01.06.2017 - 19:38
fonte
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 negativa è usare Parent se solo Child ha 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 analizziamo un po 'di più sul caso in cui la domanda si pone specificamente:

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

Formattiamolo in modo diverso, richiamando la seconda metà di una 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, presumo sia ampiamente accettato che WhichType dovrebbe essere il tipo più semplice / astratto che consente alla funzione di funzionare (a meno che non ci siano problemi di prestazioni). In questo caso il tipo corretto dovrebbe essere Parent o qualcosa di Parent da.

Questa linea di ragionamento spiega perché usare il tipo Parent è una buona scelta, ma non va in tempo le altre scelte sono cattive (non lo sono).

    
risposta data 01.06.2017 - 21:23
fonte
1

tl; dr - è preferibile utilizzare Child su Parent nell'ambito locale. Non solo aiuta con la leggibilità, ma è anche necessario assicurarsi che la risoluzione del metodo sovraccarico funzioni correttamente e aiuti a rendere efficiente la compilazione.

Nell'ambito locale,

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

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

Mantenere le informazioni complete sul tipo ha quattro vantaggi principali:

  1. Fornisce ulteriori informazioni al compilatore.
  2. Fornisce ulteriori informazioni al lettore.
  3. Pulitore, codice più standardizzato.
  4. Rende la logica del programma più mutevole.

Vantaggio 1: ulteriori informazioni sul compilatore

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

Esempio: risoluzione del metodo di overload

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 precedente, entrambi foo() chiama lavoro , ma in un caso, otteniamo la migliore risoluzione del metodo in overload.

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 precedente, entrambe le chiamate .Foo() chiamano in definitiva lo stesso metodo override che restituisce 2 . Solo, nel primo caso, esiste una ricerca metodo virtuale per trovare il metodo corretto; questa ricerca del metodo virtuale non è necessaria nel secondo caso poiché sealed di quel metodo.

Credito a @Ben che ha fornito un esempio simile in sua risposta .

Vantaggio 2: ulteriori informazioni per il lettore

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

Certo, forse non importa il codice effettivo poiché sia parent.Foo(); che child.Foo(); hanno senso, ma per qualcuno che vede il codice per la prima volta, più informazioni sono semplicemente utili.

Inoltre, a seconda dell'ambiente di sviluppo, l'IDE può essere in grado di fornire suggerimenti e metadati più utili su Child rispetto a Parent .

Vantaggio 3: più pulito, codice più standardizzato

La maggior parte degli esempi di codice C # che ho visto ultimamente utilizzano var , che in pratica è 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

La visualizzazione di una dichiarazione di dichiarazione non- var non ha effetto; se c'è un motivo situazionale per questo, fantastico, ma per il resto sembra anti-pattern.

// 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 di programma più mutabile per la prototipazione

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

Tuttavia, per le persone che usano il codice come gli altri usano Excel, cambiamo continuamente il nostro codice. Forse non abbiamo bisogno di chiamare un metodo unico per Child nella questa versione del codice, ma potremmo riutilizzare o rielaborare il codice in seguito.

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

Sommario

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

Usare var è davvero la strada da percorrere. È veloce, pulito, con motivi e aiuta il compilatore e l'IDE a fare il loro lavoro correttamente.

Importante : questa risposta riguarda circa Parent rispetto a Child nell'ambito locale di un metodo. Il problema di Parent rispetto a Child è molto diverso per i tipi restituiti, gli argomenti e i campi di classe.

    
risposta data 02.06.2017 - 07:38
fonte
0

Dato parentType foo = new childType1(); , il codice sarà limitato all'utilizzo di metodi di tipo genitore su foo , ma sarà in grado di memorizzare in foo un riferimento a qualsiasi oggetto il cui tipo deriva da parent - non solo istanze di childType1 . Se la nuova istanza childType1 è l'unica cosa a cui foo manterrà mai un riferimento, allora potrebbe essere preferibile dichiarare il tipo foo come childType1 . D'altra parte, se foo potrebbe aver bisogno di mantenere riferimenti ad altri tipi, averlo dichiarato come parentType lo renderà possibile.

    
risposta data 01.06.2017 - 23:16
fonte
0

Ti suggerisco di usare

Child x = new Child();

invece di

Parent x = new Child();

Quest'ultimo perde le informazioni mentre ex non lo fa.

Puoi fare tutto con il primo che puoi fare con quest'ultimo, ma non viceversa.

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

Base - > DerivedL1 - > DerivedL2

E vuoi inizializzare il risultato di new DerivedL2() in una variabile. L'utilizzo di un tipo di base ti lascia la possibilità di utilizzare Base o DerivedL1 .

Puoi usare

Base x = new DerivedL2();

o

DerivedL1 x = new DerivedL2();

Non vedo alcun modo logico per preferire l'uno sull'altro.

Se usi

DerivedL2 x = new DerivedL2();

non c'è nulla di cui discutere.

    
risposta data 01.06.2017 - 23:26
fonte
0

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

per es.

<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 essere tutti accettati con questo metodo.

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

Supponi che questo metodo abbia restituito Map<K, V> . Se il chiamante voleva un LinkedHashMap<K, V> , avrebbe dovuto eseguire un controllo di tipo, il cast e la gestione degli errori, il che è davvero ingombrante.

    
risposta data 02.06.2017 - 00:14
fonte

Leggi altre domande sui tag