Ownership, Borrowing, dan Lifetimes di Rust: Panduan Lengkap • Erlkim

Ownership, Borrowing, dan Lifetimes di Rust: Panduan Lengkap

Ownership, Borrowing, dan Lifetimes di Rust: Panduan Lengkap
Jun 14, 2026 ... views
15 min

Di artikel sebelumnya kita sudah belajar dasar Rust dari nol. Sekarang saatnya memahami konsep yang paling membedakan Rust dari bahasa lain: ownership, borrowing, dan lifetimes.

Tiga konsep ini adalah alasan mengapa Rust bisa menjamin keamanan memori tanpa garbage collector. Pahami ketiganya, dan kamu sudah menguasai 80 0ari Rust.

Mengapa Rust Butuh Konsep Ini?

Bahasa pemrograman lain menangani memori dengan dua cara:

  • Garbage Collector (Go, Java, Python): Runtime otomatis membersihkan memori yang tidak dipakai. Mudah tapi ada overhead performa.
  • Manual (C, C++): Programmer sendiri yang alokasi dan dealokasi memori. Cepat tapi rawan bug seperti double free, use after free, dan memory leak.

Rust mengambil jalur ketiga: ownership system. Memori dikelola saat compile time, bukan runtime. Tidak ada garbage collector, tidak ada manual free. Compiler yang memastikan memori dikelola dengan benar.

Hasilnya? Performa seperti C/C++ dengan keamanan yang bahkan lebih tinggi.

Ownership

Ownership adalah aturan paling fundamental di Rust. Ada tiga aturan:

  1. Setiap nilai di Rust punya satu owner (pemilik)
  2. Hanya ada satu owner di satu waktu
  3. Ketika owner keluar dari scope, nilai tersebut dihapus (dropped)

Contoh Dasar Ownership

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    // println!("{}", s1); // ERROR! s1 sudah tidak valid
    println!("{}", s2); // OK, s2 sekarang owner-nya
}

Apa yang terjadi di sini?

  1. s1 dibuat dengan nilai “hello”
  2. let s2 = s1 memindahkan (move) ownership dari s1 ke s2
  3. Setelah move, s1 tidak lagi valid
  4. Jika kamu coba pakai s1, compiler akan error

Ini berbeda dari bahasa lain. Di Python atau JavaScript, s2 = s1 hanya menyalin referensi. Di Rust, ownership benar-benar berpindah.

Stack vs Heap

Untuk memahami ownership, kamu perlu tahu perbedaan stack dan heap:

  • Stack: Penyimpanan cepat untuk data berukuran tetap (integer, float, boolean)
  • Heap: Penyimpanan untuk data berukuran dinamis (String, Vec, HashMap)

Saat kamu menulis let x = 5, nilai 5 disimpan di stack. Operasi copy sangat murah.

Saat kamu menulis let s = String::from("hello"), data string disimpan di heap, dan pointer-nya di stack. Operasi move memindahkan pointer, bukan data heap-nya.

Copy vs Move

Tipe data di stack yang sederhana punya trait Copy:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y); // OK! x masih valid
}

Integer, float, boolean, dan char adalah tipe Copy. Mereka disalin saat di-assign, bukan dipindahkan. Tapi String, Vec, dan tipe heap lainnya di-move, bukan di-copy.

Clone

Jika kamu ingin menyalin data heap secara eksplisit, gunakan clone():

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2); // OK! Keduanya valid
}

clone() membuat salinan lengkap data di heap. Gunakan dengan bijak karena operasi ini mahal untuk data besar.

Ownership dan Function

Saat kamu mengirim nilai ke function, ownership juga berpindah:

fn take_ownership(s: String) {
    println!("{}", s);
} // s di-drop di sini

fn main() {
    let s = String::from("hello");
    take_ownership(s);
    // println!("{}", s); // ERROR! s sudah di-move ke function
}

Ini bisa merepotkan jika kamu ingin tetap menggunakan nilai setelah dikirim ke function. Solusinya? Borrowing.

Borrowing (References)

Borrowing memungkinkan kamu menggunakan nilai tanpa mengambil ownership-nya. Bayangkan seperti meminjam buku: kamu bisa membacanya, tapi pemiliknya tetap orang lain.

Immutable References

fn calculate_length(s: &String) -> usize {
    s.len()
} // s keluar dari scope tapi tidak di-drop karena bukan owner

fn main() {
    let s = String::from("hello");
    let length = calculate_length(&s);
    println!("String "{}" panjangnya {}", s, length); // OK!
}

Tanda & di depan tipe artinya kita membuat reference. Function menerima reference, bukan ownership. Setelah function selesai, s tetap valid.

Mutable References

Jika kamu ingin mengubah nilai yang dipinjam, gunakan &mut:

fn add_world(s: &mut String) {
    s.push_str(", world!");
}

fn main() {
    let mut s = String::from("hello");
    add_world(&mut s);
    println!("{}", s); // Output: hello, world!
}

Aturan Borrowing

Rust punya aturan ketat untuk mencegah data races:

  1. Kamu bisa punya banyak immutable references (&T) sekaligus
  2. Atau satu mutable reference (&mut T) saja
  3. Tidak bisa keduanya bersamaan
