Chapter sebelumnya menegaskan bahwa Rust menolak mencampur tipe pada operasi: harga * diskon gagal dikompilasi ketika satu sisi i32 dan sisi lain f64. Akan tetapi program nyata selalu menemui data yang berpindah bentuk, misalnya hasil pembacaan sensor desimal yang perlu disimpan sebagai bilangan bulat, atau harga dari sistem 32-bit yang diterima modul 16-bit. Rust meminta setiap perpindahan tipe dinyatakan eksplisit lewat kata kunci as, sehingga setiap titik rawan presisi terlihat telanjang di dalam kode. Pada Chapter ini kita akan memakai as pada berbagai pasangan tipe primitif, lalu membedah tiga risikonya: angka yang terpotong, nilai yang menepi jangkauan, dan presisi yang hilang diam-diam.

Bentuk Dasar Konversi as #

Konversi ditulis dengan meletakkan kata kunci as diikuti tipe tujuan setelah nilai sumber, misalnya harga as f64. Kompilator mengganti representasi bit dari tipe asal ke tipe tujuan sesuai aturan pasangan tipe tersebut. Perhatikan bahwa as hanya menghubungkan tipe primitif seperti integer, floating point, dan char; konversi yang lebih aman melibatkan From dan Into akan kita bahas di section lanjutan.

Kita mulai dari skenario yang tampak tidak berbahaya, yaitu memindahkan harga dari tipe lebar i32 ke tipe sempit i16:

rust
fn main() {
    let harga_asli: i32 = 500_000;
    let harga_pendek: i16 = harga_asli as i16;

    println!("Harga asli (i32): {}", harga_asli);
    println!("Harga pendek (i16): {}", harga_pendek);
}

Output yang Dihasilkan:

text
Harga asli (i32): 500000
Harga pendek (i16): -24288

Mari kita bedah apa yang sebenarnya terjadi:

  1. Baris 2 (let harga_asli: i32 = 500_000;): nilai 500000 aman-aman saja di dalam i32 yang rentangnya mencapai lebih dari dua miliar.
  2. Baris 3 (let harga_pendek: i16 = harga_asli as i16;): i16 hanya menampung -32.768 s.d. 32.767; karena 500000 tidak muat, konversi memotong bit-bit berlebih dan hasilnya menjadi -24288.
  3. Baris 5-6 (println! ...): kedua variable kini memegang nilai yang sama sekali berbeda meskipun bersumber dari satu angka.

Konversi ke tipe sempit tidak menghasilkan galat maupun peringatan; kompilator menganggap as sebagai pernyataan “saya tahu apa yang saya lakukan”. Bilangan asli dipetakan mengikuti pola berputar (wrapping): sisa bit setelah dipotong ditafsirkan ulang sebagai nilai bertanda.

Konversi Float ke Integer Memotong Desimal #

Konversi f64 atau f32 ke integer membuang bagian desimal dengan pemotongan ke arah nol; angka 1250.9 menjadi 1250, sedangkan -7.9 menjadi -7. Tidak ada pembulatan ke atas atau ke terdekat:

rust
fn main() {
    let berat_total: f64 = 1250.9;
    let berat_utuh: i64 = berat_total as i64;

    println!("Berat total: {} kg", berat_total);
    println!("Berat utuh: {} kg", berat_utuh);
}

Output yang Dihasilkan:

text
Berat total: 1250.9 kg
Berat utuh: 1250 kg

Mari kita amati mekanismenya:

  1. Baris 2 (let berat_total: f64 = 1250.9;): variable menyimpan berat dengan satu angka desimal.
  2. Baris 3 (let berat_utuh: i64 = berat_total as i64;): konversi memotong 0.9 tanpa membulatkan, sehingga berat_utuh bernilai 1250; jika kebutuhan kita adalah pembulatan, pemotongan ini membuat laporan timbangan selalu lebih rendah.

Saturasi Saat Nilai Berada di Luar Jangkauan #

Perilaku konversi float ke integer dibedakan menjadi dua kasus. Selama nilai berada di dalam jangkauan tipe tujuan, hasilnya berupa pemotongan desimal. Sebaliknya, sejak Rust 1.45 nilai yang berada di luar jangkauan menghasilkan nilai tepi (saturasi): nilai yang terlalu besar menempel ke nilai maksimum tipe tujuan, nilai yang terlalu kecil menempel ke nilai minimum, dan NaN dikonversi menjadi 0:

rust
fn main() {
    let suhu_ekstrem: f64 = 1.0e300;
    let suhu_beku: f64 = -50.5;

    println!("1.0e300 as u8: {}", suhu_ekstrem as u8);
    println!("-50.5 as u8 : {}", suhu_beku as u8);
}

