Python

πŸ” Ownership & Borrowing di Rust

Pelajari secara mendalam ownership rules, references, lifetimes, dan move semantics β€” konsep paling penting untuk menguasai Rust

1. Mengapa Ownership Penting?

Ownership adalah konsep paling fundamental dan unik di Rust. Sistem ownership dirancang untuk menyelesaikan masalah keamanan memori yang telah menjadi sumber bug dan kerentanan keamanan selama puluhan tahun di bahasa pemrograman sistem.

Di bahasa seperti C dan C++, programmer bertanggung jawab secara manual untuk mengalokasikan dan membebaskan memori. Ini rentan terhadap berbagai jenis bug:

Jenis Bug Memori Penjelasan Dampak
Use After FreeMengakses memori yang sudah dibebaskanCrash, vulnerability
Double FreeMencoba membebaskan memori dua kaliCrash, corruption
Memory LeakMemori tidak pernah dibebaskanPenggunaan memori terus naik
Buffer OverflowMenulis di luar batas arraySecurity exploit
Data RaceDua thread mengakses data bersamaanUndefined behavior
Dangling PointerPointer menunjuk ke memori yang tidak validCrash, data corruption

Pendekatan Berbeda di Berbagai Bahasa

Bahasa Pendekatan Kelebihan Kekurangan
C / C++Manual (malloc/free)Kontrol penuh, cepatRentan bug memori
Java / GoGarbage CollectorAman, mudahOverhead, pause
PythonReference Counting + GCMudah digunakanLebih lambat
RustOwnership (compile-time)Aman + cepat, zero-costLearning curve curam
Diagram: Perbandingan Memory Management
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             PENDEKATAN MEMORY MANAGEMENT                    β”‚
β”‚                                                             β”‚
β”‚  C/C++:     Programmer ──malloc──▢ Memory ──free──▢ βœ…      β”‚
β”‚             (rentan lupa free, double free, use after free)  β”‚
β”‚                                                             β”‚
β”‚  Java/Go:   Programmer ──new──▢ Memory ──GC──▢ βœ…           β”‚
β”‚             (GC otomatis, tapi ada overhead & pause)        β”‚
β”‚                                                             β”‚
β”‚  Rust:      Compiler ──ownership──▢ Memory ──drop──▢ βœ…    β”‚
β”‚             (diperiksa saat compile, zero runtime cost!)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’‘ Kenapa Rust Berbeda?

Rust memeriksa keamanan memori saat compile time, bukan runtime. Artinya tidak ada overhead saat program berjalan, dan semua bug memori terdeteksi sebelum program dijalankan. Ini yang disebut "zero-cost safety".

2. Stack dan Heap

Untuk memahami ownership, kita perlu memahami bagaimana memori bekerja di Rust. Ada dua area penyimpanan utama: stack dan heap.

Stack β€” Penyimpanan Cepat dan Terstruktur

Rust β€” Stack
fn main() {
    // Tipe data yang disimpan di STACK:
    // - Semua integer (i32, u64, dll)
    // - Float (f32, f64)
    // - Boolean (bool)
    // - Character (char)
    // - Tuple (jika semua elemennya stack-allocated)
    // - Array dengan ukuran tetap

    let angka: i32 = 42;        // Stack: 4 byte
    let desimal: f64 = 3.14;    // Stack: 8 byte
    let aktif: bool = true;     // Stack: 1 byte
    let huruf: char = 'A';      // Stack: 4 byte

    let koordinat: (f64, f64) = (1.0, 2.0);  // Stack
    let array: [i32; 3] = [1, 2, 3];          // Stack

    // Stack sangat cepat karena:
    // 1. Alokasi hanya menggeser pointer (push)
    // 2. Deallocation otomatis saat keluar scope (pop)
    // 3. Data di stack selalu berurutan di memori (cache-friendly)

    println!("Semua tipe di atas disimpan di stack");
}

Heap β€” Penyimpanan Fleksibel

