ざっくりZig - エラー(error, try, catch, if...else, ビルトイン関数)

2024/04/24に公開

Zigのmain関数を書くとき、戻り値はvoidu8になるのですが、実際には!void!u8にすることのほうが多いかもしれません。この!は関数の処理中にエラーが発生する可能性があることを表しています。また、関数を実行するときtryを先頭に書くことがあります。これも関数を実行するとエラーが発生する可能性があることを表しています。ここではZig行うエラー処理を紹介していきます。

エラーの実体

Zigではエラーを表すものとしてerror, .....Error, anyerror, @compileErrorがあります。それぞれの意味は以下の通りです。

  • error - error.OutOfMemoryerror.InvalidCharacterなど個別のエラー
  • .....Error - std.fmt.ParseIntErrorstd.fs.File.OpenErrorなど複数のerrorを特定の分類のもと集めて定義されたエラーセット型で、関数の戻り値で利用される
  • anyerror - すべてのエラーを表す型で、関数の戻り値でanyerror!u8などとするときに利用される

エラーの定義

エラーはerror.Somethingのようにerror.に名称を続けて定義します。関数内でエラーが起きた時にreturn error.Something;を実行し、呼び出し元にエラーが起きたことを知らせることができます。その場合、関数の戻り値の型名はerror{Something}となり、実行時エラーを表します。

エラーを返す関数
fn f() error{Something} {
    return error.Something;
}

関数からエラーを返す

関数が実行されると、正常に処理されたらu8などの戻り値を返しますが、処理中にエラーが起きて処理が終了することもあります。その場合は関数の戻り値をerror{Something}!u8のように定義します。これをエラー共用型(Error Union Type)といいます。エラーの型を示さず!u8のように示すこともできます。

戻り値をエラー共用型とする場合
fn f(n: u8) error{Something}!u8 {
    if (n == 0) return n;
    return error.Something;
}

こうした関数の戻り値を出力するとき{!}を使うと正常に処理された場合はその値、そうでなければエラーが出力されます。

エラー共用型の出力
try stdout.print("f(0) = {!}\n", .{f(0)});
try stdout.print("f(1) = {!}\n", .{f(1)});

// 結果
f(0) = 0
f(1) = error.Something

エラーセット型

エラーセット型は複数のエラーを1つにまとめて新たな型とするものです。以下のようにerror {...}で複数のエラーをまとめて代入します。

// error.InvalidValue, error.OutOfMemoryを持つエラーセット型AppErrorを定義
const AppError = error{
    InvalidValue,
    OutOfMemory
};

エラーセット型は複数を結合して新たなエラーセット型を作成できます。

エラーセット型の結合
const AppError = error{
    InvalidValue,
    OutOfMemory
};
const FileError = error{
    NotFound,
    DeviceBusy
};
const AccessError = AppError || FileError;  // エラーセット型の結合

エラーセット型も関数の戻り値の宣言に組み込んでエラー共用型にできます。

エラーセット型によるエラー共用型
fn f(n: usize) AppError!usize {
    if (n == 1) return error.InvalidValue;
    if (n == 2) return error.OutOfMemory;
    return n;
}

エラー処理

関数を実行してエラーが起きた時、try, catch, if...elseのいずれかで処理できます。

try

tryはエラーでないときは結果を返し、エラーの時はエラーを出力して処理を中止します。関数の戻り値はエラー共用型とします。

tryによるエラー処理
_ = try f(0);   // エラーではないので結果を返す
_ = try f(1);   // エラーで中止

catch

catchはエラーが起きた後の処理を定義します。起きたエラーの内容を取得できますので、switchでエラーの内容ごとに処理を振り分けられます。また、catchの後にreturn, break, continueなどを続けることもできます。

catchによるエラー処理
// エラーではないのでcatch以降は実行されない
_ = f(0) catch |err| ...;
// errでerror.....を取得し、処理に利用できる
_ = f(1) catch |err| try stdout.print("f({}) catch {}\n", .{ n, err });

// 結果
f(1) catch error.InvalidValue
f(2) catch error.OutOfMemory
catchでエラーの内容ごとに処理を振り分け
for (0..3) |n| {
    _ = f(n) catch |err| switch (err) {
        error.InvalidValue => try stderr.print("f({}) 値が間違っています\n", .{n}),
        error.OutOfMemory => try stderr.print("f({}) メモリが足りません\n", .{n}),
    };
}

// 結果 - f(0)は正常に処理されるのでメッセージは出力されない
f(1) 値が間違っています
f(2) メモリが足りません

if...else

