Dart-Ausnahmebehandlung

Eine Exception ist ein Fehlerereignis, das während der Programmausführung auftritt.

Eine gute Exception-Behandlung ermöglicht es dem Programm, bei Fehlern kontrolliert zu reagieren, anstatt direkt abzustürzen.

Dieses Kapitel führt in try/catch/finally in Dart ein, das Auslösen von Exceptions mit throw, das Abfangen bestimmter Typen mit on und das erneute Auslösen mit rethrow.


try / catch / finally Grundlagen

Der try-Block enthält Code, der Exceptions auslösen kann, der catch-Block fängt Exceptions ab und behandelt sie, und der finally-Block wird ausgeführt, unabhängig davon, ob eine Exception auftritt.

Beispiel

void main() {
  // Einfaches try-catch
  try {
    int result = 10 ~/ 0;  // Division durch Null löst eine Exception aus
    print(Ergebnis: $result);  // Diese Zeile wird nicht ausgeführt
  } catch (e) {
    // e ist das Exception-Objekt
    print(Abgefangene Exception: $e);
  }

  // Exception- und Stacktrace-Informationen abrufen
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[10]);  // Zugriff auf Index außerhalb der Grenzen
  } catch (e, stackTrace) {
    // e ist die Ausnahme, stackTrace ist der Aufrufstapel
    print(BEISPIEL-Ausnahme: $e);
    print(Stacktrace-Informationen:\n$stackTrace');
  }

  // finally: wird ausgeführt, unabhängig davon, ob eine Ausnahme auftritt
  try {
    print(Versuch, die Operation auszuführen...);
    int result = 5 ~/ 0;
    print(Diese Zeile wird nicht ausgeführt: $result);
  } catch (e) {
    print(Fehler: $e);
  } finally {
    // Code im finally-Block wird immer ausgeführt, oft verwendet, um Ressourcen aufzuräumen
    print(Ressourcenbereinigung abgeschlossen (finally wird immer ausgeführt));
  }
}
捕获到异常: IntegerDivisionByZeroException
EXAMPLE 异常: RangeError (index): Invalid value: Not in inclusive range 0..2: 10
堆栈信息:
#0      List.[] (dart:core-patch/growable_array.dart:264)
#1      main (file:///...)
...
尝试执行操作...
出错: IntegerDivisionByZeroException
清理资源完成(finally 总是执行)

Der finally-Block eignet sich am besten für die Ressourcenbereinigung, wie das Schließen von Dateien, das Freigeben von Datenbankverbindungen usw. Selbst wenn es eine return-Anweisung in try oder catch gibt, wird finally trotzdem ausgeführt.


throw: Eine Exception auslösen

Mit dem Schlüsselwort throw können Sie aktiv eine Exception auslösen.

Dart erlaubt es, jedes Objekt als Exception zu werfen (nicht nur Unterklassen von Exception), aber die bewährte Vorgehensweise ist, Unterklassen von Exception oder Error zu werfen.

Beispiel

// Benutzerdefinierte Exception-Klasse
class InvalidAgeException implements Exception {
  final int age;
  final String message;

  InvalidAgeException(this.age)
      : message = Ungültiges Alter: $age (Alter muss zwischen 0 und 150 liegen);

  @override
  String toString() => 'InvalidAgeException: $message';
}

// Funktion, die eine benutzerdefinierte Exception verwendet
void validateAge(int age) {
  if (age < 0) {
    throw InvalidAgeException(age);
  }
  if (age > 150) {
    throw InvalidAgeException(age);
  }
  print(Alter $age erfolgreich validiert);
}

// Eingebaute Exception auslösen
int divide(int a, int b) {
  if (b == 0) {
    throw ArgumentError(Divisor darf nicht 0 sein);  // Eingebaute Exception verwenden
  }
  return a ~/ b;
}