Rust β€” Heap
fn main() {
    // Tipe data yang disimpan di HEAP:
    // - String (owned)
    // - Vec
    // - Box
    // - HashMap, BTreeMap, dll.

    let teks = String::from("Hello, Rust!");  // Heap: string data
    // Stack menyimpan: pointer, panjang, kapasitas

    let angka = vec![1, 2, 3, 4, 5];         // Heap: array data
    // Stack menyimpan: pointer, panjang, kapasitas

    let boxed = Box::new(42);                 // Heap: satu nilai i32
    // Stack menyimpan: pointer ke heap

    // Heap digunakan ketika:
    // 1. Ukuran data tidak diketahui saat compile
    // 2. Data perlu hidup lebih lama dari scope saat ini
    // 3. Data perlu dimodifikasi ukurannya (Vec, String)

    println!("teks: {}", teks);
    println!("angka: {:?}", angka);
    println!("boxed: {}", boxed);

    // Semua heap-allocated data di-drop otomatis
    // saat owner keluar dari scope
} // teks, angka, boxed di-drop di sini β€” memori dibebaskan
Diagram: Stack vs Heap Memory
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    STACK MEMORY                          β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚  β”‚ let s = String::from("Hi")β”‚                          β”‚
β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚                          β”‚
β”‚  β”‚ β”‚ ptr: ───────────────────┼──┐                       β”‚
β”‚  β”‚ β”‚ len: 2                 β”‚ β”‚  β”‚                       β”‚
β”‚  β”‚ β”‚ cap: 2                 β”‚ β”‚  β”‚                       β”‚
β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚  β”‚   HEAP MEMORY         β”‚
β”‚  β”‚                           β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚ let x: i32 = 42          β”‚  └─▢│  H  i     β”‚        β”‚
β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚  β”‚ β”‚ 42                    β”‚ β”‚                          β”‚
β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚                          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚                                                         β”‚
β”‚  Stack: cepat, terbatas, LIFO                           β”‚
β”‚  Heap:  fleksibel, lebih lambat, diakses via pointer    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. Ownership Rules

Rust memiliki tiga aturan dasar ownership yang harus dipahami oleh setiap programmer Rust. Compiler Rust akan mengecek aturan-aturan ini saat compile time dan menolak program yang melanggar.

⚠️ Tiga Aturan Ownership
  1. Setiap nilai di Rust memiliki tepat satu owner (pemilik)
  2. Pada satu waktu, hanya bisa ada satu owner untuk setiap nilai
  3. Ketika owner keluar dari scope, nilai tersebut akan di-drop (memori dibebaskan)

Scope dan Drop

Rust β€” Scope & Drop
fn main() {
    // s belum valid di sini karena belum dideklarasikan
    {
        let s = String::from("hello"); // s mulai valid di sini
        println!("s = {}", s);         // s digunakan di sini
    } // scope berakhir, s di-drop dan memori dibebaskan

    // s tidak lagi valid di sini
    // println!("{}", s); // ERROR: not found in this scope

    // Contoh nested scope
    let luar = String::from("luar");
    {
        let dalam = String::from("dalam");
        println!("{} dan {}", luar, dalam);
    } // dalam di-drop di sini

    println!("{}", luar); // OK, luar masih valid
    // println!("{}", dalam); // ERROR: dalam sudah di-drop
}

// Drop trait β€” disebut otomatis saat owner keluar scope
struct Database {
    nama: String,
}

impl Drop for Database {
    fn drop(&mut self) {
        println!("Menutup koneksi database: {}", self.nama);
    }
}

fn contoh_drop() {
    let db = Database {
        nama: String::from("PostgreSQL"),
    };
    println!("Menggunakan database...");
    // db di-drop otomatis di sini, memanggil method drop()
}

4. Move Semantics

Ketika Anda menetapkan satu variabel ke variabel lain untuk tipe heap-allocated, Rust memindahkan (move) ownership alih-alih menyalin data. Ini disebut move semantics.

Move pada Tipe Heap-Allocated

Rust β€” Move Semantics
fn main() {
    let s1 = String::from("hello");

    // MOVE: ownership s1 dipindahkan ke s2
    let s2 = s1;

    // s1 TIDAK LAGI VALID setelah move
    // println!("{}", s1); // COMPILE ERROR: value borrowed after move

    println!("s2 = {}", s2); // OK, s2 adalah owner baru

    // Visualisasi sebelum move:
    // Stack: s1 β†’ [ptr, len=5, cap=5] β†’ Heap: "hello"
    //
    // Visualisasi setelah move:
    // Stack: s1 (invalid)    s2 β†’ [ptr, len=5, cap=5] β†’ Heap: "hello"
    //        ↑ pointer dihapus dari s1, dipindah ke s2
}