if...elseは関数から正常に値が戻されたときはif ... |r| {...}の方の処理を実行します。rは関数から戻された値を表します。エラーが起きた時はelse |err| {...}の方の処理を実行します。errは関数から戻されたエラーを表します。

if...elseでエラーの内容ごとに処理を振り分け
for (0..3) |n| {
    if (f(n)) |r| {
        // 値が返ってきたときの処理
        try stdout.print("f({}) = {}\n", .{ n, r });
    } else |err| {
        // エラーが起きた時の処理
        try stderr.print("f({}) ", .{n});
        switch (err) {
            error.InvalidValue => try stderr.print("値が間違っています\n", .{}),
            error.OutOfMemory => try stderr.print("メモリが足りません\n", .{}),
        }
    }
}

// 結果
f(0) = 0
f(1) 値が間違っています
f(2) メモリが足りません

エラーに関連するビルトイン関数

エラーに関連するビルトイン関数(Builtin Functions)は

関数 引数 戻り値 処理
@intFromError エラー 符号なし整数 エラーから数値を取得
@errorFromInt 符号なし整数 エラー 数値からエラーを取得
@errorName エラー error.の後に続く名称 エラーから名称を取得
@panic メッセージ 処理停止(noreturn) panic状態で終了
@compileError メッセージ 処理停止(noreturn) コンパイルエラー
@errorReturnTrace[1] なし スタックトレース(?*std.builtin.StackTrace) スタックとレースを取得
@errorCast[2] 変換前のエラー 変換後のエラー エラーの型を変換

@intFromError, @errorFromInt

@intFromError関数はエラーに対し自動的に割り当てられた符号なし整数(16ビット)の値を取得します。@errorFromInt関数は逆にその整数からエラーを取得します。

エラーに割り当てられる整数
const err = error.Something;
const ife = @intFromError(err);
const efi = @errorFromInt(ife);

try stdout.print("@intFromError({}) = {}\n", .{ err, ife });
try stdout.print("@errorFromInt({}) = {}\n", .{ ife, efi });

// 結果(環境により異なる)
@intFromError(error.Something) = 19
@errorFromInt(19) = error.Something

@errorName

@errorName関数はerror.SomethingSomethingの部分を文字列で取得します。

エラーの名称を取得
const err = error.Something;
try stdout.print("@errorName({}) = {s}\n", .{ err, @errorName(err) });

// 結果
@errorName(error.Something) = Something

@panic

@panic関数は引数のメッセージを出力し、panic状態で処理を中止します。

@panicで処理終了
fn f() void {
    @panic("panic from f().");
}

pub fn main() !void {
    f();
}

// 処理終了(番号は環境により異なる)
thread 17112 panic: panic from f().
.....

@compileError

@compileError関数はコンパイルエラーを表します。コンパイル時点でこの関数に遭遇したら引数のメッセージを出力しコンパイル(zig build-exe, zig run , zig testなど)を中止します。バージョンアップにより廃止した処理、実装予定の処理、実行環境が未対応など、コンパイル不可であることを明示するときに使用します。

@compileErrorでコンパイルエラーを明示
fn f() void {
    @compileError("Not Implemented.");
}

pub fn main() !void {
    f();
}
コンパイル時
$ zig build-exe error_example.zig
error_example.zig:2:5: error: Not Implemented.
    @compileError("Not Implemented.");
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.....

まとめ

  • エラーはerror.Somethingのように定義し、関数内でreturn error.Something;を実行すると呼び出し元にエラーが起きたことが知らされる
  • 関数がエラーを返す場合は戻り値の型名をerror{Something}とするが、本来の戻り値の型が別にあるときはerror{Something}!u8のようなエラー共用型(Error Union Type)で表す
  • エラー共用型を戻り値とする関数の結果を出力するとき{!}を使うと正常に処理された結果かエラーのどちらかが出力される
  • 複数のエラーを1つにまとめたエラーセット型(Error Set Type)を定義できる
  • 2つのエラーセット型を結合してあらたなエラーセット型を定義できる
  • エラー後の処理はtry, catch, ifで実行できる
  • tryは関数から正常に値が戻されたときは処理を続行し、エラーの時は処理を中止する(関数の戻り値はエラー共用型)
  • catchはエラーを取得し、そのあとの処理を定義する
  • if...elseは関数がエラーとなったときelse |err| {...}の処理を行う
  • エラーに関連するビルトイン関数(@intfromErrorなど)が定義されている

< 関数
ざっくりZig 一覧

脚注
  1. https://ziglang.org/documentation/master/#errorReturnTrace ↩︎

  2. https://ziglang.org/documentation/master/#errorCast ↩︎

Discussion