Pernahkah anda mengalami perasaan apabila anda mula mempelajari bahasa baharu dan menjangkakan untuk berlari pantas, tetapi sebaliknya anda tersadung pada langkah pertama? Begitulah pengalaman saya dengan Rust. Saya mempunyai satu utiliti JavaScript yang berfungsi dengan baik — sesuatu yang menganalisis dan mengubah suai beberapa tatasusunan (array) — dan saya fikir saya boleh menulisnya semula semasa waktu rehat kopi. Saya membuka editor, menaip kod yang terasa semula jadi, dan menjalankan kompilator. Ia meletup. Bukan dengan ralat sintaks. Bukan dengan titik bertitik (semicolon) yang hilang. Ia memberitahu saya bahawa saya telah cuba menggunakan nilai yang telah dipindahkan (moved). Saya merenung skrin. Dipindahkan? Saya tidak memindahkan apa-apa. Saya cuma menetapkannya kepada pemboleh ubah lain.
Zon Selesa JavaScript
JavaScript melatih anda untuk melayan data seperti papan putih kongsi. Anda mengisytiharkan satu objek atau tatasusunan, menyerahkannya kepada satu fungsi, membiarkan fungsi tersebut menambah satu sifat (property), kemudian menyerahkannya ke tempat lain. Pengumpul sampah (garbage collector) duduk di latar belakang seperti tukang cuci yang tidak kelihatan, menunggu untuk menyapu apa sahaja yang anda tinggalkan. Anda tidak pernah bertanya, "Siapa yang memiliki string ini?" Anda bertanya, "Bolehkah saya membacanya?" Jika jawapannya ya, anda menyentuhnya. Jika anda memerlukan rujukan lain, anda hanya perlu menyebut let b = a dan teruskan. Kedua-dua pemboleh ubah merujuk kepada bahagian memori yang sama, dan jika a mengubah suainya, b akan melihat perubahan itu dengan serta-merta. Ia memudahkan. Ia juga huru-hara, tetapi kekacauan itu jarang memudaratkan anda kerana masa larian (runtime) mengendalikan pembersihan tersebut.
Kejutan Kompilator Pertama
Rust tidak mempercayai anda. Bunyinya agak kasar, tetapi itulah perkara pertama yang anda pelajari. Bahasa ini dibina berasaskan sistem pemilikan (ownership system) dengan tiga peraturan yang ketat. Pertama, setiap nilai mempunyai tepat satu pemilik. Kedua, hanya ada satu pemilik pada satu-satu masa. Ketiga, apabila pemilik tersebut keluar daripada skop, Rust akan membuang nilai tersebut secara automatik. Tiada pengumpul sampah. Tiada pengiraan rujukan (reference counting) di latar belakang melainkan anda memilihnya secara eksplisit. Hanya tiga peraturan ini, yang dikuatkuasakan oleh kompilator sebelum kod anda dijalankan.
Apabila saya menulis versi Rust bagi utiliti JavaScript tersebut, saya melakukan apa yang terasa semula jadi. Saya mencipta satu vektor, yang merupakan jawapan Rust kepada tatasusunan, dan menetapkannya kepada pemboleh ubah kedua. Kemudian saya cuba menggunakan yang pertama. Kompilator menolak. Dalam JavaScript, let b = a menyalin rujukan. Dalam Rust, ia memindahkan pemilikan. Pemboleh ubah asal menjadi tidak sah. Kompilator melakukan ini untuk menjamin bahawa anda tidak akan mempunyai dua laluan yang secara tidak sengaja mengganggu memori yang sama, yang merupakan punca berlakunya perlumbaan data (data races) dan pepijat use-after-free dalam sistem lain.
Mengapa Rust Merampas Mainan Anda
Lihat corak ini dalam JavaScript:
let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.
Ia sudah menjadi tabiat. Anda tidak akan berfikir dua kali. Dalam Rust, intuisi yang sama itu akan menyebabkan anda ditolak oleh kompilator sebelum anda selesai menaip. Sebaik sahaja b memiliki vektor tersebut, a hanyalah satu nama kosong. Anda tidak boleh menambah (push) kepadanya. Anda tidak boleh melihatnya pun. Memori itu kini milik b.
Ini terasa seperti satu hukuman sehinggalah anda menyedari apa yang dicegahnya. Jika dua pemboleh ubah boleh mengubah suai data heap yang sama tanpa penyelarasan, anda berisiko menghadapi kerosakan memori. Rust menghapuskan keseluruhan kategori pepijat tersebut dengan menjadikan pemindahan itu eksplisit. Kompilator tidak bersikap cerewet. Ia bertindak sebagai penjaga pintu. Ia memaksa anda untuk memutuskan, pada setiap langkah, bahagian kod mana yang bertanggungjawab terhadap data yang mana.
Peminjaman (Borrowing): Jalan Penyelesaian yang Sebenarnya adalah Ciri Utama
Sudah tentu, jika setiap penetapan memindahkan pemilikan selama-lamanya, menulis program akan menjadi sangat meletihkan. Anda perlu mengklon segalanya, membazirkan memori dan kelajuan. Rust menyelesaikan masalah ini dengan peminjaman (borrowing).
Peminjaman membolehkan anda menggunakan satu nilai tanpa mengambilnya. Terdapat dua jenis, dan perbezaannya adalah penting.
- Peminjaman tidak boleh ubah (Immutable borrows), ditulis sebagai
&T: Ini membolehkan anda membaca data. Anda boleh mempunyai sebanyak mana yang anda mahu pada masa yang sama
