Ausnahmen in C#, richtig gemacht

In diesem Tutorial erfahren Sie, wie Sie C#-Ausnahmen richtig verwenden und wann Sie sie verwenden.


Abhängig davon, wie sie ausgelöst und wie sie anschließend behandelt werden, können Ausnahmen entweder äußerst nützlich oder furchtbar lästig sein. Nach mehreren Jahren Erfahrung sind hier einige Strategien, die ich für gut befunden habe.

Die Ausnahme – nicht die Regel

Ausnahmen sollten nicht unter normalen, erwarteten Umständen ausgelöst werden. Manchmal finden Sie eine Methode, die so aussieht:

public User validate(string username, string password)
{
  if(!UserExists(username))
    throw new UserNotFoundException();
  if(!IsValid(username, password))
    throw new InvalidCredentialsException();
  return GetUser(username);
}

Das ist falsch! Ausnahmen sollen für Umstände ausgelöst werden, die es Ihrem Programm unmöglich machen, seinen normalen Betriebsablauf fortzusetzen. Sie sind für Dinge wie „Meine Festplatte fehlt“. Ungültige Zeugnisse hingegen sind ein ganz normaler und erwarteter Teil des Alltags. Wenn dies der Fall ist, sollten Sie dafür sorgen, dass der Rückgabetyp Ihrer Methode die verschiedenen möglichen Ergebnisse des Aufrufs Ihrer Methode unter normalen Umständen widerspiegelt:

public ValidationResult validate(string username, string password)
{
  if(!UserExists(username))
    return new ValidationResult(ValidationStatus.UserNotFound);
  if(!IsValid(username, password))
    return new ValidationResult(ValidationStatus.InvalidPassword);
  var user = GetUser(username);
  return new ValidationResult(ValidationStatus.Valid, user);
}

Wissen, wann Ausnahmen ausgelöst werden müssen

Jedes Mal, wenn Sie eine Situation haben, in der Ihre Methode grundlegende Annahmen über ihren Zustand oder Eingabeparameter trifft, sollten Sie diese Annahme überprüfen und eine Ausnahme auslösen, wenn sich herausstellt, dass dies nicht der Fall ist. Welche Annahmen treffen wir mit der obigen Validierungsmethode?

  1. username und password sollte nicht null sein. Wenn wir uns diese Methode ansehen, können wir nicht wissen, wie UserExists, IsValidoder GetUser würde sich verhalten, wenn ihnen Null-Strings gegeben würden. Anstatt von ihrem Verhalten abzuhängen, sollten wir eine Ausnahme auslösen, wenn diese Grundannahme nicht erfüllt ist.
  2. GetUser sollte keinen Nullwert zurückgeben. Zum Zeitpunkt des Aufrufs sollten wir bereits überprüft haben, ob der Benutzer existiert. Wenn wir hier einen Nullwert erhalten, liegt das daran, dass mit einem der beiden etwas furchtbar falsch ist UserExists oder der GetUser -Methode, oder es gab eine Art Race-Condition, bei der der Benutzer gelöscht wurde, gerade als Sie ihn validieren wollten. Keine dieser Situationen kann mit unserer Methode elegant gehandhabt werden. Wenn wir zulassen, dass ein Nullwert mit a zurückgegeben wird Valid Als Ergebnis würden wir wahrscheinlich mit a enden NullReferenceException in Code geworfen zu werden, der möglicherweise nicht in der Nähe der Ursache des Problems liegt.

Schreiben wir also die Methode um, um unsere Annahmen zu testen:

public ValidationResult validate(string username, string password)
{
  if(username == null)
    throw new ArgumentNullException("username");
  if(password == null)
    throw new ArgumentNullException("password");
  if(!UserExists(username))
    return new ValidationResult(ValidationStatus.UserNotFound);
  if(!IsValid(username, password))
    return new ValidationResult(ValidationStatus.InvalidPassword);
  var user = GetUser(username);
  if(user == null)
    throw new InvalidOperationException("GetUser should not return null.");
  return new ValidationResult(ValidationStatus.Valid, user);
}

Die Chancen stehen gut, Sie werden es nie sehen irgendein dieser Ausnahmen werden geworfen. Wenn dies jedoch der Fall ist, sagt Ihnen der Stack-Trace genau, wo das Problem aufgetreten ist, und es ist viel einfacher zu debuggen.

Wissen, wie man Ausnahmen vermeidet

