Union bertag menyimpan satu nilai dari beberapa kemungkinan tipe sekaligus, lengkap dengan penanda yang menyatakan kemungkinan mana yang sedang aktif. Topik ini membahas deklarasi union(enum), pencocokan switch atas tag dengan capture payload, keamanan akses payload, dan perbandingannya dengan union tanpa tag. Sebagai contoh penerapan, luas sebuah bentuk geometri dihitung dari varian yang aktif, dan tata letak memorinya digambarkan dalam satu diagram.
Konsep#
Tipe sum dengan union(enum)#
Struct adalah tipe hasil kali (product type) karena satu nilainya memuat semua field sekaligus. Kebalikannya adalah tipe sum (sum type), yaitu tipe yang satu nilainya berupa salah satu dari beberapa kemungkinan. Zig menyatakannya dengan union(enum), yaitu union yang tag varian aktifnya disimpan bersama datanya.
// tidak-dijalankan
const Bentuk = union(enum) {
lingkaran: f32,
persegi_panjang: [2]f32,
kosong,
};Deklarasi di atas menyatakan tiga varian. Varian lingkaran membawa satu f32 sebagai radius, varian persegi_panjang membawa dua f32 sebagai lebar dan tinggi, sedangkan varian kosong tidak membawa data apa pun. Inisialisasinya memilih satu varian sekaligus, misalnya Bentuk{ .lingkaran = 2 } atau Bentuk{ .kosong = {} }. Berbeda dari struct, tidak ada nilai Bentuk yang memuat lingkaran dan persegi panjang sekaligus, dan tag yang tersimpan bersama data itulah yang menyatakan varian mana yang sedang berlaku.
Switch atas tag dan capture payload#
Cara membaca union bertag yang dianjurkan adalah switch atas nilainya. Setiap varian menjadi satu cabang, dan payload varian aktif ditangkap (capture) ke dalam pengenal baru dengan pola |nilai|. Tipe pengenal tersebut mengikuti tipe payload variannya, sehingga cabang .lingkaran menangkap f32 dan cabang .persegi_panjang menangkap [2]f32.
// tidak-dijalankan
fn keliling(b: Bentuk) f32 {
return switch (b) {
.lingkaran => |r| 2 * std.math.pi * r,
.persegi_panjang => |sisi| 2 * (sisi[0] + sisi[1]),
.kosong => 0,
};
}Switch atas union bertag wajib exhaustif, sama seperti switch atas enum, sehingga semua varian wajib tercakup atau satu cabang else disediakan. Varian tanpa payload seperti .kosong tidak menangkap apa pun dan cukup mengembalikan nilai langsung. Karena union juga merupakan container, fungsi semacam ini dapat ditulis sebagai method di dalam deklarasi Bentuk, sehingga pemanggilannya menjadi b.luas() dari nilai mana pun.
Keamanan akses payload#
Payload juga dapat diakses langsung lewat nama field, misalnya b.lingkaran, tetapi akses semacam itu berisiko bila varian aktifnya berbeda. Kompilator menolaknya sejak saat kompilasi apabila isi union sudah diketahui, sedangkan nilai yang baru diketahui saat program berjalan memicu panic dengan pesan yang menyebut field aktif dan field yang dicoba diakses. Perilaku semacam ini termasuk perilaku ilegal (illegal behavior) yang terdeteksi pada mode aman.
Pola switch dengan capture menghindari seluruh persoalan tersebut karena payload hanya tersedia di dalam cabang yang cocok dengan tag aktifnya. Tidak ada jalur kode yang dapat menyentuh payload varian lain, sehingga kelalaian semacam itu tidak mungkin lolos. Nama varian aktif juga dapat dibaca tanpa membuka payloadnya melalui @tagName, yang berguna untuk pencatatan atau pesan error.
Union tanpa tag#
Union yang dideklarasikan tanpa (enum) tidak menyimpan tag sama sekali.
// tidak-dijalankan
const Nilai = union { bilangan: u32, teks: []const u8 };
// tanpa tag, varian aktif tidak tersimpan dalam nilai itu sendiriTanpa tag, satu nilai Nilai menjadi lebih hemat, tetapi tidak ada cara membaca varian aktifnya dari nilai tersebut. Mode aman tetap melacak varian aktif di belakang layar untuk mendeteksi penyalahgunaan, sedangkan extern union, yang dipakai demi kesesuaian tata letak dengan C, bahkan tidak dilacak sama sekali. Kasus pemakaiannya antara lain tafsir ulang pola bit yang sama dari dua sudut pandang, misalnya membagi satu u64 menjadi dua u32. Karena itu union tanpa tag dipakai hanya pada kasus khusus, sedangkan union(enum) menjadi pilihan bawaan untuk pemodelan data.
Biaya union bertag berupa ruang tambahan untuk tag dan penyesuaian ukurannya. Tag hanya berupa bilangan kecil, sedangkan area payload mengikuti varian terbesarnya. Perhitungan kasarnya digambarkan pada diagram tata letak memori di bagian akhir topik.
Contoh Lengkap#
Program utuh: luas bentuk geometri#
Program berikut menghitung luas tiga bentuk dalam satu array. Union bertag Bentuk memiliki method luas yang memakai switch exhaustif dengan capture, dan fungsi main menampilkan luas setiap bentuk beserta nama variannya.
const std = @import("std");
const Bentuk = union(enum) {
lingkaran: f32,
persegi_panjang: [2]f32,
kosong,
fn luas(self: Bentuk) f32 {
return switch (self) {
.lingkaran => |r| std.math.pi * r * r,
.persegi_panjang => |sisi| sisi[0] * sisi[1],
.kosong => 0,
};
}
};
pub fn main() void {
const daftar = [_]Bentuk{
Bentuk{ .lingkaran = 2 },
Bentuk{ .persegi_panjang = .{ 3, 5 } },
Bentuk{ .kosong = {} },
};
for (daftar) |b| {
std.debug.print("{s}: luas {d:.2}\n", .{ @tagName(b), b.luas() });
}
}lingkaran: luas 12.57
persegi_panjang: luas 15.00
kosong: luas 0.00Method luas menerima salinan Bentuk dan mengembalikan f32 dari varian yang aktif. Penentu format {d:.2} membatasi dua angka desimal, sehingga luas lingkaran beradius 2, yaitu 4 kali pi, tampil sebagai 12.57. Nama varian diambil dengan @tagName sehingga setiap baris keluaran menunjukkan varian yang sedang dihitung, sedangkan varian kosong mengembalikan nol karena tidak membawa data apa pun.
# jalankan program dari direktori topik
zig run luas-bentuk.ziglingkaran: luas 12.57
persegi_panjang: luas 15.00
kosong: luas 0.00Tata letak memori#
Satu nilai union bertag menempatkan tag di samping area payload dalam satu kesatuan yang sama. Area payload itu dibaca dengan cara yang berbeda menurut tag aktifnya, sebagai f32 untuk lingkaran, sebagai [2]f32 untuk persegi panjang, dan tidak dibaca sama sekali untuk varian kosong.
Ukuran total union bertag minimal mencakup ukuran tag ditambah ukuran payload varian terbesarnya, sedangkan urutan persis dan celah penjajaran (alignment padding) ditentukan oleh kompilator. Angka pada diagram bersifat ilustratif karena tujuannya menyatakan prinsip, yaitu tag menentukan cara membaca payload yang sama, bukan menyatakan tata letak byte yang dijanjikan bahasa. Apabila tata letak byte harus pasti, misalnya demi kesesuaian dengan C, urutannya dikendalikan secara eksplisit dengan deklarasi extern.
Kesalahan umum#
Kesalahan pertama berupa menulis switch atas union bertag tanpa cabang else padahal tag-nya bertipe enum terbuka. Kompilator menuntut kelengkapan cabang, dan menambah keadaan baru pada enum langsung menolak seluruh switch yang belum menanganinya. Keamanan ini adalah tujuan union bertag, sehingga cabangnya dilengkapi, bukan diakali dengan else untuk semua keadaan nyata.
Kesalahan kedua berupa memakai union tanpa tag untuk menyimpan variasi data dengan kepercayaan penuh. Union tanpa tag tidak membawa penanda keadaan aktifnya, sehingga pembacaan payload yang salah menghasilkan byte yang ditafsirkan sembarangan tanpa pesan error. Bila satu nilai dapat berada dalam lebih dari satu bentuk, union bertag adalah pilihan yang benar, dan union tanpa tag ditinggalkan untuk kasus penafsiran ulang memori yang dikendalikan penuh oleh pemanggilnya.
Ringkasan#
union(enum)memodelkan tipe sum, yaitu satu nilai yang berupa tepat satu varian, dengan tag yang tersimpan bersama datanya.- Switch atas union bertag wajib exhaustif dan menangkap payload varian aktif dengan pola
|nilai|. - Akses langsung field yang bukan varian aktifnya adalah perilaku ilegal yang terdeteksi mode aman, sehingga switch dengan capture menjadi cara baca yang dianjurkan.
- Union tanpa tag tidak menyimpan varian aktifnya dan dipakai hanya pada kasus khusus, sedangkan ukuran union bertag minimal mencakup tag dan payload varian terbesar.
Referensi#
- union — Zig Language Reference
— Deklarasi union dan perbedaan union biasa dengan
union(enum). - Tagged-union — Zig Language Reference — Tag dan safety pembacaan payload pada union bertag.