Optional

Menangani ketiadaan nilai dengan ?T secara eksplisit.

Daftar isi

Optional adalah tipe yang menyatakan sebuah nilai atau ketiadaannya secara eksplisit. Topik ini membahas tipe ?T, pembukaan nilai dengan .?, nilai cadangan dengan orelse, penangkapan nilai dengan if dan while, serta pointer opsional ?*T dan perbandingannya dengan pointer sentinel. Sebagai contoh penerapan, pencarian nomor kartu anggota dalam array dibuat mengembalikan indeks atau null.

Konsep#

Tipe optional ?T#

Tipe optional ditulis dengan tanda tanya di depan tipe dasarnya, misalnya ?u32. Nilai bertipe ?u32 berupa bilangan u32 atau null, dan tidak ada kemungkinan ketiga. Karena ketiadaan nilainya dinyatakan secara eksplisit, pemanggil fungsi tidak perlu menebak apakah nilai 0 atau -1 berarti kosong, dan kompilator menuntut penanganan null sebelum nilai dapat dipakai sebagai u32.

zig Contoh: deklarasi dan perubahan optional
// tidak-dijalankan
var stok: ?u32 = 24;
stok = null;
// stok kini menyatakan ketiadaan nilai

Nilai null bukan bagian dari u32, sehingga penugasan stok = null hanya sah karena tipenya optional. Perbandingan tetap nyaman dipakai. Ekspresi stok == 24 bernilai benar hanya bila stok berisi 24, sedangkan stok != null memeriksa keberadaan nilainya. Ukuran ?u32 lebih besar daripada u32 karena perlu tempat menyimpan penanda apakah nilainya ada, sedangkan ?*T justru berukuran sama dengan *T, seperti dijelaskan pada bagian pointer opsional.

Membuka nilai dengan .?#

Operator .? mengambil nilai di dalam optional, dan kegiatan ini disebut membuka (unwrap) nilai. Hasilnya bertipe T murni dan siap dipakai. Apabila optional yang dibuka ternyata null, program berhenti dengan panic yang menyatakan penggunaan nilai null pada mode Debug. Dengan demikian, .? pada hakikatnya adalah pernyataan bahwa null di posisi itu tidak mungkin.

zig Contoh: pembukaan .? dan risikonya
// tidak-dijalankan
const stok: ?u32 = 24;
const pasti: u32 = stok.?; // sah karena stok tidak null
// bila stok null, baris di atas menghentikan program

Pembukaan ini aman dipakai ketika keberadaan nilai sudah diperiksa lebih dahulu atau ketika kontrak fungsi menjamin nilainya. Di luar itu, .? menyerahkan risiko kepada jalannya program. Panic semacam itu tetap lebih baik daripada melanjutkan proses dengan nilai salah, tetapi bukan pola yang dianjurkan untuk kasus yang bisa ditangani dengan rapi. Bagian berikutnya menunjukkan dua alternatif yang menyatakan rencana cadangan secara eksplisit.

Nilai cadangan dengan orelse dan penangkapan if#

Operator orelse menyatakan nilai cadangan (fallback) yang dipakai bila optional bernilai null. Ekspresi stok orelse 0 menghasilkan u32 dalam kedua keadaan, sehingga tidak ada null yang tersisa setelahnya. Untuk alur yang lebih besar, orelse boleh diikuti blok, misalnya untuk mencetak pesan lalu return dari fungsi.

Penangkapan dengan if menawarkan cabang ganda. Pernyataan if (stok) |nilai| menjalankan blok pertama dengan nilai yang sudah terbuka, sedangkan cabang else menangani keadaan null.

zig buka-opsi.zig
const std = @import("std");

pub fn main() void {
    var stok: ?u32 = 24;

    if (stok) |nilai| {
        std.debug.print("stok = {d}\n", .{nilai});
    } else {
        std.debug.print("stok kosong\n", .{});
    }

    const aman = stok orelse 0;
    std.debug.print("aman = {d}\n", .{aman});

    std.debug.print("buka = {d}\n", .{stok.?});

    stok = null;
    const kosong = stok orelse 0;
    std.debug.print("kosong = {d}\n", .{kosong});
}
text Keluaran
stok = 24
aman = 24
buka = 24
kosong = 0

Program di atas menunjukkan ketiganya secara berurutan. Blok if menangkap nilai 24 dan mencetaknya, orelse 0 menghasilkan nilai aslinya karena stok tidak null, .? membuka nilai langsung, dan setelah stok = null dijalankan, orelse 0 menjadi satu-satunya jalan yang tersisa. Perhatikan bahwa stok dideklarasikan dengan var karena nilainya diubah menjadi null di tengah program.

bash
# jalankan program dari direktori topik
zig run buka-opsi.zig
text Keluaran
stok = 24
aman = 24
buka = 24
kosong = 0

Perulangan while dengan capture#

Perulangan while dapat menjadikan optional sebagai syaratnya. Selama optional berisi nilai, nilainya ditangkap dan badan perulangan dieksekusi, sedangkan kemunculan null pertama mengakhiri perulangan tanpa perlu pemeriksaan tambahan di dalam badan.

zig Contoh: perulangan while dengan capture optional
// tidak-dijalankan
var sisa: ?u32 = 3;
while (sisa) |n| : (sisa = if (n > 1) n - 1 else null) {
    std.debug.print("melayani nomor {d}\n", .{n});
}
// perulangan berhenti saat sisa menjadi null

