Copy types
Beberapa type di Rust benar-benar sangat simple. Mereka biasa disebut sebagai copy types. Simple types ini semuanya disimpan pada stack, dan compiler tahu ukuran mereka. Yang artinya bahwa mereka bisa dengan mudah untuk di-copy, jadi compiler selalu meng-copy di saat Anda mengirimnya ke sebuah function. Ia selalu meng-copy karena mereka cukup kecil dan mudah sehingga tidak ada alasan untuk tidak meng-copynya. Jadi Anda tidak perlu khawatir tentang ownership pada type-type ini.
Type-type yang dimaksud ini adalah: integer, float, boolean (true
dan false
), dan char
.
Bagaimana kita bisa tahu jika sebuah type mengimplementasikan copy? (implementasi = menerapkan) Kita bisa periksa hal ini pada dokumentasi. Contohnya, ini adalah dokumentasi untuk char:
https://doc.rust-lang.org/std/primitive.char.html
Pada bagian kiri dari dokumentasi tersebut, Anda bisa menemukan section Trait Implementations. Di situ, Anda akan melihat contoh implementation seperti Copy, Debug, dan Display. Dari dokumentasi itu, kita jadi tahu bahwa char
:
- akan melakukan copy di saat Anda mengirimnya ke function (Copy)
- bisa melakukan print dengan menggunakan
{}
(Display) - bisa melakukan print dengan menggunakan
{:?}
(Debug)
fn prints_number(number: i32) { // Pada fungsi ini, tidak ada -> sehingga ia tidak me-return apapun // Jika number bukan copy type, ia akan mengambilnya dan kita tidak bisa menggunakannya lagi println!("{}", number); } fn main() { let my_number = 8; prints_number(my_number); // Cetak 8. prints_number mengambil copy dari my_number prints_number(my_number); // Cetak 8 lagi. // Tidak ada problem, karena my_number adalah copy type! }
Tapi jika kita melihat dokumentasi dari String, ia bukanlah copy type.
https://doc.rust-lang.org/std/string/struct.String.html
Pada bagian kiri dari dokumentasi tersebut Trait Implementations, Anda bisa melihatnya secara alphabetical order. A, B, C... dan di sana tidak ada Copy di C. Yang ada di sana justru adalah Clone. Clone mirip dengan Copy, tapi biasanya memerlukan memori yang lebih. Juga, Anda perlu memanggilnya menggunakan method .clone()
- ia tidak akan melakukan clone dengan sendirinya.
Di contoh ini, prints_country()
akan mencetak nama negara, yang mana adalah sebuah String
. Kita ingin mencetaknya 2 kali, tapi kita tidak bisa melakukannya:
fn prints_country(country_name: String) { println!("{}", country_name); } fn main() { let country = String::from("Kiribati"); prints_country(country); prints_country(country); // ⚠️ }
Tapi sekarang kita mengerti mengapa pesan ini muncul.
error[E0382]: use of moved value: `country`
--> src\main.rs:4:20
|
2 | let country = String::from("Kiribati");
| ------- move occurs because `country` has type `std::string::String`, which does not implement the `Copy` trait
3 | prints_country(country);
| ------- value moved here
4 | prints_country(country);
| ^^^^^^^ value used here after move
Bagian terpentingnya adalah which does not implement the Copy trait
. Sedangkan di dokumentasi kita bisa lihat bahwa mengimplementasikan trait (sifat) Clone
. Jadi kita bisa menambahkan .clone()
ke code tersebut. Hal ini akan membuat sebuah clone, dan kita kirimkan clone tersebut ke function. Sekarang country
tetap hidup, sehingga kita bisa menggunakannya.
fn prints_country(country_name: String) { println!("{}", country_name); } fn main() { let country = String::from("Kiribati"); prints_country(country.clone()); // buat clonenya berikan clone tersebut ke function. Hanya clonenya saja yang masuk ke function, dan variabel country tetap hidup prints_country(country); }
Dan tentu saja, jika String
sangat besar, .clone()
bisa menggunakan banyak memory. Satu String
bisa saja panjangnya sama seperti isi dari sebuah buku yang tebal, dan setiap kita menggunakan .clone()
, ia akan menyalin buku tersebut. Jadi, menggunakan &
untuk membuat reference jauh lebih cepat, kalau memang memungkinkan untuk dilakukan. Contohnya, code di bawah akan melakukan .push_str()
terhadap sebuah &str
ke dalam String
dan kemudian membuat clone setiap ia digunakan oleh function:
fn get_length(input: String) { // mengambil ownership dari String println!("It's {} words long.", input.split_whitespace().count()); // lakukan split untuk menghitung jumlah kata } fn main() { let mut my_string = String::new(); for _ in 0..50 { my_string.push_str("Here are some more words "); // push kalimat (&str) get_length(my_string.clone()); // buat clonenya setiap saat (setiap ia digunkan oleh fungsi) } }
Hasil cetaknya adalah:
It's 5 words long.
It's 10 words long.
...
It's 250 words long.
Cara di atas menggunakan 50 clone, dimana clonenya dilakukan setelah melakukan push. Sehingga, setiap iterasi selalu memakan memori dua kali lebih besar dari panjang my_string
. Ini adalah cara dimana kita menggunakan reference untuk melakukan hal yang sama, dimana cara yang ini lebih baik daripada melakukan clone:
fn get_length(input: &String) { println!("It's {} words long.", input.split_whitespace().count()); } fn main() { let mut my_string = String::new(); for _ in 0..50 { my_string.push_str("Here are some more words "); get_length(&my_string); } }
Alih-alih membuat 50 clone seperti cara sebelumnya, code yang ini justru sama sekali tidak perlu membuat salinan (menyalinnya berkali kali seperti pada .clone()
).
Variables without values
Variabel tanpa sebuah value disebut sebagai "uninitialized" variable. Uninitialized artinya "tidak diinisialisasi" atau "belum dimulai". Cukup mudah untuk membuatnya: cukup tuliskan let
dan nama variabelnya:
fn main() { let my_variable; // ⚠️ }
Namun Anda tidak bisa menggunakannya untuk saat ini, dan Rust tidak bisa meng-compilenya apabila ada sesuatu yang uninitialized.
Tapi terkadang variabel yang tidak diinisialisasi ini sangat berguna. Contohnya adalah di saat Anda menemukan kasus seperti:
- Anda memiliki code block dan di dalam code block tersebut terdapat variabel yang memiliki value, dan
- Variabel tersebut perlu untuk tetap hidup di luar dari code block.
fn loop_then_return(mut counter: i32) -> i32 { loop { counter += 1; if counter % 50 == 0 { break; } } counter } fn main() { let my_number; { // Anggap saja kita memerlukan code block pada bagian ini let number = { // Anggap saja pada bagian ini adalah proses untuk memproses angka // Dan dari proses ini, akhirnya kita mendapatkan hasil akhirnya 57 }; my_number = loop_then_return(number); } println!("{}", my_number); }
Maka program tersebut akan mencetak 100
.
Anda bisa melihat bahwa my_number
dideklarasikan di dalam fungsi main()
, jadinya ia akan tetap hidup sampai bagian akhir program. Akan tetapi, ia mengambil valuenya dari dalam loop. Namun, value tersebut (hasil dari fungsi loop) akan tetap hidup selama my_number
juga tetap hidup, karena my_number
yang memiliki valuenya. Dan jika Anda justru menulis let my_number = loop_then_return(number)
di dalam block tersebut, maka valuenya akan mati (hangus).
Untuk membantu mempermudah Anda membayangkannya, kita buat satu contoh kasus lagi. Kita sama-sama tahu bahwaloop_then_return(number)
memberikan result 100, jadi kitaa hapus saja fungsi tersebut dan kita ganti menjadi 100
. Juga, sekarang kita tidak memerlukan number
jadinya kita hapus juga variabelnya. Maka, sekarang codenya akan terlihat seperti ini:
fn main() { let my_number; { my_number = 100; } println!("{}", my_number); }
Jadi, cara kerjanya hampir mirip seperti dengan let my_number = { 100 };
.
Dan juga, yang perlu dicatat adalah my_number
bukan mut
. Kembali ke contoh yang sebelumnya (yang menggunakan fungsi loop), kita tidak memberikan value apapun sampai akhirnya kita memberikannya angka berkelipatan 50, jadi sebenarnya nilainya tidak pernah berubah. Pada akhirnya, code dari my_number
itu hanya let my_number = 100;
, hanya saja pada kasus uninitialized variable ini, my_number
menunggu untuk diinisialisasi.