defer

Pembersihan deterministik saat lingkup berakhir, tanpa exception.

Daftar isi

defer menjadwalkan satu panggilan proc agar dijalankan saat lingkup tempat ia ditulis berakhir, baik karena alur normal maupun karena keluar lebih awal. Perangkat ini adalah cara Odin melakukan pembersihan deterministik tanpa exception, dan kuncinya hanya satu, yaitu pembersihan ditulis tepat di samping alokasinya sehingga keduanya tidak pernah terpisah antarberkas atau berbulan-bulan. Topik ini membahas semantik lingkup dan urutan eksekusi defer, perilakunya di dalam perulangan, serta kesalahan yang lazim muncul saat pertama kali memakainya.

Urutan eksekusi dan batas lingkup#

Semantik defer mengikuti dua aturan yang bekerja bersama. Aturan pertama adalah urutan terbalik, yaitu defer yang ditulis paling akhir dijalankan paling awal karena tumpukannya dibuka dari belakang. Aturan kedua adalah batas lingkup, yaitu defer hanya berlaku pada blok tempat ia ditulis, bukan pada seluruh proc yang membungkusnya. Program berikut memperlihatkan keduanya sekaligus.

odin main.odin
package main

import "core:fmt"

main :: proc() {
        fmt.println("mulai")
        defer fmt.println("tiga")
        defer fmt.println("dua")
        {
                fmt.println("blok masuk")
                defer fmt.println("blok keluar")
                fmt.println("blok berjalan")
        }
        fmt.println("setelah blok")
}
bash
odin run main.odin -file
text Keluaran
mulai
blok masuk
blok berjalan
blok keluar
setelah blok
dua
tiga

Keluaran itu dibaca berurutan sebagai serangkaian peristiwa. Blok {} di tengah proc adalah lingkup sendiri, sehingga blok keluar dijalankan tepat saat blok itu selesai, sebelum setelah blok sempat dicetak. Dua defer tingkat proc baru berjalan ketika main selesai, dan urutannya terbalik dari urutan penulisan, yaitu dua lebih dulu daripada tiga. Penulisan urutan panggilan pembersihan yang bertumpuk, misalnya menutup berkas lalu membebaskan buffer, harus selalu mengikuti aturan terbalik ini agar hasilnya sesuai maksud.

Aturan lingkup juga berarti keluar lebih awal tetap melewati defer. Apabila proc mengembalikan nilai di tengah jalan dengan return, seluruh defer proc yang sudah terdaftar tetap dijalankan sebelum kendali kembali ke pemanggil, dan perilaku inilah yang membuat pembersihan tidak tertinggal pada jalur error maupun jalur sukses.

defer di dalam perulangan#

Defer di dalam badan perulangan berlaku per iterasi, karena setiap iterasi membangun lingkupnya sendiri. Pola ini tepat untuk pekerjaan yang memperoleh dan melepaskan sesuatu berulang kali, misalnya buffer per putaran.

odin perulangan-defer.odin
package main

import "core:fmt"

main :: proc() {
        for i in 1 ..= 3 {
                fmt.printfln("iterasi %d dibuka", i)
                defer fmt.printfln("iterasi %d ditutup", i)
        }
        fmt.println("perulangan selesai")
}
bash
odin run perulangan-defer.odin -file
text Keluaran
iterasi 1 dibuka
iterasi 1 ditutup
iterasi 2 dibuka
iterasi 2 ditutup
iterasi 3 dibuka
iterasi 3 ditutup
perulangan selesai

Penutupan setiap iterasi terjadi sebelum iterasi berikutnya dibuka, sehingga sumber daya per putaran tidak menumpuk sepanjang perulangan. Apabila pembersihan justru harus menunggu seluruh perulangan selesai, defer ditempatkan di luar perulangan, dan perbedaan satu baris inilah yang menentukan umur sumber dayanya.

Menyandingkan alokasi dengan pelepasan#

Kegunaan defer yang paling sering adalah menyandingkan pembuatan dengan pembuangan. Pola yang sama berlaku untuk berkas yang dibuka, mutex yang dikunci, atau memori yang dialokasikan, dan polanya selalu berupa satu pasangan baris yang berdekatan.

odin Contoh: pembersihan menyanding sumber daya
// tidak-dijalankan
buffer := make([]u8, 256)
defer delete(buffer)