Move pada Fungsi

Rust β€” Move & Fungsi
// Memindahkan ownership ke parameter fungsi
fn proses_string(s: String) {
    println!("Memproses: {}", s);
} // s di-drop di sini

// Mengembalikan ownership dari fungsi
fn buat_string() -> String {
    let s = String::from("dibuat di dalam fungsi");
    s  // ownership dipindahkan ke pemanggil
}

// Memindahkan ownership masuk dan keluar fungsi
fn tambah_excl(s: String) -> String {
    let mut hasil = s;
    hasil.push_str("!");
    hasil  // kembalikan ownership
}

fn main() {
    let s1 = String::from("hello");
    proses_string(s1);      // s1 dipindahkan ke fungsi
    // println!("{}", s1);   // ERROR: s1 sudah dipindahkan

    let s2 = buat_string(); // ambil ownership dari fungsi
    println!("{}", s2);

    let s3 = String::from("Rust");
    let s3 = tambah_excl(s3);  // pindahkan masuk, ambil kembali
    println!("{}", s3);         // Rust!

    // Masalah: harus kembalikan ownership setiap kali ribet!
    // Solusi: gunakan REFERENCES (borrowing)
}

Move pada Collection

Rust β€” Move & Collection
fn main() {
    let v1 = vec![1, 2, 3];

    // Vec dipindahkan, bukan di-copy
    let v2 = v1;
    // println!("{:?}", v1); // ERROR: value moved

    // Elemen dalam Vec juga ikut pindah
    println!("v2 = {:?}", v2); // [1, 2, 3]

    // Struct juga menggunakan move semantics
    struct Data {
        nilai: Vec<i32>,
        nama: String,
    }

    let d1 = Data {
        nilai: vec![10, 20],
        nama: String::from("test"),
    };

    let d2 = d1; // SELURUH struct dipindahkan
    // println!("{}", d1.nama); // ERROR: d1 sudah dipindahkan
    println!("{}", d2.nama);    // OK
}
Diagram: Move Semantics
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              MOVE SEMANTICS VISUALIZATION                 β”‚
β”‚                                                          β”‚
β”‚  BEFORE: let s2 = s1;                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚   s1   │───▢│ ptr, len, cap   │───▢│ "hello"  β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚                                                          β”‚
β”‚  AFTER: let s2 = s1;                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚   s1   β”‚ (invalid)β”‚                 β”‚    β”‚          β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚   s2            │───▢│ "hello"  β”‚ β”‚
β”‚                     β”‚ ptr, len, cap   β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                                                          β”‚
β”‚  Pointer "dipindahkan" dari s1 ke s2                     β”‚
β”‚  Heap data TIDAK di-copy, hanya pointer-nya             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

5. Copy dan Clone Traits

Tidak semua tipe menggunakan move semantics. Tipe yang disimpan di stack dan implementasi Copy trait akan di-copy alih-alih dipindahkan. Sedangkan Clone adalah deep copy eksplisit.

Copy Trait

Rust β€” Copy Trait
fn main() {
    // Tipe yang memiliki Copy trait (disimpan di stack):
    // - Semua integer: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128
    // - Float: f32, f64
    // - Boolean: bool
    // - Character: char
    // - Tuple yang semua elemennya Copy: (i32, f64)
    // - Array [T; N] jika T adalah Copy

    let x: i32 = 42;
    let y = x;  // COPY, bukan move!
    println!("x = {}, y = {}", x, y); // Keduanya valid!

    let a: f64 = 3.14;
    let b = a;  // COPY
    println!("a = {}, b = {}", a, b); // Keduanya valid!

    let koordinat = (1.0, 2.0);
    let koordinat2 = koordinat; // COPY (karena f64 adalah Copy)
    println!("{:?} {:?}", koordinat, koordinat2);

    // String TIDAK memiliki Copy trait (ada data di heap)
    let s1 = String::from("hello");
    let s2 = s1; // MOVE, bukan copy!
    // println!("{}", s1); // ERROR!

    // Vec TIDAK memiliki Copy trait
    let v1 = vec![1, 2, 3];
    let v2 = v1; // MOVE
    // println!("{:?}", v1); // ERROR!
}

