switch

Memanfaatkan pencocokan exhaustif dan switch berlabel untuk mesin keadaan.

Daftar isi

Percabangan if membandingkan kondisi satu per satu, sedangkan switch mencocokkan satu nilai terhadap sekumpulan kemungkinan dalam satu pernyataan. Topik ini membahas switch atas enum dan atas bilangan, pencocokan rentang, penggabungan beberapa nilai dalam satu cabang, serta switch berlabel yang menggerakkan mesin keadaan. Setelah membacanya, pilihan yang semula ditulis sebagai rantai if yang panjang dapat ditulis sebagai pencocokan yang kelengkapannya dijaga langsung oleh kompilator.

Konsep#

Kelengkapan cabang yang ditegakkan kompilator#

switch memeriksa satu nilai operand terhadap beberapa pola, dan pola yang cocok menentukan cabang yang dieksekusi. Setiap cabang ditulis sebagai pola, tanda panah =>, lalu hasilnya, dan seluruh cabang ditutup kurung kurawal. Enum adalah tipe yang nilainya terbatas pada sekumpulan nama yang terdaftar, sehingga menjadi operand switch yang paling alami. Zig menegakkan pencocokan exhaustive, yakni kewajiban menangani setiap kemungkinan nilai, sehingga else yang berperan sebagai penadung sisanya menjadi opsional bila semua cabang sudah ditulis.

Program berikut mencocokkan status sebuah loket terhadap empat kemungkinannya tanpa memakai else sama sekali.

zig status-loket.zig
const std = @import("std");

const Status = enum { buka, layani, istirahat, tutup };

fn pesan(status: Status) []const u8 {
    return switch (status) {
        .buka => "loket buka, ambil nomor antrean",
        .layani => "loket sedang melayani",
        .istirahat => "loket istirahat sebentar",
        .tutup => "loket sudah tutup",
    };
}

pub fn main() void {
    const urutan = [_]Status{ .buka, .layani, .istirahat, .layani, .tutup };
    for (urutan) |s| {
        std.debug.print("{s}\n", .{pesan(s)});
    }
}
text Keluaran
loket buka, ambil nomor antrean
loket sedang melayani
loket istirahat sebentar
loket sedang melayani
loket sudah tutup

Di sini switch berperan sebagai ekspresi, karena setiap cabangnya menghasilkan []const u8 yang dikembalikan langsung oleh fungsi pesan. Apabila satu varian enum dihapus dari daftarnya, kompilasi berhenti dengan pesan error: switch must handle all possibilities disertai catatan yang menunjuk varian yang belum ditangani. Jaminan ini membuat penambahan varian baru menjadi aman, sebab seluruh switch yang memakai enum itu langsung ditolak sampai cabangnya dilengkapi.

Rentang dan beberapa nilai dalam satu cabang#

Pola switch tidak terbatas pada enum, karena bilangan juga dapat dicocokkan, baik satu per satu maupun per rentang. Rentang ditulis dengan tiga titik, misalnya 85...100, dan kedua ujungnya termasuk di dalam rentang. Beberapa nilai yang perlakuannya sama dapat digabung dalam satu cabang dengan dipisahkan koma. Apabila operandnya bukan enum, else wajib ada untuk menadung nilai yang tidak tercakup pola mana pun.

zig kategori-nilai-switch.zig
const std = @import("std");

fn kategori(nilai: u8) []const u8 {
    return switch (nilai) {
        85...100 => "A",
        70...84 => "B",
        55...69 => "C",
        else => "D",
    };
}

pub fn main() void {
    const daftar = [_]u8{ 87, 72, 61, 45 };
    for (daftar) |n| {
        std.debug.print("nilai {d} kategori {s}\n", .{ n, kategori(n) });
    }
}
text Keluaran
nilai 87 kategori A
nilai 72 kategori B
nilai 61 kategori C
nilai 45 kategori D

Cabang yang menggabungkan beberapa nilai memangkas duplikasi kode yang biasanya muncul saat beberapa keadaan diperlakukan identik. Program penghitung berikut mengelompokkan jenis tiket dalam satu antrean loket menjadi dua kelompok, yaitu reguler dan berdiskon, tanpa cabang yang saling menyalin.

zig rekap-antrean.zig
const std = @import("std");

const Tiket = enum { reguler, pelajar, lansia };

pub fn main() void {
    const antrean = [_]Tiket{ .reguler, .pelajar, .reguler, .lansia, .pelajar, .reguler };
    var reguler: u8 = 0;
    var diskon: u8 = 0;
    for (antrean) |t| {
        switch (t) {
            .reguler => reguler += 1,
            .pelajar, .lansia => diskon += 1,
        }
    }
    std.debug.print("tiket reguler: {d}\n", .{reguler});
    std.debug.print("tiket berdiskon: {d}\n", .{diskon});
}
text Keluaran
tiket reguler: 3
tiket berdiskon: 3

switch pada badan perulangan di atas dipakai sebagai pernyataan, sehingga cabang-cabangnya mengeksekusi aksi dan tidak menghasilkan nilai. Pencocokan tetap wajib lengkap atas ketiga varian Tiket, dan bentuk tanpa else seperti ini menguntungkan karena varian baru yang ditambahkan akan langsung ditolak oleh kompilator, bukan diam-diam jatuh ke penadung sisa.

Contoh Lengkap#

