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:
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:
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:
Produk: Kopi Gayo
Harga: Rp25000
Stok tersisa: 40 pcsMari kita bedah kode di atas baris per baris:
- Baris 1-5 (
struct Product { ... }): kata kuncistructmemperkenalkan tipe baru bernamaProductkepada kompilator. Tiga field dideklarasikan di dalam blok —namebertipeString,pricedanstockbertipeu64. Definisi ini hanya cetak biru; belum ada data yang tersimpan. - Baris 8-12 (
let coffee = Product { ... }): ekspresiProduct { ... }membuat nilai struct — tindakan ini disebut instansiasi. Setiap field diisi dengan polafield: nilai, dan seluruh field wajib terisi; Rust menolak instansiasi yang setengah jadi. - Baris 14-16 (
coffee.namedst.): operator titik mengakses field dari nilai struct.coffee.namemembaca fieldnameuntuk keperluanprintln!— pembacaan ini hanya meminjam field tersebut, kepemilikancoffeetetap 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.
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:
Akun budi ([email protected]) login sebanyak 1 kaliPenjelasan baris-baris pentingnya:
- Baris 11 (
let mut account = ...): katamutmembuat seluruh struct bisa diubah. Tanpamut, penugasan ulang field mana pun akan ditolak kompilator. - Baris 12-13 (
username,danemail,): ini adalah field init shorthand. Barisusername,setara denganusername: username— kompilator mencocokkan nama field dengan nama variabel lokal yang tersedia di scope. - Baris 17 (
account.login_count += 1;): operator titik mengarah ke fieldlogin_count, lalu nilai0bertambah menjadi1. Field bertipeu64bersifatCopy, sehingga pembacaan tidak memindahkan kepemilikan. - Baris 19-22 (
println!(...)): ketiga field dibaca sekaligus dalam satu format string; structaccounttetap 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:
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:
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:
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:
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:
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:
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.