Rust adalah bahasa pemrograman sistem yang muda dan ambisius. Ini menerapkan manajemen memori otomatis tanpa pengumpul sampah dan overhead lain dari waktu eksekusi. Selain itu, bahasa default digunakan dalam bahasa Rust, ada aturan yang belum pernah terjadi sebelumnya untuk mengakses data yang bisa berubah, dan masa pakai tautan juga diperhitungkan. Ini memungkinkan dia untuk menjamin keamanan memori dan memfasilitasi pemrograman multi-threaded, karena kurangnya data balap.

Semua ini sudah diketahui semua orang yang mengikuti perkembangan teknologi pemrograman modern setidaknya sedikit. Tetapi bagaimana jika Anda bukan pemrogram sistem, dan tidak ada banyak kode multithread di proyek Anda, tetapi Anda masih tertarik dengan kinerja Rust. Apakah Anda akan mendapat manfaat tambahan dari penggunaannya dalam aplikasi? Atau semua yang dia akan berikan kepada Anda adalah perkelahian yang sulit dengan kompiler, yang akan memaksa Anda untuk menulis program sehingga terus mengikuti aturan bahasa tentang pinjaman dan kepemilikan?
Artikel ini telah mengumpulkan puluhan keuntungan tidak jelas dan tidak diiklankan secara khusus menggunakan Rust, yang, saya harap, akan membantu Anda memutuskan pilihan bahasa ini untuk proyek Anda.
1. Keuniversalan bahasa
Terlepas dari kenyataan bahwa Rust diposisikan sebagai bahasa untuk pemrograman sistem, itu juga cocok untuk memecahkan masalah yang diterapkan tingkat tinggi. Anda tidak harus bekerja dengan pointer mentah kecuali Anda membutuhkannya untuk tugas Anda. Pustaka bahasa standar telah menerapkan sebagian besar jenis dan fungsi yang mungkin diperlukan dalam pengembangan aplikasi. Anda juga dapat dengan mudah menghubungkan perpustakaan eksternal dan menggunakannya. Sistem tipe dan pemrograman umum dalam Rust memungkinkan penggunaan abstraksi dari tingkat yang cukup tinggi, meskipun tidak ada dukungan langsung untuk OOP dalam bahasa tersebut.
Mari kita lihat beberapa contoh sederhana menggunakan Rust.
Contoh menggabungkan dua iterator menjadi satu iterator pada pasangan elemen:
let zipper: Vec<_> = (1..).zip("foo".chars()).collect(); assert_eq!((1, 'f'), zipper[0]); assert_eq!((2, 'o'), zipper[1]); assert_eq!((3, 'o'), zipper[2]);
Lari
Catatan: panggilan ke name!(...)
format name!(...)
adalah panggilan ke makro fungsional. Nama-nama makro seperti itu di Rust selalu diakhiri dengan simbol !
sehingga mereka dapat dibedakan dari nama fungsi dan pengidentifikasi lainnya. Manfaat menggunakan makro akan dibahas di bawah ini.
Contoh menggunakan perpustakaan regex
eksternal untuk bekerja dengan ekspresi reguler:
extern crate regex; use regex::Regex; let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); assert!(re.is_match("2018-12-06"));
Lari
Contoh penerapan Add
untuk struktur Point
sendiri untuk membebani operator tambahan:
use std::ops::Add; struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2;
Lari
Contoh menggunakan tipe generik dalam struktur:
struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 };
Lari
Pada Rust Anda dapat menulis utilitas sistem yang efisien, aplikasi desktop besar, layanan mikro, aplikasi web (termasuk bagian klien, karena Rust dapat dikompilasi dalam Wasm), aplikasi mobile (meskipun ekosistem bahasa masih kurang berkembang dalam arah ini). Fleksibilitas semacam itu dapat menjadi keuntungan bagi tim multi-proyek, karena memungkinkan Anda untuk menggunakan pendekatan yang sama dan modul yang sama di banyak proyek yang berbeda. Jika Anda terbiasa dengan fakta bahwa setiap alat dirancang untuk bidang aplikasi yang sempit, maka cobalah untuk melihat Rust sebagai kotak alat dengan keandalan dan kenyamanan yang sama. Mungkin inilah tepatnya yang Anda lewatkan.
2. Alat manajemen yang mudah dibangun dan ketergantungan
Ini jelas tidak diiklankan, tetapi banyak yang memperhatikan bahwa Rust memiliki salah satu sistem manajemen pembangunan dan ketergantungan terbaik yang tersedia saat ini. Jika Anda memprogram dalam C atau C ++, dan pertanyaan tentang penggunaan pustaka eksternal yang tidak menyakitkan bagi Anda, maka menggunakan Rust dengan alat bangunnya dan manajer ketergantungan Cargo akan menjadi pilihan yang baik untuk proyek baru Anda.
Selain fakta bahwa Cargo akan mengunduh dependensi untuk Anda dan mengelola versinya, membangun dan menjalankan aplikasi Anda, menjalankan tes, dan menghasilkan dokumentasi, Cargo juga dapat diperluas dengan plugin untuk fungsi berguna lainnya. Misalnya, ada ekstensi yang memungkinkan Cargo untuk menentukan dependensi usang proyek Anda, melakukan analisis statis terhadap kode sumber, membangun dan menggunakan kembali bagian-bagian klien dari aplikasi web, dan banyak lagi.
File konfigurasi Cargo menggunakan bahasa marka toml ramah dan minimal untuk menggambarkan pengaturan proyek. Berikut adalah contoh Cargo.toml
konfigurasi Cargo.toml
khas:
[package] name = "some_app" version = "0.1.0" authors = ["Your Name <you@example.com>"] [dependencies] regex = "1.0" chrono = "0.4" [dev-dependencies] rand = "*"
Dan di bawah ini adalah tiga perintah khas untuk menggunakan Cargo:
$ cargo check $ cargo test $ cargo run
Dengan bantuan mereka, kode sumber akan diperiksa untuk kesalahan kompilasi, perakitan proyek dan peluncuran tes, perakitan dan peluncuran program untuk dieksekusi, masing-masing.
3. Tes bawaan
Tes unit menulis di Rust sangat mudah dan sederhana sehingga Anda ingin melakukannya berulang kali. :) Seringkali akan lebih mudah untuk menulis unit test daripada mencoba menguji fungsionalitas dengan cara lain. Berikut adalah contoh fungsi dan tes untuk mereka:
pub fn is_false(a: bool) -> bool { !a } pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod test { use super::*; #[test] fn is_false_works() { assert!(is_false(false)); assert!(!is_false(true)); } #[test] fn add_two_works() { assert_eq!(1, add_two(-1)); assert_eq!(2, add_two(0)); assert_eq!(4, add_two(2)); } }
Lari
Fungsi-fungsi dalam modul test
, ditandai dengan atribut #[test]
, adalah tes unit. Mereka akan dieksekusi secara paralel ketika perintah cargo test
dipanggil. Atribut kompilasi bersyarat #[cfg(test)]
, yang menandai seluruh modul dengan tes, akan mengarah pada fakta bahwa modul hanya akan dikompilasi ketika tes dijalankan, tetapi tidak akan masuk ke perakitan normal.
Sangat mudah untuk menempatkan tes dalam modul yang sama dengan fungsional yang diuji, cukup dengan menambahkan submodule test
ke dalamnya. Dan jika Anda memerlukan tes integrasi, cukup tempatkan tes Anda di direktori tests
di root proyek, dan gunakan aplikasi Anda di dalamnya sebagai paket eksternal. Modul test
terpisah dan arahan kompilasi bersyarat dalam kasus ini tidak perlu ditambahkan.
Contoh-contoh khusus dari dokumentasi yang dilaksanakan sebagai ujian patut mendapat perhatian khusus, tetapi ini akan dibahas di bawah ini.
Tes kinerja built-in (tolok ukur) juga tersedia, tetapi belum stabil, oleh karena itu tes ini hanya tersedia di majelis malam kompiler. Di Rust yang stabil, Anda harus menggunakan pustaka eksternal untuk jenis pengujian ini.
4. Dokumentasi yang baik dengan contoh saat ini
Perpustakaan Rust standar didokumentasikan dengan sangat baik. Dokumentasi HTML secara otomatis dihasilkan dari kode sumber dengan deskripsi penurunan harga di dermaga komentar. Selain itu, komentar dokumen dalam kode Rust berisi kode sampel yang dijalankan saat tes dijalankan. Ini memastikan relevansi contoh:
Dokumentasi
Berikut adalah contoh penggunaan metode as_bytes
dari tipe String
let s = String::from("hello"); assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());
akan dieksekusi sebagai tes selama peluncuran tes.
Selain itu, praktik membuat contoh penggunaannya dalam bentuk program independen kecil yang terletak di direktori examples
di root proyek adalah umum untuk perpustakaan Rust. Contoh-contoh ini juga merupakan bagian penting dari dokumentasi dan mereka juga dikompilasi dan dieksekusi selama uji coba, tetapi mereka dapat dijalankan secara independen dari tes.
5. Pengurangan otomatis tipe cerdas
Dalam program Karat, Anda tidak dapat menentukan jenis ekspresi secara eksplisit jika kompilator dapat mengeluarkannya secara otomatis berdasarkan konteks penggunaan. Dan ini tidak hanya berlaku untuk tempat-tempat di mana variabel dinyatakan. Mari kita lihat sebuah contoh:
let mut vec = Vec::new(); let text = "Message"; vec.push(text);
Lari
Jika kami mengatur jenis anotasi, maka contoh ini akan terlihat seperti ini:
let mut vec: Vec<&str> = Vec::new(); let text: &str = "Message"; vec.push(text);
Yaitu, kita memiliki vektor irisan string dan variabel irisan string tipe. Tetapi dalam kasus ini, menentukan jenis sepenuhnya berlebihan, karena kompiler dapat mengeluarkannya sendiri (menggunakan versi diperpanjang dari algoritma Hindley-Milner ). Fakta bahwa vec
adalah vektor sudah jelas dengan jenis nilai pengembalian dari Vec::new()
, tetapi belum jelas jenis elemennya. Fakta bahwa jenis text
adalah irisan string dapat dimengerti oleh fakta bahwa itu diberikan literal jenis ini. Dengan demikian, setelah vec.push(text)
, jenis elemen vektor menjadi jelas. Perhatikan bahwa jenis variabel vec
sepenuhnya ditentukan oleh penggunaannya dalam utas eksekusi, dan bukan pada tahap inisialisasi.
Sistem inferensi tipe seperti itu menghilangkan noise dari kode dan membuatnya sesingkat kode dalam beberapa bahasa pemrograman yang diketik secara dinamis. Dan ini sambil mempertahankan pengetikan statis yang ketat!
Tentu saja, kita tidak bisa sepenuhnya menghapus pengetikan dalam bahasa yang diketik secara statis. Program harus memiliki titik di mana jenis objek dijamin diketahui, sehingga di tempat lain jenis ini dapat ditampilkan. Poin seperti itu di Rust adalah deklarasi tipe data yang ditentukan pengguna dan fungsi tanda tangan, di mana orang tidak bisa tidak menentukan jenis yang digunakan. Tapi Anda bisa memasukkan "meta-variabel jenis" di dalamnya, menggunakan pemrograman umum.
6. Pencocokan pola pada titik-titik deklarasi variabel
let
operasi
let p = Point::new();
tidak benar-benar terbatas hanya dengan mendeklarasikan variabel baru. Apa yang sebenarnya dia lakukan adalah mencocokkan ekspresi di sebelah kanan tanda sama dengan pola di sebelah kiri. Dan variabel baru dapat diperkenalkan sebagai bagian dari sampel (dan hanya demikian). Lihatlah contoh berikut, dan itu akan menjadi lebih jelas bagi Anda:
let Point { x, y } = Point::new();
Lari
Destrukturisasi dilakukan di sini: perbandingan seperti itu akan memperkenalkan variabel x
dan y
, yang akan diinisialisasi dengan nilai bidang x
dan y
dari objek struktur Point
, yang dikembalikan dengan memanggil Point::new()
. Pada saat yang sama, perbandingannya benar, karena jenis ekspresi di sebelah kanan sesuai dengan pola Point
dari tipe Point
di sebelah kiri. Dengan cara yang sama, Anda bisa mengambil, misalnya, dua elemen pertama dari sebuah array:
let [a, b, _] = [1, 2, 3];
Dan masih banyak lagi yang harus dilakukan. Hal yang paling luar biasa adalah bahwa perbandingan semacam itu dilakukan di semua tempat di mana nama variabel baru dapat dimasukkan dalam Rust, yaitu: dalam match
, let
, if let
, while let
if let
, di header for
loop, dalam argumen fungsi dan penutupan. Berikut adalah contoh penggunaan pola yang cocok secara elegan dalam for
loop:
for (i, ch) in "foo".chars().enumerate() { println!("Index: {}, char: {}", i, ch); }
Lari
Metode enumerate
, dipanggil pada iterator, membangun iterator baru, yang akan beralih bukan nilai awal, tetapi tupel, pasang "indeks ordinal, nilai awal". Masing-masing tupel selama iterasi siklus akan dipetakan ke pola yang ditentukan (i, ch)
, sebagai akibatnya variabel i
akan menerima nilai pertama dari tuple - indeks, dan variabel ch
- yang kedua, yaitu karakter string. Lebih lanjut dalam tubuh loop kita dapat menggunakan variabel-variabel ini.
Contoh populer lain dari menggunakan pola dalam for
:
for _ in 0..5 {
Di sini kita mengabaikan nilai iterator menggunakan pola _
. Karena kita tidak menggunakan nomor iterasi di badan loop. Hal yang sama dapat dilakukan, misalnya, dengan argumen fungsi:
fn foo(a: i32, _: bool) {
Atau saat mencocokkan dalam pernyataan match
:
match p { Point { x: 1, .. } => println!("Point with x == 1 detected"), Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"), _ => (),
Lari
Pencocokan pola membuat kode sangat kompak dan ekspresif, dan dalam pernyataan match
umumnya tidak tergantikan. Operator match
adalah operator analisis variatif lengkap, sehingga Anda tidak akan dapat secara tidak sengaja lupa untuk memeriksa beberapa kecocokan yang mungkin untuk ekspresi yang dianalisis di dalamnya.
7. Ekstensi sintaks dan DSL khusus
Sintaksis Rust terbatas, sebagian besar disebabkan oleh kompleksitas jenis sistem yang digunakan dalam bahasa. Misalnya, Rust tidak memiliki nama argumen fungsi atau fungsi dengan sejumlah variabel argumen. Tetapi Anda dapat menyiasati ini dan keterbatasan lainnya dengan makro. Karat memiliki dua jenis makro: deklaratif dan prosedural. Dengan makro deklaratif, Anda tidak akan pernah memiliki masalah yang sama dengan makro di C, karena mereka higienis dan tidak bekerja di tingkat penggantian teks, tetapi pada tingkat penggantian di pohon sintaksis abstrak. Macro memungkinkan Anda untuk membuat abstraksi pada tingkat sintaksis bahasa. Sebagai contoh:
println!("Hello, {name}! Do you know about {}?", 42, name = "User");
Selain fakta bahwa makro ini memperluas kemampuan sintaksis memanggil "fungsi" dari mencetak string yang diformat, itu juga akan dalam implementasinya memverifikasi bahwa argumen input cocok dengan string format yang ditentukan pada waktu kompilasi, dan tidak pada saat run time. Dengan menggunakan makro, Anda dapat memasukkan sintaksis ringkas untuk kebutuhan desain Anda sendiri, membuat dan menggunakan DSL. Berikut adalah contoh penggunaan kode JavaScript di dalam program Rust yang dikompilasi dalam Wasm:
let name = "Bob"; let result = js! { var msg = "Hello from JS, " + @{name} + "!"; console.log(msg); alert(msg); return 2 + 2; }; println!("2 + 2 = {:?}", result);
Makro js!
didefinisikan dalam paket stdweb
dan memungkinkan Anda untuk menyematkan kode JavaScript lengkap dalam program Anda (dengan pengecualian string dan operator yang dikutip tunggal tidak dilengkapi dengan tanda titik koma) dan menggunakan objek dari kode Rust menggunakan sintaks @{expr}
.
Makro menawarkan peluang luar biasa untuk mengadaptasi sintaks program Rust dengan tugas-tugas spesifik dari bidang subjek tertentu. Mereka akan menghemat waktu dan perhatian Anda saat mengembangkan aplikasi yang kompleks. Bukan dengan meningkatkan overhead runtime, tetapi dengan meningkatkan waktu kompilasi. :)
8. Pembuatan kode dependen secara otomatis
Makro turunan prosedural Rust secara luas digunakan untuk secara otomatis mengimplementasikan sifat-sifat dan pembuatan kode lainnya. Berikut ini sebuah contoh:
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] struct Point { x: i32, y: i32, }
Karena semua jenis ini ( Copy
, Clone
, Debug
, Default
, PartialEq
dan Eq
) dari pustaka standar diimplementasikan untuk jenis bidang struktur i32
, implementasinya dapat secara otomatis ditampilkan untuk seluruh struktur secara keseluruhan. Contoh lain:
extern crate serde_derive; extern crate serde_json; use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 };
Lari
Di sini, menggunakan Deserialize
dan Deserialize
dari perpustakaan serde
untuk struktur Point
, metode untuk serialisasi dan deserialisasi secara otomatis dihasilkan. Kemudian Anda dapat mengirimkan instance dari struktur ini ke berbagai fungsi serialisasi, misalnya, mengubahnya menjadi string JSON.
Anda dapat membuat macro prosedural Anda sendiri yang akan menghasilkan kode yang Anda butuhkan. Atau gunakan banyak makro yang sudah dibuat oleh pengembang lain. Selain menyelamatkan pemrogram dari menulis kode boilerplate, makro juga memiliki keuntungan bahwa Anda tidak perlu mempertahankan bagian kode yang berbeda dalam keadaan konsisten. Misalnya, jika bidang ketiga z
ditambahkan ke struktur Point
, maka untuk melakukan serialisasi dengan benar, jika Anda menggunakan turunan, Anda tidak perlu melakukan hal lain. Jika kita sendiri akan menerapkan sifat-sifat yang diperlukan untuk serialisasi Point
, maka kita harus memastikan bahwa implementasi ini selalu konsisten dengan perubahan terbaru dalam struktur Point
.
9. Tipe data aljabar
Sederhananya, tipe data aljabar adalah tipe data komposit yang merupakan gabungan dari struktur. Lebih formal, ini adalah tipe-jumlah dari jenis produk. Di Rust, tipe ini didefinisikan menggunakan kata kunci enum
:
enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), }
Jenis nilai tertentu dari variabel tipe Message
hanya dapat menjadi salah satu dari tipe struktur yang tercantum dalam Message
. Ini adalah struktur Quit
tanpa bidang yang mirip unit, salah satu struktur tuple ChangeColor
atau Write
dengan bidang tanpa nama, atau struktur Move
biasa. Tipe enumerasi tradisional dapat direpresentasikan sebagai kasus khusus tipe data aljabar:
enum Color { Red, Green, Blue, White, Black, Unknown, }
Dimungkinkan untuk mengetahui jenis apa yang sebenarnya mengambil nilai dalam kasus tertentu menggunakan pencocokan pola:
let color: Color = get_color(); let text = match color { Color::Red => "Red", Color::Green => "Green", Color::Blue => "Blue", _ => "Other color", }; println!("{}", text); ... fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s), }; }
Lari
Dalam bentuk tipe data aljabar, Rust mengimplementasikan tipe penting seperti Option
dan Result
, yang digunakan untuk mewakili nilai yang hilang dan hasil yang benar / salah. Begini cara Option
didefinisikan di perpustakaan standar:
pub enum Option<T> { None, Some(T), }
Rust tidak memiliki nilai nol, sama seperti kesalahan menjengkelkan dari panggilan tak terduga ke sana. Sebaliknya, jika benar-benar diperlukan untuk menunjukkan kemungkinan nilai yang hilang, Option
digunakan:
fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), }
Lari
Tipe data aljabar adalah alat yang kuat dan ekspresif yang membuka pintu bagi Pengembangan Tipe-Didorong. Program yang ditulis secara kompeten dalam paradigma ini memberikan sebagian besar pemeriksaan kebenaran pekerjaannya ke sistem tipe. Karena itu, jika Anda kekurangan Haskell dalam pemrograman industri sehari-hari, Rust bisa menjadi outlet Anda. :)
10. Refactoring mudah
Sistem tipe statis ketat yang dikembangkan di Rust dan upaya untuk melakukan sebanyak mungkin pemeriksaan selama kompilasi, mengarah pada fakta bahwa memodifikasi dan refactoring kode menjadi sangat sederhana dan aman. Jika, setelah perubahan, program dikompilasi, ini berarti hanya meninggalkan kesalahan logis yang tidak terkait dengan fungsi yang verifikasi ditugaskan ke kompiler. Dikombinasikan dengan kemudahan menambahkan tes unit untuk menguji logika, ini mengarah pada jaminan serius keandalan program dan peningkatan kepercayaan diri programmer dalam operasi kode yang benar setelah melakukan perubahan.
Mungkin ini yang ingin saya bicarakan di artikel ini. Tentu saja, Rust memiliki banyak keunggulan lain, serta sejumlah kelemahan (beberapa kelemahan bahasa, kurangnya idiom pemrograman yang akrab, dan sintaksis "non-sastra"), yang tidak disebutkan di sini. Jika Anda memiliki sesuatu untuk diceritakan tentang mereka, tulis di komentar. Secara umum, coba Rust dalam praktik. Dan mungkin kelebihannya bagi Anda akan melebihi semua kekurangannya, seperti yang terjadi dalam kasus saya. Dan akhirnya, Anda akan mendapatkan set alat yang Anda butuhkan untuk waktu yang lama.