fn main() {
    let mut s = String::from("hello");

    // OK: Banyak immutable references
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // OK: Satu mutable reference
    let r3 = &mut s;
    r3.push_str("!");
    println!("{}", r3);

    // ERROR: Tidak bisa immutable dan mutable bersamaan
    // let r4 = &s;
    // let r5 = &mut s;
    // println!("{}, {}", r4, r5);
}

Mengapa Aturan Ini Penting?

Aturan ini mencegah data races yang terjadi ketika:

  • Dua pointer mengakses data yang sama secara bersamaan
  • Setidaknya satu pointer menulis data
  • Tidak ada mekanisme sinkronisasi

Data races menyebabkan bug yang sangat sulit di-debug. Rust mencegahnya di compile time.

Dangling References

Rust juga mencegah dangling references yaitu reference ke data yang sudah di-drop:

// fn dangle() -> &String {
//     let s = String::from("hello");n//     &s // ERROR! s akan di-drop saat function selesai
// }

fn no_dangle() -> String {
    let s = String::from("hello");
    s // Pindahkan ownership ke caller
}

Compiler Rust menolak kode yang bisa menghasilkan dangling references. Ini menghilangkan seluruh kategori bug yang umum di C/C++.

Contoh Praktis: Membuat Struct dengan Ownership

Mari kita lihat contoh nyata bagaimana ownership bekerja di struct:

#[derive(Debug)]
struct User {
    name: String,
    email: String,
    age: u32,
}

impl User {
    fn new(name: String, email: String, age: u32) -> Self {
        User { name, email, age }
    }

    fn display(&self) {
        println!("{} ({}) - umur {}", self.name, self.email, self.age);
    }

    fn update_email(&mut self, new_email: String) {
        self.email = new_email;
    }

    fn into_name(self) -> String {
        self.name // Ownership pindah, User di-drop
    }
}

fn main() {
    let mut user = User::new(
        String::from("ERLKIM"),
        String::from("erlkim@mail.com"),
        25,
    );

    user.display();
    user.update_email(String::from("new@mail.com"));
    user.display();

    let name = user.into_name();
    println!("Name: {}", name);
    // user.display(); // ERROR! user sudah di-move
}

Perhatikan tiga jenis reference di method:

  • &self (immutable borrow): Membaca data tanpa mengubah
  • &mut self (mutable borrow): Membaca dan mengubah data
  • self (ownership): Mengambil ownership, struct di-drop setelah method selesai

Contoh: Memory Safety tanpa Garbage Collector

fn main() {
    let mut data = vec![1, 2, 3, 4, 5];

    // Immutable borrow: baca data
    let sum: i32 = data.iter().sum();
    println!("Sum: {}", sum);

    // Mutable borrow: ubah data
    data.push(6);

    // Immutable borrow lagi
    for item in &data {
        print!("{} ", item);
    }
    println!();

    // Transfer ownership
    let data2 = data;
    println!("Data2: {:?}", data2);
    // println!("{:?}", data); // ERROR! data sudah di-move
}

Semua operasi di atas dijamin aman oleh compiler. Tidak ada use-after-free, tidak ada data races, tidak ada dangling pointers.

Tips untuk Menguasai Ownership

1. Jangan Melawan Compiler

Jika compiler error, biasanya ada alasan yang bagus. Jangan paksa dengan clone() berlebihan. Pahami pesan error dan perbaiki kodenya.

2. Gunakan clone() dengan Bijak

clone() memang bisa mengatasi masalah ownership, tapi menambah overhead. Gunakan hanya jika memang perlu salinan baru.

3. Prefer &str dari pada &String

Function yang menerima string sebaiknya pakai &str bukan &String. Ini lebih fleksibel:

// Lebih fleksibel
fn greet(name: &str) {
    println!("Halo, {}!", name);
}

fn main() {
    let owned = String::from("ERLKIM");
    let literal = "Guest";

    greet(&owned); // OK
    greet(literal); // OK
}

4. Baca Pesan Error

Pesan error Rust terkenal sangat informatif. Compiler memberitahu persis apa yang salah dan bagaimana cara memperbaikinya. Biasakan membaca pesan error sampai habis.

5. Latihan, Latihan, Latihan

Ownership adalah konsep yang butuh waktu untuk dipahami. Semakin banyak kamu menulis Rust, semakin natural konsep ini terasa.

Kesimpulan

Ownership, borrowing, dan lifetimes adalah tiga pilar keamanan memori di Rust. Sistem ini memungkinkan Rust menjamin:

  • Tidak ada data races di compile time
  • Tidak ada dangling references
  • Tidak ada double free
  • Tidak ada use after free
  • Tidak perlu garbage collector

Hasilnya adalah kode yang aman, cepat, dan bisa dipercaya. Inilah mengapa Rust digunakan untuk sistem operasi, browser engine, database, dan aplikasi yang membutuhkan keamanan dan performa tinggi.

Di artikel selanjutnya, kita akan belajar tentang error handling, trait, dan generic di Rust.

~Erlkim

Komentar

0/2000