オフィスアワーがそろそろ始まるよ!()

defer/errdefer

defer / errdeferを使うことで、特定の処理をスコープを抜けるタイミングまで遅延させることができます。defer / errdeferはしばしばリソース解放に使用されます。defer / errdeferを使用し、リソース解放処理を容易に記述することができます。

defer

deferは特定の処理をそのスコープを抜けるタイミングで実行します。deferキーワードから始まり、式;またはブロック ({}) が続きます。

次のコードでは、Aを出力するdefer文の方が、Bを出力する文より前に書かれています。

examples/ch04-basic-syntax/src/defer.zen:5:8

test "defer" {
    defer std.debug.warn("A\n", .{});
    std.debug.warn("B\n", .{});
}

それにも関わらず、このコードの実行結果は次の通り、Bが先に出力されます。

B
A

複数のdefer文がある場合、より後ろに書かれているdeferから逆順に実行されます。

examples/ch04-basic-syntax/src/defer.zen:10:15

test "execution order of defers" {
    defer std.debug.warn("A\n", .{});
    defer std.debug.warn("B\n", .{});
    defer std.debug.warn("C\n", .{});
    defer std.debug.warn("D\n", .{});
}

実行結果は、次の通りです。

D
C
B
A

deferの一番の使いみちは、リソース解放です。例えば、次のコードを考えてみます。これは特に意味のあるコードではありませんが、メモリリークがどのような状況で発生するかを説明するには十分です。メモリアロケータについて8章 メモリ管理で説明していますが、ここではconst allocated = try heap.create(allocator, u32);でヒープ領域のメモリを動的確保し、heap.destroy(allocator, allocated);で確保したメモリ領域を解放していることだけ、頭の片隅に置いておいて下さい。

examples/ch04-basic-syntax/src/defer.zen:17:28}}

fn memoryLeak(input: u64) !void {
    const allocator = heap.page_allocator;
    const allocated = try heap.create(allocator, u32);

    if (input == 0) {
        // `allocated`のメモリ解放漏れ!
        return error.IsZero;
    }
    // 何かやる
    heap.destroy(allocator, allocated);
    return;
}

このように、メモリの確保と解放とが離れた場所にあると、容易にメモリリークが発生してしまいます。

そこで、deferを使って、メモリ確保のコードの直後にメモリ解放処理を書きます。

examples/ch04-basic-syntax/src/defer.zen:34:50

fn releaseWithDefer(input: u64) !void {
    const allocator = heap.page_allocator;
    // メモリ確保の直後にdeferで解放処理を書く
    const allocated = try heap.create(allocator, u64);
    defer {
        heap.destroy(allocator, allocated);
        std.debug.warn("heap destroyed!\n", .{});
    }

    if (input == 0) {
        // スコープを抜ける時に`defer`が実行される
        return error.IsZero;
    }
    // 何かやる
    // スコープを抜ける時に`defer`が実行される
    return;
}

次のテストを書いて、コードを実行してみましょう。

examples/ch04-basic-syntax/src/defer.zen:52:54

test "releaseWithDefer" {
    releaseWithDefer(0) catch |_| {};
}

次のメッセージが出力されるはずです。

heap destroyed!

複数行にわたらない場合は、次のように書くこともできます。

defer heap.destroy(allocator, allocated);

errdefer

errdeferdeferと似ていますが、関数がエラーを返す場合のみ実行される点が、deferと異なります。

例えば、リソースを確保し、リソースに対して何らかの処理を行ってから、そのリソースを上位関数に返すことを考えます。確保したリソースに対する処理は、失敗する場合があります。

このような典型的な例は、ネットワークにおけるソケット通信の開始でしょう。以下に、Zenの標準ライブラリstd.net.connectUnixSocket関数を簡略化した例を示します (このコードはコンパイルできません) 。

/// simplified std.net.connectUnixSocket
const os = std.os;
fn connectUnixSocket(path: []u8) !std.fs.File {
    // ソケットファイルをオープンする
    const sockfd = try os.socket(
        os.AF_UNIX,
        os.SOCK_STREAM | os.SOCK_CLOEXEC | os.SOCK_NONBLOCK,
        0,
    );
    errdefer os.close(sockfd);

    var sock_addr = os.sockaddr{
        // ...
    };
    // 中略
    const size = @intCast(u32, @sizeOf(os.sa_family_t) + path.len);
    try os.connect(sockfd, &sock_addr, size);

    return std.fs.File.openHandle(sockfd);
}

まずソケットのファイルをオープンし (os.socket) 、その後、ソケットを接続しています (os.connect) 。接続できると、ファイルはオープンしたまま、そのファイルディスクリプタのハンドラを上位関数に返す、というのが標準のフローです。

しかし、os.connectは失敗し、エラーを返す場合があります。その場合、オープンしたファイルはクローズしなければなりません。繰り返しますが、標準フローの場合は、ファイルをオープンにしたままにしなければなりません。

そこでerrdeferの登場です。errdefer os.close(sockfd);を書いておくことで、os.connectが失敗した場合のみ、ファイルをクローズします。

errdefer ブロックでは、戻り値となるエラー種別をキャプチャすることができます。

examples/ch04-basic-syntax/src/errdefer.zen:4:13

fn foo() !i32 {
    errdefer |err| {
        ok(err == error.One);
    }
    return error.One;
}

test "errdefer with payload" {
    _ = foo() catch return;
}

Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8

Chapter 9

Chapter 10

Chapter 11

Chapter 12

Chapter 13

Chapter 14

Chapter 15

Appendix

Error Explanation

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.