Lewati ke konten

Ownership di Rust

Ownership (kepemilikan) adalah ide yang membuat Rust benar-benar Rust. Dengan ownership, Rust menjamin keamanan memori tanpa garbage collector dan tanpa kamu harus membebaskan memori secara manual. Awalnya memang terasa asing, dan itu wajar: hampir semua orang merasakannya. Tapi ide intinya sederhana. Kita bangun pelan-pelan, ya.

Saat program berjalan, ia menyimpan data di memori. Pada suatu saat memori itu harus dibersihkan. Kalau tidak, program mengalami kebocoran memori (memory leak) dan lama-lama crash. Ada dua cara umum yang dipakai bahasa pemrograman untuk menangani ini:

  • Manual (C/C++): kamu sendiri yang membebaskan memori. Lupa, memori bocor. Terlalu cepat, program crash dan muncul celah keamanan.
  • Garbage collector (Java, Go, Python): sebuah proses di latar belakang membersihkannya untukmu. Praktis, tapi ada harga yang dibayar dari sisi performa dan prediktabilitas.

Rust mengambil jalan ketiga: ia menentukan saat kompilasi kapan tepatnya setiap nilai harus dibersihkan, dengan seperangkat aturan tentang ownership. Tidak perlu membebaskan memori manual, tidak ada garbage collector.

Inilah seluruh sistemnya. Semua hal lain mengikuti dari tiga aturan ini:

  1. Setiap nilai punya satu owner (pemilik), yaitu variabel yang “memiliki” nilai itu.
  2. Hanya boleh ada satu owner pada satu waktu.
  3. Saat owner keluar dari scope (cakupan), nilainya otomatis dibersihkan.

Mari kita lihat setiap aturan beraksi.

Scope adalah area di antara { dan }. Saat scope sebuah variabel berakhir, Rust otomatis membebaskan nilainya. Kamu tidak pernah menulis free():

fn main() {
{
let message = String::from("halo"); // message valid di sini
println!("{message}");
} // <- scope berakhir, `message` otomatis dibersihkan
// println!("{message}"); // ❌ error: message sudah tidak ada di sini
}
▶ Coba sendiri

Kamu tidak perlu membebaskan apa pun. Rust melakukannya tepat saat message keluar dari scope.

Nah, di sinilah mulai menarik. Perhatikan apa yang terjadi saat kamu memberikan satu variabel ke variabel lain:

fn main() {
let s1 = String::from("halo");
let s2 = s1; // ownership PINDAH dari s1 ke s2
println!("{s2}"); // ✅ berhasil
// println!("{s1}"); // ❌ error: s1 sudah di-move
}
▶ Coba sendiri

Saat kamu menulis let s2 = s1, Rust tidak menyalin string-nya. Ia memindahkan (move) ownership dari s1 ke s2. Karena aturan 2 bilang hanya boleh ada satu owner, s1 sekarang tidak valid lagi. Kalau kamu mencoba memakainya, hasilnya:

error[E0382]: borrow of moved value: `s1`

Pesan ini artinya kamu mencoba memakai s1 padahal nilainya sudah dipindahkan.

Kalau kamu memang ingin salinan kedua yang benar-benar terpisah, panggil .clone():

fn main() {
let s1 = String::from("halo");
let s2 = s1.clone(); // membuat salinan lengkap
println!("s1 = {s1}, s2 = {s2}"); // ✅ sekarang keduanya bisa dipakai
}
▶ Coba sendiri

clone() sengaja dibuat eksplisit. Menyalin bisa mahal, jadi Rust ingin kamu memintanya secara terang-terangan.

Mungkin kamu bertanya-tanya kenapa kode ini tidak error:

fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}"); // ✅ keduanya bisa dipakai
}

Nilai kecil berukuran tetap seperti integer, float, boolean, dan char murah untuk disalin, jadi Rust langsung menyalinnya alih-alih memindahkannya. Sebagai pemula, pegangan praktisnya begini:

  • Nilai sederhana (angka, bool, char) → disalin. Variabel aslinya tetap valid.
  • Data yang dimiliki (seperti String) → di-move. Variabel aslinya jadi tidak valid.

Mengirim nilai ke sebuah fungsi juga memindahkannya, persis seperti memberikannya ke variabel lain:

fn take_ownership(s: String) {
println!("Sekarang aku pemiliknya: {s}");
} // s dibuang di sini
fn main() {
let text = String::from("halo");
take_ownership(text);
// println!("{text}"); // ❌ error: text sudah di-move ke dalam fungsi
}

Inilah alasan topik berikutnya, borrowing (peminjaman), ada. Borrowing membuat sebuah fungsi bisa memakai nilai tanpa mengambil ownership-nya, sehingga kamu masih bisa memakainya sesudahnya.

Coba kerjakan sendiri dulu, di Rust Playground atau di proyek lokal. Error dari compiler itu hal biasa selama latihan: baca pesannya, biasanya ia menunjuk langsung ke tempat terjadinya move.

Program ini tidak bisa dikompilasi. Perbaiki supaya ia mencetak draf -> draf v2, tanpa menghapus salah satu variabelnya.