Clone Trait β€” Deep Copy Eksplisit

Rust β€” Clone
fn main() {
    // Clone membuat salinan lengkap (deep copy)
    let s1 = String::from("hello, dunia!");
    let s2 = s1.clone(); // Deep copy β€” data di heap di-copy

    println!("s1 = {}", s1); // OK! s1 masih valid
    println!("s2 = {}", s2); // OK! s2 adalah salinan

    // Vec clone
    let v1 = vec![1, 2, 3, 4, 5];
    let v2 = v1.clone();

    println!("v1 = {:?}", v1); // [1, 2, 3, 4, 5]
    println!("v2 = {:?}", v2); // [1, 2, 3, 4, 5]

    // Struct clone
    #[derive(Debug, Clone)]
    struct Mahasiswa {
        nama: String,
        umur: u32,
    }

    let m1 = Mahasiswa {
        nama: String::from("Budi"),
        umur: 21,
    };

    let m2 = m1.clone();
    println!("m1 = {:?}", m1); // OK
    println!("m2 = {:?}", m2); // OK

    // Perhatian: clone bisa mahal untuk data besar!
    let besar = vec![0; 1_000_000]; // 1 juta elemen
    let salinan = besar.clone(); // Meng-copy 1 juta elemen!
    // Gunakan clone dengan bijak
}

Implementasi Manual Clone dan Copy

Rust β€” Implement Clone & Copy
// Derive otomatis (paling umum digunakan)
#[derive(Debug, Clone, Copy)]
struct Titik {
    x: f64,
    y: f64,
}

// Struct yang TIDAK bisa Copy (karena ada field heap-allocated)
#[derive(Debug, Clone)]
struct Kontak {
    nama: String,   // String di heap β†’ tidak bisa Copy
    telepon: u64,   // di stack β†’ bisa Copy
}

fn main() {
    let p1 = Titik { x: 1.0, y: 2.0 };
    let p2 = p1; // COPY (karena Titik memiliki Copy trait)
    println!("p1 = {:?}", p1); // OK
    println!("p2 = {:?}", p2); // OK

    let k1 = Kontak {
        nama: String::from("Budi"),
        telepon: 628123456789,
    };
    let k2 = k1.clone(); // CLONE (harus eksplisit)
    // let k3 = k1; // MOVE (bukan Copy karena ada String)
    println!("k1 = {:?}", k1); // OK karena kita clone, bukan move
    println!("k2 = {:?}", k2);
}

6. References dan Borrowing

Alih-alih memindahkan ownership setiap kali, kita bisa meminjam (borrow) data menggunakan references. Reference memungkinkan Anda mengakses data tanpa mengambil ownership-nya.

Immutable References (&T)

Rust β€” Immutable References
// Fungsi menerima reference, bukan ownership
fn hitung_panjang(s: &String) -> usize {
    s.len()
    // s (reference) di-drop, tapi data asli tidak
}

fn cetak_berulang(s: &str, n: usize) {
    for _ in 0..n {
        print!("{} ", s);
    }
    println!();
}

fn main() {
    let s = String::from("Hello, Rust!");

    // &s membuat immutable reference ke s
    let panjang = hitung_panjang(&s);

    // s masih valid karena kita hanya meminjam
    println!("\"{}\" panjangnya {} karakter", s, panjang);

    // Multiple immutable references β€” DIIZINKAN
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("r1={}, r2={}, r3={}", r1, r2, r3);

    // Immutable reference berarti READ-ONLY
    let r = &s;
    // r.push_str("!"); // ERROR: cannot borrow as mutable

    cetak_berulang("Rust", 3); // Rust Rust Rust
}

// Praktik terbaik: gunakan &str (string slice) sebagai parameter
// bukan &String. Ini lebih fleksibel!
fn cetak_pesan(pesan: &str) {
    println!("{}", pesan);
}

fn contoh_fleksibilitas() {
    let owned = String::from("owned string");
    let literal = "string literal";

    cetak_pesan(&owned);  // &String bisa di-convert ke &str
    cetak_pesan(literal);  // &str langsung
}

7. Mutable References

