[Exception] Wann ist es richtig, dass ein Konstruktor eine Ausnahme auslöst?


Answers

Eric Lippert sagt, dass es 4 Arten von Ausnahmen gibt.

  • Tödliche Ausnahmen sind nicht deine Schuld, du kannst sie nicht verhindern, und du kannst sie nicht vernünftig beseitigen.
  • Boneheaded Exceptions sind deine eigenen Fehler, du könntest sie verhindert haben und daher sind sie Bugs in deinem Code.
  • Ärgerliche Ausnahmen sind das Ergebnis unglücklicher Designentscheidungen. Ärgerliche Ausnahmen werden in einem völlig nicht außergewöhnlichen Fall ausgelöst und müssen daher ständig abgefangen und gehandhabt werden.
  • Und schließlich scheinen exogene Ausnahmen etwas wie ärgerliche Ausnahmen zu sein, außer dass sie nicht das Ergebnis unglücklicher Designentscheidungen sind. Sie sind vielmehr das Ergebnis unordentlicher äußerer Realitäten, die auf Ihre schöne, scharfe Programmlogik treffen.

Ihr Konstruktor sollte nie allein eine schwerwiegende Ausnahme auslösen, aber Code, der ausgeführt wird, kann eine schwerwiegende Ausnahme verursachen. Etwas wie "out of memory" kann man nicht kontrollieren, aber wenn es in einem Konstruktor vorkommt, passiert es.

Boneheaded Exceptions sollten niemals in irgendeinem Code auftreten, also sind sie direkt out.

Ärgerliche Ausnahmen (das Beispiel ist Int32.Parse() ) sollten nicht von Konstruktoren ausgelöst werden, da sie keine Ausnahmeereignisse haben.

Schließlich sollten exogene Ausnahmen vermieden werden, aber wenn Sie etwas in Ihrem Konstruktor tun, das von äußeren Umständen abhängt (wie dem Netzwerk oder dem Dateisystem), wäre es angemessen, eine Ausnahme auszulösen.

Question

Wann ist es richtig, dass ein Konstruktor eine Ausnahme auslöst? (Oder im Falle von Objective C: Wann ist es richtig für einen Initiator, nil zurückzugeben?)

Es scheint mir, dass ein Konstruktor fehlschlagen sollte - und somit die Erstellung eines Objekts ablehnen sollte, wenn das Objekt nicht vollständig ist. Dh, der Konstruktor sollte einen Vertrag mit seinem Aufrufer haben, um ein funktionales und funktionierendes Objekt zur Verfügung zu stellen, auf dem Methoden sinnvoll aufgerufen werden können. Ist das vernünftig?




Soweit ich sagen kann, präsentiert niemand eine naheliegende Lösung, die das Beste aus der einstufigen und der zweistufigen Konstruktion verkörpert.

Hinweis: Diese Antwort setzt C # voraus, aber die Prinzipien können in den meisten Sprachen angewendet werden.

Erstens, die Vorteile von beiden:

Eine Bühne

Die einstufige Konstruktion kommt uns zugute, indem verhindert wird, dass Objekte in einem ungültigen Zustand existieren, wodurch alle Arten der fehlerhaften Zustandsverwaltung und alle damit verbundenen Fehler vermieden werden. Einige von uns fühlen sich jedoch seltsam an, weil wir nicht wollen, dass unsere Konstruktoren Ausnahmen auslösen, und manchmal müssen wir das tun, wenn die Initialisierungsargumente ungültig sind.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
        }

        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }
}

Zweistufige Via-Validierungsmethode

Die zweistufige Konstruktion kommt uns zugute, da unsere Validierung außerhalb des Konstruktors ausgeführt werden kann und somit das Auslösen von Ausnahmen im Konstruktor verhindert wird. Es hinterlässt jedoch "ungültige" Instanzen, was bedeutet, dass wir den Status für die Instanz verfolgen und verwalten müssen, oder wir werfen ihn sofort nach der Heap-Zuweisung weg. Es stellt sich die Frage: Warum führen wir eine Heap-Zuweisung und somit eine Speichersammlung für ein Objekt durch, das wir nicht einmal verwenden?

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public void Validate()
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(Name));
        }

        if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }
    }
}

Einstufig über privaten Konstruktor

Wie können wir Ausnahmen von unseren Konstruktoren fernhalten und uns davon abhalten, die Heap-Zuweisung für Objekte durchzuführen, die sofort verworfen werden? Es ist ziemlich einfach: Wir machen den Konstruktor privat und erstellen Instanzen über eine statische Methode, die dazu bestimmt ist, eine Instanziierung und damit eine Heap-Zuweisung nur nach der Validierung durchzuführen.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    private Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public static Person Create(
        string name,
        DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }

        return new Person(name, dateOfBirth);
    }
}

Async Einstufig über privaten Konstruktor

Abgesehen von den oben genannten Vorteilen bei der Validierungs- und Heap-Allokationsprävention bietet uns die bisherige Methodik einen weiteren nützlichen Vorteil: Async-Unterstützung. Dies ist nützlich, wenn es sich um eine mehrstufige Authentifizierung handelt, z. B. wenn Sie vor der Verwendung der API ein Bearer-Token abrufen müssen. Auf diese Weise haben Sie keinen ungültigen "abgemeldeten" API-Client, sondern können den API-Client einfach neu erstellen, wenn Sie beim Versuch, eine Anfrage auszuführen, einen Autorisierungsfehler erhalten.

public class RestApiClient
{
    public RestApiClient(HttpClient httpClient)
    {
        this.httpClient = new httpClient;
    }

    public async Task<RestApiClient> Create(string username, string password)
    {
        if (username == null)
        {
            throw new ArgumentNullException(nameof(username));
        }

        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
        var basicAuthValue = Convert.ToBase64String(basicAuthBytes);

        var authenticationHttpClient = new HttpClient
        {
            BaseUri = new Uri("https://auth.example.io"),
            DefaultRequestHeaders = {
                Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
            }
        };

        using (authenticationHttpClient)
        {
            var response = await httpClient.GetAsync("login");
            var content = response.Content.ReadAsStringAsync();
            var authToken = content;
            var restApiHttpClient = new HttpClient
            {
                BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
                DefaultRequestHeaders = {
                    Authentication = new AuthenticationHeaderValue("Bearer", authToken)
                }
            };

            return new RestApiClient(restApiHttpClient);
        }
    }
}

Die Nachteile dieser Methode sind meiner Erfahrung nach wenig.

Im Allgemeinen bedeutet dies, dass Sie die Klasse nicht mehr als DTO verwenden können, da die Deserialisierung für ein Objekt ohne einen öffentlichen Standardkonstruktor bestenfalls schwierig ist. Wenn Sie das Objekt jedoch als DTO verwenden, sollten Sie das Objekt selbst nicht wirklich validieren, sondern die Werte des Objekts beim Versuch, sie zu verwenden, ungültig machen, da die Werte technisch gesehen nicht "ungültig" sind zum DTO.

Dies bedeutet auch, dass Sie am Ende Factory-Methoden oder -Klassen erstellen, wenn Sie einem IOC-Container das Erstellen des Objekts erlauben müssen, da der Container sonst nicht weiß, wie das Objekt instanziiert wird. In vielen Fällen werden die Factory-Methoden jedoch selbst zu Create Methoden.




Es ist immer ziemlich zweifelhaft, besonders wenn Sie Ressourcen innerhalb eines Konstruktors zuweisen; Abhängig von Ihrer Sprache wird der Destruktor nicht aufgerufen, Sie müssen also manuell aufräumen. Es hängt davon ab, wann die Lebensdauer eines Objekts in Ihrer Sprache beginnt.

Das einzige Mal, wenn ich es wirklich gemacht habe, ist, wenn irgendwo ein Sicherheitsproblem aufgetreten ist, das bedeutet, dass das Objekt nicht erstellt, sondern erstellt werden sollte.




Wenn Sie UI-Steuerelemente (ASPX, WinForms, WPF, ...) schreiben, sollten Sie das Auslösen von Ausnahmen im Konstruktor vermeiden, da der Designer (Visual Studio) beim Erstellen der Steuerelemente nicht mit ihnen umgehen kann. Kennen Sie Ihren Kontroll-Lebenszyklus (Kontrollereignisse) und verwenden Sie, wo immer möglich, eine verzögerte Initialisierung.




Wegen all der Probleme, die eine teilweise erstellte Klasse verursachen kann, würde ich niemals sagen.

Wenn Sie während der Konstruktion etwas überprüfen müssen, machen Sie den Konstruktor privat und definieren Sie eine öffentliche statische Factory-Methode. Die Methode kann auslösen, wenn etwas ungültig ist. Aber wenn alles auscheckt, ruft es den Konstruktor auf, der garantiert nicht werfen wird.




Ich kann in Objective-C nicht auf bewährte Methoden eingehen, aber in C ++ ist es in Ordnung, wenn ein Konstruktor eine Ausnahme auslöst. Zumal es keine andere Möglichkeit gibt, dafür zu sorgen, dass eine außergewöhnliche Bedingung, die beim Bau auftritt, gemeldet wird, ohne auf eine isOK () -Methode zurückgreifen zu müssen.

Die Funktion try block feature wurde speziell entwickelt, um Fehler bei der elementweisen Initialisierung des Konstruktors zu unterstützen (obwohl sie auch für reguläre Funktionen verwendet werden kann). Es ist die einzige Möglichkeit, die Ausnahmeinformation zu ändern oder anzureichern, die ausgelöst wird. Aufgrund seines ursprünglichen Entwurfszwecks (Verwendung in Konstruktoren) erlaubt es jedoch nicht, dass die Ausnahme durch eine leere catch () -Klausel verschluckt wird.




Der beste Rat, den ich über Exceptions gesehen habe, ist, eine Exception auszulösen, wenn und nur wenn die Alternative darin besteht, eine Nachbedingung nicht zu erfüllen oder eine Invariante aufrechtzuerhalten.

Dieser Rat ersetzt eine unklare subjektive Entscheidung (ist es eine gute Idee ) mit einer technischen, präzisen Frage, die auf Designentscheidungen (Invarianten und Postbedingungen) basiert, die Sie bereits hätten machen sollen.

Konstruktoren sind nur ein spezieller, aber nicht spezieller Fall für diesen Rat. Es stellt sich also die Frage, welche Invarianten eine Klasse haben sollte. Befürworter einer separaten Initialisierungsmethode, die nach der Konstruktion aufgerufen wird, weisen darauf hin, dass die Klasse zwei oder mehr Betriebsmodi aufweist , wobei nach der Erstellung ein unready- Modus und nach der Initialisierung mindestens ein ready- Modus eingegeben wird. Das ist eine zusätzliche Komplikation, aber akzeptabel, wenn die Klasse ohnehin mehrere Betriebsmodi hat. Es ist schwer zu sehen, wie sich diese Komplikation lohnt, wenn die Klasse ansonsten keine Betriebsmodi hätte.

Beachten Sie, dass das Einrichten einer separaten Initialisierungsmethode es nicht ermöglicht, Ausnahmen zu vermeiden. Ausnahmen, die Ihr Konstruktor möglicherweise ausgelöst hat, werden jetzt von der Initialisierungsmethode ausgelöst. Alle nützlichen Methoden Ihrer Klasse müssen Ausnahmen auslösen, wenn sie für ein nicht initialisiertes Objekt aufgerufen werden.

Beachten Sie auch, dass die Vermeidung von Ausnahmen, die von Ihrem Konstruktor ausgelöst werden, mühsam ist und in vielen Standardbibliotheken in vielen Fällen unmöglich ist . Dies liegt daran, dass die Entwickler dieser Bibliotheken glauben, dass das Auslösen von Ausnahmen von Konstruktoren eine gute Idee ist. Insbesondere kann jede Operation fehlschlagen, die versucht, eine nicht gemeinsam nutzbare oder begrenzte Ressource zu erhalten (z. B. das Zuweisen von Speicher), und dieser Fehler wird normalerweise in OO-Sprachen und -Bibliotheken angezeigt, indem eine Ausnahme ausgelöst wird.







ctors sollen keine "smarten" Dinge tun, also ist das Werfen einer Ausnahme sowieso nicht nötig. Verwenden Sie eine Init () - oder Setup () -Methode, wenn Sie ein komplizierteres Objekt einrichten möchten.




Wenn Sie einen Konstruktor mit unzulässigen Werten initialisieren, sollte er immer eine Ausnahme auslösen. So wird es nicht in einem schlechten Zustand aufgebaut.




Sie sollten unbedingt eine Ausnahme von einem Konstruktor auslösen, wenn Sie kein gültiges Objekt erstellen können. Auf diese Weise können Sie in Ihrer Klasse die richtigen Invarianten bereitstellen.

In der Praxis müssen Sie sehr vorsichtig sein. Denken Sie daran, dass in C ++ der Destruktor nicht aufgerufen wird. Wenn Sie also nach dem Zuweisen Ihrer Ressourcen werfen, müssen Sie sehr sorgfältig darauf achten, dass dies richtig gehandhabt wird!

Auf dieser Seite wird die Situation in C ++ ausführlich besprochen.




Eine Exception während der Konstruktion zu werfen ist eine großartige Möglichkeit, Ihren Code komplexer zu gestalten. Dinge, die einfach scheinen würden, werden plötzlich hart. Angenommen, Sie haben einen Stapel. Wie knallst du den Stapel und gibst den oberen Wert zurück? Nun, wenn die Objekte im Stack ihre Konstruktoren einwerfen können (indem sie das Temporäre so konstruieren, dass es zum Aufrufer zurückkehrt), können Sie nicht garantieren, dass Sie keine Daten verlieren (Dekrementieren des Stack Pointers, Konstruieren des Rückgabewertes mit dem Copy Konstruktor des Wertes in) Stapel, der wirft, und jetzt einen Stapel haben, der gerade einen Gegenstand verloren hat)! Aus diesem Grund gibt std :: stack :: pop keinen Wert zurück und Sie müssen std :: stack :: top aufrufen.

Dieses Problem wird hier gut beschrieben, siehe Punkt 10, Schreiben eines Ausnahme-sicheren Codes.