fn main() {
let original = String::from("draf");
let mut edited = original;
edited.push_str(" v2");
println!("{original} -> {edited}");
}
error[E0382]: borrow of moved value: `original`
--> src/main.rs:5:16
|
2 | let original = String::from("draf");
| -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait
3 | let mut edited = original;
| -------- value moved here
4 | edited.push_str(" v2");
5 | println!("{original} -> {edited}");
| ^^^^^^^^ value borrowed here after move

Compiler memberi tahu bahwa original sudah di-move di baris 3 (“value moved here”), lalu kamu mencoba memakainya lagi di baris 5.

Petunjuk: kamu butuh dua string yang terpisah, supaya mengubah yang satu tidak menyentuh yang lain.

Lihat solusi
fn main() {
let original = String::from("draf");
let mut edited = original.clone(); // salinan lengkap yang terpisah
edited.push_str(" v2");
println!("{original} -> {edited}");
}

Output:

draf -> draf v2
▶ Coba sendiri

let mut edited = original; memindahkan string-nya, jadi original tidak valid lagi. .clone() membuat string kedua dengan memorinya sendiri. Sekarang push_str hanya mengubah edited, dan original tetap berisi draf.

Tulis fungsi add_signature yang mengambil ownership sebuah String, menambahkan " -- Ferris" di akhirnya, lalu mengembalikan string tersebut. Panggil dari main dan cetak hasilnya.

Petunjuk: sebuah fungsi bisa mengembalikan nilai yang ia terima. Tulis -> String di signature-nya, dan ambil parameternya sebagai mut text: String supaya kamu boleh mengubahnya.

Lihat solusi
fn add_signature(mut text: String) -> String {
text.push_str(" -- Ferris");
text // ownership pindah kembali ke pemanggil
}
fn main() {
let letter = String::from("Sampai jumpa!");
let letter = add_signature(letter);
println!("{letter}");
}

Output:

Sampai jumpa! -- Ferris
▶ Coba sendiri

Ownership pindah masuk ke fungsi lewat pemanggilan, lalu pindah keluar lagi lewat nilai kembalian. Kita menangkapnya di variabel letter yang baru (shadowing variabel yang lama). Cara ini berhasil, tapi mengoper nilai masuk-keluar terus-menerus lama-lama melelahkan. Justru masalah itulah yang diselesaikan oleh borrowing.

Program ini tidak bisa dikompilasi. Perbaiki supaya ia mencetak Ana masuk ke daftar, lalu ["Ana"].

fn main() {
let mut names = Vec::new();
let name = String::from("Ana");
names.push(name);
println!("{name} masuk ke daftar");
println!("{:?}", names);
}
error[E0382]: borrow of moved value: `name`
--> src/main.rs:5:16
|
3 | let name = String::from("Ana");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
4 | names.push(name);
| ---- value moved here
5 | println!("{name} masuk ke daftar");
| ^^^^ value borrowed here after move

Petunjuk: push mengambil ownership, sama seperti pemanggilan fungsi. Siapa pemilik string itu setelah baris 4? Pakai name sebelum terjadi move, atau tanya pemilik barunya.

Lihat solusi
fn main() {
let mut names = Vec::new();
let name = String::from("Ana");
println!("{name} masuk ke daftar"); // pakai selagi masih milik kita
names.push(name); // sekarang vektor yang memilikinya
println!("{:?}", names);
}

Output:

Ana masuk ke daftar
["Ana"]
▶ Coba sendiri

Setelah names.push(name), vektor itulah pemiliknya, dan name tidak bisa dipakai lagi. Mencetak lebih dulu adalah salah satu solusinya. Cara lain: biarkan urutannya seperti semula, tapi cetak names[0], karena string-nya masih ada di dalam vektor. Clone juga bisa, tapi di sini tidak ada alasan untuk menyalin.

  • Ownership membuat Rust bisa mengelola memori dengan aman tanpa garbage collector.
  • Tiga aturan: satu owner per nilai, satu owner pada satu waktu, dan nilai dibuang saat owner keluar dari scope.
  • Memberikan data yang dimiliki (seperti String) ke variabel lain akan memindahkannya (move), dan variabel aslinya jadi tidak valid.
  • Pakai .clone() untuk salinan yang terpisah; tipe sederhana (angka, bool, char) disalin otomatis.
  • Mengirim nilai ke fungsi juga memindahkannya, dan itulah yang membawa kita ke borrowing.

Kuis singkat

1. Berapa banyak owner yang boleh dimiliki sebuah nilai pada satu waktu di Rust?

2. Method apa yang kamu panggil untuk membuat salinan terpisah sebuah String, alih-alih memindahkannya?

3. Tipe mana yang otomatis disalin, bukan di-move, saat diberikan ke variabel lain?

4. Apa yang terjadi saat kamu mengirim String ke fungsi yang menerimanya by value (bukan lewat referensi)?

5. Menurut halaman ini, kapan Rust otomatis membersihkan (drop) sebuah nilai?

Skor: 0 / 5