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:

rust
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:

text
Budi Santoso saldo Rp250000
Siti Rahma saldo Rp0
Saldo minimum: Rp10000
Saldo Siti Rahma di bawah minimum? true
Budi Santoso saldo Rp325000

Mari kita bedah kode di atas baris per baris:

  1. Baris 7 (fn new(owner: &str, initial_balance: u64) -> Self): tidak ada self di daftar parameter — inilah associated function. Nama new adalah konvensi nama constructor di Rust, bukan kata kunci bahasa. Fungsi ini menerima nama pemilik sebagai string slice lalu saldo awal.
  2. Baris 8 (Self {): Self (huruf S besar) di dalam blok impl merujuk pada tipe yang sedang diimplementasi — di sini Self sama dengan Account. Nilai kembalian Self pada baris 7 dan instansiasi Self { ... } saling menguatkan: bila nama struct kelak berubah, bagian dalam impl tidak ikut diubah.
  3. Baris 9 (owner: String::from(owner)): constructor mengubah pinjaman &str menjadi String milik rekening. Setelah fungsi ini selesai, rekening tidak lagi bergantung pada data pemanggil — pola yang sama persis dengan String::from yang selama ini kita pakai.
  4. Baris 14 (fn new_zero(owner: &str) -> Self): constructor kedua untuk skenario rekening baru tanpa saldo. Kehadiran dua constructor menunjukkan bahwa new bukan satu-satunya pintu — kita bebas menamai pintu lain sesuai kebutuhan domain, misalnya new_zero di sini.
  5. Baris 21 (fn minimum_balance() -> u64): associated function tanpa parameter sama sekali. Nilai 10000 berlaku untuk seluruh tipe, bukan untuk satu rekening — tidak ada alasan menempelkannya pada instance tertentu.
  6. Baris 25 dan 29 (fn deposit(&mut self, ...) dan fn 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().
  7. Baris 35-36 (Account::new(...) dan Account::new_zero(...)): pemanggilan associated function memakai Type::fungsi(). Pada titik ini belum ada instance Account — kedua variabel justru terisi dari hasil pemanggilan tersebut.
  8. Baris 43-44 (wallet.deposit(5000) dst.): setelah instance lahir, method dipanggil dengan operator titik seperti biasa. Saldo wallet menjadi 5000, masih di bawah minimum 10000, sehingga is_below_minimum() mengembalikan true.

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.