void main() {
  // Benutzerdefinierte Exception testen
  try {
    validateAge(-5);
  } catch (e) {
    print(e);
  }

  try {
    validateAge(200);
  } catch (e) {
    print(e);
  }

  validateAge(25);  // Normalfall

  // Auslösen einer integrierten Exception testen
  try {
    divide(10, 0);
  } catch (e) {
    print(BEISPIEL Fehler: $e);
  }
}
InvalidAgeException: 无效的年龄: -5(年龄必须在 0 到 150 之间)
InvalidAgeException: 无效的年龄: 200(年龄必须在 0 到 150 之间)
年龄 25 验证通过
EXAMPLE 错误: Invalid argument(s): 除数不能为 0

Der Unterschied zwischen Exception und Error: Exception ist ein vorhersehbarer Fehler, den das Programm behandeln kann (z. B. Netzwerk-Timeout, Datei nicht gefunden); Error ist ein unvorhersehbares Problem, das normalerweise auf einen Programmfehler hinweist (z. B. Typfehler, Nullzeiger). Ihr Code sollte Exception abfangen, nicht Error.


on: Einen angegebenen Typ abfangen

Das Schlüsselwort on kann Ausnahmen nach Typ abfangen, was eine granularere Fehlerbehandlung ermöglicht.

Beispiel

void main() {
  // Unterschiedliche Ausnahmen nach Typ abfangen
  try {
    // Verschiedene Arten von Ausnahmen simulieren
    int result = performOperation('divide_by_zero');
    print(Ergebnis: $result);
  } on IntegerDivisionByZeroException {
    // Nur Division-durch-Null-Ausnahme abfangen
    print(BEISPIEL Fehler: Division durch Null nicht möglich);
  } on FormatException catch (e) {
    // Format-Ausnahme abfangen und Ausnahmeinformationen abrufen
    print('BEISPIEL Formatfehler: ${e.message}');
  } on RangeError {
    // Nur Bereichsfehler abfangen
    print('BEISPIEL-Fehler: Index außerhalb des Bereichs');
  } catch (e) {
    // Fallback: alle anderen Ausnahmen abfangen
    print('Unbekannter Fehler: $e');
  } finally {
    print('Operation beendet\n');
  }

  // Beispiel für Übereinstimmung in Reihenfolge
  print('--- Verschiedene Operationen testen ---');
  for (var op in ['divide_by_zero', 'parse_error', 'out_of_range', 'normal']) {
    try {
      performOperation(op);
    } on IntegerDivisionByZeroException {
      print('$op -> Ausnahme wegen Division durch Null');
    } on FormatException {
      print('$op -> Format-Ausnahme');
    } on RangeError {
      print('$op -> Bereichs-Ausnahme');
    } catch (e) {
      print('$op -> andere Ausnahme: $e');
    }
  }
}

int performOperation(String operation) {
  switch (operation) {
    case 'divide_by_zero':
      return 10 ~/ 0;  // IntegerDivisionByZeroException auslösen
    case 'parse_error':
      int.parse('not_a_number');  // FormatException auslösen
      return 0;
    case 'out_of_range':
      var list = [1, 2, 3];
      return list[100];  // RangeError auslösen
    case 'normal':
      return 42;
    default:
      throw Exception('Unbekannte Operation');
  }
}
EXAMPLE 错误: 不能除以零
操作结束

--- 测试不同的操作 ---
divide_by_zero -> 除零异常
parse_error -> 格式异常
out_of_range -> 范围异常

Die Reihenfolge von on und catch ist wichtig: Dart gleicht Ausnahmetypen von oben nach unten ab. Spezifischere Ausnahmetypen sollten zuerst platziert werden, allgemeinere zuletzt. Wenn catch (e) am Anfang steht, werden die nachfolgenden on-Klauseln nie ausgeführt.


rethrow: Die Ausnahme erneut auslösen

Manchmal müssen Sie nach dem Abfangen einer Ausnahme eine Verarbeitung durchführen (z. B. Protokollierung) und die Ausnahme dann weiter nach oben propagieren lassen.

Das Schlüsselwort rethrow wird verwendet, um die aktuell abgefangene Ausnahme erneut auszulösen und dabei die ursprünglichen Stack-Informationen beizubehalten.

