Apa itu Ownership?
Ownership (Kepemilikan) adalah sekumpulan aturan yang ngatur gimana program Rust ngelola memori. Semua program harus ngatur cara mereka pake memori komputer pas lagi jalan. Beberapa bahasa punya garbage collection (GC) yang secara rutin nyari memori yang udah nggak kepake pas programnya jalan; di bahasa lain, programmer harus secara eksplisit ngalokasiin dan ngebebasin memorinya. Rust pake pendekatan ketiga: memori dikelola lewat sistem ownership dengan sekumpulan aturan yang dicek sama compiler. Kalau ada aturan yang dilanggar, programnya nggak bakal ke-compile. Nggak ada satu pun fitur dari ownership yang bakal bikin program kita jadi lemot pas lagi jalan.
Karena ownership itu konsep baru buat banyak programmer, emang butuh waktu buat terbiasa. Kabar baiknya, makin kita berpengalaman sama Rust dan aturan sistem ownership-nya, kita bakal makin gampang buat nulis kode yang aman dan efisien secara alami. Semangat terus ya!
Pas kita paham ownership, kita bakal punya pondasi yang kuat buat mahamin fitur-fitur yang bikin Rust unik. Di bab ini, kita bakal belajar ownership lewat beberapa contoh yang fokus ke struktur data yang sangat umum: strings.
Stack dan Heap
Banyak bahasa pemrograman nggak nuntut kita buat sering-sering mikirin soal stack sama heap. Tapi di bahasa pemrograman sistem kayak Rust, apakah sebuah nilai ada di stack atau heap itu ngaruh ke gimana bahasanya berperilaku dan kenapa kita harus ngambil keputusan tertentu. Bagian-bagian dari ownership bakal dijelasin hubungannya sama stack dan heap nanti di bab ini, jadi ini penjelasan singkat buat persiapan.
Baik stack maupun heap adalah bagian dari memori yang tersedia buat dipake kode kita pas runtime, tapi mereka disusun dengan cara yang beda. Stack nyimpen nilai sesuai urutan yang dia dapet terus ngapus nilainya dengan urutan kebalikannya. Ini disebut last in, first out (LIFO). Bayangin tumpukan piring: pas kita nambahin piring lagi, kita taruh di atas tumpukannya, dan pas kita butuh piring, kita ambil satu dari paling atas. Nambahin atau ngambil piring dari tengah atau bawah nggak bakal semudah itu! Nambahin data disebut pushing onto the stack, dan ngambil data disebut popping off the stack. Semua data yang disimpan di stack harus punya ukuran yang udah tau dan tetap. Data dengan ukuran yang nggak tau pas compile time atau ukuran yang mungkin berubah harus disimpan di heap.
Heap itu kurang teratur: pas kita naruh data di heap, kita minta sejumlah tempat tertentu. Memory allocator bakal nemuin tempat kosong di heap yang cukup gede, nandain tempat itu lagi dipake, terus balikin sebuah pointer, yaitu alamat dari lokasi itu. Proses ini disebut allocating on the heap dan kadang disingkat jadi allocating doang (naruh nilai ke stack nggak dianggap sebagai allocating). Karena pointer ke heap itu ukurannya udah tau dan tetap, kita bisa nyimpen pointer-nya di stack, tapi pas kita mau datanya benar-benar, kita harus ngikutin pointer-nya. Bayangin kayak duduk di restoran. Pas masuk, kita bilang jumlah orang di grup kita, terus pelayannya nemuin meja kosong yang pas buat semuanya terus nganterin kita ke sana. Kalau ada temen kita yang telat dateng, mereka bisa nanya kita duduk di mana buat nemuin kita.
Pushing to the stack itu lebih cepet daripada allocating on the heap karena allocator nggak perlu cari-cari tempat buat nyimpen data baru; lokasinya selalu di paling atas stack. Sebagai perbandingan, ngalokasiin tempat di heap butuh kerja ekstra karena allocator harus nemuin dulu tempat yang cukup gede buat nampung datanya terus ngelakuin pembukuan buat persiapan alokasi selanjutnya.
Akses data di heap umumnya lebih lambat daripada akses data di stack karena kita harus ngikutin pointer buat nyampe ke sana. Prosesor zaman sekarang bakal lebih cepet kalau mereka nggak terlalu banyak lompat-lompat di memori. Lanjutin analoginya, bayangin seorang pelayan di restoran yang ngambil orderan dari banyak meja. Bakal paling efisien kalau dia ngambil semua orderan di satu meja sebelum lanjut ke meja berikutnya. Ngambil orderan dari meja A, terus meja B, terus meja A lagi, terus meja B lagi bakal jadi proses yang jauh lebih lambat. Dengan cara yang sama, prosesor biasanya bisa ngerjain tugasnya lebih baik kalau dia kerja sama data yang deket sama data lainnya (kayak di stack) bukannya yang jauh (kayak yang mungkin terjadi di heap).
Pas kode kita manggil sebuah fungsi, nilai-nilai yang dimasukin ke fungsinya (termasuk, mungkin, pointer ke data di heap) sama variabel lokal fungsinya bakal di-push ke stack. Pas fungsinya kelar, nilai-nilai itu bakal di-pop keluar dari stack.
Mantau bagian kode mana yang lagi pake data apa di heap, minimalisir jumlah data duplikat di heap, dan ngebersihin data yang udah nggak kepake di heap biar nggak keabisan tempat adalah masalah-masalah yang diselesein sama ownership. Sekali kita paham ownership, kita nggak bakal butuh sering- sering mikirin soal stack sama heap, tapi tau kalau tujuan utama ownership adalah buat ngelola data heap bisa bantu jelasin kenapa dia cara kerjanya kayak gitu.
Aturan Ownership
Pertama, yuk kita liat aturan-aturan ownership. Inget terus aturan ini pas kita ngerjain contoh-contoh yang bakal ngejelasin aturan ini:
- Tiap nilai di Rust punya seorang owner (pemilik).
- Cuma boleh ada satu owner dalam satu waktu.
- Pas owner-nya keluar dari scope, nilainya bakal di-drop (dihapus).
Scope Variabel
Sekarang setelah kita ngelewatin sintaks dasar Rust, kita nggak bakal masukin
semua kode fn main() { di contoh-contohnya, jadi kalau kita lagi ngikutin,
pastiin buat masukin contoh-contoh berikut ke dalem fungsi main secara manual.
Hasilnya, contoh-contoh kita bakal lebih singkat, biar kita bisa fokus ke
detail aslinya bukannya kode boilerplate.
Sebagai contoh pertama dari ownership, kita bakal liat scope dari beberapa variabel. Sebuah scope adalah range di dalem program di mana sebuah item itu valid. Coba liat variabel ini:
#![allow(unused)]
fn main() {
let s = "hello";
}
Variabel s ngerujuk ke sebuah literal string, di mana nilai string-nya di-hardcoded
ke teks program kita. Variabelnya valid dari titik di mana dia dideklarasikan
sampe akhir dari scope saat ini. Listing 4-1 nunjukin program dengan komentar
yang dianotasi di mana variabel s bakal valid.
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
Dengan kata lain, ada dua titik waktu penting di sini:
- Pas
smasuk ke dalam scope, dia jadi valid. - Dia tetep valid sampe dia keluar dari scope.
Sampai titik ini, hubungan antara scope sama kapan variabel itu valid mirip
sama bahasa pemrograman lainnya. Sekarang kita bakal kembangin pemahaman ini
dengan ngenalin tipe String.
Tipe String
Buat gambarin aturan ownership, kita butuh tipe data yang lebih kompleks dari
yang udah kita bahas di bagian “Tipe Data” di Bab 3. Tipe-tipe
yang udah dibahas sebelumnya ukurannya udah tau, bisa disimpan di stack dan
di-pop keluar dari stack pas scope-nya abis, dan bisa di-copy secara cepet
dan gampang buat bikin instance baru yang independen kalau bagian kode lain
perlu pake nilai yang sama di scope yang beda. Tapi kita mau liat data yang
disimpan di heap dan eksplor gimana Rust tau kapan harus ngebersihin data itu,
dan tipe String adalah contoh yang oke sekali.
Kita bakal fokus ke bagian-bagian String yang terkait sama ownership.
Aspek-aspek ini juga berlaku buat tipe data kompleks lainnya, baik yang
disediain standard library maupun yang kita bikin sendiri. Kita bakal bahas
String lebih dalem di Bab 8.
Kita udah liat literal string, di mana nilai string-nya di-hardcoded ke program
kita. Literal string emang nyaman, tapi mereka nggak cocok buat semua situasi
di mana kita mungkin mau pake teks. Salah satu alasannya karena mereka itu
immutable. Alasan lainnya karena nggak semua nilai string bisa diketahuin pas
kita nulis kode: misalnya, gimana kalau kita mau ngambil input user terus nyimpannya?
Buat situasi kayak gini, Rust punya tipe string kedua, yaitu String. Tipe ini
ngelola data yang dialokasikan di heap dan makanya dia bisa nyimpen sejumlah
teks yang ukurannya nggak kita ketahuin pas compile time. Kita bisa bikin
sebuah String dari literal string pake fungsi from, kayak gini:
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
Operator titik dua ganda :: ngebolehin kita buat ngelempokin fungsi from
ini di bawah tipe String bukannya pake nama kayak string_from. Kita bakal
bahas sintaks ini lebih lanjut di bagian “Sintaks Method” di
Bab 5, dan pas kita bahas soal namespacing pake modul di “Path buat Ngerujuk Item di Pohon Modul”
di Bab 7.
Jenis string ini bisa diubah (mutated):
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{s}"); // this will print `hello, world!`
}
Jadi, apa bedanya di sini? Kenapa String bisa diubah tapi literal nggak bisa?
Bedanya ada di gimana kedua tipe ini nanganin memori.
Memori dan Alokasi
Di kasus literal string, kita tau isinya pas compile time, jadi teksnya di-hardcoded langsung ke file executable final-nya. Ini kenapa literal string itu cepet dan efisien. Tapi sifat-sifat ini cuma dateng dari sifat immutability literal string-nya. Sayangnya, kita nggak bisa naruh sepotong memori ke dalem biner buat tiap teks yang ukurannya nggak tau pas compile time dan ukurannya mungkin berubah pas lagi jalanin programnya.
Dengan tipe String, buat support sepotong teks yang mutable dan bisa nambah
ukurannya, kita perlu ngalokasiin sejumlah memori di heap, yang nggak tau pas
compile time, buat nampung isinya. Ini artinya:
- Memorinya harus diminta dari memory allocator pas runtime.
- Kita butuh cara buat balikin memori ini ke allocator pas kita udah selese
pake
Stringkita.
Bagian pertama itu kita yang ngerjain: pas kita manggil String::from,
implementasinya minta memori yang dia butuhin. Ini hal yang cukup universal di
bahasa pemrograman.
Tapi, bagian kedua itu beda. Di bahasa yang punya garbage collector (GC), GC
bakal terus mantau dan ngebersihin memori yang udah nggak dipake lagi, dan kita
nggak perlu mikirin itu. Di kebanyakan bahasa tanpa GC, itu tanggung jawab kita
buat ngenalin kapan memori udah nggak dipake lagi terus manggil kode buat secara
eksplisit ngebebasinnya, sama kayak pas kita memintanya. Ngelakuin ini dengan
bener secara historis udah jadi masalah pemrograman yang susah. Kalau kita lupa,
kita bakal buang-buang memori. Kalau kita lakuin terlalu cepet, kita bakal punya
variabel yang nggak valid. Kalau kita lakuin dua kali, itu juga sebuah bug.
Kita perlu masangin tepat satu allocate sama tepat satu free.
Rust ngambil jalur yang beda: memorinya otomatis dibalikin begitu variabel yang
punya (owns) memori itu keluar dari scope. Ini versi contoh scope kita dari
Listing 4-1 pake String bukannya literal string:
fn main() {
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
}
Ada titik waktu alami di mana kita bisa balikin memori yang dibutuhin String
kita ke allocator: pas s keluar dari scope. Pas sebuah variabel keluar dari
scope, Rust manggil fungsi khusus buat kita. Fungsi ini namanya drop,
dan di situlah pembuat String bisa naruh kode buat balikin memorinya. Rust
manggil drop secara otomatis di kurung kurawal tutup.
Catatan: Di C++, pola nge-dealokasi resource di akhir masa hidup sebuah item ini kadang disebut Resource Acquisition Is Initialization (RAII). Fungsi
dropdi Rust bakal terasa familiar kalau kita pernah pake pola-pola RAII.
Pola ini punya pengaruh yang sangat dalem ke gimana kode Rust ditulis. Mungkin keliatan simpel sekarang, tapi perilaku kodenya bisa jadi nggak terduga di situasi yang lebih ribet pas kita mau punya banyak variabel pake data yang udah kita alokasiin di heap. Yuk kita eksplor beberapa situasi itu sekarang.
Interaksi Variabel dan Data dengan Move
Beberapa variabel bisa berinteraksi sama data yang sama dengan berbagai cara di Rust. Yuk kita liat contoh pake integer di Listing 4-2.
fn main() {
let x = 5;
let y = x;
}
x ke yKita mungkin bisa nebak apa yang dilakuin kode ini: “bind nilai 5 ke x; terus
bikin copy dari nilai di x terus bind ke y.” Kita sekarang punya dua
variabel, x sama y, dan keduanya sama dengan 5. Ini emang bener yang
terjadi, karena integer adalah nilai simpel dengan ukuran yang udah tau dan
tetap, dan dua nilai 5 ini di-push ke stack.
Sekarang yuk liat versi String-nya:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
Ini keliatannya mirip sekali, jadi kita mungkin asumsikan kalau cara kerjanya
bakal sama: yaitu, baris kedua bakal bikin copy dari nilai di s1 terus bind
ke s2. Tapi nggak gitu yang sebenernya terjadi.
Coba liat Gambar 4-1 buat liat apa yang terjadi di String di balik layar.
Sebuah String disusun dari tiga bagian, yang ditunjukin di sebelah kiri:
sebuah pointer ke memori yang nampung isi string-nya, sebuah length (panjang),
dan sebuah capacity (kapasitas). Grup data ini disimpan di stack. Di sebelah
kanan adalah memori di heap yang nampung isinya.
Gambar 4-1: Representasi di memori dari sebuah String
yang nampung nilai "hello" yang di-bind ke s1
Length itu seberapa banyak memori, dalam byte, yang lagi dipake isinya
String saat ini. Capacity itu total jumlah memori, dalam byte, yang diterima
String dari allocator. Perbedaan antara length sama capacity itu penting,
tapi nggak di konteks ini, jadi buat sekarang, cuekin aja capacity-nya.
Pas kita nge-assign s1 ke s2, data String-nya di-copy, artinya kita copy
pointer, length, dan capacity yang ada di stack. Kita nggak copy data
yang ada di heap yang dirujuk sama pointer-nya. Dengan kata lain,
representasi data di memori keliatannya kayak Gambar 4-2.
Gambar 4-2: Representasi di memori dari variabel s2
yang punya copy dari pointer, length, dan capacity dari s1
Representasinya nggak keliatan kayak Gambar 4-3, yang merupakan penampakan
memori kalau misalnya Rust malah ikut copy data heap-nya juga. Kalau Rust
lakuin ini, operasi s2 = s1 bisa jadi sangat mahal dalam hal performa
Pas runtime kalau datanya di heap itu sangat besar.
Gambar 4-3: Kemungkinan lain soal apa yang mungkin
dilakuin s2 = s1 kalau Rust ikut copy data heap-nya juga
Tadi kita bilang kalau pas sebuah variabel keluar dari scope, Rust otomatis
manggil fungsi drop dan ngebersihin memori heap buat variabel itu. Tapi
Gambar 4-2 nunjukin kedua pointer data nunjuk ke lokasi yang sama. Ini masalah:
pas s2 sama s1 keluar dari scope, mereka berdua bakal nyoba buat ngebebasin
memori yang sama. Ini dikenal sebagai double free error dan merupakan salah
satu bug memory safety yang kita sebutin sebelumnya. Ngebebasin memori dua
kali bisa bikin kerusakan memori (memory corruption), yang berpotensi memicu
kerentanan keamanan.
Buat mastiin keamanan memori, setelah baris let s2 = s1;, Rust nganggep s1
udah nggak valid lagi. Makanya, Rust nggak perlu ngebebasin apa pun pas s1
keluar dari scope. Coba liat apa yang terjadi pas kita nyoba pake s1 setelah
s2 dibuat; nggak bakal bisa:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
Kita bakal dapet error kayak gini karena Rust ngelarang kita pake referensi yang udah nggak valid:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Kalau kita pernah denger istilah shallow copy (copy dangkal) sama deep copy
(copy dalem) pas lagi belajar bahasa lain, konsep nyalin pointer, length,
dan capacity tanpa nyalin datanya mungkin kedengeran kayak lagi bikin shallow
copy. Tapi karena Rust juga ngebatalin variabel pertamanya, bukannya disebut
shallow copy, ini dikenal sebagai move (pindah). Di contoh ini, kita bakal
bilang kalau s1 udah di-move ke dalem s2. Jadi, apa yang benar-benar terjadi
ditunjukin di Gambar 4-4.
Gambar 4-4: Representasi di memori setelah s1 udah
dibatalkan
Itu nyelesein masalah kita! Dengan cuma s2 yang valid, pas dia keluar dari
scope cuma dia sendiri yang bakal ngebebasin memorinya, dan beres deh.
Sebagai tambahan, ada pilihan desain yang tersirat dari sini: Rust nggak bakal pernah otomatis bikin “deep” copy dari data kita. Makanya, penyalinan otomatis apa pun bisa diasumsikan nggak mahal dalam hal performa pas runtime.
Scope dan Assignment
Kebalikan dari ini juga bener buat hubungan antara scoping, ownership, dan
memori yang dibebasin lewat fungsi drop juga. Pas kita ngasih nilai yang
bener-bener baru ke variabel yang udah ada, Rust bakal manggil drop dan
ngebebasin memori nilai aslinya langsung. Coba liat kode ini, contohnya:
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
Kita awalnya mendeklarasikan variabel s terus di-bind ke sebuah String
dengan nilai "hello". Terus kita langsung bikin String baru dengan nilai
"ahoy" terus di-assign ke s. Di titik ini, nggak ada apa pun yang ngerujuk
ke nilai asli di heap sama sekali.
Gambar 4-5: Representasi di memori setelah nilai awal udah diganti seluruhnya.
String aslinya makanya langsung keluar dari scope. Rust bakal jalanin fungsi
drop padanya dan memorinya bakal langsung dibebasin. Pas kita nyetak nilainya
di akhir, hasilnya bakal "ahoy, world!".
Interaksi Variabel dan Data dengan Clone
Kalau kita emang mau copy data heap dari String secara dalem (deeply copy),
nggak cuma data stack-nya aja, kita bisa pake method umum namanya clone.
Kita bakal bahas sintaks method di Bab 5, tapi karena method adalah fitur umum
di banyak bahasa pemrograman, kita mungkin udah pernah liat sebelumnya.
Ini contoh method clone beraksi:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
}
Ini jalan dengan lancar dan secara eksplisit ngasilin perilaku yang ditunjukin di Gambar 4-3, di mana data heap-nya emang ikut di-copy.
Pas kita liat pemanggilan ke clone, kita tau kalau ada sejumlah kode sembarang
yang lagi dijalankan dan kode itu mungkin mahal harganya. Ini adalah indikator
visual kalau ada sesuatu yang beda yang lagi terjadi.
Data Khusus Stack: Copy
Ada hal unik lain yang belum kita bahas. Kode yang pake integer ini—yang sebagiannya ditunjukin di Listing 4-2—jalan dan valid:
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
}
Tapi kode ini kayaknya bertentangan sama apa yang baru aja kita pelajari: kita
nggak manggil clone, tapi x tetep valid dan nggak di-move ke dalem y.
Alasannya karena tipe-tipe kayak integer yang ukurannya udah tau pas compile
time disimpan seluruhnya di stack, jadi nyalin nilai aslinya itu cepet buat
dilakuin. Itu artinya nggak ada alasan kenapa kita mau ngelarang x buat tetep
valid setelah kita bikin variabel y. Dengan kata lain, nggak ada bedanya
antara deep copy sama shallow copy di sini, jadi manggil clone nggak bakal
ngelakuin hal yang beda dari shallow copy biasa, makanya kita bisa
ngelewatinnya.
Rust punya anotasi khusus namanya trait Copy yang bisa kita taruh di tipe-tipe
yang disimpan di stack, kayak integer (kita bakal bahas traits lebih banyak
di Bab 10). Kalau sebuah tipe mengimplementasikan trait Copy,
variabel yang pakenya nggak bakal di-move, tapi lebih ke disalin secara
sepele, bikin mereka tetep valid setelah di-assign ke variabel lain.
Rust nggak bakal ngebolehin kita ngasih anotasi Copy ke sebuah tipe kalau
tipe itu, atau bagian apa pun darinya, udah mengimplementasikan trait Drop.
Kalau tipenya butuh sesuatu yang khusus terjadi pas nilainya keluar dari scope
terus kita nambahin anotasi Copy ke tipe itu, kita bakal dapet compile-time
error. Buat belajar gimana cara nambahin anotasi Copy ke tipe kita buat
mengimplementasikan trait-nya, liat “Derivable Traits” di
Lampiran C.
Jadi, tipe apa aja yang mengimplementasikan trait Copy? Kita bisa cek
dokumentasi buat tipe tertentu buat mastiin, tapi sebagai aturan umum,
kumpulan nilai scalar simpel apa pun bisa mengimplementasikan Copy, dan nggak
ada satu pun yang butuh alokasi atau bentuk resource apa pun yang bisa
mengimplementasikan Copy. Ini beberapa tipe yang mengimplementasikan Copy:
- Semua tipe integer, kayak
u32. - Tipe Boolean,
bool, dengan nilaitruesamafalse. - Semua tipe floating-point, kayak
f64. - Tipe karakter,
char. - Tuple, kalau isinya cuma tipe-tipe yang juga mengimplementasikan
Copy. Contohnya,(i32, i32)mengimplementasikanCopy, tapi(i32, String)nggak.
Ownership dan Fungsi
Mekanisme masukin nilai ke sebuah fungsi mirip sama pas kita ngasih nilai ke sebuah variabel. Masukin variabel ke fungsi bakal nge-move atau copy, sama kayak assignment. Listing 4-3 punya contoh dengan beberapa anotasi yang nunjukin di mana variabel masuk dan keluar dari scope.
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
Kalau kita nyoba pake s setelah manggil takes_ownership, Rust bakal ngelepar
compile-time error. Pengecekan statis ini ngelindungin kita dari kesalahan.
Coba tambahin kode ke main yang pake s sama x buat liat di mana kita bisa
pake mereka dan di mana aturan ownership ngelarang kita buat ngelakuin itu.
Nilai Return dan Scope
Balikin nilai (returning values) juga bisa mentransfer ownership. Listing 4-4 nunjukin contoh fungsi yang balikin sebuah nilai, dengan anotasi yang mirip kayak di Listing 4-3.
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
// a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
Ownership sebuah variabel ngikutin pola yang sama tiap kalinya: ngasih nilai
ke variabel lain bakal nge-move nilainya. Pas sebuah variabel yang isinya
data di heap keluar dari scope, nilainya bakal dibersihin sama drop kecuali
kalau ownership datanya udah di-move ke variabel lain.
Walaupun ini jalan, ngambil ownership terus balikin lagi di tiap fungsi itu agak ribet. Gimana kalau kita mau ngebolehin sebuah fungsi pake sebuah nilai tapi nggak usah ngambil ownership-nya? Agak nyebelin kan kalau apa pun yang kita masukin juga harus dibalikin lagi kalau kita mau pake lagi, ditambah data apa pun hasil dari body fungsinya yang mungkin juga mau kita balikin.
Rust ngebolehin kita buat balikin banyak nilai pake tuple, kayak yang ditunjukin di Listing 4-5.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
Tapi ini terlalu banyak upacaranya (ceremony) dan kerjaan sekali buat konsep yang harusnya umum. Untungnya buat kita, Rust punya fitur buat pake sebuah nilai tanpa mentransfer ownership, namanya references (referensi).