Jika Anda ingin mengubah data yang dipinjam, Anda perlu mutable reference (&mut T). Rust memiliki aturan ketat tentang mutable references untuk mencegah data race.

Aturan Mutable References

⚠️ Aturan Mutable References

Pada satu waktu dalam satu scope, Anda bisa memiliki:

  • Banyak immutable references (&T) β€” ATAU
  • Tepat satu mutable reference (&mut T)
  • Tapi TIDAK keduanya sekaligus!
Rust β€” Mutable References
fn tambah_nilai(v: &mut Vec<i32>, nilai: i32) {
    v.push(nilai);
}

fn normalisasi(data: &mut [f64]) {
    let max = data.iter().cloned().fold(f64::MIN, f64::max);
    for val in data.iter_mut() {
        *val /= max;
    }
}

fn main() {
    // Mutable reference
    let mut angka = vec![1, 2, 3];

    {
        let r = &mut angka;
        r.push(4);
        r.push(5);
    } // r keluar scope, mutable borrow selesai

    println!("angka = {:?}", angka); // [1, 2, 3, 4, 5]

    // Hanya SATU mutable reference pada satu waktu
    let mut data = String::from("hello");
    let r1 = &mut data;
    r1.push_str(" world");
    // let r2 = &mut data; // ERROR: already borrowed

    println!("{}", r1);

    // Tapi setelah r1 selesai digunakan, bisa buat yang baru
    let r2 = &mut data;
    r2.push_str("!");
    println!("{}", r2);

    // Contoh: normalisasi data
    let mut skor = vec![80.0, 95.0, 70.0, 88.0];
    normalisasi(&mut skor);
    println!("Skor ternormalisasi: {:?}", skor);
    // [0.842, 1.0, 0.737, 0.926]

    // Aturan: TIDAK bisa gabung mutable dan immutable
    let mut s = String::from("hello");
    let r_immut = &s;        // immutable borrow dimulai
    println!("{}", r_immut);  // immutable borrow terakhir digunakan
    // borrow selesai di sini (NLL - Non-Lexical Lifetimes)

    let r_mut = &mut s;       // OK! immutable borrow sudah selesai
    r_mut.push_str("!");
    println!("{}", r_mut);
}

Non-Lexical Lifetimes (NLL)

Rust β€” NLL
fn main() {
    let mut data = vec![1, 2, 3];

    // NLL: borrow scope berakhir di penggunaan TERAKHIR, bukan di }
    let first = &data[0];     // immutable borrow dimulai
    println!("first: {}", first); // immutable borrow TERAKHIR
    // Borrow selesai di sini (NLL)!

    data.push(4); // OK! Tidak ada borrow aktif
    println!("data: {:?}", data);

    // Pola umum: cek lalu ubah
    let mut kata = String::from("hello");
    if kata.len() < 10 {
        // kata.len() mengambil &kata β€” tapi selesai di sini
        kata.push_str(", dunia!");
    }
    println!("{}", kata);
}

8. Lifetimes

Lifetimes adalah konsep Rust yang memastikan semua references valid dan tidak menjadi dangling reference. Lifetime pada dasarnya adalah cara compiler melacak berapa lama suatu reference masih valid.

Dangling References

Rust β€” Dangling Reference
// INI AKAN ERROR β€” dangling reference!
// fn buat_reference() -> &String {
//     let s = String::from("hello");
//     &s  // ERROR: s di-drop saat fungsi selesai, reference tidak valid
// }

// SOLUSI 1: Kembalikan ownership, bukan reference
fn buat_string() -> String {
    let s = String::from("hello");
    s  // pindahkan ownership ke pemanggil
}

// SOLUSI 2: Gunakan 'static lifetime (hidup selama program)
fn buat_static() -> &'static str {
    "hello"  // string literal memiliki lifetime 'static
}

fn main() {
    let s = buat_string();
    println!("{}", s);

    let static_ref = buat_static();
    println!("{}", static_ref);

    // Dangling reference dicegah oleh compiler:
    let r;
    {
        let x = 5;
        r = &x; // ERROR di Rust: x terlalu pendek lifetime-nya
    }
    // println!("{}", r); // x sudah di-drop!
}

Bagaimana Compiler Menentukan Lifetime

Rust β€” Lifetime Elision
// ATURAN ELISION (compiler menentukan lifetime otomatis):

