defer dan errdefer

Menjadwalkan pembersihan deterministik pada setiap jalur keluar fungsi.

Daftar isi

Sumber daya yang diperoleh program, seperti memori yang dialokasikan atau berkas yang dibuka, wajib dilepaskan kembali, dan Zig menjamin pelepasan itu melalui defer serta errdefer. Topik ini membahas penjadwalan pembersihan pada setiap jalur keluar fungsi, urutan eksekusinya yang terbalik, serta pembagian tugas antara defer yang selalu berjalan dan errdefer yang hanya berjalan saat gagal. Setelah membacanya, pembersihan tidak lagi tersebar di setiap jalur kembalian, melainkan tertulis satu kali tepat di samping perolehan sumber dayanya.

Konsep#

defer menunda sampai keluar blok#

Pernyataan defer menjadwalkan satu ekspresi untuk dieksekusi saat eksekusi keluar dari blok tempat defer itu ditulis, apapun cara keluarnya. Cara keluar mencakup akhir fungsi yang normal, pernyataan return, maupun penyebaran error, sehingga tidak ada jalur yang melewati pembersihan. Istilah deterministik di sini berarti waktunya pasti dan dapat dibaca dari kode, berbeda dari pengumpul sampah yang menjalankan pembersihan pada waktu yang tidak dapat diprediksi.

zig urutan-defer.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("buka arsip\n", .{});
    defer std.debug.print("tutup arsip\n", .{});
    {
        std.debug.print("buka sesi hitung\n", .{});
        defer std.debug.print("tutup sesi hitung\n", .{});
        std.debug.print("menghitung rekap\n", .{});
    }
    std.debug.print("pekerjaan selesai\n", .{});
}
text Keluaran
buka arsip
buka sesi hitung
menghitung rekap
tutup sesi hitung
pekerjaan selesai
tutup arsip

Perhatikan bahwa tutup sesi hitung dicetak lebih dulu dibandingkan tutup arsip, meskipun kedua defer ditulis pada fungsi yang sama. Blok kurung kurawal di tengah fungsi adalah cakupannya sendiri, sehingga defer yang berada di dalamnya dieksekusi begitu blok itu selesai, jauh sebelum fungsi berakhir. Pola ini berguna untuk pembersihan yang masa hidupnya lebih pendek dari fungsi, misalnya sesi hitung sementara di dalam proses yang lebih panjang.

Urutan eksekusi terbalik#

Apabila beberapa defer terdaftar pada blok yang sama, seluruhnya dieksekusi berlawanan dengan urutan pendaftarannya, yaitu pola LIFO (last in, first out). Yang terakhir didaftarkan berjalan lebih dulu, dan yang pertama didaftarkan berjalan paling akhir. Diagram berikut merangkum hubungan antara urutan pendaftaran dan urutan eksekusi tersebut.

Pendaftaran saat fungsi berjalan1defer lepas A2defer lepas Bterdaftar dari A menuju Bfungsi keluarEksekusi saat fungsi keluar1lepas B2lepas Aurutan dibalik, yang terakhir didaftarkan duluan
Alur defer: pendaftaran berurutan, eksekusi terbalik saat fungsi keluar

Urutan terbalik ini bukan keanehan, melainkan konsekuensi dari ketergantungan antar sumber daya. Sumber daya yang diperoleh belakangan sering memakai sumber daya yang lebih dulu, sehingga membongkarnya harus dimulai dari yang paling akhir diperoleh. Berkas catatan yang ditulis ke dalam arsip, misalnya, selesai dan ditutup lebih dulu sebelum arsipnya sendiri ditutup.

errdefer untuk jalur gagal#

Pernyataan errdefer mirip dengan defer, tetapi ekspresinya hanya dieksekusi apabila fungsi keluar dengan mengembalikan error, sedangkan defer tetap berjalan pada semua jalur keluar. Pasangan keduanya cocok untuk sumber daya yang nasibnya berbeda antara sukses dan gagal, misalnya sumber daya yang dibebaskan saat sukses tetapi dibatalkan sekaligus dilepas saat gagal.

zig bongkar-muat.zig
const std = @import("std");

