EMZETT.
Login

Kurz: Zig hat keine Exceptions. Fehlbare Funktionen liefern eine Fehlerunion E!T (Fehler oder Wert); !T lässt den Compiler die Fehlermenge erschließen.

Teil des Kurses Zig

Kapitel 4 von 8 im Kurs Zig (Abschnitt „Robuster Code“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Fehler als Werte

Zig hat keine Exceptions. Fehlbare Funktionen liefern eine Fehlerunion E!T (Fehler oder Wert); !T lässt den Compiler die Fehlermenge erschließen.

const std = @import("std");
 
const RechenFehler = error{
    DivisionDurchNull,
    NegativeWurzel,
};
 
fn teile(a: i32, b: i32) RechenFehler!i32 {
    if (b == 0) return error.DivisionDurchNull;
    return @divTrunc(a, b);
}
 
fn wurzel(x: f64) RechenFehler!f64 {
    if (x < 0) return RechenFehler.NegativeWurzel;
    return @sqrt(x);
}
 
fn berechne(a: i32, b: i32) !i32 {
    const q = try teile(a, b);          // try: Fehler an den Aufrufer weitergeben
    return q * 2;
}
 
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    try out.print("{d}\n", .{try teile(10, 2)});
 
    const r = teile(1, 0) catch |err| blk: {         // catch: Fehler behandeln
        try out.print("Fehler: {s}\n", .{@errorName(err)});
        break :blk -1;
    };
    try out.print("{d}\n", .{r});
 
    try out.print("{d}\n", .{teile(9, 0) catch 0});    // Standardwert
 
    if (wurzel(-4)) |w| {
        try out.print("{d}\n", .{w});
    } else |err| {
        try out.print("wurzel scheitert: {s}\n", .{@errorName(err)});
    }
 
    if (berechne(5, 0)) |_| {
        try out.print("kein Fehler\n", .{});
    } else |e| switch (e) {                          // Fehler in switch prüfen
        error.DivisionDurchNull => try out.print("durch null!\n", .{}),
        else => unreachable,
    }
 
    const zahl = std.fmt.parseInt(u8, "300", 10) catch |e| {
        try out.print("parseInt: {s}\n", .{@errorName(e)});
        return;
    };
    try out.print("nie erreicht {d}\n", .{zahl});
}

Ausgabe:

5
Fehler: DivisionDurchNull
-1
0
wurzel scheitert: NegativeWurzel
durch null!
parseInt: Overflow

Wird ein Fehler bis zu main durchgereicht, beendet Zig das Programm mit einer Fehlermeldung samt Stack-Trace (im Debug-Modus). Der Compiler zwingt dich, jeden Fehler zu behandeln oder weiterzugeben.

defer und errdefer

defer führt Code am Ende des Gültigkeitsbereichs aus, ideal fürs Aufräumen. errdefer nur, wenn der Bereich mit einem Fehler verlassen wird:

const std = @import("std");
 
fn arbeite(out: anytype, fehler: bool) !void {
    try out.print("öffne Ressource\n", .{});
    defer out.print("schließe Ressource\n", .{}) catch {};
    errdefer out.print("Aufräumen nach Fehler\n", .{}) catch {};
 
    try out.print("arbeite ...\n", .{});
    if (fehler) return error.Kaputt;
    try out.print("fertig\n", .{});
}
 
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    arbeite(out, false) catch {};
    try out.print("--\n", .{});
    arbeite(out, true) catch |e| try out.print("Fehler: {s}\n", .{@errorName(e)});
 
    // defer-Reihenfolge: rückwärts
    {
        defer out.print("A ", .{}) catch {};
        defer out.print("B ", .{}) catch {};
        defer out.print("C\n", .{}) catch {};
    }
}

Ausgabe:

öffne Ressource
arbeite ...
fertig
schließe Ressource
--
öffne Ressource
arbeite ...
Aufräumen nach Fehler
schließe Ressource
Fehler: Kaputt
C
B A

Dateien

const std = @import("std");
 
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    var dir = std.fs.cwd();
 
    {
        const datei = try dir.createFile("demo.txt", .{});
        defer datei.close();
        try datei.writeAll("Zeile 1\nZeile 2\nZeile 3\n");
    }
 
    var puffer: [256]u8 = undefined;
    const datei = try dir.openFile("demo.txt", .{});
    defer datei.close();
    const n = try datei.readAll(&puffer);
    try out.print("{d} Bytes\n", .{n});
 
    var zeilen = std.mem.splitScalar(u8, puffer[0..n], '\n');
    var zaehler: usize = 0;
    while (zeilen.next()) |z| {
        if (z.len == 0) continue;
        zaehler += 1;
        try out.print("{d}: {s}\n", .{ zaehler, z });
    }
 
    const fehler = dir.openFile("gibt_es_nicht.txt", .{});
    try out.print("{any}\n", .{fehler});
    try dir.deleteFile("demo.txt");
}

Ausgabe:

24 Bytes
1: Zeile 1
2: Zeile 2
3: Zeile 3
error.FileNotFound

Sicherheit: Panic, unreachable, assert

const std = @import("std");
 
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const arr = [_]i32{ 1, 2, 3 };
    var i: usize = 0;
    while (i < arr.len) : (i += 1) {
        try out.print("{d} ", .{arr[i]});
    }
    try out.print("\n", .{});
    std.debug.assert(arr.len == 3);                // bricht im Debug-Modus ab, wenn falsch
    // arr[5] wäre ein Panic: index out of bounds
    // @panic("Selbst ausgelöst");
    // unreachable: behauptet "hier kommt man nie hin"
    try out.print("alles in Ordnung\n", .{});
}

Ausgabe:

1 2 3
alles in Ordnung

Merke

  • Fehler sind Werte: error{...}, E!T, try (weiterreichen), catch (behandeln)
  • if (res) |wert| ... else |err| ... und switch auf Fehler
  • defer räumt am Blockende auf, errdefer nur bei Fehler
  • Dateien: std.fs.cwd().createFile/openFile, immer defer datei.close()
  • Debug-Modus fängt Überläufe, Grenzfehler und unreachable ab

Übungsaufgabe

Schreibe eine Funktion parseAlter(text) !u8, die bei ungültiger Eingabe einen eigenen Fehler liefert.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Funktionen, Arrays, Slices und Strings

Weiter: Structs, Enums, Unions und Speicherverwaltung

Alle Kapitel: Zig im Überblick