ざっくりZig - エラー(error, try, catch, if...else, ビルトイン関数)
Zigのmain関数を書くとき、戻り値はvoidやu8になるのですが、実際には!voidや!u8にすることのほうが多いかもしれません。この!は関数の処理中にエラーが発生する可能性があることを表しています。また、関数を実行するときtryを先頭に書くことがあります。これも関数を実行するとエラーが発生する可能性があることを表しています。ここではZig行うエラー処理を紹介していきます。
エラーの実体
Zigではエラーを表すものとしてerror, .....Error, anyerror, @compileErrorがあります。それぞれの意味は以下の通りです。
- 
error-error.OutOfMemoryやerror.InvalidCharacterなど個別のエラー - 
.....Error-std.fmt.ParseIntErrorやstd.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 f(0);   // エラーではないので結果を返す
_ = try f(1);   // エラーで中止
catch
catchはエラーが起きた後の処理を定義します。起きたエラーの内容を取得できますので、switchでエラーの内容ごとに処理を振り分けられます。また、catchの後にreturn, break, continueなどを続けることもできます。
// エラーではないので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
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は関数から戻されたエラーを表します。
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.SomethingのSomethingの部分を文字列で取得します。
const err = error.Something;
try stdout.print("@errorName({}) = {s}\n", .{ err, @errorName(err) });
// 結果
@errorName(error.Something) = Something
@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など)を中止します。バージョンアップにより廃止した処理、実装予定の処理、実行環境が未対応など、コンパイル不可であることを明示するときに使用します。
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など)が定義されている 
< 関数
関数の終了処理(defer, errdefer, unreachable, @panic) >
ざっくりZig 一覧
Discussion