Ekspresi lanjutan setelah tanda titik dua dijalankan pada setiap akhir iterasi dan menulis kembali sisa. Pada contoh tersebut, nomor antrian dikurangi satu per satu hingga cadangannya habis, dan sisa ditetapkan null sehingga perulangan berhenti dengan sendirinya. Pola ini cocok untuk antrean, pemrosesan bertahap, dan penelusuran yang berhenti pada kegagalan pertama.

Pointer opsional ?*T dan pointer sentinel#

Tipe ?*T menyimpan alamat memori yang boleh tidak ada. Nilai null di sini berarti tidak menunjuk apa pun, dan bentuk ini muncul lazim pada struktur data bertaut serta parameter fungsi yang boleh menerima ketiadaan. Ukuran ?*T sama persis dengan *T, karena alamat nol yang tidak pernah menjadi alamat sah dipakai sebagai wakil null tanpa menambah satu byte pun.

zig Contoh: pointer opsional di atas pointer sentinel
// tidak-dijalankan
const teks: [*:0]const u8 = "akhir";
// teks selalu menunjuk memori, dan byte 0 menandai akhir datanya
var dicari: ?[*:0]const u8 = null;
// dicari dapat menyatakan ketiadaan alamat secara eksplisit

Perbedaan dengan pointer sentinel (sentinel-terminated pointer) perlu dicermati. Pointer sentinel seperti [*:0]const u8 selalu menunjuk memori yang ada, dan nilai pengakhirnya adalah bagian dari data, bukan tanda ketiadaan alamat. Ketiadaan hanya dapat dinyatakan dengan membungkusnya menjadi optional seperti ?[*:0]const u8. String pada Zig memakai pola sentinel untuk menandai akhir datanya, sedangkan optional menjawab soal yang berbeda, yaitu ada atau tidaknya alamat yang ditunjuk.

Contoh Lengkap#

Program utuh: pencarian indeks kartu anggota#

Program berikut mencari sebuah nomor kartu dalam array dan melaporkan posisinya. Fungsi cari mengembalikan ?usize, yaitu indeks temuan atau null bila nomor tidak terdaftar, dan pemanggilnya wajib menangani kedua kemungkinan tersebut.

zig cari-indeks.zig
const std = @import("std");

fn cari(data: []const u32, target: u32) ?usize {
    for (data, 0..) |nilai, i| {
        if (nilai == target) return i;
    }
    return null;
}

pub fn main() void {
    const nomor_kartu = [_]u32{ 1105, 2380, 3007, 4142 };

    const indeks = cari(&nomor_kartu, 3007) orelse {
        std.debug.print("kartu tidak terdaftar\n", .{});
        return;
    };
    std.debug.print("kartu 3007 pada indeks {d}\n", .{indeks});

    if (cari(&nomor_kartu, 9999)) |i| {
        std.debug.print("kartu 9999 pada indeks {d}\n", .{i});
    } else {
        std.debug.print("kartu 9999 tidak terdaftar\n", .{});
    }
}
text Keluaran
kartu 3007 pada indeks 2
kartu 9999 tidak terdaftar

Fungsi cari menelusuri slice dengan for yang menyediakan indeks, lalu mengembalikan indeks pada kecocokan pertama atau null setelah seluruh data dilewati. Pemanggil pertama memakai orelse dengan blok yang mencetak pesan dan menghentikan main, sehingga indeks setelahnya sudah pasti terbuka. Pemanggil kedua memakai capture if dan jatuh ke cabang else karena 9999 tidak ada dalam daftar.

bash
# jalankan program dari direktori topik
zig run cari-indeks.zig
text Keluaran
kartu 3007 pada indeks 2
kartu 9999 tidak terdaftar

Kesalahan umum#

Kesalahan pada optional biasanya berkaitan dengan pembukaan nilai yang tergesa. Tiga kasus berikut paling sering muncul beserta perbaikannya.

  1. .? dipanggil pada optional yang bisa null. Program berhenti dengan panic penggunaan nilai null pada mode Debug. Gunakan orelse, capture if, atau periksa != null terlebih dahulu ketika null adalah keadaan yang wajar.
  2. Nilai ?T diteruskan ke parameter bertipe T. Kompilator menolaknya karena keduanya tipe yang berbeda. Buka nilai terlebih dahulu dengan orelse atau capture sebelum diteruskan.
  3. Perulangan while atas optional tidak pernah mencapai null. Tanpa ekspresi lanjutan yang pada akhirnya menetapkan null, badan perulangan terus berputar. Pastikan ada satu jalur yang menetapkan null, misalnya pada ekspresi lanjutan setelah titik dua.

Ringkasan#

  • Tipe ?T menyatakan nilai atau null secara eksplisit, dengan null berada di luar himpunan nilai T.
  • .? membuka nilai dan berhenti dengan panic bila null, sedangkan orelse dan capture if menyatakan rencana cadangan secara eksplisit.
  • while atas optional berhenti pada null pertama, dan capture-nya menyediakan nilai yang sudah terbuka di dalam badan perulangan.
  • ?*T berukuran sama dengan *T karena null diwakili alamat nol, berbeda dari pointer sentinel yang selalu menunjuk data yang ada.

Referensi#