Lifetimes
Lifetime berarti "seberapa lama variabel akan hidup". Anda hanya perlu untuk memikirkan tentang lifetime jika kita berbicara soal reference. Ini karena reference tidak bisa hidup lebih lama daripada objek asalnya. Sebagai contoh, function ini tidak akan berjalan:
fn returns_reference() -> &str { let my_string = String::from("I am a string"); &my_string // ⚠️ } fn main() {}
Problem adalah bahwa my_string
hanya hidup di dalam returns_reference
. Kita coba untuk mengembalikan &my_string
, tetapi &my_string
tidak bisa exist tanpa my_string
. Sehingga compiler akan mengatakan tidak.
Code ini juga tidak akan bekerja:
fn returns_str() -> &str { let my_string = String::from("I am a string"); "I am a str" // ⚠️ } fn main() { let my_str = returns_str(); println!("{}", my_str); }
Tapi ia hampir berjalan. Compiler mengatakan:
error[E0106]: missing lifetime specifier
--> src\main.rs:6:21
|
6 | fn returns_str() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
6 | fn returns_str() -> &'static str {
| ^^^^^^^^
missing lifetime specifier
artinya adalah kita perlu untuk menambahkan '
dengan lifetime. Kemudian ia mengatakan bahwa ia contains a borrowed value, but there is no value for it to be borrowed from
. Itu artinya adalah I am a str
tidak dipinjam (borrowed) dari manapun. Ia mengatakan consider using the 'static lifetime
dengan cara menuliskan &'static str
. Jadi menurut compiler, kita harus mencoba mengatakan bahwa ini adalah string literal.
Sekarang codenya bekerja:
fn returns_str() -> &'static str { let my_string = String::from("I am a string"); "I am a str" } fn main() { let my_str = returns_str(); println!("{}", my_str); }
Itu dikarenakan kita mengembalikan sebuah &str
menggunakan lifetime static
. Sedangkan, my_string
hanya bisa dikembalikan sebagai String
: kita tidak bisa mengembalikan sebuah reference dari String
tersebut karena ia akan hangus pada baris berikutnya.
Jadi, fn returns_str() -> &'static str
meberitahukan kepada Rust: "Jangan khawatir, kita hanya akan mengembalikan sebuah string literal". String literals tetap hidup pada seluruh bagian program, sehingga Rust akan menerimanya. Anda akan menyadari bahwa lifetime sebenarnya mirip dengan generic. Saat kita memberitahukan compiler sesuatu seperti <T: Display>
, kita membuat semacam janji kepada compiler bahwa kita hanya akan menggunakan inputan dengan trait Display
. Lifetime juga sama: kita tidak mengubah lifetime dari variabel apapun. Kita hanya memberi tahu compiler bahwa akan menjadi seperti apa lifetime dari inputan tersebut.
Namun 'static
bukanlah satu-satunya lifetime. Sebenarnya, setiap variabel memiliki lifetime, tapi biasanya kita tidak perlu untuk menuliskannya. Compiler Rust sangat cerdas dan biasanya bisa mengetahuinya sendiri. Kita hanya perlu menuliskan lifetimenya saat compiler tidak mengetahuinya.
Ini adalah contoh lain dar lifetime. Bayangkan kita ingin membuat sebuah struct City
dan memberikannya &str
untuk field name. Kita mungkin ingin melakukan hal seperti itu karena ingin memberikan performa yang lebih cepat daripada menggunakan String
. Sehingga kita menuliskannya seperti ini, meskipun code dibawah ini tentunya tidak berhasil:
#[derive(Debug)] struct City { name: &str, // ⚠️ date_founded: u32, } fn main() { let my_city = City { name: "Ichinomiya", date_founded: 1921, }; }
Compiler akan mengatakan:
error[E0106]: missing lifetime specifier
--> src\main.rs:3:11
|
3 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
2 | struct City<'a> {
3 | name: &'a str,
|
Rust memerlukan lifetime untuk &str
karena &str
adalah sebuah reference. Apa yang terjadi apabila value yang merujuk kepada name
menghilang/hangus/hancur? Tentu saja itu tidak aman (unsafe).
Bagaimana tentang 'static
, apakah ia menjadi bisa dijalankan? Kita telah menggunakannya sebelumnya, oleh karenanya mari kita coba di contoh yang ini:
#[derive(Debug)] struct City { name: &'static str, // ubah &str ke &'static str date_founded: u32, } fn main() { let my_city = City { name: "Ichinomiya", date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
Okay, programnya berjalan. Dan mungkin inilah yang Anda inginkan untuk struct. Namun, perhatikan bahwa kita hanya bisa mengambil "string literals", bukan reference ke value tertentu. Sehingga code dibawah ini tidak akan bekerja:
#[derive(Debug)] struct City { name: &'static str, // hidup di seluruh bagian program date_founded: u32, } fn main() { let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // city_names tidak hidup di seluruh program let my_city = City { name: &city_names[0], // ⚠️ Ini adalah &str, bukan &'static str. Ini merupakan reference ke sebuah value di dalam city_names date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
Compiler akan mengatakan:
error[E0597]: `city_names` does not live long enough
--> src\main.rs:12:16
|
12 | name: &city_names[0],
| ^^^^^^^^^^
| |
| borrowed value does not live long enough
| requires that `city_names` is borrowed for `'static`
...
18 | }
| - `city_names` dropped here while still borrowed
Hal ini sangatlah penting untuk dipahami, karena reference yang kita berikan itu sebenarnya memiliki masa hidup yang cukup lama. Tapi, kita berjanji kepada compiler bahwa kita hanya akan memberikan &'static str
, dan itulah letak masalahnya.
Jadinya, sekarang kita ingin mencoba apa yang compiler sarankan sebelumnya. Ia menyarankan untuk menulis struct City<'a>
dan name: &'a str
. Ini berarti bahwa compiler hanya akan mengambil reference untuk name
jika ia hidup sama panjangnya dengan City
.
#[derive(Debug)] struct City<'a> { // City memiliki lifetime 'a name: &'a str, // dan name juga memiliki lifetime 'a. date_founded: u32, } fn main() { let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; let my_city = City { name: &city_names[0], date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
Juga harus diingat bahwa kita bisa menuliskan apapun selain 'a
jika Anda mau. Ini sama seperti generic dimana kita menulis T
dan U
, namun sebenarnya bisa digantikan dengan apapun.
#[derive(Debug)] struct City<'city> { // lifetimenya sekarang bernama 'city name: &'city str, // dan name sekarang memiliki lifetime 'city date_founded: u32, } fn main() {}
Jadi biasanya Anda akan menuliskan 'a, 'b, 'c
, dst. karena itu lebih cepat dan merupakan cara yang paling umum digunakan untuk menuliskannya. Tapi Anda selalu bisa menggantinya jika Anda ingin. Salah satu tips yang baik adalah mengubah lifetime menjadi nama yang "human-readable", yang mana bisa membantu Anda membaca code jika code tersebut sangant rumit.
Mari kita lihat lagi trait untuk generic. Contohnya:
use std::fmt::Display; fn prints<T: Display>(input: T) { println!("T is {}", input); } fn main() {}
Di saat Anda menuliskan T: Display
, itu berarti "tolong ambil T jika ia memiliki Display".
Bukan berarti: "Saya berikan trait Display ke T".
Hal yang sama pula berlaku pada lifetimes. Di saat Anda menulis 'a pada program dibawah ini:
#[derive(Debug)] struct City<'a> { name: &'a str, date_founded: u32, } fn main() {}
Itu berarti "tolong hanya ambil inputan dari name
jika ia hidup setidaknya sepanjang City
".
Bukan berarti: "Saya akan membuat inputan dari name
sama panjangnya dengan City
".
Sekarang kita bisa mempelajari tentang <'_>
yang kita lihat sebelumnya. Ini disebut dengan "anonymous lifetime" dan ini adalah indikator bahwa referencenya sedang digunakan. Sebagai contoh, Rust akan menyarankannya kepada Anda di saat Anda mengimplementasikan struct. Pada contoh di bawah ini, ada satu struct yang hampir bisa berjalan (dengan kata lain, belum bisa dijalankan):
// ⚠️ struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } fn main() {}
Jadinya kita melakukan apa yang kita perlu lakukan untuk struct
: Pertama-tama, kita mengatakan bahwa name
datang dari &str
. Itu berarti kita perlu lifetime, jadi kita perlu memberikannya <'a>
. Kemudian kita melakukan hal yang sama pada struct
untuk menunjukkan bahwa mereka memiliki lifetime yang sama panjangnya. Tapi kemudian Rust memberi tahu kita untuk melakukan hal ini:
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:6:6
|
6 | impl Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
Ia ingin kita menambahkan anonymous lifetime untuk menunjukkan bahwa disitu ada reference yang sedang digunakan. Sehingga jika kita menuliskannya, compilernya berjalan dengan mulus:
struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer<'_> { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } fn main() {}
Anonymous lifetime dibuat agar Anda tidak selalu menuliskan hal-hal seperti impl<'a> Adventurer<'a>
, karena structnya sendiri telah menunjukkan lifetimenya.
Lifetime terkadang bisa menjadi sulit juga rumit di Rust, tapi berikut ini adalah beberapa tips untuk menghindari kebingungan di saat berurusan dengan lifetime:
- Anda tetap bisa menggunakan owned types, menggunakan clone dll. jika Anda ingin menghindarinya untuk saat ini. (melakukan refactor kemudian)
- Seringkali, di saat compiler menginginkan lifetime, Anda akan menuliskan <'a> "dimana-mana" dan kemudian codenya berjalan. Jangan bingung jika code Anda dipenuhi dengan
<'a>
. Itu hanya cara untuk mengatakan kepada compiler bahwa "Jangan khawatir, Compiler, Saya tidak akan memberikan apapun yang masa hidupnya tidak cukup lama". - Anda bisa mengeksplorasi mengenai lifetime sedikit demi sedikit. Tulis code menggunakan owned values, kemudian ubah ia menjadi reference. Compiler akan mulai menegur Anda, tapi juga memberikan beberapa saran. Dan jika dirasa terlalu rumit, maka cukup batalkan perubahan tersebut dan coba lagi di lain waktu.
Mari lakukan ini pada code kita dan lihat apa yang compiler katakan. Pertama-tama, kita akan kembali ke awal dan membuang semua lifetimenya. Dan juga mengimplementasikan Display
. Display
hanya akan mencetak nama dari Adventurer
.
// ⚠️ struct Adventurer { name: &str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() {}
Teguran pertama yang kita terima adalah seperti berikut:
error[E0106]: missing lifetime specifier
--> src\main.rs:2:11
|
2 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 | struct Adventurer<'a> {
2 | name: &'a str,
|
Compiler menyarankan untuk melakukan: menulis <'a>
setelah Adventurer, dan &'a str
. Jadi kita akan melakukan saran tersebut:
// ⚠️ struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() {}
Sekarang, bagian yang sebelumnya dikomplain oleh compiler sudah berjalan dengan baik, tapi kemudian compiler akan menanyakan kita perihal block impl
. Compiler ingin kita menyebutkan bahwa impl tersebut sedang menggunakan reference:
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:6:6
|
6 | impl Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:12:28
|
12 | impl std::fmt::Display for Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
Okay, akan kita tuliskan apa yang disarankan tersebut... dan sekarang codenya bekerja! Sekarang kita bisa membuat Adventurer
dan melakukan sesuatu terhadapnya.
struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer<'_> { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() { let mut billy = Adventurer { name: "Billy", hit_points: 100_000, }; println!("{}", billy); billy.take_damage(); }
Hasilnya adalah:
Billy has 100000 hit points.
Billy has 99980 hit points left!
Jadi, Anda bisa melihat bahwa lifetimes yang dituliskan itu adalah hal dimana compiler seringkali hanya ingin memastikan seberapa lama suatu variabel hidup. Dan compiler biasanya cukup cerdas untuk menebak hampir semua lifetime yang Anda inginkan, dan hanya perlu Anda memberitahukannya sehingga lifetimenya bisa dipastikan.