Output yang Dihasilkan:

text
1.0e300 as u8: 255
-50.5 as u8 : 0

Mari kita bedah kedua hasilnya:

  1. Baris 5 (suhu_ekstrem as u8): 1.0e300 jauh melampaui jangkauan u8 (0 s.d. 255), sehingga saturasi menempelkan hasil ke nilai maksimum 255.
  2. Baris 6 (suhu_beku as u8): tipe tujuan u8 tidak mengenal bilangan negatif, sehingga nilai tepi terdekat yang dipilih adalah minimum 0.

Saturasi mencegah hasil yang acak, tetapi nilai tepi tetap menandai data yang sudah tidak bisa dipercaya. Sebaiknya kita memvalidasi rentang sebelum mengonversi setiap angka dari sumber tak terkendali.

Konversi char dan Kode ASCII #

Setiap char memiliki kode numerik Unicode, dan as menjadi jembatan dua arah di antara keduanya. Konversi char as u8 menghasilkan kode ASCII untuk karakter Latin, sedangkan u8 as char menghidupkan kembali karakter dari kodenya:

rust
fn main() {
    let huruf: char = 'B';
    let kode_ascii: u8 = huruf as u8;
    let huruf_kembali: char = kode_ascii as char;

    println!("Kode ASCII dari 'B': {}", kode_ascii);
    println!("Huruf dari kode {}: {}", kode_ascii, huruf_kembali);
}

Output yang Dihasilkan:

text
Kode ASCII dari 'B': 66
Huruf dari kode 66: B

Mari kita telaah kedua arah konversinya:

  1. Baris 3 (let kode_ascii: u8 = huruf as u8;): karakter 'B' diubah menjadi kode numeriknya, yaitu 66.
  2. Baris 4 (let huruf_kembali: char = kode_ascii as char;): arah sebaliknya menghidupkan karakter B dari angka 66, membuktikan konversi ini tidak merusak data.

Perlu dicatat bahwa hanya u8 yang boleh di-cast langsung menjadi char, karena setiap nilai u8 selalu berpadanan dengan satu titik kode Unicode yang sah. Percobaan seperti i32 as char ditolak kompilator dengan galat E0604 yang menyarankan char::from_u32, yaitu method yang memeriksa keabsahan titik kode terlebih dahulu sebelum menghasilkan karakter.

Kehilangan Presisi dari f64 ke f32 #

Risiko paling senyap terjadi pada konversi antar floating point. f64 membawa sekitar 15-16 angka signifikan, sedangkan f32 hanya sekitar 7 angka, sehingga f64 as f32 meratakan digit-digit ekornya:

rust
fn main() {
    let nilai_pi: f64 = 3.141592653589793;
    let nilai_pi_f32: f32 = nilai_pi as f32;

    println!("Nilai pi (f64): {}", nilai_pi);
    println!("Nilai pi (f32): {}", nilai_pi_f32);
}

Output yang Dihasilkan:

text
Nilai pi (f64): 3.141592653589793
Nilai pi (f32): 3.1415927

Mari kita amati kemerosotan presisinya:

  1. Baris 2 (let nilai_pi: f64 = 3.141592653589793;): variable memegang nilai pi dengan 16 angka signifikan.
  2. Baris 3 (let nilai_pi_f32: f32 = nilai_pi as f32;): setelah konversi, hanya tujuh angka pertama yang bertahan; digit ke delapan sudah meleset dari 5 menjadi 7 akibat pembulatan representasi f32.

Pada data sensor umum, tujuh angka signifikan sudah memadai, dan f32 menghemat separuh memori. Namun untuk akumulasi nilai uang atau perhitungan ilmiah, merosot ke f32 bisa merusak hasil akhir tanpa satu pun galat yang dilaporkan.

Ringkasan #

Kata kunci as mengonversi tipe primitif secara eksplisit tanpa konfirmasi tambahan dari kompilator. Konversi ke integer yang lebih sempit memotong bit berlebih hingga nilai berubah drastis, konversi float ke integer membuang desimal ke arah nol, nilai di luar jangkauan menepi ke batas tipe tujuan, dan f64 as f32 mengikis angka signifikan secara diam-diam. Konversi char dan u8 menjadi pasangan dua arah yang aman untuk karakter ASCII. Karena tiap as adalah titik rawan, tuliskan konversi hanya di tempat yang benar-benar diperlukan. Selanjutnya kita membuka topik bagaimana kompilator sebenarnya menentukan tipe variable tanpa anotasi.