Generics memberi kita satu struct yang bekerja untuk banyak tipe, dan newtype memberi kita identitas tipe yang dijaga kompilator. Kadang kita ingin keduanya sekaligus dalam bentuk yang aneh: satu struct generik Length<Unit> yang membawa satuan sebagai tag tipe — Length<Meters> dan Length<Millimeters> harus dianggap tipe berbeda — tetapi data yang disimpan hanyalah satu angka u32, tanpa menyimpan apa pun tentang Unit saat program berjalan. Niat seperti ini ditolak mentah-mentah oleh kompilator: type parameter yang tidak dipakai field mana pun dianggap error. Rust menyediakan jalan keluar resmi bernama PhantomData — tipe marker ukuran nol yang membuat type parameter “dipakai” di mata pemeriksa tipe tanpa menempati satu byte pun di memori runtime. Chapter ini membedah cara kerja marker hantu tersebut beserta bukti bahwa perlindungan dan efisiensinya sama sekali tidak hantu.

Niat yang Ditolak Kompilator #

Mari kita mulai dari desain yang gagal. Kita ingin struct panjang dengan satu field nilai, plus type parameter Unit yang berfungsi sebagai label satuan:

rust
struct Length<Unit> {
    value: u32,
}

fn main() {
    let desk = Length::<u8> { value: 120 };
    println!("{} cm", desk.value);
}

Program di atas tidak dapat dikompilasi. Pesan errornya:

text
error[E0392]: type parameter `Unit` is never used
 --> main.rs:1:15
  |