Switch berlabel untuk mesin keadaan#

Mesin keadaan (state machine) adalah model yang menyimpan satu keadaan aktif dan memindahkannya menurut peristiwa yang datang, dan switch berlabel adalah alat Zig untuk menuliskannya secara langsung. Label ditulis sebelum kata kunci switch, misalnya siklus: switch (...), dan perpindahan keadaan dilakukan dengan continue :siklus diikuti nilai keadaan berikutnya. Eksekusi kembali ke awal switch yang sama dengan operand baru, tanpa perulangan luar dan tanpa variabel keadaan yang ditugaskan manual.

zig lampu-lalu-lintas.zig
const std = @import("std");

const Lampu = enum { merah, hijau, kuning };

pub fn main() void {
    var langkah: u8 = 0;
    siklus: switch (Lampu.merah) {
        .merah => {
            std.debug.print("merah, kendaraan berhenti\n", .{});
            langkah += 1;
            if (langkah < 6) continue :siklus .hijau;
        },
        .hijau => {
            std.debug.print("hijau, kendaraan jalan\n", .{});
            langkah += 1;
            if (langkah < 6) continue :siklus .kuning;
        },
        .kuning => {
            std.debug.print("kuning, kendaraan melambat\n", .{});
            langkah += 1;
            if (langkah < 6) continue :siklus .merah;
        },
    }
    std.debug.print("siklus berakhir setelah {d} peralihan\n", .{langkah});
}
text Keluaran
merah, kendaraan berhenti
hijau, kendaraan jalan
kuning, kendaraan melambat
merah, kendaraan berhenti
hijau, kendaraan jalan
kuning, kendaraan melambat
siklus berakhir setelah 6 peralihan

Setiap cabang menaikkan pencacah langkah, dan peralihan hanya berlanjut selama pencacah itu belum mencapai enam. Ketika syaratnya tidak lagi terpenuhi, cabang selesai tanpa continue, sehingga eksekusi keluar dari switch dan program mencetak baris penutupnya. Struktur seperti ini menjaga seluruh perpindahan keadaan tetap berada di satu tempat, sehingga lintasan keadaan yang keliru mudah ditemukan.

Switch atas union bertag#

Union bertag (tagged union) menyimpan satu nilai dari beberapa kemungkinan tipe sekaligus penanda tag yang menyatakan kemungkinan mana yang sedang aktif. Karena tipe isinya bergantung pada tag, pembacaan isinya juga dilakukan melalui switch, dan setiap tag diperlakukan sebagai cabang tersendiri. Kompilator menegakkan kelengkapan yang sama seperti pada enum, sehingga tag baru langsung ditolak sampai cabangnya ditulis.

zig Contoh: switch atas union bertag
// tidak-dijalankan
switch (bentuk) {
    .lingkaran => |r| std.debug.print("jari-jari {d}\n", .{r}),
    .persegi => |s| std.debug.print("sisi {d}\n", .{s}),
}

Capture setelah tag, misalnya |r| pada cabang lingkaran, mengambil nilai yang tersimpan di balik tag itu, dan tipe capture-nya otomatis sesuai dengan field pada tag tersebut. Pola ini menjamin isi hanya dibaca setelah tag-nya diperiksa, sehingga tidak ada jalur yang membaca data dengan tipe yang keliru. Pencocokan atas tag menjadi jembatan yang aman antara nilai yang bentuknya berubah-ubah dan kode yang mengonsumsinya.

Kesalahan umum#

Kesalahan pertama adalah meninggalkan satu kemungkinan tanpa cabang. Kompilasi berhenti dengan pesan error: switch must handle all possibilities, dan catatan di bawahnya menunjuk nilai enumerasi yang belum ditangani. Perbaikannya adalah menuliskan cabangnya secara eksplisit, atau menambahkan else apabila perlakuan sisanya memang identik dan jumlah kemungkinannya terlalu banyak untuk disebutkan satu per satu.

Kesalahan kedua adalah menuliskan rentang yang tumpang-tindih, misalnya 70...84 dan 80...89 pada operand yang sama. Kompilator menolaknya dengan pesan error: duplicate switch ranges beserta catatan rentang yang saling menimpa. Rapikan batas rentang hingga tidak ada nilai yang tercakup dua kali, karena urutan penulisan cabang tidak menyelamatkan pencocokan yang ambigu.

Kesalahan ketiga adalah mengira cabang switch saling berlanjut seperti pada bahasa yang memerlukan break di akhir cabang. Pada Zig, setiap cabang berhenti pada batasnya sendiri, sehingga break di akhir cabang tidak diperlukan dan penulisan cabang berturut-turut tidak pernah menyebabkan eksekusi meluber ke cabang berikutnya.

Ringkasan#

  • switch mencocokkan satu operand terhadap beberapa pola, dan kelengkapannya atas enum dijaga oleh kompilator sehingga varian yang terlewat menghentikan kompilasi.
  • Rentang ditulis dengan tiga titik seperti 85...100, dan beberapa nilai dapat digabung dalam satu cabang dengan pemisah koma.
  • switch berlabel memindahkan keadaan melalui continue :label nilai, sehingga mesin keadaan ditulis tanpa perulangan luar.
  • Union bertag dibaca melalui switch atas tag-nya, dan capture pada setiap cabang mengambil isi sesuai tipe field tag tersebut.

Referensi#