Kita sebenarnya sudah lama memakai hasil kerja chapter ini tanpa menyadarinya. Setiap kali menulis String::from("Halo"), kita memanggil fungsi milik tipe String — tetapi perhatikan: tidak ada instance String yang dipakai sebagai penerima panggilan itu, karena justru fungsi inilah yang membuat instancenya. Fungsi semacam itu bernama associated function: fungsi yang tinggal di blok impl sebuah tipe, tetapi tidak menerima self. Perannya biasanya dua — constructor yang menyiapkan instance dengan nilai awal yang benar, dan helper yang melayani tipe tanpa butuh data instance. Pada chapter ini kita akan membangun keduanya untuk tipe Account, memakai kata kunci Self, dan melihat pemisah sintaks antara associated function dan method.
Method yang Terikat Instance, Fungsi yang Terikat Tipe #
Bedanya satu parameter. Method — seperti subtotal(&self) dari chapter sebelumnya — selalu menerima instance sebagai parameter pertamanya (self, &self, atau &mut self), sehingga method hanya bisa dipanggil pada sebuah nilai lewat operator titik. Associated function tidak memiliki parameter self sama sekali; fungsi jenis ini terikat pada tipe, bukan pada instance, sehingga tipe tersebut belum tentu ada ketika fungsi dipanggil. Justru karena itulah associated function cocok menjadi pintu masuk pembuat instance: yang dipanggil dulu adalah fungsinya, instance-nya lahir setelahnya.
Kedua jenis fungsi tersebut hidup berdampingan di blok impl yang sama. Pemanggilannya dibedakan oleh sintaks: method memakai titik (instance.method()), associated function memakai operator dua titik bertumpuk (Type::function()) — operator :: yang sama dengan String::from.
Konstruktor dan Helper dalam Satu impl #
Mari kita bangun tipe Account untuk rekening bank fiktif. Kebutuhannya: pembuatan rekening dengan saldo awal, pembuatan rekening kosong, angka saldo minimum yang berlaku untuk semua rekening, lalu dua method untuk setoran dan pemeriksaan saldo:
struct Account {
owner: String,
balance: u64,
}
impl Account {
fn new(owner: &str, initial_balance: u64) -> Self {
Self {
owner: String::from(owner),
balance: initial_balance,
}
}
fn new_zero(owner: &str) -> Self {
Self {
owner: String::from(owner),
balance: 0,
}
}
fn minimum_balance() -> u64 {
10000
}
fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
fn is_below_minimum(&self) -> bool {
self.balance < Account::minimum_balance()
}
}
fn main() {
let mut savings = Account::new("Budi Santoso", 250000);
let mut wallet = Account::new_zero("Siti Rahma");
println!("{} saldo Rp{}", savings.owner, savings.balance);
println!("{} saldo Rp{}", wallet.owner, wallet.balance);
println!("Saldo minimum: Rp{}", Account::minimum_balance());
wallet.deposit(5000);
println!("Saldo {} di bawah minimum? {}", wallet.owner, wallet.is_below_minimum());
savings.deposit(75000);
println!("{} saldo Rp{}", savings.owner, savings.balance);
}Output yang Dihasilkan:
Budi Santoso saldo Rp250000
Siti Rahma saldo Rp0
Saldo minimum: Rp10000
Saldo Siti Rahma di bawah minimum? true
Budi Santoso saldo Rp325000Mari kita bedah kode di atas baris per baris:
- Baris 7 (
fn new(owner: &str, initial_balance: u64) -> Self): tidak adaselfdi daftar parameter — inilah associated function. Namanewadalah konvensi nama constructor di Rust, bukan kata kunci bahasa. Fungsi ini menerima nama pemilik sebagai string slice lalu saldo awal. - Baris 8 (
Self {):Self(huruf S besar) di dalam blokimplmerujuk pada tipe yang sedang diimplementasi — di siniSelfsama denganAccount. Nilai kembalianSelfpada baris 7 dan instansiasiSelf { ... }saling menguatkan: bila nama struct kelak berubah, bagian dalam impl tidak ikut diubah. - Baris 9 (
owner: String::from(owner)): constructor mengubah pinjaman&strmenjadiStringmilik rekening. Setelah fungsi ini selesai, rekening tidak lagi bergantung pada data pemanggil — pola yang sama persis denganString::fromyang selama ini kita pakai. - Baris 14 (
fn new_zero(owner: &str) -> Self): constructor kedua untuk skenario rekening baru tanpa saldo. Kehadiran dua constructor menunjukkan bahwanewbukan satu-satunya pintu — kita bebas menamai pintu lain sesuai kebutuhan domain, misalnyanew_zerodi sini. - Baris 21 (
fn minimum_balance() -> u64): associated function tanpa parameter sama sekali. Nilai10000berlaku untuk seluruh tipe, bukan untuk satu rekening — tidak ada alasan menempelkannya pada instance tertentu. - Baris 25 dan 29 (
fn deposit(&mut self, ...)danfn is_below_minimum(&self)): dua method biasa hidup di blok yang sama. Perhatikan baris 30 — method boleh memanggil associated function lewat nama tipe:Account::minimum_balance(). - Baris 35-36 (
Account::new(...)danAccount::new_zero(...)): pemanggilan associated function memakaiType::fungsi(). Pada titik ini belum ada instanceAccount— kedua variabel justru terisi dari hasil pemanggilan tersebut. - Baris 43-44 (
wallet.deposit(5000)dst.): setelah instance lahir, method dipanggil dengan operator titik seperti biasa. Saldowalletmenjadi5000, masih di bawah minimum10000, sehinggais_below_minimum()mengembalikantrue.
Peran Constructor Menjaga Validitas Data #
Constructor bukan sekadar jalan pintas penulisan. Ia adalah tempat menaruh aturan pembentukan supaya seluruh program hanya punya satu cara sah membuat instance. Pada contoh di atas, siapa pun yang ingin membuat Account harus melewati new atau new_zero — tidak ada jalur lain yang bisa melupakan konversi &str ke String atau salah mengisi saldo awal. Bila kelak ada syarat tambahan (misalnya saldo awal tidak boleh negatif atau nama tidak boleh kosong), pemeriksaan tersebut cukup ditulis satu kali di dalam constructor, dan seluruh pemanggil di program otomatis tunduk pada aturan itu.
Pilihan parameter constructor juga bagian dari desain. Constructor Account::new menerima &str, bukan String, sehingga pemanggil boleh menyuplik string literal atau meminjam teks yang masih dipakai tanpa kehilangan kepemilikan apa pun — konversi ke String milik rekening terjadi tepat di dalam constructor. Semakin sedikit kepemilikan yang diminta sebuah fungsi, semakin longgar pemanggilnya; pelajaran yang sama dari Section Ownership dan Borrowing, kini diterapkan pada tanda tangan constructor.
Satu hal terakhir soal penataan: sebuah tipe boleh memiliki lebih dari satu blok impl. Kita bisa memisahkan constructor di satu blok dan method di blok lainnya, dan kompilator menggabungkan semuanya seolah ditulis bersama. Sebagian besar program kecil memilih satu blok saja agar mudah dipindai, namun pengetahuan ini berguna ketika sebuah tipe tumbuh panjang.
Ringkasan #
Associated function mengisi blok impl dengan fungsi tanpa self: constructor seperti Account::new menyiapkan instance dari nol melalui Self, sedangkan helper seperti minimum_balance() menyimpan nilai yang berlaku untuk seluruh tipe. Operator :: memisahkan pemanggilan fungsi tipe dari pemanggilan method instance yang memakai titik, dan kedua jenis fungsi tersebut dapat saling memanggil dalam satu blok. Dengan data yang terkelompok di struct dan perilaku yang lengkap di impl, satu bentuk data masih kosong dari peta pemilihan: bagaimana merepresentasikan nilai yang pasti satu dari beberapa kemungkinan? Jawabannya dimulai pada Chapter Enum Dasar.