1 | struct Length<Unit> {
  |               ^^^^ unused type parameter
  |
  = help: consider removing `Unit`, referring to it in a field, or using a marker such as `PhantomData`
  = help: if you intended `Unit` to be a const parameter, use `const Unit: /* Type */` instead

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0392`.

Error E0392 menyebut persoalannya apa adanya: parameter Unit tidak pernah dipakai. Penolakan ini bukan kehendak bebas kompilator. Setiap type parameter secara resmi memengaruhi identitas tipe, aturan kepemilikan, dan interaksi trait dari hasil generiknya; parameter yang menggantung tanpa dipakai field mana pun membuat janji-janji tersebut tidak bisa diverifikasi. Pesan bantuannya pun jujur: hapus parameternya, pakai di sebuah field, atau gunakan marker seperti PhantomData. Karena tujuan kita justru mempertahankan parameter tanpa menyimpan datanya, pilihan ketiganyalah yang kita ambil.

Menandai Tipe dengan PhantomData #

PhantomData<T> tinggal di modul std::marker dan menempati nol byte. Diletakkan sebagai field, PhantomData membuat type parameter dianggap dipakai — pemeriksaan tipe berjalan penuh — sementara layout struct sama sekali tidak bertambah. Mari kita perbaiki desain Length:

rust
use std::marker::PhantomData;

struct Meters;
struct Millimeters;

struct Length<Unit> {
    value: u32,
    unit: PhantomData<Unit>,
}

impl Length<Meters> {
    fn from_meters(value: u32) -> Length<Meters> {
        Length { value, unit: PhantomData }
    }
}

impl Length<Millimeters> {
    fn from_millimeters(value: u32) -> Length<Millimeters> {
        Length { value, unit: PhantomData }
    }
}

fn combine_meters(a: &Length<Meters>, b: &Length<Meters>) -> Length<Meters> {
    Length { value: a.value + b.value, unit: PhantomData }
}

fn main() {
    let room_width = Length::from_meters(4);
    let room_length = Length::from_meters(5);
    let total = combine_meters(&room_width, &room_length);

    println!("Lebar ruangan: {} m", room_width.value);
    println!("Panjang ruangan: {} m", room_length.value);
    println!("Gabungan dua sisi: {} m", total.value);
}

Output yang Dihasilkan:

text
Lebar ruangan: 4 m
Panjang ruangan: 5 m
Gabungan dua sisi: 9 m

Mari kita bedah kode di atas baris per baris:

  1. Baris 3-4 (struct Meters; dan struct Millimeters;): kedua satuan dideklarasikan sebagai unit struct — tipe tanpa field yang menempati nol byte. Peran keduanya hanya sebagai marker tipe di level kompilasi, bukan penyimpan data.
  2. Baris 6 (struct Length<Unit> {): struct generik dengan satu type parameter. Berbeda dari newtype Meters(u32) pada Chapter sebelumnya, definisi ini menulis sekali dan bekerja untuk satuan apa pun.
  3. Baris 8 (unit: PhantomData<Unit>): kunci dari seluruh pola. Field ini “memakai” Unit sehingga E0392 hilang, tetapi PhantomData berukuran nol sehingga tidak menambah data runtime.
  4. Baris 11-21 (dua blok impl): constructor from_meters dan from_millimeters masing-masing mengunci tipe hasilnya. Notasi turbofish tidak diperlukan di pemanggil karena tipe kembalian constructor sudah memastikan Unit yang mana yang dipakai.
  5. Baris 13 (Length { value, unit: PhantomData }): membuat nilai dengan menulis PhantomData — tipe ini berupa unit struct tanpa isi, jadi pembuatannya tidak menyimpan apa pun.
  6. Baris 23 (fn combine_meters(a: &Length<Meters>, b: &Length<Meters>) -> Length<Meters>): fungsi penjumlah hanya menerima Length<Meters>. Signature inilah pagar yang nanti kita uji dengan nilai satuan yang berbeda.
  7. Baris 28-29 (let room_width = ...; let room_length = ...;): dua nilai dibuat lewat constructor berbeda; keduanya bertipe Length<Meters> dan aman dijumlahkan pada baris 30.

Satuan Campur Tetap Tertangkap #

Sekarang kita uji pagarnya. Program berikut memakai definisi yang sama persis, tetapi main mengirim Length<Millimeters> ke fungsi penjumlah meter:

rust
use std::marker::PhantomData;

struct Meters;
struct Millimeters;

struct Length<Unit> {
    value: u32,
    unit: PhantomData<Unit>,
}

impl Length<Meters> {
    fn from_meters(value: u32) -> Length<Meters> {
        Length { value, unit: PhantomData }
    }
}

impl Length<Millimeters> {
    fn from_millimeters(value: u32) -> Length<Millimeters> {
        Length { value, unit: PhantomData }
    }
}

fn combine_meters(a: &Length<Meters>, b: &Length<Meters>) -> Length<Meters> {
    Length { value: a.value + b.value, unit: PhantomData }
}

fn main() {
    let room_width = Length::from_meters(4);
    let cable_length = Length::from_millimeters(3500);

    let total = combine_meters(&room_width, &cable_length);
    println!("Gabungan: {} m", total.value);
}

Kompilasi berakhir dengan error:

text
error[E0308]: mismatched types
  --> main.rs:31:45
   |
31 |     let total = combine_meters(&room_width, &cable_length);
   |                 --------------              ^^^^^^^^^^^^^ expected `&Length<Meters>`, found `&Length<Millimeters>`
   |                 |
   |                 arguments to this function are incorrect
   |
   = note: expected reference `&Length<Meters>`
              found reference `&Length<Millimeters>`
note: function defined here
  --> main.rs:23:4
   |
23 | fn combine_meters(a: &Length<Meters>, b: &Length<Meters>) -> Length<Meters> {
   |    ^^^^^^^^^^^^^^                     ------------------

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Pesan E0308 memperlihatkan hasil yang kita kejar: Length<Meters> dan Length<Millimeters> diperlakukan sebagai tipe yang benar-benar berbeda oleh kompilator, padahal keduanya lahir dari satu definisi struct yang sama. Perlindungan ini setara dengan newtype Meters(u32) dari Chapter Newtype Pattern, tetapi dengan satu keunggulan struktural: menambah satuan baru — misalnya struct Inches; beserta from_inches — tidak memerlukan definisi struct tambahan, hanya marker dan constructor baru pada Length yang sudah ada.

Type Safety Tanpa Satu Byte Pun #

Janji “tanpa data runtime” sebaiknya dibuktikan, bukan dipercaya. Kita ukur ukuran tipe-tipe yang terlibat:

rust
use std::marker::PhantomData;

struct Id<T> {
    value: u32,
    marker: PhantomData<T>,
}

fn main() {
    println!("Ukuran PhantomData<u8>: {} byte", std::mem::size_of::<PhantomData<u8>>());
    println!("Ukuran Id<u8>         : {} byte", std::mem::size_of::<Id<u8>>());
    println!("Ukuran Id<String>     : {} byte", std::mem::size_of::<Id<String>>());
}

Output yang Dihasilkan:

text
Ukuran PhantomData<u8>: 0 byte
Ukuran Id<u8>         : 4 byte
Ukuran Id<String>     : 4 byte

Tiga angka pada output tersebut menceritakan semuanya:

  1. PhantomData<u8> = 0 byte: tipe marker ini tidak menyimpan apa pun, untuk T apa pun.
  2. Id<u8> = 4 byte: struct dengan u32 plus PhantomData<u8> hanya sebesar u32 itu sendiri — marker tidak menambah satu byte.
  3. Id<String> = 4 byte: pengamatan paling penting. Menuliskan PhantomData<String> tidak menyimpan String, tidak mengalokasikan heap, dan tidak memicu drop. String di posisi marker hanya berperan sebagai label tipe; ukuran Id tetap 4 byte. Kompilator menjalankan pemeriksaan tipe atas parameter tersebut, lalu membuangnya dari layout memori.

Selain pelabelan satuan, PhantomData dipakai std untuk dua tugas penting lain yang baiknya kita kenali wujudnya tanpa dibedah mendalam: memberi tahu kompilator bahwa struct yang menyimpan raw pointer secara konseptual memiliki (owns) data bertipe T — memengaruhi kapan drop-check berjalan — dan menyatakan kepemilikan tipe pada binding FFI. Keduanya muncul lagi pada Section Unsafe Rust; untuk kode aplikasi sehari-hari, pelabelan tipe seperti contoh Length dan Id adalah wajah PhantomData yang paling sering kita jumpai.

Ringkasan #

PhantomData<T> membuat type parameter yang tidak menyimpan data tetap lolos E0392: field unit: PhantomData<Unit> mengikat parameter Unit pada identitas tipe, sehingga Length<Meters> dan Length<Millimeters> dijaga sebagai tipe berbeda — pengecampuran keduanya ditolak lewat E0308 seperti halnya newtype. Seluruh mekanisme berlangsung di level kompilasi: PhantomData berukuran nol byte, Id<String> tetap 4 byte, dan marker satuan berupa unit struct tidak menyentuh layout memori sama sekali. Dibanding newtype, pola ini menulis definisi sekali untuk semua varian; dibanding type alias, pola ini benar-benar memisahkan tipe. Berikutnya kita berpindah dari pelabelan tipe ke jembatan antar tipe: trait From dan Into.