Der Konstruktor sollte im Allgemeinen keine Methoden aufrufen

12

Ich habe einem Kollegen beschrieben, warum ein Konstruktor, der eine Methode aufruft, ein Antipattern sein kann.

Beispiel (in meinem rostigen C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Ich möchte diese Tatsache durch Ihren zusätzlichen Beitrag besser motivieren. Wenn Sie Beispiele, Buchreferenzen, Blogseiten oder Namen von Prinzipien haben, wären sie sehr willkommen.

Edit: Ich spreche im Allgemeinen, aber wir codieren in Python.

Stefano Borini
quelle
Ist dies eine allgemeine Regel oder spezifisch für bestimmte Sprachen?
ChrisF
Welche Sprache? In C ++ ist es mehr als ein Anti-Pattern: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, im OP geht es um "Klassenmethoden", was für mich zumindest Nicht-Instanzmethoden bedeutet . Welches kann daher nicht virtuell sein.
Péter Török
3
@Alb In Java ist es vollkommen in Ordnung. Was Sie jedoch nicht tun sollten, ist die explizite Übergabe thisan eine der vom Konstruktor aufgerufenen Methoden.
biziclop
3
@Stefano Borini: Wenn Sie in Python codieren, warum nicht das Beispiel in Python anstelle von rostigem C ++ zeigen? Erklären Sie auch, warum dies eine schlechte Sache ist. Wir machen es die ganze Zeit.
S.Lott

Antworten:

26

Sie haben keine Sprache angegeben.

In C ++ muss ein Konstruktor beim Aufrufen einer virtuellen Funktion aufpassen, dass die eigentliche Funktion, die er aufruft, die Klassenimplementierung ist. Wenn es sich um eine rein virtuelle Methode ohne Implementierung handelt, liegt eine Zugriffsverletzung vor.

Ein Konstruktor kann nicht virtuelle Funktionen aufrufen.

Wenn Ihre Sprache Java ist und die Funktionen standardmäßig virtuell sind, ist es sinnvoll, besonders vorsichtig zu sein.

C # scheint die Situation so zu handhaben, wie Sie es erwarten würden: Sie können virtuelle Methoden in Konstruktoren aufrufen und es ruft die endgültigste Version auf. Also in C # kein Anti-Pattern.

Ein häufiger Grund für den Aufruf von Methoden aus Konstruktoren besteht darin, dass mehrere Konstruktoren eine gemeinsame "init" -Methode aufrufen möchten.

Beachten Sie, dass Destruktoren dasselbe Problem mit virtuellen Methoden haben. Daher können Sie keine virtuelle "Aufräum" -Methode verwenden, die sich außerhalb Ihres Destruktors befindet und erwartet, dass sie vom Destruktor der Basisklasse aufgerufen wird.

Java und C # haben keine Destruktoren, sondern Finalizer. Ich kenne das Verhalten mit Java nicht.

C # scheint die Bereinigung in dieser Hinsicht korrekt zu handhaben.

(Beachten Sie, dass Java und C # zwar eine Garbage Collection haben, aber nur die Speicherzuordnung verwalten. Es gibt eine andere Bereinigung, die Ihr Destruktor durchführen muss, ohne Speicher freizugeben.)

Goldesel
quelle
13
Hier gibt es ein paar kleine Fehler. Methoden in C # sind standardmäßig nicht virtuell. C # hat eine andere Semantik als C ++, wenn eine virtuelle Methode in einem Konstruktor aufgerufen wird. Die virtuelle Methode für den am häufigsten abgeleiteten Typ wird aufgerufen, nicht die virtuelle Methode für den Teil des Typs, der gerade erstellt wird. C # nennt seine Finalisierungsmethoden "Destruktoren", aber Sie haben Recht, dass sie die Semantik von Finalisierern haben. In C # -Destruktoren aufgerufene virtuelle Methoden funktionieren genauso wie in Konstruktoren. die am meisten abgeleitete Methode heißt.
Eric Lippert
@ Péter: Ich habe Instanzmethoden vorgesehen. Entschuldigung für die Verwirrung.
Stefano Borini
1
@ Eric Lippert. Vielen Dank für Ihr Fachwissen zu C #. Ich habe meine Antwort entsprechend bearbeitet. Ich kann diese Sprache nicht verstehen, ich kenne C ++ sehr gut und Java weniger gut.
CashCow
5
Bitte. Beachten Sie, dass das Aufrufen einer virtuellen Methode in einem Basisklassenkonstruktor in C # immer noch eine ziemlich schlechte Idee ist.
Eric Lippert
Wenn Sie eine (virtuelle) Methode in Java von einem Konstruktor aus aufrufen, wird immer die am häufigsten abgeleitete Überschreibung aufgerufen. Was Sie jedoch "so nennen, wie Sie es erwarten", würde ich als verwirrend bezeichnen. Während Java die am häufigsten abgeleitete Überschreibung aufruft, werden bei dieser Methode nur die abgelegten Initialisierer verarbeitet, nicht jedoch der Konstruktor seiner eigenen Klasse. Das Aufrufen einer Methode für eine Klasse, deren Invariante noch nicht festgelegt ist, kann gefährlich sein. Daher denke ich, dass C ++ hier die bessere Wahl getroffen hat.
5gon12eder
18

OK, jetzt, da die Verwirrung zwischen Klassenmethoden und Instanzmethoden beseitigt ist, kann ich eine Antwort geben :-)

Das Problem besteht nicht darin, Instanzmethoden im Allgemeinen von einem Konstruktor aus aufzurufen. Es ist mit dem Aufruf von virtuellen Methoden (direkt oder indirekt). Der Hauptgrund ist, dass das Objekt im Konstruktor noch nicht vollständig konstruiert ist . Und vor allem die Unterklassenteile werden überhaupt nicht erstellt, während der Basisklassenkonstruktor ausgeführt wird. Daher ist sein interner Zustand in sprachabhängiger Weise inkonsistent, und dies kann in verschiedenen Sprachen verschiedene subtile Fehler verursachen.

C ++ und C # wurden bereits von anderen diskutiert. In Java wird die virtuelle Methode des am häufigsten abgeleiteten Typs aufgerufen, der Typ ist jedoch noch nicht initialisiert. Wenn diese Methode also Felder aus dem abgeleiteten Typ verwendet, sind diese Felder zu diesem Zeitpunkt möglicherweise noch nicht ordnungsgemäß initialisiert. Dieses Problem wird ausführlich in Effecive Java 2nd Edition , Punkt 17: Entwurf und Dokument zur Vererbung besprochen oder untersagt .

Beachten Sie, dass dies ein Sonderfall des allgemeinen Problems ist , Objektreferenzen vorzeitig zu veröffentlichen . Instanzmethoden haben einen impliziten thisParameter, die thisexplizite Übergabe an eine Methode kann jedoch ähnliche Probleme verursachen. Insbesondere in gleichzeitigen Programmen, in denen der Objektverweis vorzeitig auf einen anderen Thread veröffentlicht wird, kann dieser Thread bereits Methoden für ihn aufrufen, bevor der Konstruktor im ersten Thread fertig ist.

Péter Török
quelle
3
(+1) "Während sich das Objekt im Konstruktor befindet, ist es noch nicht vollständig konstruiert." Entspricht "Klassenmethoden vs Instanz". Einige Programmiersprachen betrachten es als konstruiert, wenn Sie den Konstruktor eingeben, als würde der Programmierer dem Konstruktor Werte zuweisen.
Umlcat
7

Ich würde Methodenaufrufe hier nicht als Antimuster an sich betrachten, eher als Code-Geruch. Wenn eine Klasse eine resetMethode bereitstellt, die ein Objekt in den ursprünglichen Zustand reset()zurückversetzt, lautet der Aufruf des Konstruktors DRY. (Ich mache keine Aussagen über Reset-Methoden).

Hier ist ein Artikel, der dazu beitragen könnte, Ihren Appell an die Behörde zu erfüllen: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Es geht nicht wirklich darum, Methoden aufzurufen, sondern um Konstruktoren, die zu viel bewirken. IMHO ist das Aufrufen von Methoden in einem Konstruktor ein Geruch, der möglicherweise darauf hinweist, dass ein Konstruktor zu schwer ist.

Dies hängt damit zusammen, wie einfach es ist, Ihren Code zu testen. Gründe sind:

  1. Unit-Tests beinhalten viel Kreation und Zerstörung - daher sollte der Aufbau schnell gehen.

  2. Je nachdem, was diese Methoden tun, kann es schwierig sein, einzelne Codeeinheiten zu testen, ohne sich auf eine im Konstruktor festgelegte (möglicherweise nicht testbare) Voraussetzung zu verlassen (z. B. Informationen von einem Netzwerk abrufen).

Paul Butcher
quelle
3

Philosophisch gesehen besteht der Zweck des Konstruktors darin, einen rohen Teil der Erinnerung in eine Instanz umzuwandeln. Während der Ausführung des Konstruktors ist das Objekt noch nicht vorhanden. Daher ist es eine schlechte Idee, seine Methoden aufzurufen. Sie wissen vielleicht doch nicht, was sie intern tun, und sie können zu Recht davon ausgehen, dass das Objekt zumindest existiert (duh!), Wenn sie aufgerufen werden.

In C ++ und insbesondere in Python ist es technisch gesehen möglicherweise nichts Falsches daran, dass Sie vorsichtig sein müssen.

In der Praxis sollten Sie Aufrufe nur auf solche Methoden beschränken, mit denen Klassenmitglieder initialisiert werden.


quelle
2

Es ist kein allgemeines Problem. Dies ist ein Problem in C ++, insbesondere bei der Verwendung von Vererbungs- und virtuellen Methoden, da die Objektkonstruktion rückwärts erfolgt und die vtable-Zeiger mit jeder Konstruktorebene in der Vererbungshierarchie zurückgesetzt werden. Wenn Sie also eine virtuelle Methode aufrufen, ist dies möglicherweise nicht der Fall Am Ende erhalten Sie diejenige, die tatsächlich der Klasse entspricht, die Sie erstellen möchten, was den gesamten Zweck der Verwendung virtueller Methoden zunichte macht.

In Sprachen mit vernünftiger OOP-Unterstützung, bei denen der vtable-Zeiger von Anfang an korrekt gesetzt wurde, besteht dieses Problem nicht.

Mason Wheeler
quelle
2

Es gibt zwei Probleme beim Aufrufen einer Methode:

  • Aufrufen einer virtuellen Methode, die entweder etwas Unerwartetes ausführen kann (C ++) oder Teile der Objekte verwendet, die noch nicht initialisiert wurden
  • Aufruf einer öffentlichen Methode (die die Klasseninvarianten erzwingen sollte), da das Objekt noch nicht unbedingt vollständig ist (und daher möglicherweise nicht die Invariante enthält)

Es ist nichts Falsches daran, eine Hilfsfunktion aufzurufen, solange sie nicht in die beiden vorherigen Fälle fällt.

Matthieu M.
quelle
1

Ich kaufe das nicht. In einem objektorientierten System können Sie praktisch nur eine Methode aufrufen. Tatsächlich ist das mehr oder weniger die Definition von "objektorientiert". Also, wenn ein Konstruktor keine Methoden aufrufen kann, was kann er dann tun?

Jörg W. Mittag
quelle
Initialisieren Sie das Objekt.
Stefano Borini
@Stefano Borini: Wie? In einem objektorientierten System können Sie nur Methoden aufrufen. Oder um es aus dem entgegengesetzten Blickwinkel zu betrachten: Alles wird durch Aufrufen von Methoden erledigt. Und "irgendetwas" schließt offensichtlich die Objektinitialisierung ein. Wenn Sie zum Initialisieren des Objekts Methoden aufrufen müssen, Konstruktoren jedoch keine Methoden aufrufen können, wie kann dann ein Konstruktor das Objekt initialisieren?
Jörg W Mittag
Es ist absolut nicht wahr, dass Sie nur Methoden aufrufen können. Sie können den Status einfach ohne Aufruf direkt an die Interna Ihres Objekts initialisieren ... Der Konstruktor hat den Zweck, ein Objekt in einen konsistenten Status zu versetzen. Wenn Sie andere Methoden aufrufen, haben diese möglicherweise Probleme beim Behandeln eines Objekts in einem Teilzustand, es sei denn, es handelt sich um Methoden, die speziell für den Aufruf vom Konstruktor erstellt wurden (normalerweise als Hilfsmethoden)
Stefano Borini,
@Stefano Borini: "Sie können den Status einfach ohne Aufruf direkt an die Interna Ihres Objekts initialisieren." Was machen Sie, wenn es sich um eine Methode handelt? Code kopieren und einfügen?
S.Lott
1
@ S.Lott: nein, ich rufe es auf, aber ich versuche, es als Modulfunktion anstelle einer Objektmethode zu belassen, und lasse es Rückgabedaten bereitstellen, die ich im Konstruktor in den Objektzustand versetzen kann. Wenn ich wirklich eine Objektmethode haben muss, werde ich sie als privat kennzeichnen und klarstellen, dass sie für die Initialisierung vorgesehen ist, z. Ich würde jedoch niemals eine öffentliche Methode aufrufen, um den Objektstatus über den Konstruktor festzulegen.
Stefano Borini
0

In der OOP-Theorie sollte es keine Rolle spielen, aber in der Praxis behandelt jede OOP-Programmiersprache unterschiedliche Konstruktoren . Ich verwende nicht sehr oft statische Methoden.

In C ++ & Delphi füge ich einige sekundäre Methoden als Erweiterung der Konstruktoren hinzu, wenn ich einigen Eigenschaften ("Feldelementen") Anfangswerte zuweisen musste und der Code sehr erweitert ist.

Und nenne keine anderen Methoden, die komplexere Dinge tun.

Bei den Eigenschaften "getters" und "setters" verwende ich normalerweise private / protected-Variablen zum Speichern ihres Status sowie die Methoden "getters" und "setters".

Im Konstruktor weise ich den Eigenschaftsstatusfeldern "Standard" -Werte zu, OHNE die "Accessoren" aufzurufen.

umlcat
quelle