Integer overflow di Rust
Setiap tipe integer (bilangan bulat) di Rust punya ukuran yang tetap. Sebuah u8 hanya punya tempat untuk angka 0 sampai 255, tidak lebih. Jadi apa yang terjadi kalau kamu punya 255 lalu menambahkan 1? Hasilnya tidak muat. Inilah yang disebut integer overflow (luapan bilangan bulat).
Bayangkan odometer mobil tua yang cuma punya tiga digit. Setelah 999 kilometer, angkanya kembali ke 000. Untuk mainan sih tidak masalah, tapi untuk saldo rekening bank, itu bencana. Rust membantumu di sini dengan dua cara. Ia menangkap masalahnya dengan jelas selama kamu masih mengembangkan program. Dan ia memberimu method-method sederhana untuk menentukan dengan tepat apa yang harus terjadi. Tenang, jumlahnya cuma empat, dan semuanya mengikuti pola yang sama. Halaman ini melanjutkan materi dari Tipe data dan Operator.
Seberapa besar setiap tipe integer?
Section titled “Seberapa besar setiap tipe integer?”Setiap tipe punya nilai terkecil (MIN) dan nilai terbesar (MAX). Kamu bisa mencetaknya dengan konstanta seperti u8::MAX atau i32::MIN:
fn main() { println!("u8: {} sampai {}", u8::MIN, u8::MAX); println!("i8: {} sampai {}", i8::MIN, i8::MAX); println!("u16: {} sampai {}", u16::MIN, u16::MAX); println!("i32: {} sampai {}", i32::MIN, i32::MAX); println!("u32: {} sampai {}", u32::MIN, u32::MAX); println!("i64: {} sampai {}", i64::MIN, i64::MAX); println!("u64: {} sampai {}", u64::MIN, u64::MAX); println!("u128: {} sampai {}", u128::MIN, u128::MAX);}Output:
u8: 0 sampai 255i8: -128 sampai 127u16: 0 sampai 65535i32: -2147483648 sampai 2147483647u32: 0 sampai 4294967295i64: -9223372036854775808 sampai 9223372036854775807u64: 0 sampai 18446744073709551615u128: 0 sampai 340282366920938463463374607431768211455Berikut seluruh keluarganya dalam satu tabel:
| Tipe | Bit | Terkecil | Terbesar |
|---|---|---|---|
i8 / u8 |
8 | -128 / 0 | 127 / 255 |
i16 / u16 |
16 | -32.768 / 0 | 32.767 / 65.535 |
i32 / u32 |
32 | sekitar -2,1 miliar / 0 | sekitar 2,1 miliar / 4,3 miliar |
i64 / u64 |
64 | sekitar -9,2 × 10^18 / 0 | sekitar 9,2 × 10^18 / 1,8 × 10^19 |
i128 / u128 |
128 | sekitar -1,7 × 10^38 / 0 | sekitar 1,7 × 10^38 / 3,4 × 10^38 |
isize / usize |
64 di kebanyakan komputer | sama dengan i64 / 0 |
sama dengan i64 / u64 |
Tipe signed (bertanda, i) memakai separuh rentangnya untuk angka negatif. Tipe unsigned (tidak bertanda, u) mulai dari 0, jadi nilai terbesarnya kira-kira dua kali lipat. Apa pun di luar rentang ini adalah overflow, dan turun di bawah MIN (seperti 0 - 1 pada u8) juga termasuk. Yang terakhir ini kadang disebut underflow, tapi Rust memperlakukannya dengan cara yang sama.
Apa yang dilakukan Rust saat angka overflow
Section titled “Apa yang dilakukan Rust saat angka overflow”Di build debug, program panic
Section titled “Di build debug, program panic”Saat kamu menjalankan programmu dengan cargo run (build debug), Rust memeriksa setiap +, -, dan *. Kalau hasilnya tidak muat, program berhenti dengan pesan yang jelas. Di sini sebuah game menambahkan poin bonus ke skor bertipe u8:
fn add_points(score: u8, bonus: u8) -> u8 { score + bonus}
fn main() { let total = add_points(250, 10); println!("total: {total}");}Saat dijalankan, kamu mendapat:
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtracePesan attempt to add with overflow artinya “mencoba menjumlahkan, tapi hasilnya overflow”, dan baris pertama menunjukkan file serta nomor baris tempat itu terjadi. Nilai-nilainya sengaja dikirim lewat parameter fungsi. Kalau kamu menulis angkanya langsung di sebelah +, compiler akan melihat masalahnya bahkan sebelum program dijalankan (sebentar lagi kamu akan melihatnya). Program yang crash mungkin terasa kejam, tapi sebenarnya itu bentuk kebaikan: kamu menemukan bug-nya di komputermu sendiri, lengkap dengan nomor barisnya, alih-alih mendapat skor yang salah di production. Penjelasan lebih lanjut tentang panic ada di halaman Panic.
Di build release, angkanya berputar balik
Section titled “Di build release, angkanya berputar balik”Saat kamu membangun dengan cargo run --release, pemeriksaan itu dimatikan supaya kodenya sedikit lebih cepat. Angkanya berputar balik (wrap around) seperti odometer tadi: 250 + 10 pada u8 menjadi 4 (karena 260 - 256 = 4).
cargo run --releasetotal: 4Tidak ada panic, tidak ada peringatan, hanya angka yang diam-diam salah. Itulah sebabnya kamu sebaiknya tidak mengandalkan overflow yang terjadi “secara kebetulan”. Kalau kamu memang ingin angkanya berputar balik, katakan dengan jelas lewat wrapping_add (dibahas di bawah). Tombol “Coba sendiri” di situs ini menjalankan build debug, jadi wrapping_add juga cara untuk melihat perilaku ini di sana:
fn main() { let score: u8 = 250; println!("{}", score.wrapping_add(10));}Output:
4Kalau kamu ingin build release juga panic, kamu bisa menyalakan lagi pemeriksaannya di Cargo.toml:
[profile.release]overflow-checks = trueDengan angka langsung, kodenya bahkan tidak bisa dikompilasi
Section titled “Dengan angka langsung, kodenya bahkan tidak bisa dikompilasi”Kalau compiler bisa melihat seluruh perhitungannya, ia menolak untuk membangun program itu:
fn main() { let level: u8 = 255 + 1; println!("{level}");}error: this arithmetic operation will overflow --> src/main.rs:2:21 |2 | let level: u8 = 255 + 1; | ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow | = note: `#[deny(arithmetic_overflow)]` on by defaultCompiler bilang operasi aritmetika ini akan overflow, karena u8::MAX + 1 tidak muat. Hal yang sama berlaku untuk literal yang memang terlalu besar, seperti let level: u8 = 256;, yang menghasilkan pesan literal out of range for u8 (literal di luar rentang u8).
Memilih perilakunya sendiri
Section titled “Memilih perilakunya sendiri”Daripada berharap overflow tidak pernah terjadi, kamu bisa memilih apa yang harus terjadi. Setiap tipe integer punya empat keluarga method untuk ini. Semuanya bekerja dengan cara yang sama pada u8, i32, u64, dan tipe lainnya.
checked_: “beri tahu aku kalau tidak muat”
Section titled “checked_: “beri tahu aku kalau tidak muat””checked_add, checked_sub, dan checked_mul mengembalikan Option: Some(hasil) kalau muat, None kalau tidak. Ini pilihan paling aman setiap kali angka yang salah akan jadi masalah serius, misalnya soal uang. Saldo bank tidak bisa di bawah 0, jadi u64 dengan checked_sub menolak penarikan itu untukmu:
fn withdraw(balance: u64, amount: u64) -> Option<u64> { balance.checked_sub(amount)}
fn order_total(price: u32, quantity: u32, shipping: u32) -> Option<u32> { price.checked_mul(quantity)?.checked_add(shipping)}
fn main() { let balance: u64 = 100;
for amount in [30, 250] { match withdraw(balance, amount) { Some(left) => println!("Menarik {amount}, saldo sekarang {left}"), None => println!("Tidak bisa menarik {amount}: uangnya tidak cukup"), } }
println!("{:?}", order_total(20, 3, 5)); println!("{:?}", order_total(2_000_000_000, 3, 5));}Output:
Menarik 30, saldo sekarang 70Tidak bisa menarik 250: uangnya tidak cukupSome(65)NoneKarena hasilnya berupa Option, operator ? juga bisa dipakai. Di order_total, kalau perkaliannya overflow, fungsinya langsung mengembalikan None.
saturating_: “berhenti di batas”
Section titled “saturating_: “berhenti di batas””saturating_add dan saturating_sub tidak pernah melewati MIN atau MAX. Keduanya berhenti saja di sana, seperti gelas yang sudah penuh. Ini cocok sekali untuk hal-hal yang punya batas bawah atau batas atas alami: bar nyawa di game, kenop volume, jumlah barang di stok.
fn main() { let health: u8 = 20; let after_hit = health.saturating_sub(35); println!("nyawa setelah serangan besar: {after_hit}");
let volume: u8 = 90; let louder = volume.saturating_add(25).min(100); println!("volume setelah dinaikkan: {louder}");
let stock: u32 = 3; let left = stock.saturating_sub(5); println!("sisa barang di stok: {left}");}Output:
nyawa setelah serangan besar: 0volume setelah dinaikkan: 100sisa barang di stok: 0Baris volume butuh satu langkah tambahan. saturating_add hanya berhenti di batas milik tipenya sendiri, yaitu 255 untuk u8. Kenop volumemu berhenti di 100, jadi .min(100) membatasinya ke batasmu sendiri.
wrapping_: “berputar balik dengan sengaja”
Section titled “wrapping_: “berputar balik dengan sengaja””Kadang berputar balik justru yang kamu inginkan. Misalnya penghitung nomor tiket yang berjalan 254, 255, 0, 1. Atau fungsi hash sederhana, di mana angka-angkanya memang seharusnya teraduk. wrapping_add dan wrapping_mul melakukan ini di semua build, baik debug maupun release:
fn simple_hash(text: &str) -> u32 { let mut hash: u32 = 5381; for byte in text.bytes() { hash = hash.wrapping_mul(33).wrapping_add(byte as u32); } hash}
fn main() { let mut ticket: u8 = 254; for _ in 0..4 { println!("tiket {ticket}"); ticket = ticket.wrapping_add(1); }
println!("hash dari \"hello\": {}", simple_hash("hello")); println!("hash dari \"a much longer sentence\": {}", simple_hash("a much longer sentence"));}Output:
tiket 254tiket 255tiket 0tiket 1hash dari "hello": 261238937hash dari "a much longer sentence": 3373490575Teks yang di-hash sengaja dibiarkan dalam bahasa Inggris, karena nilai hash-nya dihitung dari setiap byte teks itu: kata lain akan menghasilkan angka lain. Kalimat panjang tadi akan membuat u32 overflow berkali-kali kalau memakai * dan + biasa. Dengan method wrapping_, itu tidak masalah: memang begitulah cara kerja hash ini.
overflowing_: “beri aku keduanya”
Section titled “overflowing_: “beri aku keduanya””overflowing_add mengembalikan tuple: hasil yang sudah berputar balik dan sebuah bool yang menunjukkan apakah terjadi overflow. Ini berguna kalau kamu ingin nilai yang berputar balik, tapi juga ingin tahu bahwa hal itu terjadi:
fn main() { let (value, overflowed) = 250u8.overflowing_add(10); println!("{value}, overflow: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10); println!("{value}, overflow: {overflowed}");}Output:
4, overflow: true60, overflow: falseYang satu ini akan lebih jarang kamu pakai dibanding yang lain. Kalau kamu ragu harus pilih yang mana, checked_ adalah pilihan bawaan yang bagus.
Overflow saat casting dengan as
Section titled “Overflow saat casting dengan as”Mengonversi tipe dengan as tidak pernah panic, bahkan di build debug. Kalau nilainya tidak muat, nilainya diam-diam dipotong supaya muat. Ini mengejutkan banyak orang, jadi hati-hati:
fn main() { let big: i32 = 300; let negative: i32 = -1;
println!("300 as u8 = {}", big as u8); println!("-1 as u32 = {}", negative as u32);
match u8::try_from(big) { Ok(small) => println!("muat: {small}"), Err(e) => println!("300 tidak muat di u8: {e}"), }
match u8::try_from(200) { Ok(small) => println!("muat: {small}"), Err(e) => println!("200 tidak muat di u8: {e}"), }}Output:
300 as u8 = 44-1 as u32 = 4294967295300 tidak muat di u8: out of range integral type conversion attemptedmuat: 200Pesan out of range integral type conversion attempted berasal dari Rust sendiri, artinya “mencoba konversi tipe integer di luar rentang”. 300 as u8 hanya menyimpan 8 bit terbawah, yaitu 44. -1 as u32 berubah menjadi u32 terbesar. Keduanya tidak memberi peringatan apa pun. u8::try_from memeriksa dulu dan memberimu Result, jadi kamu bisa memutuskan apa yang harus dilakukan. Kebiasaan yang baik: pakai as hanya kalau kamu yakin nilainya muat, dan try_from kalau nilainya datang dari luar programmu. (Float berperilaku berbeda: 300.7 as u8 berhenti di 255, bukan berputar balik.) Halaman Konversi tipe dan Konversi (From, Into) membahasnya lebih dalam.
Kejutan lain seputar angka
Section titled “Kejutan lain seputar angka”Beberapa hal lain yang sering bikin orang tersandung, dikumpulkan di satu tempat:
fn main() { println!("7 / 2 = {}", 7 / 2); println!("-7 / 2 = {}", -7 / 2);
println!("i32::MIN.checked_abs() = {:?}", i32::MIN.checked_abs()); println!("i32::MIN.unsigned_abs() = {}", i32::MIN.unsigned_abs());
println!("10u32.checked_pow(9) = {:?}", 10u32.checked_pow(9)); println!("10u32.checked_pow(10) = {:?}", 10u32.checked_pow(10));}Output:
7 / 2 = 3-7 / 2 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- Pembagian integer membuang sisanya. Hasilnya tidak dibulatkan, tapi dipotong ke arah nol, jadi
-7 / 2hasilnya-3. Pakai%untuk mendapatkan sisa bagi, atau pakai float kalau kamu butuh desimal. abs()bisa overflow.i32bisa turun sampai -2147483648 tapi hanya naik sampai 2147483647, jadi versi positif darii32::MINtidak muat.i32::MIN.abs()akan panic di build debug dengan pesanattempt to negate with overflow(mencoba membuat negatif, tapi overflow). Pakaichecked_abs(), atauunsigned_abs(), yang mengembalikanu32sehingga hasilnya muat.powtumbuh dengan cepat. 10 pangkat 10 saja sudah terlalu besar untuku32. Ada jugachecked_pow,saturating_pow, danwrapping_pow.
Kalau sebuah nilai selalu harus berputar balik atau selalu harus berhenti di batas, kamu bisa membungkusnya dengan std::num::Wrapping atau std::num::Saturating. Dengan begitu, + dan - biasa langsung berperilaku benar, dan kamu tidak mungkin lupa:
use std::num::{Saturating, Wrapping};
fn main() { let mut ticket = Wrapping(254u8); ticket += 1; ticket += 1; ticket += 1; println!("tiket: {ticket}");
let mut battery = Saturating(10u8); battery -= 25; println!("baterai: {battery}");}Output:
tiket: 1baterai: 0Method mana yang harus dipakai?
Section titled “Method mana yang harus dipakai?”| Keluarga method | Mengembalikan | Pakai kalau |
|---|---|---|
+ - * |
Angkanya (panic di debug, berputar balik di release) | Kamu yakin tidak mungkin overflow |
checked_add dan kawan-kawan |
Option<T> |
Angka yang salah berarti bug: uang, ukuran, ID |
saturating_add dan kawan-kawan |
Angkanya, tertahan di MIN atau MAX |
Ada batas bawah atau atas alami: nyawa, volume, stok |
wrapping_add dan kawan-kawan |
Angkanya, setelah berputar balik | Berputar balik memang tujuannya: hash, penghitung yang berulang |
overflowing_add dan kawan-kawan |
(T, bool) |
Kamu ingin nilai yang berputar balik sekaligus tahu bahwa itu terjadi |
T::try_from(x) |
Result<T, _> |
Mengonversi nilai yang mungkin tidak muat di tipe yang lebih kecil |
Kesalahan umum
Section titled “Kesalahan umum”Mengurangi 1 dari panjang yang kosong
Section titled “Mengurangi 1 dari panjang yang kosong”len() mengembalikan usize, yang tidak bisa negatif. Pada list yang kosong, len() - 1 akan bernilai -1, jadi terjadi overflow:
fn last_index(tasks: &[String]) -> usize { tasks.len() - 1}
fn main() { let tasks: Vec<String> = Vec::new(); println!("tugas terakhir ada di indeks {}", last_index(&tasks));}Saat dijalankan, kamu mendapat:
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to subtract with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtracePesan attempt to subtract with overflow artinya “mencoba mengurangi, tapi hasilnya overflow”. Kesalahan ini menjebak hampir semua orang setidaknya sekali. Cara memperbaikinya adalah membiarkan tipenya memberi tahu kamu bahwa list-nya mungkin kosong: kembalikan tasks.len().checked_sub(1), yang bertipe Option<usize>. Kalau kamu cuma butuh item terakhir, tasks.last() lebih sederhana lagi.
Menjumlahkan angka kecil di tipe yang kecil
Section titled “Menjumlahkan angka kecil di tipe yang kecil”Tiga nilai ujian, 90, 85, dan 100, masing-masing muat di u8. Tapi totalnya, 275, tidak muat. Jadi let total: u8 = scores.iter().sum(); akan panic dengan pesan attempt to add with overflow. Panic-nya menunjuk ke dalam standard library, yang bisa membingungkan, tapi penyebabnya adalah u8 milikmu. Konversikan setiap nilai ke tipe yang lebih besar sebelum dijumlahkan:
fn main() { let scores: Vec<u8> = vec![90, 85, 100]; let total: u32 = scores.iter().map(|&s| s as u32).sum(); println!("total: {total}, rata-rata: {}", total / 3);}Output:
total: 275, rata-rata: 91Mengubah u8 menjadi u32 dengan as selalu aman, karena setiap u8 pasti muat di u32.
Latihan
Section titled “Latihan”1. Baterai ponsel
Section titled “1. Baterai ponsel”Level baterai ponsel adalah u8 dari 0 sampai 100. Tulis charge(level: u8, amount: u8) -> u8 dan drain(level: u8, amount: u8) -> u8 yang tidak pernah melewati 100 atau turun di bawah 0. Mulai dari 80, isi daya sebanyak 30, lalu kurangi sebanyak 130, dan cetak level baterainya setelah setiap langkah. Petunjuk: saturating_add ditambah .min(100) untuk mengisi daya, saturating_sub untuk mengurangi.
Lihat solusi
fn charge(level: u8, amount: u8) -> u8 { level.saturating_add(amount).min(100)}
fn drain(level: u8, amount: u8) -> u8 { level.saturating_sub(amount)}
fn main() { let mut level: u8 = 80; println!("awal: {level}%");
level = charge(level, 30); println!("setelah diisi daya: {level}%");
level = drain(level, 130); println!("setelah seharian dipakai: {level}%");}Output:
awal: 80%setelah diisi daya: 100%setelah seharian dipakai: 0%saturating_sub berhenti di 0 dengan sendirinya. Untuk mengisi daya, batas tipenya adalah 255, jadi .min(100) menambahkan batasmu sendiri di atasnya.
2. Faktorial yang tahu batasnya
Section titled “2. Faktorial yang tahu batasnya”Tulis fn factorial(n: u64) -> Option<u64> yang mengalikan 1 × 2 × … × n, dan mengembalikan None kalau hasilnya terlalu besar untuk u64. Cetak hasilnya untuk 5, 20, dan 21. Petunjuk: mulai dengan let mut result: u64 = 1; dan pakai result.checked_mul(i)? di dalam perulangan for.
Lihat solusi
fn factorial(n: u64) -> Option<u64> { let mut result: u64 = 1; for i in 1..=n { result = result.checked_mul(i)?; } Some(result)}
fn main() { for n in [5, 20, 21] { match factorial(n) { Some(value) => println!("{n}! = {value}"), None => println!("{n}! terlalu besar untuk u64"), } }}Output:
5! = 12020! = 243290200817664000021! terlalu besar untuk u64? langsung mengembalikan None dari seluruh fungsi begitu ada satu perkalian yang tidak muat. 20! adalah faktorial terbesar yang bisa ditampung u64.
Ringkasan
Section titled “Ringkasan”- Setiap tipe integer punya rentang yang tetap, dari
T::MINsampaiT::MAX. Keluar dari rentang itu disebut overflow. - Build debug akan panic dengan pesan
attempt to add with overflow. Build release diam-diam berputar balik. Overflow yang bisa dilihat compiler menjadi error saat kompilasi. - Pilih perilakunya sendiri:
checked_memberiOption,saturating_berhenti di batas,wrapping_berputar balik,overflowing_memberi nilainya sekaligus sebuahbool. astidak pernah memeriksa dan bisa diam-diam mengubah angka. PakaiT::try_fromkalau sebuah nilai mungkin tidak muat.- Hati-hati dengan
len() - 1pada list kosong, penjumlahan di tipe yang kecil,abs()dariMIN, danpow.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa yang terjadi pada 250u8 + 10 di build debug, kalau nilainya berasal dari variabel?
2. Apa yang dikembalikan 100u64.checked_sub(250)?
3. Bar nyawa tidak boleh turun di bawah 0. Method mana yang paling cocok?
4. Berapa hasil 300_i32 as u8?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Integer Overflow — The Rust Book (bahasa Inggris): penjelasan resmi tentang panic di build debug, perputaran balik di build release, dan empat keluarga method.
u8— dokumentasi standard library (bahasa Inggris): semua methodchecked_,saturating_,wrapping_, danoverflowing_, masing-masing dengan contoh kecil.i32— dokumentasi standard library (bahasa Inggris): method yang sama untuk angka bertanda, ditambahchecked_abs,unsigned_abs, danchecked_pow.- Overflow — The Rust Reference (bahasa Inggris): aturan persis tentang operasi mana yang bisa overflow dan apa yang terjadi saat itu.
- Casting — Rust by Example (bahasa Inggris): contoh yang bisa dijalankan tentang cast dengan
asyang berputar balik, memotong, dan berhenti di batas.