Converting between number types in Rust (as, From and TryFrom)
How to convert i32 to f64, u64 to usize and back in Rust with as, From and TryFrom, and the truncation and wrapping traps of as.
Rust never converts between number types behind your back. An i32 won't silently become an i64, even though that would always be safe:
fn main() {
let items: i32 = 7;
let total: i64 = items;
println!("{total}");
}
error[E0308]: mismatched types
That strictness is deliberate: implicit conversions are a classic source of bugs in C and JavaScript. You choose the conversion, and there are three tools.
1. as: fast, never fails, can lose information
value as Type converts between any number types (and char/u8, bool to integer):
fn main() {
let sum = 87 + 92 + 78;
let count = 3;
let average = sum as f64 / count as f64;
println!("{average:.2}");
println!("{}", 3.99_f64 as i32);
println!("{}", 300_i32 as u8);
println!("{}", -1_i32 as u32);
println!("{}", 1e10_f64 as i32);
println!("{}", 'A' as u8);
}
85.67
3
44
4294967295
2147483647
65
Read the output carefully, because each line is a rule:
- Float to integer drops the fraction (rounds toward zero):
3.99becomes3. Out-of-range values saturate to the type'sMAXorMIN, andNaNbecomes0. - Integer to a smaller integer keeps only the low bits:
300 as u8is44(300 - 256). - Signed to unsigned reinterprets the bits:
-1becomesu32::MAX. - Integer division happens before the cast.
(sum / count) as f64would compute85first. Cast the operands, then divide.
2. From / into(): lossless conversions
When every value of the source type fits in the target, the standard library implements From: i64::from(x) for an i32, f64::from(x) for an i32 or f32, u32::from(c) for a char. The same conversion is available as x.into() when the target type is known. If someone later changes the source type so the conversion could lose data, From stops compiling, while as keeps quietly truncating.
3. TryFrom / try_into(): checked conversions
For conversions that might not fit, u8::try_from(x) returns a Result: Ok(value) if it fits, Err(...) if not. This is how you safely turn a u64 from a file into a usize index, or an i32 from user input into a u8.
At work
- Prefer
From/into()for widening andTryFromfor narrowing. Keepasfor float/int math where you've thought about the rounding. - Clippy's pedantic lints (
cast_possible_truncation,cast_sign_loss) flag riskyascasts; many teams enable them in code that handles money or sizes. asbetween floats and integers never panics, so it's easy to hide a bug: a huge value silently saturates.- To round instead of truncate, call
.round()first:2.7_f64.round() as i32is3.
Example
fn main() {
let cents: i32 = 1999;
let dollars = f64::from(cents) / 100.0;
println!("price ${dollars:.2}");
let big: i64 = i64::from(cents) * 1_000_000;
println!("wide math: {big}");
for raw in [200_i32, 300, -5] {
match u8::try_from(raw) {
Ok(b) => println!("{raw} fits in a u8: {b}"),
Err(e) => println!("{raw} does not fit: {e}"),
}
}
let ratio = 2.0_f64 / 3.0;
println!("truncated {} rounded {}", (ratio * 100.0) as i32, (ratio * 100.0).round() as i32);
}
Output:
price $19.99
wide math: 1999000000
200 fits in a u8: 200
300 does not fit: number too large to fit in target type
-5 does not fit: number too small to fit in target type
truncated 66 rounded 67