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 Free | Mengakses memori yang sudah dibebaskan | Crash, vulnerability |
| Double Free | Mencoba membebaskan memori dua kali | Crash, corruption |
| Memory Leak | Memori tidak pernah dibebaskan | Penggunaan memori terus naik |
| Buffer Overflow | Menulis di luar batas array | Security exploit |
| Data Race | Dua thread mengakses data bersamaan | Undefined behavior |
| Dangling Pointer | Pointer menunjuk ke memori yang tidak valid | Crash, data corruption |
Pendekatan Berbeda di Berbagai Bahasa
| Bahasa | Pendekatan | Kelebihan | Kekurangan |
|---|---|---|---|
| C / C++ | Manual (malloc/free) | Kontrol penuh, cepat | Rentan bug memori |
| Java / Go | Garbage Collector | Aman, mudah | Overhead, pause |
| Python | Reference Counting + GC | Mudah digunakan | Lebih lambat |
| Rust | Ownership (compile-time) | Aman + cepat, zero-cost | Learning curve curam |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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!) β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
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
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
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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.
- Setiap nilai di Rust memiliki tepat satu owner (pemilik)
- Pada satu waktu, hanya bisa ada satu owner untuk setiap nilai
- Ketika owner keluar dari scope, nilai tersebut akan di-drop (memori dibebaskan)
Scope dan 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
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
// 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
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
}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
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
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
// 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)
// 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
Pada satu waktu dalam satu scope, Anda bisa memiliki:
- Banyak immutable references (
&T) β ATAU - Tepat satu mutable reference (
&mut T) - Tapi TIDAK keduanya sekaligus!
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)
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
// 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
// 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
// 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
// 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
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
// β 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
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
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?
2. Mengapa kode ini tidak bisa dikompile?
let r;
{ let x = 5; r = &x; }
println!("{}", r);
{ let x = 5; r = &x; }
println!("{}", r);