fn muat(gagal: bool) error{GagalMuat}!void {
    std.debug.print("sumber daya pertama siap\n", .{});
    defer std.debug.print("lepas sumber daya pertama\n", .{});
    errdefer std.debug.print("batalkan sumber daya pertama\n", .{});

    std.debug.print("sumber daya kedua siap\n", .{});
    defer std.debug.print("lepas sumber daya kedua\n", .{});

    if (gagal) {
        return error.GagalMuat;
    }
    std.debug.print("muat selesai\n", .{});
}

pub fn main() void {
    muat(false) catch {};
    std.debug.print("---\n", .{});
    muat(true) catch |err| {
        std.debug.print("ditangkap: {}\n", .{err});
    };
}
text Keluaran
sumber daya pertama siap
sumber daya kedua siap
muat selesai
lepas sumber daya kedua
lepas sumber daya pertama
---
sumber daya pertama siap
sumber daya kedua siap
lepas sumber daya kedua
batalkan sumber daya pertama
lepas sumber daya pertama
ditangkap: error.GagalMuat

Jalur pertama berakhir sukses, sehingga kedua defer berjalan dan errdefer diabaikan sama sekali. Jalur kedua berakhir dengan error, sehingga seluruh penjadwal yang sudah terdaftar dieksekusi berlawanan urutan, yaitu defer kedua, lalu errdefer pertama, lalu defer pertama. Pernyataan errdefer pada contoh ini tidak membaca error yang dikembalikannya, dan capture berbentuk errdefer |err| sudah tidak tersedia pada 0.17.0, sehingga pencatatan nama error dilakukan pada catch milik pemanggil.

Contoh Lengkap#

Pola dua sumber daya dengan try#

Gabungan try, defer, dan errdefer menghasilkan pola baku untuk fungsi yang membuka lebih dari satu sumber daya. Setiap kali satu sumber daya berhasil diperoleh, tuliskan defer dan errdefer miliknya tepat setelahnya, lalu perolehan berikutnya dilakukan dengan try. Apabila perolehan selanjutnya gagal, try menyebarkan error keluar fungsi, dan seluruh penjadwal yang sudah terdaftar tetap dieksekusi karena penyebaran itu pun adalah salah satu jalur keluar.

zig Contoh: try memicu pembersihan yang sudah terdaftar
// tidak-dijalankan
const arsip = try buka("arsip");
defer tutup(arsip);
errdefer batalkan(arsip);

const log = try buka("log");
defer tutup(log);

Apabila buka("log") gagal, sumber daya arsip tetap ditutup oleh defer dan dibatalkan oleh errdefer yang terdaftar sebelum titik gagal itu. Tidak ada jalur yang lupa, karena penjadwalan tertulis di samping perolehannya dan kompilator yang mengingatkan urutannya. Pola ini menghilangkan blok pembersihan yang disalin-tempel ke setiap titik gagal, yang merupakan sumber kebocoran paling lazim pada bahasa tanpa penjadwal serupa.

Kesalahan umum#

Kesalahan pertama adalah berharap errdefer berjalan pada jalur sukses. Pada jalur sukses, errdefer diabaikan sepenuhnya, sehingga sumber daya yang memang harus dibebaskan saat sukses wajib dijadwalkan dengan defer. Apabila keduanya ditulis, perlakukan defer sebagai pembebasan dan errdefer sebagai pembatalan yang khusus menyertai jalur gagal.

Kesalahan kedua adalah mengira defer menunggu akhir fungsi, padahal batas cakupannya adalah blok tempat ia ditulis. Pembersihan bisa terjadi jauh lebih awal dari yang diharapkan apabila defer berada di dalam blok kurung kurawal kecil, misalnya blok if atau blok sementara. Periksa batas bloknya sebelum menyimpulkan waktu pelepasan, dan pindahkan pernyataannya ke blok yang lebih luas apabila masa hidup sumber dayanya memang harus lebih panjang.

Ringkasan#

  • defer mengeksekusi ekspresinya saat eksekusi keluar dari blok tempat ia ditulis, pada semua jalur keluar termasuk jalur error.
  • Beberapa defer dieksekusi berlawanan urutan pendaftarannya, yaitu pola LIFO, karena sumber daya yang diperoleh belakangan sering bergantung pada yang lebih dulu.
  • errdefer hanya dieksekusi saat fungsi mengembalikan error, sehingga pasangan defer dan errdefer memisahkan pembebasan dari pembatalan.
  • Pola try diikuti pasangan penjadwal di samping setiap perolehan menjamin tidak ada jalur yang lupa membersihkan sumber daya.

Referensi#