Struct mengelompokkan data, tetapi perilaku data tersebut masih tersebar sebagai fungsi-fungsi bebas di berbagai sudut file. Fungsi subtotal menerima &OrderItem, fungsi format_label menerima &OrderItem juga, dan keduanya tidak punya ikatan formal dengan tipe yang mereka layani — pembaca kode harus menebak-nebak hubungan itu sendiri. Rust menyatukan data dan perilaku lewat blok impl: semua fungsi yang melayani sebuah tipe dideklarasikan di satu tempat, dan fungsi yang menerima self disebut method — dipanggil dengan sintaks titik yang sudah akrab dari pemanggilan teks.len() atau teks.push_str(...) pada String. Pada chapter ini kita akan memindahkan perilaku item pesanan ke dalam blok impl dan memahami tiga bentuk penerimaan self.

Dari Fungsi Bebas ke Metode #

Mari kita mulai dari keadaan lama: struct OrderItem plus fungsi bebas yang membacanya.

rust
struct OrderItem {
    name: String,
    price: u64,
    quantity: u64,
}

fn subtotal(item: &OrderItem) -> u64 {
    item.price * item.quantity
}

fn main() {
    let item = OrderItem {
        name: String::from("Kopi Gayo"),
        price: 25000,
        quantity: 3,
    };

    println!("{} subtotal Rp{}", item.name, subtotal(&item));
}

Output yang Dihasilkan:

text
Kopi Gayo subtotal Rp75000

Kode ini sah dan berjalan, tetapi perhatikan dua hal. Pertama, hubungan antara OrderItem dan subtotal hanya hidup di kepala pembaca — tidak ada yang memaksa subtotal diletakkan dekat struct-nya. Kedua, pemanggilan subtotal(&item) menuntut kita mengingat nama fungsi lalu menyuplik pinjaman &item secara manual. Semakin banyak operasi untuk satu tipe, semakin banyak beban ingatan ini. Blok impl menyelesaikan keduanya sekaligus.

Kata kunci impl diikuti nama tipe membuka blok yang menjadi rumah resmi bagi seluruh method tipe tersebut. Mari kita pindahkan perilaku OrderItem:

rust
struct OrderItem {
    name: String,
    price: u64,
    quantity: u64,
}

impl OrderItem {
    fn subtotal(&self) -> u64 {
        self.price * self.quantity
    }

    fn label(&self) -> String {
        format!("{} x{} (Rp{})", self.name, self.quantity, self.subtotal())
    }

    fn apply_discount(&mut self, percent: u64) {
        self.price = self.price * (100 - percent) / 100;
    }
}

fn main() {
    let mut item = OrderItem {
        name: String::from("Kopi Gayo"),
        price: 25000,
        quantity: 3,
    };

    println!("Sebelum diskon: {}", item.label());

    item.apply_discount(20);

    println!("Setelah diskon: {}", item.label());
    println!("Subtotal akhir: Rp{}", item.subtotal());
}

Output yang Dihasilkan:

text
Sebelum diskon: Kopi Gayo x3 (Rp75000)
Setelah diskon: Kopi Gayo x3 (Rp60000)
Subtotal akhir: Rp60000

