Programmierung

Fehlerbehandlung (Teil 2)

Nachdem ich im letzten Artikel zur Fehlerbehandlung bereits die grundlegende Funktionsweise der Behandlung von Ausnahmen mit try-Blöcken vorgestellt habe, gehe ich in diesem Artikel auf Variationen bei der Verwendung von try-Blöcken ein.


trysub

Neben dem try-Block, kann der Entwickler zur Fehlerbehandlung auch den trysub-Block verwenden. Im Gegensatz zum try-Block übernimmt der trysub-Block die Fehlerkontrolle für alle untergeordneten Funktionsebenen, anstatt nur für die aktuelle.

Beispiel

sub Anw1()
{
  Anw1.1();
  Anw1.2();
}

sub Anw2()
{
  Anw2.1();
  Anw2.2();
}

main
{
  Anw1();
  Anw2();
}

Für die Ausnahmebehandlung dieser verschalteten Funktionen bieten sich zwei Möglichkeiten an:

Möglichkeit 1: Mehrere, verteilte try-Blöcke
sub Anw1()
{
  try
  {
    Anw1.1(); // Löst diese Anweisung einen Fehler aus …
    Anw1.2(); // … wird diese Anweisung nicht ausgeführt
  }
}

sub Anw2()
{
  try
  {
    Anw2.1(); // Löst diese Anweisung einen Fehler aus …
    Anw2.2(); // … wird diese Anweisung nicht ausgeführt
  }
}

main

  local
  {
    tErr : int;
  }

{
  try
  {
    Anw1(); // Löst diese Anweisung einen Fehler aus …
    Anw2(); // … wird diese Anweisung nicht ausgeführt
  }

  tErr # ErrGet();
  if (tErr != _ErrOK)
  {
    // Fehlerbehandlung
  }
}
Möglichkeit 2: Ein einzelner, übergeordneter trysub-Block
sub Anw1()
{
  Anw1.1(); // Löst diese Anweisung innerhalb eines trysub-Blocks
            // einen Fehler aus …
  Anw1.2(); // … wird diese Anweisung nicht ausgeführt
}

sub Anw2()
{
  Anw2.1(); // Löst diese Anweisung innerhalb eines trysub-Blocks
            // einen Fehler aus …
  Anw2.2(); // … wird diese Anweisung nicht ausgeführt
}

main

  local
  {
    tErr : int;
  }

{
  trysub
  {
    Anw1(); // Löst diese Anweisung einen Fehler aus …
    Anw2(); // … wird diese Anweisung nicht ausgeführt
  }

  tErr # ErrGet();
  if (tErr != _ErrOK)
  {
    // Fehlerbehandlung
  }
}

Der offensichtliche Vorteil durch die Verwendung eines einzelnen trysub-Blockes liegt im geringeren Programmieraufwand und der Zentralisierung der Fehlerkontrolle in eine übergeordnete Funktion.

Nachteilig ist allerdings, dass man den einzelnen – unter Fehlerkontrolle stehenden – Funktionen nicht ansieht, dass sie im Ausnahmefall abbrechen und die Fehlerbehandlung an anderer, übergeordneter Stelle vorgenommen wird. Sie müssen immer im Kontext eines trysub-Blocks aufgerufen werden, um das gewünschte Verhalten im Fehlerfall zu erzielen. Andernfalls würde die Verarbeitung ohne Ausnahmebehandlung durchgeführt werden.

Letzten Endes liegt es also in Ihrer „Entwicklungsphilosophie“, welche der beiden Varianten Sie verwenden. Gegebenenfalls kann es auch sinnvoll sein, von Fall zu Fall zu entscheiden, welche Methode die geeignetere ist. Die Verfahren können auch miteinander kombiniert werden: Die Fehlerkontrolle durch einen trysub-Block einer übergeordneten Funktion kann durch einen try-Block einer untergeordneten Funktion „überschrieben“ werden.

Zur Besserung Identifizierung der Fehlerquelle bei verschachtelten Funktionen, kann bei der Fehlerbehandlung die Funktion ErrThrowProc() aufgerufen werden, um die Funktion zu ermitteln, die den Fehler ausgelöst hat:

tErr # ErrGet();
if (tErr != _ErrOK)
{
  // Fehlerbehandlung

  switch (ErrThrowProc())
  {
    // Fehler in Anw1.1()
    case 'Proc:Anw1.1' : …
    // Fehler in Anw1.2()
    case 'Proc:Anw1.2' : …
  }
}

