Dua bentuk struct sebelumnya — struct bernama dan tuple struct — keduanya menyimpan data. Rust masih menyimpan satu bentuk lagi yang sekilas tampak aneh: unit struct, struct tanpa field sama sekali. Palet warna tanpa cat, kotak tanpa isi. Justru karena kosong, unit struct tidak dipakai untuk menyimpan data, melainkan untuk menjadi tanda: sebuah penanda bahwa suatu kondisi sudah terpenuhi, atau identitas untuk sebuah konsep di program. Pada chapter ini kita akan melihat bagaimana struct nol byte ini memainkan peran nyata dalam alur data, dan mengapa “tidak menyimpan apa-apa” bukan berarti “tidak berguna”.
Struct Tanpa Isi #
Definisi unit struct paling ringkas di antara ketiganya: kata kunci struct, nama tipe, dan titik koma — tanpa blok field, tanpa kurung nilai.
struct Sanitized;Baris tersebut memperkenalkan tipe bernama Sanitized yang nilainya tidak memuat apa pun. Nilai Sanitized dibuat cukup dengan menuliskan Sanitized sebagai ekspresi — tanpa kurung, tanpa daftar field. Dari sisi memori, tipe ini menempati nol byte; std::mem::size_of::<Sanitized>() melaporkan angka 0, sehingga unit struct sering disebut zero-sized type (tipe berukuran nol).
Kegunaannya muncul dari sifat sistem tipe Rust: sebuah nilai unit struct tetap mengikuti aturan ownership — ia bisa dibuat, disimpan dalam variabel, dikirim ke fungsi, dan dijatuhkan saat scope berakhir. Dengan memanfaatkan alur tersebut, keberadaan nilai Sanitized di tangan sebuah fungsi menjadi bukti terkontrol bahwa suatu proses benar-benar sudah dijalankan.
Penanda yang Berpindah antar Fungsi #
Mari kita lihat pola tersebut dalam aksi. Program pelaporan berikut punya aturan sederhana: laporan hanya boleh dicetak setelah teksnya melewati pembersihan. Alih-alih mengandalkan boolean yang bisa disalahpahami atau lupa dicek, fungsi sanitize mengembalikan bukti berupa nilai Sanitized:
struct Sanitized;
fn sanitize(input: String) -> (String, Sanitized) {
let cleaned = input.trim().to_lowercase();
(cleaned, Sanitized)
}
fn print_report(text: &str, _proof: &Sanitized) {
println!("Laporan: {}", text);
}
fn main() {
let raw = String::from(" Data Penjualan Mingguan ");
let (clean, proof) = sanitize(raw);
println!("Ukuran tipe Sanitized: {} byte", std::mem::size_of::<Sanitized>());
println!("Ukuran nilai proof: {} byte", std::mem::size_of_val(&proof));
print_report(&clean, &proof);
}Output yang Dihasilkan:
Ukuran tipe Sanitized: 0 byte
Ukuran nilai proof: 0 byte
Laporan: data penjualan mingguanMari kita bedah kode di atas baris per baris:
- Baris 1 (
struct Sanitized;): unit struct dideklarasikan tanpa blok dan tanpa kurung. TipeSanitizedkini resmi ada di program. - Baris 3 (
fn sanitize(input: String) -> (String, Sanitized)): fungsi mengambil kepemilikan teks mentah dan mengembalikan tuple berisi teks bersih plus penanda. Nilai kembalian menyatakan: “teks sudah bersih, dan inilah buktinya”. Pembersihan sendiri dilakukaninput.trim()yang memangkas spasi di kedua ujung sertato_lowercase()yang menyeragamkan huruf. - Baris 8 (
fn print_report(text: &str, _proof: &Sanitized)): parameter kedua meminta pinjaman penandaSanitized. Tanda garis bawah pada_proofmenyatakan nilainya tidak dibaca isi — memang tidak ada isi — tetapi keberadaannya di tanda tangan membuat fungsi ini mustahil dipanggil tanpa bukti. Pemanggil yang tidak memiliki nilaiSanitizedtidak bisa lolos dari pemeriksaan tipe. - Baris 15 (
let (clean, proof) = sanitize(raw);): hasil fungsi dibongkar menjadi dua variabel — teks bersih dan bukti. Keduanya berpindah bersama dari fungsi ke pemanggil, sehingga pemanggil tidak bisa menyimpan teks bersih tanpa ikut menyimpan buktinya. - Baris 17-18 (
size_ofdansize_of_val):std::mem::size_of::<Sanitized>()mengukur tipe, sedangkanstd::mem::size_of_val(&proof)mengukur nilai konkret yang kita pegang. Keduanya melaporkan 0 byte — penanda ini benar-benar gratis di memori. - Baris 20 (
print_report(&clean, &proof);): pemanggilan sah karena kedua syarat terpenuhi — teks dan pinjaman bukti.
Perhatikan bahwa meski nol byte, nilai proof tetap mengikuti hukum ownership: penanda ini tidak bisa muncul dari udara tipis di tengah main, ia harus dibuat oleh sanitize dan mengalir keluar dari sana. Posisi parameter _proof: &Sanitized pada print_report bekerja seperti gerbang — hanya jalur yang melewati sanitize yang sampai ke gerbang tersebut sambil membawa penanda.
Penanda sebagai Field dengan Biaya Nol #
Bentuk pemakaian kedua adalah menempatkan unit struct sebagai field di tipe lain. Misalnya cache harga yang hanya sah dipakai setelah seluruh data dimuat ke memori:
struct Loaded;
struct PriceCache {
names: [String; 3],
prices: [u64; 3],
_state: Loaded,
}
fn main() {
let cache = PriceCache {
names: [String::from("kopi"), String::from("teh"), String::from("gula")],
prices: [25000, 8000, 14500],
_state: Loaded,
};
for index in 0..cache.names.len() {
println!("{}: Rp{}", cache.names[index], cache.prices[index]);
}
println!("Ukuran penanda Loaded: {} byte", std::mem::size_of::<Loaded>());
println!("Ukuran cache utuh: {} byte", std::mem::size_of::<PriceCache>());
println!("Ukuran tanpa penanda: {} byte", std::mem::size_of::<([String; 3], [u64; 3])>());
}Output yang Dihasilkan:
kopi: Rp25000
teh: Rp8000
gula: Rp14500
Ukuran penanda Loaded: 0 byte
Ukuran cache utuh: 96 byte
Ukuran tanpa penanda: 96 bytePenjelasan baris-baris pentingnya:
- Baris 6 (
_state: Loaded,): field_statebertipe unit struct. Nama field diawali garis bawah karena nilainya tidak pernah dibaca — fungsinya hanya hadir dalam tipe, bukan dalam perhitungan. - Baris 20-22 (
size_oftiga versi): ukuranPriceCache(96 byte) persis sama dengan gabungan[String; 3]dan[u64; 3]tanpa penanda. FieldLoadedmenambah nol byte — kompilator memangkas tipe berukuran nol dari perhitungan layout.
Dua baris terakhir pada output adalah bukti numeriknya: menambahkan penanda tidak mengubah ukuran struct sedikit pun. Kombinasi “nol byte” dan “harus dibuat oleh fungsi tertentu” inilah yang membuat unit struct sering dipakai sebagai penanda tahap (state) pada tipe.
Kegunaan Lain dan Batasannya #
Unit struct juga kerap menjadi wadah trait implementation — tipe yang perilakunya lebih penting daripada datanya, misalnya penanda struct AppLog; yang kelak diisi implementasi trait pencatatan. Kita belum membahas cara menuliskan trait sendiri (materi Course Traits), tetapi sintaks dasarnya kelak berdiri di atas struct AppLog; yang kita kuasai sekarang.
Satu batasan patut dicatat: karena tidak menyimpan apa pun, unit struct tidak bisa membedakan satu nilai dari nilai lain — semua nilai Sanitized setara. Penanda hanya bercerita tentang jenis kondisi, bukan rincian kondisi. Ketika kita perlu tahu versi pembersih mana yang dipakai atau kapan pembersihan terjadi, data harus benar-benar disimpan — dan saat itulah kita kembali ke struct bernama atau tuple struct.
Dari sisi pembacaan kode, nama tipe yang dipilih untuk unit struct menanggung seluruh beban komunikasi. Nama Sanitized, Loaded, atau Verified langsung menyampaikan maksudnya; nama seperti Flag1 atau Data justru menambah kabut. Perlakukan penamaan unit struct dengan keseriusan yang sama seperti menamai fungsi, karena pembaca kode akan bertemu nama tersebut justru pada tempat-tempat penting: tanda tangan fungsi dan definisi field.
Ringkasan #
Unit struct adalah tipe bernama tanpa data: didefinisikan dengan struct Nama;, bernilai nol byte, dan tetap tunduk pada aturan ownership. Kekuatannya bukan menyimpan melainkan menyatakan — keberadaan nilai penanda di tanda tangan fungsi memaksa pemanggil melewati proses yang berhak mengeluarkan penanda tersebut, dan field _state: Loaded menandai tahap sebuah tipe tanpa menambah satu byte pun. Ketika kebutuhan bergeser dari “menyatakan kondisi” ke “menyimpan kondisi”, waktunya melengkapi struct dengan perilaku lewat blok impl pada Chapter Metode dengan impl.