Pernahkah Anda merasakan sensasi saat mempelajari bahasa baru dan berharap bisa langsung berlari kencang, tetapi malah tersandung di langkah pertama? Itulah yang saya alami dengan Rust. Saya memiliki utilitas JavaScript yang berfungsi dengan baik — sesuatu yang mengurai dan mengubah beberapa array — dan saya pikir saya bisa menulis ulang itu saat istirahat kopi. Saya membuka editor, mengetik kode yang terasa alami, dan menjalankan compiler. Hasilnya meledak. Bukan karena kesalahan sintaksis. Bukan karena titik koma yang hilang. Compiler memberi tahu saya bahwa saya mencoba menggunakan nilai yang sudah dipindahkan (moved). Saya menatap layar. Dipindahkan? Saya tidak memindahkan apa pun. Saya hanya menetapkannya ke variabel lain.
Zona Nyaman JavaScript
JavaScript melatih Anda untuk memperlakukan data seperti papan tulis bersama. Anda mendeklarasikan sebuah objek atau array, menyerahkannya ke sebuah fungsi, membiarkan fungsi tersebut menambahkan properti, lalu menyerahkannya ke tempat lain. Garbage collector duduk di latar belakang seperti petugas kebersihan yang tak terlihat, menunggu untuk menyapu apa pun yang Anda tinggalkan. Anda tidak pernah bertanya, "Siapa pemilik string ini?" Anda bertanya, "Bisakah saya membacanya?" Jika jawabannya ya, Anda menyentuhnya. Jika Anda butuh referensi lain, Anda cukup menulis let b = a dan lanjut bekerja. Kedua variabel menunjuk ke bagian memori yang sama, dan jika a mengubahnya, b akan langsung melihat perubahannya. Ini memang praktis. Namun, ini juga kacau, tetapi kekacauan tersebut jarang merugikan Anda karena runtime menangani pembersihannya.
Kejutan Pertama dari Compiler
Rust tidak memercayai Anda. Kedengarannya kasar, tetapi itulah hal pertama yang Anda pelajari. Bahasa ini dibangun di sekitar sistem kepemilikan (ownership system) dengan tiga aturan kaku. Pertama, setiap nilai memiliki tepat satu pemilik. Kedua, hanya ada satu pemilik pada satu waktu. Ketiga, ketika pemilik tersebut keluar dari cakupan (scope), Rust akan menghapus (drop) nilai tersebut secara otomatis. Tanpa garbage collector. Tanpa penghitungan referensi (reference counting) di latar belakang kecuali Anda memilihnya secara eksplisit. Hanya tiga aturan ini, yang ditegakkan oleh compiler sebelum kode Anda dijalankan.
Saat saya menulis versi Rust dari utilitas JavaScript tersebut, saya melakukan apa yang terasa alami. Saya membuat sebuah vector, yang merupakan jawaban Rust untuk array, dan menetapkannya ke variabel kedua. Kemudian saya mencoba menggunakan variabel yang pertama. Compiler menolaknya. Di JavaScript, let b = a menyalin referensinya. Di Rust, itu memindahkan kepemilikannya (moves ownership). Variabel aslinya menjadi tidak valid. Compiler melakukan ini untuk menjamin bahwa Anda tidak akan pernah memiliki dua jalur yang secara tidak sengaja menginjak memori yang sama, yang merupakan penyebab terjadinya data races dan bug use-after-free di sistem lain.
Mengapa Rust Mengambil Mainan Anda
Perhatikan pola ini di JavaScript:
let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.
Ini sudah menjadi insting. Anda tidak akan berpikir dua kali. Di Rust, intuisi yang sama akan membuat Anda ditolak oleh compiler bahkan sebelum Anda selesai mengetik. Begitu b memiliki vector tersebut, a hanyalah sebuah nama kosong. Anda tidak bisa melakukan push ke dalamnya. Anda bahkan tidak bisa melihatnya. Memori tersebut sekarang milik b.
Ini terasa seperti hukuman sampai Anda menyadari apa yang dicegahnya. Jika dua variabel dapat mengubah data heap yang sama tanpa koordinasi, Anda berisiko mengalami korupsi memori. Rust menghilangkan seluruh kategori bug tersebut dengan membuat transfernya menjadi eksplisit. Compiler tidak sedang bersikap cerewet. Ia sedang bertindak sebagai penjaga gerbang. Ia memaksa Anda untuk memutuskan, di setiap langkah, bagian kode mana yang bertanggung jawab atas data mana.
Borrowing: Solusi Alternatif yang Sebenarnya adalah Fitur
Tentu saja, jika setiap penetapan (assignment) memindahkan kepemilikan selamanya, menulis program akan sangat melelahkan. Anda harus mengkloning segalanya, yang membuang-buang memori dan kecepatan. Rust mengatasi hal ini dengan borrowing.
Borrowing memungkinkan Anda menggunakan suatu nilai tanpa mengambilnya. Ada dua jenis, dan perbedaannya sangat penting.
- Immutable borrows, ditulis
&T: Ini memungkinkan Anda membaca data. Anda dapat memiliki sebanyak yang Anda inginkan pada saat yang sama
