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.