// bekerja dengan buffer di sini, dan jalur keluar mana pun
// tetap melewati delete

Pola ini menjadi tulang punggung disiplin memori Odin pada modul memori dan alokator, yaitu setiap make disandingkan dengan delete melalui defer sehingga jalur error tidak menghasilkan kebocoran. Nilai yang dikembalikan proc juga dapat dihasilkan setelah defer terdaftar, karena defer tetap berjalan saat return dievaluasi.

Keluar lebih awal tetap melewati defer#

Sifat pembersihan jalur error dan jalur sukses diperlihatkan paling jelas oleh proc yang bisa kembali lebih awal. Proc cari berikut mendaftarkan satu defer, lalu memiliki dua jalur keluar, yaitu return di tengah perulangan ketika target ditemukan dan return di akhir ketika tidak ada.

odin keluar-cepat.odin
package main

import "core:fmt"

cari :: proc(nilai: []int, target: int) -> (int, bool) {
        defer fmt.println("cari selesai, defer berjalan")
        for v, i in nilai {
                if v == target {
                        return i, true
                }
        }
        return -1, false
}

main :: proc() {
        fmt.println(cari([]int{10, 20, 30}, 20))
        fmt.println(cari([]int{10, 20, 30}, 99))
}
bash
odin run keluar-cepat.odin -file
text Keluaran
cari selesai, defer berjalan
1 true
cari selesai, defer berjalan
-1 false

Kedua pemanggilan menghasilkan jalur yang berbeda, yaitu kembalian di tengah perulangan dan kembalian di akhir proc, dan defer tetap berjalan pada keduanya sebelum hasilnya dicetak oleh pemanggil. Dengan satu pendaftaran, pembersihan berlaku pada seluruh jalur keluar proc, dan inilah yang dipaksakan oleh defer tanpa perlu membebani setiap return dengan panggilan pembersihan yang sama.

Kesalahan umum#

Kesalahan pertama adalah mengira defer berlaku per proc, bukan per lingkup. Defer yang ditulis di dalam blok bersyarat atau blok pekerjaan dijalankan saat blok itu berakhir, dan bila yang dimaksud adalah pembersihan di akhir proc, defer harus ditulis di lingkup proc. Pemindahan satu tingkat ke atas mengubah waktu eksekusinya secara langsung.

Kesalahan kedua adalah menaruh panggilan yang hasilnya dibutuhkan ke dalam defer. Defer hanya menjalankan panggilan di akhir lingkup dan tidak mengembalikan nilai ke alur program, sehingga pemanggilan yang kembalinya menjadi bagian dari logika, misalnya pembacaan data dari berkas, tetap ditulis secara normal dan hanya pembersihannya yang dijadwalkan.

Kesalahan ketiga adalah menumpuk defer dengan urutan yang menganggap eksekusinya berurutan ke depan. Tumpukan defer dieksekusi terbalik, sehingga dua pembersihan yang saling bergantung urutannya, misalnya menutup berkas sebelum menghapus berkas sementaranya, harus ditulis dalam urutan terbalik dari urutan pelaksanaan yang diinginkan.

Kesalahan keempat adalah menulis iterasi slice dengan urutan variabel yang terbalik. Perulangan for v, i in slice menaruh nilai pada variabel pertama dan indeks pada variabel kedua, sehingga penulisan for i, v in slice tetap lolos compiler namun membandingkan dua hal yang berbeda, dan gejalanya hanya terlihat sebagai hasil pencarian yang selalu gagal. Pemeriksaan cepatnya sederhana, yaitu mencetak kedua variabel pada iterasi pertama sebelum logika ditulis di atasnya.

Ringkasan#

  • defer menjadwalkan panggilan pada akhir lingkup tempat ia ditulis, termasuk saat keluar lebih awal dengan return, sehingga pembersihan tidak tertinggal.
  • Tumpukan defer dieksekusi terbalik dari urutan penulisannya, dan defer di dalam perulangan berlaku per iterasi.
  • Pola utamanya adalah menyandingkan alokasi dengan pelepasan, yaitu make dan delete berdampingan, yang menjadi disiplin memori Odin.
  • Kesalahan lazimnya adalah salah membaca batas lingkup, memakai defer untuk panggilan yang hasilnya dibutuhkan, atau salah membaca urutan tumpukan.

Referensi#