Zig 入門 #8 - Errors

2022-05-06  /  ZigProgramming

ziglearn.org を参考に Zig の基本を一通りさらってみます。今回は Chapter 1 - Basics | ziglearn.org からErrors について。

Errors

参考: Errors

Zig のエラーの扱いは特殊だなぁという印象です。

Error Set

Zig でエラーを定義するには Error Set を定義します。Enum のような構文で記述します。

const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

ある Error Set が別の Error Set のサブセット(要素が同じか一部分)であるときは、スーパーセットと合わせて扱うことができます。

const AllocationError = error{OutOfMemory};
// FileOpenError ⊇ AllocationError として扱われる
const err: FileOpenError = AllocationError.OutOfMemory;

// どちらの `OutOfMemory` も同じ
std.log.info("Same error: {}", .{err == FileOpenError.OutOfMemory});
std.log.info("Same error: {}", .{FileOpenError.OutOfMemory == AllocationError.OutOfMemory});

マージ

Error Set はマージすることができます。

const A = error{ NotDir, PathNotFound };
const B = error{ OutOfMemory, PathNotFound };
const C = A || B;

anyerror

anyerror は全てのエラーセットのスーパーセットになるエラーセットになります。この型を通常使用するのは避けた方が良いようです。

Error Union Type

! を利用して Error Setと他の型を合わせた Error Union Typeを定義することができます。関数型言語は詳しくないのですが Either のようなものでしょうか。

// AllocationError か u16 のどちらかという型(error union型)
var mayby_error: AllocationError!u16 = 10;

関数の戻り値も Error Union Type で定義することができます。

// 関数の戻値を error union として定義できる
fn failingFunction() error{Oops}!void {
    // error しか返さないので !void となる
    return error.Oops;
}

Catch

Error Union Type はそのままではエラーか値か不明なので catch を利用して値を取得する必要があります。

// errorとのユニオン型は catch を用いて unwrap する
// catch の後にエラーだった際の値を指定する
const no_error = mayby_error catch 0;

// エラーではないので 10
std.log.info("no_error: {}", .{no_error});

// errorを代入する
mayby_error = AllocationError.OutOfMemory;
const catched_error = mayby_error catch 100;

// エラーだったので catch で指定された 100
std.log.info("no_error: {}", .{catched_error});

Payload Capturing

catch の際にエラーを利用するためには payload capturing を行います。 catch |err| ... のように記述し err の中にエラーが代入されます。payload capturing で指定する変数が上位のスコープで定義されている場合には、それが使用されます。なので既存の var で定義された再代入可能な変数である必要があります。未定義であれば、そのcatch のみで利用可能なスコープの変数になります。

// err はすでに const で定義済みなので再代入できずコンパイルエラーになる
// _ = mayby_error catch |err| {
//     std.log.info("error: {}", .{err});
// };

_ = mayby_error catch |captured_err| {
    std.log.info("error: {}", .{captured_err});
    // info: error: error.OutOfMemory
};

// captured_err は2回目だが catch でスコープが閉じているため再利用できる
_ = mayby_error catch |captured_err| {
    std.log.info("error(2回目): {}", .{captured_err});
    // info: error: error.OutOfMemory
};

catch の後のブロック

catch |err| の直後にはブロック {} を記述することが可能ですが if などと同等のブロックであるため return を記述すると catchが属している上位のブロックの制御がそこで返ることになります。

fn catchError() void {
    failingFunction() catch |err| { // ← payload capturing
        std.log.info("Oops: {}", .{err == error.Oops});  // true
        // catch のブロックから return は関数自体の return になる
        return;
    };

    // ここには到達しない
    std.log.info("unreached", .{});
}

またブロックを利用しつつ値を返したい場合には catch label: { break :lable value } という構文を利用します。いくつか GitHub の Issue など見ていたのですが、ラベルには blk を用いられているようです。(簡略化して記述できるようになるとうれしいですが)

const returned_value = mayby_error catch |captured_err| blk: {
    std.log.info("error(3回目): {}", .{captured_err});
    break :blk 1000;
};
std.log.info("returned_value: {}", .{returned_value});
// info: returned_value: 1000

Try

tryx catch |err| return err の糖衣構文になります。

fn tryError() error{Oops}!i32 {
    // try は catch したエラーをそのまま返す syntax sugar
    // failingFunction() catch |err| { return err; };
    try failingFunction();

    // ↑ error.Oops を返しているのでここは到達しない
    return 12;
}

もちろんエラーが無ければ通常の値を返します。

const t: anyerror!u8 = 10;
// try は xcath |err| return err なのでエラーで無ければ通常の値となる
const v = try t;
std.log.info("v: {}", .{v});
// info: v: 10

Error Defer

errordeferdefer と似ていますがエラーが返されるときのみ実行されます。

var problem: u32 = 98;
// ! の前の error の型は返される型から推論される
fn errorDefer() !u32 {
    errdefer problem += 1;
    try failingFunction();

    // ↑ error.Oops を返しているのでここは到達しない
    return 1;
}
_ = errorDefer() catch blk: {
    // errodefer で +1 されているので 99 になる
    std.log.info("problem: {}", .{problem});
    break :blk 100;
};

今回使用したコードはこちらです。