Mari kita bedah kode di atas baris per baris:

  1. Baris 7 (impl OrderItem {): blok impl dibuka dengan nama tipe yang dilayani. Semua fn di dalamnya secara otomatis terikat pada OrderItem.
  2. Baris 8 (fn subtotal(&self) -> u64): parameter pertama &self adalah ciri method. self adalah nama khusus untuk instance yang memanggil, dan & di depannya berarti method ini hanya meminjam — isi baris 9 mengalikan self.price dengan self.quantity sehingga 25000 * 3 menghasilkan 75000 tanpa mengambil alih kepemilikan item. Penulisan &self adalah singkatan dari self: &OrderItem.
  3. Baris 12 (fn label(&self) -> String): method kedua juga meminjam, lalu membentuk teks baru dengan format! di baris 13. Perhatikan pemanggilan self.subtotal() di baris yang sama — method boleh memanggil method lain pada instance yang sama melalui self.
  4. Baris 16 (fn apply_discount(&mut self, percent: u64)): tanda &mut self menyatakan method ini mengubah isi struct. Rumus di baris 17 memangkas harga 20 persen: 25000 * 80 / 100 menjadi 20000.
  5. Baris 22 (let mut item = ...): variabel harus mut karena akan diubah oleh apply_discount — mutabilitas variabel dan mutabilitas penerima self harus sejalan.
  6. Baris 30 (item.apply_discount(20);): pemanggilan method memakai operator titik — item.apply_discount(20), bukan apply_discount(&mut item, 20). Kompilator yang menyuplik referensi &mut secara otomatis.
  7. Baris 32-33 (item.label() dan item.subtotal()): dua method baca dipanggil berurutan setelah perubahan. Subtotal akhir 20000 * 3 adalah 60000, sesuai output.

Mekanisme Pemanggilan Method di Balik Layar #

Sintaks titik menyembunyikan satu langkah kerja kompilator bernama automatic referencing and dereferencing (referensi dan dereferensi otomatis). Saat kita menulis item.apply_discount(20), kompilator menimbang tipe penerima: method meminta &mut OrderItem, sedangkan item adalah OrderItem, maka kompilator menyusun (&mut item).apply_discount(20) secara diam-diam. Proses yang sama bekerja untuk &self — item.subtotal() diterjemahkan menjadi (&item).subtotal(). Berkat mekanisme ini kita tidak perlu menulis & atau &mut pada setiap pemanggilan method; pencocokan dilakukan berdasarkan signature method yang dituju.

Tiga Bentuk self: Pinjam, Pinjam Ubah, atau Miliki #

Ketiga bentuk penerima di bawah ini adalah seluruh pilihan yang tersedia, dan aturannya persis aturan borrowing dari Section sebelumnya:

  • &self — method pembaca. Instance tetap hidup dan bisa dipakai lagi. Cocok untuk kalkulasi dan format seperti subtotal dan label.
  • &mut self — method pengubah. Hanya boleh dipanggil pada variabel mut, dan selama pemanggilan berlangsung tidak ada peminjam lain yang aktif. Cocok untuk mutasi di tempat seperti apply_discount.
  • self — method pemilik. Instance dikonsumsi; setelah pemanggilan selesai, pemanggil kehilangan nilai tersebut. Cocok untuk transformasi yang mengubah nilai menjadi sesuatu yang lain, misalnya method yang mengemas OrderItem menjadi struk final lalu item aslinya memang tidak dibutuhkan lagi.

Ketiga bentuk tersebut juga berlaku untuk enum — blok impl tidak eksklusif milik struct. Pemilihan bentuk mengikuti kebutuhan method terhadap data: semakin sedikit akses yang dibutuhkan, semakin banyak kebebasan yang diberikan kepada pemanggil. Method &self bisa dipanggil pada instance apa pun, method self hanya pada instance yang sah untuk dimakan.

Sebagai gambaran bentuk ketiga, bayangkan OrderItem dilengkapi method fn into_receipt(self) -> String yang membentuk struk final dari nama, harga, dan jumlah item. Tanda tangan self tanpa & menyatakan bahwa pemanggil menyerahkan item-nya untuk selamanya — struk tercetak, item lenyap, dan pemanggil yang mencoba memanggil item.label() setelahnya akan berjumpa E0382 persis seperti kasus struct yang dipindahkan ke fungsi pada chapter pertama Section ini. Method pemilik jarang dibutuhkan pada data yang dipakai berulang, tetapi tepat untuk data yang memang sekali pakai pada satu alur transformasi.

Ringkasan #

Blok impl menjadikan struct sebagai rumah resmi bagi perilakunya: method dideklarasikan dengan fn nama(&self, ...) dan dipanggil lewat operator titik, sementara kompilator menyuplik & atau &mut secara otomatis dari signature method. Tiga bentuk self — pinjam, pinjam ubah, dan miliki — mengikuti aturan borrowing yang telah kita kuasai tanpa pengecualian. Method yang kita tulis sejauh ini selalu terikat pada instance, padahal String::from dan Vec::new dipanggil tanpa instance sama sekali. Bentuk fungsi impl semacam itu akan kita bahas pada Chapter Associated Functions.