Während das Auslösen von Ausnahmen wichtig ist, wenn Ihr Programm in einen unerwarteten Zustand gerät, ist es noch besser, wenn Sie Code schreiben können, der es von vornherein unmöglich macht, das Falsche zu tun.

Betrachten Sie die Art und Weise, wie wir die verwenden ValidationResult Klasse in den obigen Beispielen. Es wäre falsch, a bereitzustellen User Objekt mit allem außer a Valid Status. Dadurch würde angezeigt, dass ein Fehler vorlag, der nicht vom aufrufenden Code erfasst wurde. Was wäre, wenn wir unsere Klasse umschreiben würden, um es unmöglich zu machen, a bereitzustellen User außer beim Erstellen von a Valid Ergebnis?

public abstract class ValidationResult
{
  public ValidationStatus Status {get; private set;}
  protected ValidationResult(ValidationStatus status) {
    Status = status;
  }
}
public class UserNotFoundResult : ValidationResult
{
  public UserNotFoundResult () : 
    base(ValidationStatus.UserNotFound) {}
}
public class InvalidPasswordResult : ValidationResult
{
  public InvalidPasswordResult () : 
    base(ValidationStatus.InvalidPassword) {}
}
public class ValidResult : ValidationResult
{
  public User {get; private set;}
  public ValidResult(User user) : base(ValidationStatus.Valid)
  {
    if(user == null) throw new ArgumentNullException("user");
    User = user;
  }
}

Jetzt zwingen wir die Verbraucher, für jede Situation einen bestimmten Konstruktor auszuwählen. Wir werden ihnen nicht erlauben, a bereitzustellen User für ungültige Ergebnisse, und wir werden sie zwingen, a User für gültige Ergebnisse. Außerdem einmal a ValidationResult zurückgegeben wird, kann der aufrufende Code nicht auf die zugreifen User -Eigenschaft, ohne sie zuerst als zu umwandeln ValidResultum sicherzustellen, dass sie niemals eine bekommen null Wert aus dem User Eigentum.

Dies ist nur eine von vielen Strategien, die wir hätten anwenden können. Stellen Sie nur sicher, dass Sie, wenn Sie versucht sind, eine Ausnahme auszulösen, überlegen, ob die Ausnahme vollständig vermieden werden kann, indem Sie Konstrukte zur Kompilierzeit nutzen.

Essen Sie keine Ausnahmen

Da Ausnahmen nicht als Teil des normalen Arbeitsablaufs Ihres Programms erwartet werden, wissen Sie oft nicht genau, was Sie mit einer Ausnahme tun sollen, wenn sie auftritt. Vermeiden Sie dieses Anti-Pattern:

try
{
  DoIntegration();
} 
catch (Exception e)
{
}

Mit dem obigen Code könnten Ihre Integrationen wochenlang fehlschlagen, bevor Sie erkennen, dass es ein Problem gibt, und Sie mit Bergen von schlechten oder verlorenen Daten zurücklassen.

Hier sind einige gut Umgang mit Ausnahmen:

1: Ignoriere sie

Das stimmt. In den allermeisten Fällen gibt es überhaupt keinen wirklichen Grund, eine Ausnahme abzufangen. Wenn Sie nichts Nützliches damit anfangen können, kümmern Sie sich nicht einmal um a try/catch.

2: Fangen und erneutes Werfen

Manchmal möchten Sie als Reaktion auf eine ausgelöste Ausnahme etwas tun, während die ursprüngliche Ausnahme weiterhin ausgelöst wird.

try
{
  DoSomethingImportant();
}
catch (Exception e)
{
  EmailEmergencyResponseTeam(e);
  throw;
}

Beachten Sie, dass der obige Code sagt throw;. Dadurch wird die abgefangene Ausnahme erneut ausgelöst, während der bis zu diesem Punkt angesammelte Stack-Trace beibehalten wird. Verwende nicht throw e;da dies die Ausnahme auslöst, als ob sie gerade erstellt worden wäre, wodurch der vorhandene Stack-Trace gelöscht wird.

2: Fangen und wickeln

Ausnahmemeldungen können sehr nützlich sein, wenn es darum geht, Fehler aufzuspüren, wenn sie alle relevanten Details über den Zustand des Systems liefern, als der Fehler aufgetreten ist. Es ist oft eine gute Idee, Ausnahmen abzufangen, nur damit Sie sie mit nützlicheren Informationen umschließen können:

try
{
  DoIntegration(integrationId, paramString);
}
catch (Exception e)
{
  throw new Exception("Failed to run integration: " + 
    new{ integrationId, paramString }, e);
}

