Sejauh ini kita bekerja dengan variabel yang berdiri sendiri: satu variabel untuk nama produk, satu lagi untuk harga, satu lagi untuk stok. Selama program masih kecil, pendekatan tersebut masih terkendali. Namun begitu sebuah entitas dunia nyata — misalnya produk di toko daring — tumbuh menjadi lima atau sepuluh atribut, parameter fungsi mulai menumpuk, urutan argumen mudah tertukar, dan data yang seharusnya hidup bersama tersebar di berbagai tempat. Rust menjawab kebutuhan pengelompokan ini dengan struct: tipe bentukan sendiri yang memberi nama pada kumpulan data yang saling berkaitan. Pada chapter ini kita akan mendefinisikan struct, membuat nilainya, membaca dan mengubah field-nya, serta melihat bagaimana struct tersusun di memori.

Masalah Data yang Terpisah #

Bayangkan kita menulis fungsi pencetak struk untuk toko kopi. Tanpa pengelompokan, fungsinya harus menerima potongan-potongan data lepas:

rust
fn print_receipt(name: &String, price: u64, stock: u64) {
    println!("{} - Rp{} (stok {})", name, price, stock);
}

Pola seperti ini punya tiga kelemahan nyata. Pertama, tidak ada jaminan bahwa name, price, dan stock yang dikirim berasal dari produk yang sama — kompilator membiarkan kita mencampur nama produk A dengan harga produk B. Kedua, setiap kali atribut produk bertambah, kita harus mengubah tanda tangan fungsi beserta seluruh pemanggilnya. Ketiga, kode tidak lagi menceritakan model domainnya: pembaca tidak melihat adanya konsep “produk”, hanya deretan nilai lepas. Struct mengikat data yang menjadi satu kesatuan ke dalam satu tipe bernama, sehingga kompilator dapat menjaga kebersamaan data tersebut.

Mendefinisikan dan Menginstansiasi Struct #

Definisi struct dimulai dengan kata kunci struct, diikuti nama tipe dan blok yang berisi daftar field beserta tipenya. Mari kita bentuk tipe Product untuk toko kopi:

rust
struct Product {
    name: String,
    price: u64,
    stock: u64,
}

fn main() {
    let coffee = Product {
        name: String::from("Kopi Gayo"),
        price: 25000,
        stock: 40,
    };

    println!("Produk: {}", coffee.name);
    println!("Harga: Rp{}", coffee.price);
    println!("Stok tersisa: {} pcs", coffee.stock);
}

Output yang Dihasilkan:

text
Produk: Kopi Gayo
Harga: Rp25000
Stok tersisa: 40 pcs

Mari kita bedah kode di atas baris per baris:

  1. Baris 1-5 (struct Product { ... }): kata kunci struct memperkenalkan tipe baru bernama Product kepada kompilator. Tiga field dideklarasikan di dalam blok — name bertipe String, price dan stock bertipe u64. Definisi ini hanya cetak biru; belum ada data yang tersimpan.
  2. Baris 8-12 (let coffee = Product { ... }): ekspresi Product { ... } membuat nilai struct — tindakan ini disebut instansiasi. Setiap field diisi dengan pola field: nilai, dan seluruh field wajib terisi; Rust menolak instansiasi yang setengah jadi.
  3. Baris 14-16 (coffee.name dst.): operator titik mengakses field dari nilai struct. coffee.name membaca field name untuk keperluan println! — pembacaan ini hanya meminjam field tersebut, kepemilikan coffee tetap utuh.

Nilai coffee selanjutnya mengikuti seluruh aturan ownership yang sudah kita kenal: coffee menjadi pemilik String di dalamnya, dan saat coffee keluar dari scope, buffer di heap ikut di-drop.

Membaca dan Mengubah Field #

Field diakses lewat operator titik. Perubahan nilai field membutuhkan variabel struct yang mut — dan mutabilitas berlaku untuk keseluruhan struct, bukan per field. Rust juga menyediakan field init shorthand: jika nama variabel lokal sama persis dengan nama field, kita cukup menuliskan nama field tanpa field: nilai.

rust
struct UserAccount {
    username: String,
    email: String,
    login_count: u64,
}

fn main() {
    let username = String::from("budi");
    let email = String::from("[email protected]");

    let mut account = UserAccount {
        username,
        email,
        login_count: 0,
    };

    account.login_count += 1;

    println!(
        "Akun {} ({}) login sebanyak {} kali",
        account.username, account.email, account.login_count
    );
}

Output yang Dihasilkan:

text
Akun budi ([email protected]) login sebanyak 1 kali

Penjelasan baris-baris pentingnya:

  1. Baris 11 (let mut account = ...): kata mut membuat seluruh struct bisa diubah. Tanpa mut, penugasan ulang field mana pun akan ditolak kompilator.
  2. Baris 12-13 (username, dan email,): ini adalah field init shorthand. Baris username, setara dengan username: username — kompilator mencocokkan nama field dengan nama variabel lokal yang tersedia di scope.
  3. Baris 17 (account.login_count += 1;): operator titik mengarah ke field login_count, lalu nilai 0 bertambah menjadi 1. Field bertipe u64 bersifat Copy, sehingga pembacaan tidak memindahkan kepemilikan.
  4. Baris 19-22 (println!(...)): ketiga field dibaca sekaligus dalam satu format string; struct account tetap hidup dan sah dipakai setelahnya.

Struct di Memori #