Beispiel

// Simuliere ein mehrschichtiges Aufrufszenario
void databaseOperation() {
  print(' Verbindung zur Datenbank wird hergestellt...');
  throw Exception('BEISPIEL Datenbankverbindung fehlgeschlagen');
}

void serviceLayer() {
  try {
    print(' Service-Schicht: Datenbankoperation wird aufgerufen');
    databaseOperation();
  } catch (e) {
    // Zuerst protokollieren, dann erneut auslösen
    print(' [Log] Datenbankoperationsausnahme: $e');
    rethrow;  // Ursprüngliche Ausnahme und Stack beibehalten, weiter nach oben propagieren
  }
}

void controllerLayer() {
  try {
    print('Controller-Schicht: Anfrage wird verarbeitet');
    serviceLayer();
  } catch (e, stackTrace) {
    // Äußerste Schicht fängt ab und behandelt
    print('Controller-Schicht hat eine Ausnahme abgefangen');
    print('Fehlermeldung: $e');
    print('Rückgabe an Benutzer: "Dienst vorübergehend nicht verfügbar, bitte später erneut versuchen"');
  }
}

void main() {
  controllerLayer();
  print('\n--- Unterschied zwischen rethrow und throw e ---');

  // Demonstriert den Unterschied zwischen rethrow und throw e
  try {
    try {
      throw FormatException('Ursprüngliche Ausnahme');
    } catch (e) {
      // throw e: setzt den Stacktrace zurück, wodurch die ursprüngliche Aufrufstelle verloren geht
      print('Verwendung von throw e (verliert den ursprünglichen Stacktrace)...');
      throw e;  // Stacktrace beginnt hier
    }
  } catch (e, stack) {
    print('Gefangen: $e');
    print('Stacktrace zeigt hierhin (nicht auf die ursprüngliche Stelle):\n$stack');
  }
}
控制器层: 处理请求
  服务层: 调用数据库操作
  正在连接数据库...
  [日志] 数据库操作异常: Exception: EXAMPLE 数据库连接失败
控制器层捕获到异常
错误信息: Exception: EXAMPLE 数据库连接失败
给用户返回: "服务暂时不可用,请稍后重试"

--- rethrow vs throw e 的区别 ---
使用 throw e(丢失原始堆栈)...
捕获: FormatException: 原始异常
堆栈指向这里(不是原始位置):
...

Der Unterschied zwischen rethrow und throw e ist entscheidend: rethrow bewahrt den vollständigen Stacktrace der ursprünglichen Ausnahme, wodurch sich die Grundursache leichter finden lässt; throw e setzt den Stack zurück und beginnt von der aktuellen Position aus neu. Wenn Sie die Ausnahme nach dem Protokollieren weiter propagieren müssen, verwenden Sie rethrow.


Bewährte Verfahren für die Ausnahmebehandlung

ÜbungBeschreibung
Fange nur Ausnahmen ab, die du behandeln kannst.Fange nicht blind alle Ausnahmen ab, nur um "Fehler zu vermeiden". Wenn du nicht weißt, wie du eine Ausnahme behandeln sollst, lass sie nach oben weiterreichen.
Nach Typ abfangenVerwende on, um den Ausnahmetyp anzugeben, und vermeide ein einzelnes catch, das alle Ausnahmen verschluckt.
finally: Ressourcen aufräumenRessourcen wie Dateien und Netzwerkverbindungen sollten in finally geschlossen werden.
Ausnahmen nicht verschluckenEine Ausnahme abzufangen und nichts zu tun (leerer catch-Block) ist gefährlich; protokolliere sie zumindest.
rethrow verwendenWenn du die Ausnahme protokollieren und weiter propagieren musst, verwende rethrow statt throw e.
Ausnahmemeldungen sollten spezifisch seinthrow Exception('Database connection failed: 192.168.1.1:3306') ist nützlicher als throw Exception('An error occurred')
Weitere Erweiterungen