Im nächsten Artikel werden wir das Ignorieren von zulässigen Fehlerfällen bei der Ausnahmebehandlung und die Verarbeitung von Laufzeitfehlern näher beleuchten.

7 Kommentare

7 Kommentare “Fehlerbehandlung (Teil 2)”

  1. @Michael: Hm, du hast recht damit, dass du (gestern) darauf hinweist, dass es auch im C16-Code eine Stelle gibt, die immer durchlaufen wird, nämlich der Code unmittelbar nach dem Try-Block. Ich müsste also dort zwischen Try-Block und Fehlerbehandlungs-Block meinen quasi Finally-Code hinschreiben, try-finally-except statt try-except-finally. Das ist im Prinzip in Ordnung.
    Allerdings darf in diesem Finally nichts ausgeführt werden, was die globale Fehlervariable setzt (oder man müsste sie ggf. später wieder zurücksetzen), weil man sonst die nachfolgende Fehlerbehandlung kompromittiert.
    Oder man müsste Fehlerbehandlung und Bereinigung mischen, was bei mehr als einem Fehlerbehandlungszweig zu doppeltem Code führen dürfte und insgesamt nicht so übersichtlich und klar ist.

    Nochmal zur Unterscheidung: Der Grundgedanke des Finally-Blocks ist es, auf jeden Fall ausgeführt zu werden und zwar unabhängig vom Vorhergehenden und (da er danach ausgeführt wird) auch ohne Einfluss auf die Fehlerbehandlung zu nehmen. Und um dies durchzusetzen, gibt es offenbar im momentanen Design des Exceptionhandlings in C16 noch keine Instrumente. Das heisst nicht, dass man in C16 keine ordentliche Fehlerbehandlung betreiben kann, sondern sollte nur eine Anregung sein, welche Verbesserungsmöglichkeiten es noch gibt.

    Meine Idee von gestern funktioniert jedenfalls nicht, wie ich inzwischen mit Hilfe Eures Supports festgestellt habe, da die globale Fehlervariable zu Beginn jedes Try-Blocks wieder zurückgesetzt wird 🙂

  2. @Kilian
    Was spricht gegen die Verwendung nur eines einzigen try-Blocks?
    Die Fehlerbehandlung kann dann (Ausnahme aufgetreten oder auch nicht) nach dem try-Block erfolgen.
    Generell sollte die Fehlerbehandlung so sein, dass dort keine weiteren Ausnahmen mehr ausgelöst werden. Soweit mir bekannt ist, werden Ausnahmen im finally-Block (bei anderen Programmiersprachen) auch nicht durch das catch des vorangehenen try-Blocks abgefangen.
    Das bedeutet, dass das finally widerum in ein try-catch gekapselt werden müsste, damit dort eine Ausnahme abgefangen werden kann, usw…

  3. @Andrej: Aha, also existiert diese Unterscheidung hauptsächlich aus Gründen der Kompatibilität.

    @Florian: Danke für den Hinweis. Das war mir nicht klar, d.h. hier liegt der Fall umgekehrt zu dem von mir geschilderten Beispiel (implizites Weiterreichen versus explizites).

    @Michael: Mir geht’s nicht um das Schlüsselwort, sondern um das dahinter stehende Konzept. In python wird ein finally-Block in *jedem* Fall ausgeführt. Die Ausführungsreihefolge könnte etwa sein:
    – durchlaufe try-Block bis zu einem Fehler
    – durchlaufe entsprechenden except-Block bis zum Ende, zu einem Fehler oder einem Reraise
    – *führe den lokalen finally-block aus*
    – falls vorhanden: folge dem Reraise oder der vom Fehler im Exceptionhandling ausgelösten Exception, sonst: weiter im Code.
    Das Fehlen eines finally-Blocks in C16 bedeutet, dass ich u. U. sehr genau überlegen muss, wo ich Code unterbringe, der auf jeden Fall durchlaufen werden soll, eine Möglichkeit wäre, dass ich die Fehlerbehandlung meines try-Blocks ihrerseits wieder innerhalb eines neuen try-Blocks unterbringe und dann das Finally nach diesem neuen try-Block aber vor dessen Fehlerbehandlung setze:

    try {
    VerdächtigerCode()

    }

    try {
    Switch(ErrGet()) {
    Case cMeineFehlerKonstante : {
    EvtlFehlerhafterCode() // hier liegt das Problem
    // wenn hier ein Fehler auftritt
    // geht’s woanders weiter
    // evtl. nachfolgende Auf-
    // räumaktionen finden
    // nicht mehr statt
    // also muss man auch diesen
    // Block hier mit try-Klammern
    // usw…
    }
    ….
    }
    }

    FinallyCodeZumErstenTryBlock()

    // Fehlerbehandlung des zweiten try-Blocks
    Switch(ErrGet()) {
    ….
    }

  4. @Kilian
    Zwar gibt es in CONZEPT 16 kein Schlüsselwort finally. Der try-block wird jedoch in jedem Fall beendet, so dass der Code, nach dem try-block in jedem Fall durchlaufen wird. ErrGet gibt in diesem Fall Aufschluss darüber, ob eine „Exception“ aufgetreten ist oder nicht.

  5. Zum Weiterreichen (re-raise) einer Ausnahme:

    Das "Werfen" einer Ausnahme entspricht in CONZEPT 16 dem Aufruf der Funktion ErrSet() mit einem Wert ungleich 0. Die Ausnahme bleibt solange aktiv bis sie – typischerweise nach Behandlung der Ausnahme – durch einen erneuten Aufruf von ErrSet() mit _ErrOK (0) entfernt wird oder eine weitere Ausnahmebehandlung durch einen try- oder trysub-Block eingeleitet wird. Solange eine Ausnahme nicht entfernt wird, wird sie an alle übergeordneten Funktionen weitergereicht. Jede dieser Funktionen kann den Fehler behandeln und aus der Verarbeitung entfernen.

  6. @Kilian
    Zum Zeitpunkt der Einführung von try sollten sich bereits exisitierende Funktionen (beispielsweise auch A-) weiterhin verwenden lassen, ohne das man diese für das Auftreten von Exceptions modifizieren musste.

    Falls alle aufgerufen Funktionen für Exceptions ausgelegt sind, kann trysub verwendet werden.

  7. Die hier besprochenen Möglichkeiten zur Fehlerbehandlung sind m. E. im Grossen und Ganzen vergleichbar mit dem Exceptionhandling in anderen Programmiersprachen und es ist eine gute Sache, dass es dergleichen gibt.

    Im Zusammenhang mit dem hier geschilderten trysub und dessen Vor- und Nachteilen habe ich folgende Fragen/Vorschläge:
    – Wieso gibt es überhaupt einen Unterschied zw. try und trysub? Bzw. welchen Grund gibt es dafür, dass das einfache try keine Fehler in untergeordneten Funktionen abfängt, ich sehe da keinerlei Vorteil.
    Zur Verdeutlichung ein Vergleich mit einer anderen Sprache, z.B. Python: Wenn ich dort eine selbstgeschriebene Funktion innerhalb eines try-except-Blocks verwende, werden die Exceptions, die innerhalb dieser Funktion auftreten abgefangen, wenn sie nicht bereits in der Funktion selbst abgefangen werden (also genau wie die Kombi trysub und try hier).
    – in vielen anderen Sprachen ist es möglich, eine Exception weiterzureichen (re-raise), d.h. ich kann in meiner Funktion ein eigenes Exceptionhandling implementieren und entscheiden: a) welche Fehler ich in der Funktion selbst komplett behandele, b) welche ich ignoriere, so dass sie ggf. übergeordnet abgefangen werden oder c) welche ich teilweise abhandele und dann nach oben weiterreiche.
    – Eine wichtige Sache im Zusammenhang mit Exceptions ist der finally-Block (oder wie immer er jeweils heisst). D. h. ein Code-Block, der garantiert ausgeführt wird, egal, ob eine Exception aufgetreten ist oder nicht, selbst wenn fehlerhafter Code im Except-Zweig vorhanden ist (so zumindest in Python). Wenn ich es richtig sehe, gibt es diese Möglichkeit in C16 derzeit nicht und man könnte den quasi-finally-Code auch nicht einfach hinter die Stelle zur Fehlerbehandlung schreiben, da diese Stelle nicht überwunden wird, wenn der Code zur Fehlerbehanldung fehlerhaft ist (im Gegensatz zu finally).

Kommentar abgeben