// 1. Setiap parameter reference mendapat lifetime terpisah
// fn foo(x: &str, y: &str) -> &str
// menjadi: fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &str

// 2. Jika tepat satu input lifetime, output mendapat lifetime yang sama
// fn foo(x: &str) -> &str
// menjadi: fn foo<'a>(x: &'a str) -> &'a str

// 3. Jika ada &self atau &mut self, lifetime output = lifetime self
// fn foo(&self, x: &str) -> &str
// menjadi: fn foo<'a>(&'a self, x: &'a str) -> &'a str

// Contoh elision bekerja:
fn pertama(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    s
}

// Contoh elision TIDAK cukup β€” perlu annotation:
// fn longer(s1: &str, s2: &str) -> &str { ... }
// Compiler tidak tahu output terkait s1 atau s2

fn main() {
    let kalimat = "Hello Rust World";
    let kata_pertama = pertama(kalimat);
    println!("Kata pertama: {}", kata_pertama); // Hello
}

9. Lifetime Annotations

Ketika compiler tidak bisa menentukan lifetime secara otomatis, Anda perlu menambahkan lifetime annotations secara eksplisit. Annotation tidak mengubah lifetime β€” hanya membantu compiler memahami hubungan antar references.

Sintaks Lifetime Annotation

Rust β€” Lifetime Annotations
// Sintaks: &'a T atau &'a mut T
// 'a dibaca "lifetime a" β€” dimulai dengan apostrof

// Fungsi dengan lifetime annotation
// "Kembalikan reference yang hidup selama kedua input hidup"
fn lebih_panjang<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let string1 = String::from("string panjang");
    let result;
    {
        let string2 = String::from("xyz");
        result = lebih_panjang(string1.as_str(), string2.as_str());
        println!("Lebih panjang: {}", result); // OK di sini
    }
    // println!("{}", result); // ERROR: string2 sudah di-drop

    // Contoh: lifetime annotation yang berbeda
    // Mengembalikan SELALU s1, tidak peduli s2
    fn selalu_pertama<'a, 'b>(s1: &'a str, _s2: &'b str) -> &'a str {
        s1
    }

    let s1 = String::from("hello");
    let hasil;
    {
        let s2 = String::from("world");
        hasil = selalu_pertama(&s1, &s2);
    }
    println!("{}", hasil); // OK! karena return lifetime = s1
}

Lifetime pada Struct

Rust β€” Struct dengan Lifetime
// Struct yang menyimpan reference HARUS memiliki lifetime annotation
struct Excerpt<'a> {
    bagian: &'a str,
}

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    // Method yang mengembalikan reference β€” elision bekerja
    fn ambil_bagian(&self) -> &str {
        self.bagian
    }

    // Method dengan parameter tambahan β€” perlu annotation
    fn umumkan(&self, pengumuman: &str) -> &str {
        println!("Perhatian: {}", pengumuman);
        self.bagian
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let kalimat_pertama = novel.split('.').next().expect("Tidak ada titik");

    let i = Excerpt {
        bagian: kalimat_pertama,
    };

    println!("Kutipan: {}", i.bagian);
    println!("Level: {}", i.level());

    // Excerpt tidak bisa hidup lebih lama dari data yang dipinjamnya
}

// Multiple lifetime parameters
struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

fn contoh_pair() {
    let s1 = String::from("hello");
    let pair;
    {
        let s2 = String::from("world");
        pair = Pair { first: &s1, second: &s2 };
        println!("{} {}", pair.first, pair.second);
    }
}

'static Lifetime

Rust β€” 'static Lifetime
fn main() {
    // 'static lifetime β€” hidup selama program berjalan

    // String literal selalu 'static
    let s: &'static str = "Saya hidup selamanya!";
    println!("{}", s);

    // Contoh penggunaan: error messages, constants
    const PESAN: &'static str = "Ini adalah konstanta global";

    // Jangan sembarang gunakan 'static!
    // Gunakan hanya ketika data BENAR-BENAR hidup selama program
    // Bukan sebagai shortcut untuk menghindari lifetime errors

    // Tip: jika compiler meminta 'static, biasanya ada masalah
    // pada ownership, bukan perlu 'static.
}

10. Patterns dan Best Practices