Instansiasi Product menghasilkan satu nilai yang tersimpan di stack: dua bilangan bulat (price dan stock) berjajar plus pointer, panjang, dan kapasitas milik String. Buffer teks “Kopi Gayo” tetap tinggal di heap karena field String hanya menyimpan pengelola yang menunjuk ke sana. Kita bisa membuktikan susunan tersebut dengan std::mem::size_of — fungsi standar yang melaporkan ukuran tipe dalam byte:

rust
struct Product {
    name: String,
    price: u64,
    stock: u64,
}

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

    let coffee = Product {
        name: String::from("Kopi Gayo"),
        price: 25000,
        stock: 40,
    };

    println!("{} dijual Rp{} (stok {})", coffee.name, coffee.price, coffee.stock);
}

Output yang Dihasilkan:

text
Ukuran Product: 40 byte
Ukuran String: 24 byte
Ukuran u64: 8 byte
Kopi Gayo dijual Rp25000 (stok 40)

Angka 40 byte terbentuk dari 24 byte pengelola String ditambah 8 byte price dan 8 byte stock. Perhatikan bahwa teks “Kopi Gayo” (9 karakter) tidak menambah ukuran struct — panjang teks hanya memengaruhi alokasi di heap, bukan ukuran field. Relasi tersebut tergambar di bawah ini:

Diagram di atas menegaskan bahwa struct hanyalah wadah berdampingan di stack; hanya field bertipe String yang membawa tanggung jawab alokasi heap. Konsekuensinya penting: karena String tidak Copy, struct Product juga otomatis tidak Copy — penyerahan coffee ke fungsi akan memindahkan kepemilikannya seutuhnya.

Dua Kesalahan Kompilasi yang Sering Muncul #

Kesalahan pertama menyangkut field init shorthand yang tidak cocok. Jika variabel lokal bernama name dipakai untuk field username, penulisan singkat name, justru membuat kompilator mencari field bernama name:

rust
struct UserAccount {
    username: String,
    email: String,
}

fn main() {
    let name = String::from("budi");
    let email = String::from("[email protected]");

    let account = UserAccount {
        name,
        email,
    };

    println!("Akun: {}", account.username);
}

Pesan error kompilator:

text
error[E0560]: struct `UserAccount` has no field named `name`
  --> main.rs:11:9
   |
11 |         name,
   |         ^^^^ `UserAccount` does not have this field
   |
   = note: available fields are: `username`

error: aborting due to 1 previous error

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

Pesan E0560 menegaskan bahwa shorthand name, berarti “isi field bernama name”, bukan “isi field apa pun dengan nilai dari variabel name”. Perbaikannya satu dari dua: ganti nama variabel lokal menjadi username, atau tulis eksplisit username: name.

Kesalahan kedua melibatkan ownership. Karena struct memuat String, struct tidak mengimplementasikan Copy — mengirimkannya ke fungsi berarti memindahkan kepemilikan seluruh struct:

rust
struct Product {
    name: String,
    price: u64,
}

fn print_receipt(product: Product) {
    println!("{} - Rp{}", product.name, product.price);
}

fn main() {
    let coffee = Product {
        name: String::from("Kopi Gayo"),
        price: 25000,
    };

    print_receipt(coffee);

    println!("Cek stok lewat harga Rp{}", coffee.price);
}

Pesan error kompilator:

text
error[E0382]: borrow of moved value: `coffee`
  --> main.rs:18:43
   |
11 |     let coffee = Product {
   |         ------ move occurs because `coffee` has type `Product`, which does not implement the `Copy` trait
...
16 |     print_receipt(coffee);
   |                   ------ value moved here
17 |
18 |     println!("Cek stok lewat harga Rp{}", coffee.price);
   |                                           ^^^^^^^^^^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `print_receipt` to borrow instead if owning the value isn't necessary
  --> main.rs:6:27
   |
 6 | fn print_receipt(product: Product) {
   |    -------------          ^^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
note: if `Product` implemented `Clone`, you could clone the value
  --> main.rs:1:1
   |
 1 | struct Product {
   | ^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
16 |     print_receipt(coffee);
   |                   ------ you could clone this value

error: aborting due to 1 previous error

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

Pesan E0382 mengingatkan hukum yang sudah kita kuasai di Section Ownership: pemanggilan print_receipt(coffee) memindahkan coffee — termasuk seluruh data heap miliknya — ke dalam fungsi, sehingga akses field setelahnya tidak sah. Dua catatan tambahan pada pesan tersebut menawarkan dua jalan keluar: mengubah parameter menjadi pinjaman, atau menurunkan Clone supaya nilai bisa digandakan. Solusi termurah sudah kita kenal — ubah parameter menjadi &Product dan kirim &coffee. Aturan lama berlaku tanpa pengecualian; struct hanyalah wadah baru bagi nilai-nilai tersebut.

Ringkasan #

Struct memberi nama pada kumpulan data yang saling berkaitan: definisi struct memetakan field dan tipe, instansiasi mengisi seluruh field, dan operator titik membaca serta mengubah field melalui variabel mut. Nilai struct menempati stack selaras dengan field-nya, sementara field bertipe String tetap menunjuk ke heap. Field init shorthand merapikan instansiasi, tetapi hanya bekerja ketika nama variabel sama dengan nama field (E0560 bila keliru), dan struct non-Copy tunduk pada move semantics penuh (E0382 bila dipakai setelah pindah). Dengan data yang kini terkelompok, pertanyaan berikutnya muncul: bagaimana melengkapi struct dengan perilaku? Jawabannya adalah blok impl pada Chapter Metode dengan impl.