Sepanjang section ini kita sering menulis let harga = 50000; tanpa menyebut tipe apa pun, padahal Rust dikategorikan sebagai bahasa bertipe statis yang ketat. Kombinasi keduanya mungkin terdengar kontradiktif, tetapi justru di situlah letak keanggunan Rust: kompilator menjalankan type inference, yakni proses menalar tipe setiap variable dari bentuk literal dan cara variable tersebut dipakai. Kita mendapatkan ketatnya pemeriksaan tipe tanpa beban menulis anotasi di setiap baris. Pada Chapter ini kita akan mengintip cara kerja penalaran tersebut, mengenali tipe bawaan untuk literal, melihat bagaimana konteks penggunaan memengaruhi hasil inferensi, dan mengetahui kapan anotasi tipe tetap perlu ditulis tangan.
Bagaimana Kompilator Menalar Tipe #
Saat bertemu let, kompilator tidak langsung memutuskan tipe. Kompilator terlebih dahulu mengumpulkan petunjuk: bentuk literal (bilangan bulat, desimal, tanda kutip), operasi yang mengikutsertakan variable tersebut, serta anotasi tipe variable lain di sekitarnya. Dari seluruh petunjuk itu, kompilator memilih satu tipe yang konsisten untuk semua penggunaan. Jika petunjuknya saling bertentakan, kompilasi gagal; jika petunjuknya kosong, kompilator memakai tipe bawaan.
Contoh paling jelas terlihat pada tuple yang anggotanya beragam tipe:
fn main() {
let data_siswa = (150, 72.5, true);
println!("Tinggi: {} cm", data_siswa.0);
println!("Berat: {} kg", data_siswa.1);
println!("Aktif: {}", data_siswa.2);
}Output yang Dihasilkan:
Tinggi: 150 cm
Berat: 72.5 kg
Aktif: trueMari kita telaah penalaran yang dilakukan kompilator:
- Baris 2 (
let data_siswa = (150, 72.5, true);): tiga petunjuk lahir sekaligus;150adalah integer,72.5adalah floating point, dantrueadalah boolean, sehingga tipe tuple disimpulkan sebagai(i32, f64, bool). - Baris 4 (
println!("Tinggi: {} cm", data_siswa.0);): pencetakandata_siswa.0memakai tipei32hasil inferensi tanpa satu pun anotasi dari kita. - Baris 5-6 (
data_siswa.1dandata_siswa.2): anggota kedua dan ketiga membawa tipef64danboolhasil penalaran yang sama.
Proses ini berjalan pada saat kompilasi, bukan saat program berjalan. Berbeda dengan PHP yang memeriksa tipe di tengah eksekusi, program hasil kompilasi Rust sudah menyimpan tipe final setiap variable.
Tipe Bawaan untuk Literal #
Ketika tidak ada satu pun petunjuk lain, literal mengikuti tipe bawaan: bilangan bulat menjadi i32, dan bilangan desimal menjadi f64. Kita bisa membuktikan tipe bawaan ini dengan sebuah uji kecil yang sengaja menuntut tipe berbeda:
fn main() {
let tinggi = 150;
let tinggi_float: f64 = tinggi;
println!("Tinggi: {}", tinggi_float);
}Kompilasi gagal dengan pesan berikut:
error[E0308]: mismatched types
--> main.rs:3:29
|
3 | let tinggi_float: f64 = tinggi;
| --- ^^^^^^ expected `f64`, found integer
| |
| expected due to thisMari kita baca bukti yang diberikan kompilator:
- Baris 2 (
let tinggi = 150;): tidak ada anotasi dan tidak ada operasi lain, sehingga aturan bawaan menempatkantinggipada tipei32; pesan galat menyebut kata integer sebagai jenis nilainya. - Baris 3 (
let tinggi_float: f64 = tinggi;): kita menuntutf64, sementaratinggisudah terkunci sebagai integer; Rust tidak pernah mengonversi integer ke float secara diam-diam, sehingga galat muncul.
Sebaliknya, dua literal desimal akan saling menalar sebagai f64 dan perhitungan berjalan mulus. Contoh berikut menghitung BMI dari berat dan tinggi badan:
fn main() {
let berat = 72.5;
let tinggi = 1.78;
let bmi = berat / (tinggi * tinggi);
println!("Berat: {} kg", berat);
println!("Tinggi: {} m", tinggi);
println!("BMI: {:.2}", bmi);
}Output yang Dihasilkan:
Berat: 72.5 kg
Tinggi: 1.78 m
BMI: 22.88Mari kita amati penalarannya:
- Baris 2-3 (
let berat = 72.5; let tinggi = 1.78;): kedua literal desimal tanpa anotasi mengikuti bawaanf64. - Baris 5 (
let bmi = berat / (tinggi * tinggi);): operator/menuntut kedua operan bertipe sama; karena kedua sisi sudahf64, pembagian desimal sah dan menghasilkan22.88.... - Baris 9 (
println!("BMI: {:.2}", bmi);): placeholder{:.2}membulatkan tampilan menjadi dua angka di belakang koma, sehingga tercetak22.88.
Inferensi dari Konteks Penggunaan #
Inferensi tidak berhenti pada literal. Ketika variable berpartisipasi dalam operasi bersama variable bertipe eksplisit, kompilator menarik tipe variable baru mengikuti operasi tersebut. Perhatikan program transfer saldo berikut:
fn main() {
let saldo: i64 = 9_000_000;
let transfer = 1_500_000;
let sisa = saldo - transfer;
println!("Sisa saldo: Rp{}", sisa);
}Output yang Dihasilkan:
Sisa saldo: Rp7500000Mari kita telusuri penalaran kompilator langkah demi langkah:
- Baris 2 (
let saldo: i64 = 9_000_000;): anotasi eksplisit mematoksaldosebagaii64. - Baris 3 (
let transfer = 1_500_000;):transferbelum bertipe; secara bawaan literal akan menjadii32, tetapi… - Baris 5 (
let sisa = saldo - transfer;): operator-menuntut kedua operan bertipe sama, dansaldosudah pastii64; kompilator karenanya menariktransfermenjadii64supaya pengurangan sah, mengesampingkan bawaani32.
Kita bisa membuktikan bahwa transfer benar-benar berubah tipe menjadi i64 dengan uji sonde berikut:
fn main() {
let saldo: i64 = 9_000_000;
let transfer = 1_500_000;
let sisa = saldo - transfer;
let uji_tipe: i32 = transfer;
println!("Sisa saldo: Rp{}", sisa);
}Kompilasi gagal dengan pesan inti berikut:
error[E0308]: mismatched types
--> main.rs:6:25
|
6 | let uji_tipe: i32 = transfer;
| --- ^^^^^^^^ expected `i32`, found `i64`Frasa found i64 adalah kwitansi bahwa transfer memang dinalar sebagai i64, bukan i32. Kompilator juga menawarkan saran perbaikan berupa try_into(); sarana tersebut membahas konsep yang belum kita pelajari, jadi cukup abaikan dulu pada tahap ini.
Kapan Anotasi Tipe Diperlukan #
Pada kode sederhana, anotasi nyaris tidak pernah wajib karena literal dan operasi selalu memberi petunjuk. Anotasi tetap penting karena tiga alasan praktis. Pertama, kita ingin tipe yang lebih spesifik daripada bawaan, misalnya u8 untuk data kecil agar hemat memori. Kedua, nilai berada di dekat batas tipe sehingga rentang perlu dinyatakan gamblang. Ketiga, konstanta menuntut tipe eksplisit sesuai aturan pada Chapter Konstanta. Contoh berikut memilih u8 secara sadar untuk stok roti:
fn main() {
let jumlah_roti: u8 = 200;
let tambahan_roti: u8 = 50;
let total_roti = jumlah_roti + tambahan_roti;
println!("Total roti: {} buah", total_roti);
}Output yang Dihasilkan:
Total roti: 250 buahMari kita bedah peran anotasinya:
- Baris 2-3 (
let jumlah_roti: u8 = 200; let tambahan_roti: u8 = 50;): anotasiu8membatasi rentang 0 s.d. 255 dan mendokumentasikan maksud di tempat yang sama dengan datanya. - Baris 5 (
let total_roti = jumlah_roti + tambahan_roti;): inferensi menyimpulkantotal_rotisebagaiu8karena kedua operannyau8; penjumlahan250masih aman di dalam rentang.
Ringkasan #
Type inference membuat Rust menjaga ketat tipe tanpa anotasi di setiap baris. Literal bulat bermuara ke i32 dan literal desimal ke f64 ketika tidak ada petunjuk lain, sementara operasi dan anotasi variable di sekitarnya dapat menarik tipe variable baru mengikuti kebutuhan konsistensi. Anotasi eksplisit tetap kita pakai untuk memilih tipe yang lebih spesifik, mendokumentasikan rentang, dan memenuhi syarat konstanta. Dengan pemahaman ini, kita siap menghadapi topik yang menguji batas tipe secara langsung: perilaku integer overflow.