Berikut beberapa pola dan praktik terbaik saat bekerja dengan ownership dan borrowing di Rust.

Menghindari Clone yang Tidak Perlu

Rust β€” Best Practices
// ❌ BURUK: clone berlebihan
fn proses_buruk(data: &Vec<String>) -> Vec<String> {
    let mut hasil = data.clone(); // Clone seluruh Vec
    hasil.push(String::from("tambahan"));
    hasil
}

// βœ… BAIK: gunakan reference
fn proses_baik(data: &[String]) {
    for item in data {
        println!("{}", item);
    }
}

// βœ… BAIK: borrow, bukan take ownership
fn cetak_skor(nama: &str, skor: &[i32]) {
    println!("{}: {:?}", nama, skor);
}

// βœ… BAIK: return owned value jika caller butuh ownership
fn gabung_string(a: &str, b: &str) -> String {
    format!("{} {}", a, b)
}

fn main() {
    let nama = String::from("Budi");
    let skor = vec![85, 90, 78, 92];

    cetak_skor(&nama, &skor); // Pinjam, bukan pindahkan
    println!("{} masih punya datanya: {:?}", nama, skor);

    let hasil = gabung_string("Hello", "Rust");
    println!("{}", hasil);
}

Pola Entry API dengan HashMap

Rust β€” Entry Pattern
use std::collections::HashMap;

fn main() {
    let mut cache: HashMap<String, Vec<i32>> = HashMap::new();

    // ❌ BURUK: dua kali lookup
    // if cache.contains_key("data") {
    //     cache.get_mut("data").unwrap().push(42);
    // } else {
    //     cache.insert("data".to_string(), vec![42]);
    // }

    // βœ… BAIK: gunakan Entry API
    cache.entry("data".to_string())
        .or_insert_with(Vec::new)
        .push(42);

    cache.entry("data".to_string())
        .or_insert_with(Vec::new)
        .push(43);

    println!("Cache: {:?}", cache);
    // {"data": [42, 43]}
}

Pola Rc<T> untuk Shared Ownership

Rust β€” Rc (Reference Counting)
use std::rc::Rc;

fn main() {
    // Rc β€” Reference Counted: shared ownership
    // Berguna ketika beberapa bagian kode perlu "memiliki" data yang sama
    // Hanya untuk single-threaded!

    let data = Rc::new(vec![1, 2, 3, 4, 5]);

    // Rc::clone menambah reference count, BUKAN deep copy
    let data2 = Rc::clone(&data);
    let data3 = Rc::clone(&data);

    println!("data: {:?}", data);
    println!("data2: {:?}", data2);
    println!("data3: {:?}", data3);
    println!("Reference count: {}", Rc::strong_count(&data)); // 3

    // Data di-drop ketika reference count = 0
    drop(data3);
    println!("Setelah drop data3, count: {}", Rc::strong_count(&data)); // 2
}

πŸ“ Quiz Pemahaman Ownership & Borrowing

Uji pemahaman Anda tentang ownership dan borrowing di Rust!

1. Apa yang terjadi saat kita melakukan let s2 = s1.clone(); untuk String?

a) Ownership dipindahkan dari s1 ke s2
b) s1 dan s2 menjadi invalid
c) Deep copy dilakukan β€” keduanya valid dengan data terpisah
d) Hanya pointer yang di-copy

2. Mengapa kode ini tidak bisa dikompile?
let r;
{ let x = 5; r = &x; }
println!("{}", r);

a) Variabel x belum dideklarasikan
b) Tidak bisa menggunakan reference di println
c) x di-drop sebelum r digunakan β€” dangling reference
d) Variabel r harus mutable

3. Berapa banyak immutable references yang bisa ada bersamaan?

a) Hanya satu
b) Maksimal dua
c) Banyak (tidak terbatas), selama tidak ada mutable reference
d) Tidak ada yang diizinkan

4. Apa fungsi lifetime annotation seperti <'a>?

a) Mengatur berapa lama variabel hidup di runtime
b) Memberitahu compiler hubungan lifetime antar references
c) Membuat reference menjadi static
d) Meng-copy data secara otomatis

5. Tipe mana yang memiliki Copy trait?

a) String
b) Vec<i32>
c) i32 dan bool
d) HashMap
πŸ” Zoom
100%
🎨 Tema