Hier schließen wir die ursprüngliche Ausnahme als InnerException ein, damit wir keine Informationen darüber verlieren, was ursprünglich schief gelaufen ist, aber wir schließen auch Informationen von dieser Ebene des Aufrufstapels ein, die möglicherweise dort nicht verfügbar waren, wo die ursprüngliche Ausnahme ausgelöst wurde. Anonyme Objekte in C# erzeugen eine schöne Zeichenfolge mit den Namen und Werten all ihrer Eigenschaften, wodurch sie praktisch ideal zum Erstellen von Ausnahmemeldungen sind.

Hinweis: Wenn Ihr Nachrichtenerstellungscode eine Ausnahme auslöst, gehen die ursprünglichen Ausnahmedaten verloren. Es kann sich für Sie lohnen, Hilfsmethoden zu erstellen, die Ausnahmen umschließen und Debug-Meldungen erstellen können, während alle Fehler ordnungsgemäß behandelt werden.

catch (Exception e)
{
  throw e.Wrap(() => "Failed to run integration: " + 
    DebugStringHelper.GetDebugString(integrationParams));
}

Sie könnten versucht sein, einen Serializer zu verwenden, um Debug-Strings für komplexe Objekte zu erstellen, aber das kann sehr gefährlich sein. Sie könnten beispielsweise den gesamten Inhalt Ihrer Datenbank von einer Lazy-Loading-ORM-Entität protokollieren. Ich empfehle die Verwendung von Helfern, die die Menge an Debug-Informationen, die sie erzeugen, explizit begrenzen.

3. Fangen und reagieren

Dies ist der klassische Fall für try/catch: Sie wissen, dass bei einer Operation etwas schief gehen könnte, und Sie wissen, wie Ihr Programm in diesem Fall am besten reagiert. Wenn beispielsweise die Verbindung zu Ihrer Datenbank manchmal fehlschlägt, sollten Sie Ihre Verbindungspools zurücksetzen und es erneut versuchen, bevor Sie aufgeben:

try
{
  return query.ToList();
}
catch (EntityCommandExecutionException e)
{
  Log.Error("Failed to query the DB. Trying again.", e);
  return query.ToList();
}

In solchen Fällen möchten Sie normalerweise den spezifischen Ausnahmetyp abfangen, mit dem Sie umgehen können, anstatt ihn abzufangen Exceptions allgemein.

4. Einfangen in der Präsentationsschicht

Ausnahmen passieren. Ob es an einem Hardwareproblem liegt bzw Ihre eigene beschissene Software, wird irgendwann eine Ausnahme in die oberste Schicht des Stapels Ihrer Anwendung sprudeln. An diesem Punkt, es muss gefangen werden. Benutzer werden gelegentlich Fehler in Kauf nehmen – sie verzeihen weitaus weniger, wenn Ihre Software abstürzt. Sie müssen sicherstellen, dass alle Ausnahmen auf der Präsentationsschicht abgefangen, protokolliert und in eine benutzerfreundliche Fehlermeldung umgewandelt werden. Abhängig vom verwendeten Framework können Sie dies häufig global mit einem einzigen Handler tun.

Auf dieser Ebene Ihrer Anwendung haben Sie normalerweise nicht genügend Informationen über den Fehler, um dem Benutzer mitzuteilen, was schief gelaufen ist. Meistens interessieren sie sich nicht für die Details. Das Beste, was Sie in diesem Fall tun können, ist ihnen zu sagen, dass es Ihnen leid tut, eine Catch-all-Lösung empfehlen Das könnte ihr Problem lösen und ihnen eine einfache Möglichkeit geben, das Problem zu melden, insbesondere wenn es weiterhin besteht.
_ Faustregel : Werfen Sie Fehler immer in dem Moment aus, in dem sie auftreten, und protokollieren Sie immer Ausnahmen, die Sie abfangen und nicht erneut auslösen.Wenn Sie dies konsequent tun, müssen Sie an keiner anderen Stelle in Ihrem Code Fehler protokollieren. Sie können einfach darauf vertrauen, dass die Ausnahme schließlich protokolliert wird, wenn sie abgefangen wird, und an diesem Punkt wird der Ausnahme ein viel informativerer Stack-Trace zugeordnet.

Fazit

Das Erlernen der korrekten Verwendung von Ausnahmen ist entscheidend, um Datenverluste zu vermeiden und es Entwicklern zu ermöglichen, auftretende Fehler einfach zu reproduzieren. Haben Sie weitere Vorschläge? Bitte teilen Sie sie im Kommentarbereich unten.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *