it-swarm.com.de

Ist "Parent x = new Child ();" anstelle von "Child x = new Child ();" eine schlechte Praxis, wenn wir die letztere verwenden können?

Zum Beispiel hatte ich einige Codes gesehen, die ein Fragment wie dieses erzeugen:

Fragment myFragment=new MyFragment();

dies deklariert eine Variable als Fragment anstelle von MyFragment, wobei MyFragment eine untergeordnete Klasse von Fragment ist. Ich bin mit dieser Codezeile nicht zufrieden, weil ich denke, dass dieser Code sein sollte:

MyFragment myFragment=new MyFragment();

was ist genauer, ist das wahr?

Oder ist es in der Verallgemeinerung der Frage eine schlechte Praxis, Folgendes zu verwenden:

Parent x=new Child();

anstatt

Child x=new Child();

ob wir das erstere ohne Kompilierungsfehler in das letztere ändern können?

32
ggrr

Es hängt vom Kontext ab, aber ich würde argumentieren, dass Sie den Typ am abstraktesten für möglich erklären sollten. Auf diese Weise wird Ihr Code so allgemein wie möglich und hängt nicht von irrelevanten Details ab.

Ein Beispiel wäre ein LinkedList und ArrayList, die beide von List abstammen. Wenn der Code mit jeder Art von Liste gleich gut funktioniert, gibt es keinen Grund, ihn willkürlich auf eine der Unterklassen zu beschränken.

69
JacquesB

Die Antwort von JacquesB ist richtig, wenn auch etwas abstrakt. Lassen Sie mich einige Aspekte deutlicher hervorheben:

In Ihrem Code-Snippet verwenden Sie explizit das Schlüsselwort new und möchten möglicherweise mit weiteren Initialisierungsschritten fortfahren, die wahrscheinlich spezifisch für den konkreten Typ sind: Dann müssen Sie die Variable mit diesem bestimmten konkreten Typ deklarieren.

Die Situation ist anders, wenn Sie diesen Artikel von einem anderen Ort erhalten, z. IFragment fragment = SomeFactory.GiveMeFragments(...);. Hier sollte die Factory normalerweise den abstraktesten Typ zurückgeben, und der Abwärtscode sollte nicht von Implementierungsdetails abhängen.

11
Bernhard Hiller

Es gibt möglicherweise Leistungsunterschiede.

Wenn die abgeleitete Klasse versiegelt ist, kann der Compiler inline und optimieren oder eliminieren, was sonst virtuelle Aufrufe wären. Es kann also einen signifikanten Leistungsunterschied geben.

Beispiel: ToString ist ein virtueller Aufruf, aber String ist versiegelt. Unter String ist ToString ein No-Op. Wenn Sie also als object deklarieren, ist dies ein virtueller Aufruf. Wenn Sie ein String deklarieren, weiß der Compiler nein Die abgeleitete Klasse hat die Methode überschrieben, da die Klasse versiegelt ist. Das ist also ein No-Op. Ähnliche Überlegungen gelten für ArrayList vs LinkedList.

Wenn Sie den konkreten Typ des Objekts kennen (und es keinen Grund für die Kapselung gibt, es zu verbergen), sollten Sie diesen Typ deklarieren. Da Sie das Objekt gerade erstellt haben, kennen Sie den konkreten Typ.

6
Ben

Der Hauptunterschied ist die erforderliche Zugriffsebene. Und jede gute Antwort auf die Abstraktion betrifft Tiere, oder so wird mir gesagt.

Nehmen wir an, Sie haben ein paar Tiere - CatBird und Dog. Nun, diese Tiere haben einige häufige Verhaltensweisen - move(), eat() und speak(). Sie alle essen unterschiedlich und sprechen unterschiedlich, aber wenn ich mein Tier zum Essen oder Sprechen brauche, ist es mir egal, wie sie es tun.

Aber manchmal schon. Ich habe noch nie eine Wachkatze oder einen Wachvogel hart getroffen, aber ich habe einen Wachhund. Wenn also jemand in mein Haus einbricht, kann ich mich nicht einfach darauf verlassen, zu sprechen, um den Eindringling abzuschrecken. Ich brauche wirklich meinen Hund zum Bellen - das ist etwas anderes als sein Sprechen, was nur ein Schuss ist.

In Code, der einen Eindringling erfordert, muss ich wirklich Dog fido = getDog(); fido.barkMenancingly(); ausführen

Aber die meiste Zeit kann ich gerne Tiere dazu bringen, Tricks zu machen, bei denen sie für Leckereien weh tun, miauen oder twittern. Animal pet = getAnimal(); pet.speak();

Um genauer zu sein, hat ArrayList einige Methoden, die List nicht hat. Insbesondere trimToSize(). In 99% der Fälle ist uns das egal. Das List ist fast nie groß genug, damit wir uns darum kümmern können. Aber es gibt Zeiten, in denen es so ist. Daher müssen wir gelegentlich speziell nach einem ArrayList fragen, um trimToSize() auszuführen und sein Hintergrundarray zu verkleinern.

Beachten Sie, dass dies nicht bei der Konstruktion erfolgen muss - es kann über eine Rückgabemethode oder sogar eine Besetzung erfolgen, wenn wir absolut sicher sind.

4
corsiKa

Im Allgemeinen sollten Sie verwenden

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

oder

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

für lokale Variablen, es sei denn, es gibt einen guten Grund dafür, dass die Variable einen anderen Typ hat als der, mit dem sie initialisiert wurde.

(Hier ignoriere ich die Tatsache, dass der C++ - Code höchstwahrscheinlich einen intelligenten Zeiger verwenden sollte. Es gibt Fälle, in denen nicht und ohne mehr als eine Zeile ich eigentlich nicht wissen kann.)

Die allgemeine Idee hier ist, Sprachen zu verwenden, die die automatische Typerkennung von Variablen unterstützen. Dadurch wird der Code leichter lesbar und es wird das Richtige getan (das heißt, das Typsystem des Compilers kann so viel wie möglich optimieren und die Initialisierung so ändern, dass sie neu ist Typ funktioniert auch, wenn die meisten abstrakten verwendet wurden).

2
Joshua

Gut, weil es leicht zu verstehen und diesen Code leicht zu ändern ist:

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

Gut, weil es Absicht kommuniziert:

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

Ok, weil es üblich und leicht zu verstehen ist:

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

Die einzige Option, die wirklich schlecht ist, ist die Verwendung von Parent, wenn nur Child eine Methode DoSomething() hat. Es ist schlecht, weil es die Absicht falsch kommuniziert:

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

Lassen Sie uns nun etwas näher auf den Fall eingehen, den die Frage speziell stellt:

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

Lassen Sie uns dies anders formatieren, indem wir die zweite Hälfte zu einem Funktionsaufruf machen:

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

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

Dies kann verkürzt werden zu:

FunctionCall(new Child());

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

Hier gehe ich davon aus, dass allgemein anerkannt ist, dass WhichType der grundlegendste/abstrakteste Typ sein sollte, mit dem die Funktion funktionieren kann (es sei denn, es gibt Leistungsprobleme). In diesem Fall wäre der richtige Typ entweder Parent oder etwas, von dem Parent abgeleitet ist.

Diese Argumentation erklärt, warum die Verwendung des Typs Parent eine gute Wahl ist, aber es geht nicht um das Wetter, die anderen Entscheidungen sind schlecht (sie sind es nicht).

1
Peter

tl; dr - Die Verwendung von Child gegenüber Parent ist im lokalen Bereich vorzuziehen Umfang. Dies verbessert nicht nur die Lesbarkeit, sondern muss auch sicherstellen, dass die überladene Methodenauflösung ordnungsgemäß funktioniert und eine effiziente Kompilierung ermöglicht.


Im lokalen Bereich

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

Konzeptionell geht es darum, die bestmöglichen Typinformationen zu erhalten. Wenn wir ein Downgrade auf Parent durchführen, werden im Wesentlichen nur Typinformationen entfernt, die nützlich gewesen sein könnten.

Das Beibehalten der vollständigen Typinformationen hat vier Hauptvorteile:

  1. Bietet dem Compiler weitere Informationen.
  2. Bietet dem Leser weitere Informationen.
  3. Saubererer, standardisierter Code.
  4. Macht die Programmlogik veränderlicher.

Vorteil 1: Weitere Informationen zum Compiler

Der scheinbare Typ wird bei der Auflösung überladener Methoden und bei der Optimierung verwendet.

Beispiel: Überladene Methodenauflösung

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

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

Im obigen Beispiel rufen beide foo() work Auf, aber in einem Fall erhalten wir die bessere überladene Methodenauflösung.

Beispiel: Compileroptimierung

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; }
}

Im obigen Beispiel rufen beide .Foo() Aufrufe letztendlich dieselbe override -Methode auf, die 2 Zurückgibt. Nur im ersten Fall gibt es eine virtuelle Methodensuche, um die richtige Methode zu finden. Diese virtuelle Methodensuche wird im zweiten Fall nicht benötigt, da diese Methode sealed ist.

Dank an @Ben, der ein ähnliches Beispiel in seine Antwort geliefert hat.

Vorteil 2: Weitere Informationen für den Leser

Wenn Sie den genauen Typ kennen, d. H. Child, erhalten Sie mehr Informationen für jeden, der den Code liest, sodass Sie leichter sehen können, was das Programm tut.

Sicher, vielleicht spielt es für den eigentlichen Code keine Rolle, da sowohl parent.Foo(); als auch child.Foo(); sinnvoll sind, aber für jemanden, der den Code zum ersten Mal sieht, sind weitere Informationen einfach hilfreich.

Abhängig von Ihrer Entwicklungsumgebung kann die IDE) möglicherweise hilfreichere Tooltips und Metadaten zu Child als Parent bereitstellen.

Vorteil 3: Saubererer, standardisierter Code

Die meisten C # -Codebeispiele, die ich in letzter Zeit gesehen habe, verwenden var, was im Grunde eine Abkürzung für Child ist.

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

Das Anzeigen einer Nicht -var -Deklarationsanweisung sieht einfach nicht gut aus. Wenn es einen situativen Grund dafür gibt, großartig, aber ansonsten sieht es anti-pattern aus.

// 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();   

Vorteil 4: Veränderlichere Programmlogik für das Prototyping

Die ersten drei Vorteile waren die großen; das ist viel situativer.

Für Leute, die Code wie andere verwenden, die Excel verwenden, ändern wir unseren Code jedoch ständig. Möglicherweise müssen wir in this Version des Codes keine für Child eindeutige Methode aufrufen, aber wir können den Code später erneut verwenden oder überarbeiten.

Der Vorteil eines starken Typsystems besteht darin, dass es uns bestimmte Metadaten über unsere Programmlogik liefert, wodurch die Möglichkeiten leichter ersichtlich werden. Dies ist beim Prototyping unglaublich nützlich, daher ist es am besten, es so weit wie möglich aufzubewahren.

Zusammenfassung

Die Verwendung von Parent bringt die überladene Methodenauflösung durcheinander, verhindert einige Compileroptimierungen, entfernt Informationen aus dem Reader und macht den Code hässlicher.

Die Verwendung von var ist wirklich der richtige Weg. Es ist schnell, sauber, strukturiert und hilft dem Compiler und IDE, ihre Arbeit richtig zu machen.


Wichtig: Bei dieser Antwort geht es um Parent vs. Child in einer Methode lokaler Geltungsbereich. Das Problem von Parent vs. Child ist für Rückgabetypen, Argumente und Klassenfelder sehr unterschiedlich.

1
Nat

Bei parentType foo = new childType1(); kann Code nur übergeordnete Methoden für foo verwenden, kann jedoch in foo einen Verweis auf ein Objekt speichern, dessen Typ von parent - nicht nur Instanzen von childType1. Wenn die neue childType1 - Instanz das einzige ist, auf das foo jemals eine Referenz enthält, ist es möglicherweise besser, den Typ von foo als childType1. Wenn foo möglicherweise Verweise auf andere Typen enthalten muss, wird dies durch die Deklaration als parentType möglich.

0
supercat

Ich schlage vor, zu verwenden

Child x = new Child();

anstatt

Parent x = new Child();

Letzterer verliert Informationen, Ersterer nicht.

Sie können alles mit dem ersteren machen, was Sie mit dem letzteren machen können, aber nicht umgekehrt.


Um den Zeiger weiter auszuarbeiten, haben Sie mehrere Vererbungsebenen.

Base -> DerivedL1 -> DerivedL2

Und Sie möchten das Ergebnis von new DerivedL2() mit einer Variablen initialisieren. Bei Verwendung eines Basistyps haben Sie die Möglichkeit, Base oder DerivedL1 Zu verwenden.

Sie können verwenden

Base x = new DerivedL2();

oder

DerivedL1 x = new DerivedL2();

Ich sehe keinen logischen Weg, einen dem anderen vorzuziehen.

Wenn du benutzt

DerivedL2 x = new DerivedL2();

es gibt nichts zu streiten.

0
R Sahu

Ich würde vorschlagen, Variablen immer als den spezifischsten Typ zurückzugeben oder zu speichern, aber Parameter als den breitesten Typ zu akzeptieren.

Z.B.

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

Die Parameter sind List<T>, Ein sehr allgemeiner Typ. ArrayList<T>, LinkedList<T> Und andere können mit dieser Methode akzeptiert werden.

Wichtig ist, dass der Rückgabetyp LinkedHashMap<K, V> Und nicht Map<K, V> Ist. Wenn jemand das Ergebnis dieser Methode einem Map<K, V> Zuordnen möchte, kann er dies tun. Es zeigt deutlich, dass dieses Ergebnis mehr als nur eine Karte ist, sondern eine Karte mit einer definierten Reihenfolge.

Angenommen, diese Methode hat Map<K, V> Zurückgegeben. Wenn der Anrufer einen LinkedHashMap<K, V> Wünschen würde, müsste er eine Typprüfung, Umwandlung und Fehlerbehandlung durchführen, was wirklich umständlich ist.