as Casting and Its Limitations
The as operator performs an explicit primitive cast between scalar types (expression as TargetType). Unlike TryFrom—which returns a Result and fails when a value is out of range—an as cast never panics and never fails at runtime, even in debug builds! Instead, as silently truncates high bits, reinterprets sign bits, or saturates floating-point values according to deterministic machine rules.
Allowed as Cast Combinations
The as operator is restricted to a specific set of primitive conversions:
| Source Type | Allowed as Target Types |
Exact Runtime Behavior |
|---|---|---|
Any integer (i8..usize) |
Any integer (i8..usize) |
Widens (zero-extend for unsigned, sign-extend for signed), truncates high bits (if narrowing), or reinterprets two's complement bits (if same width) |
| Any integer | f32 or f64 |
Converts numeric value to nearest IEEE 754 float (rounds to nearest even if integer has more bits of precision than float mantissa) |
f32 or f64 |
Any integer | Truncates fractional part toward zero (3.9 as i32 == 3, -3.9 as i32 == -3) and saturates on overflow (NaN -> 0) |
f32 <-> f64 |
f64 or f32 |
f32 as f64 widens exactly; f64 as f32 rounds to nearest f32 (or ±INFINITY if > f32::MAX) |
bool |
Any integer | false as T -> 0; true as T -> 1 |
char |
Any integer | Converts Unicode code point to integer (truncating if cast to u8 or u16!) |
u8 (and only u8) |
char |
Maps 0..=255 to Unicode scalar U+0000..=U+00FF |
Fieldless enum |
Any integer | Extracts the variant's integer discriminant |
| References / Pointers | Raw pointers / usize |
&T as *const T, *const T as usize |
Integer-to-Integer as Cast Semantics
When casting one integer type to another with as, the CPU manipulates the binary representation directly without checking whether the mathematical value changed:
1. Narrowing (Larger Width -> Smaller Width): Bit Truncation
Casting a wider integer (such as u16 or i32) to a narrower integer (such as u8) simply discards the upper bits and keeps only the lowest $N$ bits (equivalent to modulo $2^N$):
u16 value `1000` (0x03E8):
[ 0000_0011 | 1110_1000 ]
───────── ─────────
Discarded! Kept as u8 (0xE8 == 232)
fn main() {
let original: u16 = 1000; // 0b0000_0011_1110_1000 (3 * 256 + 232)
let truncated: u8 = original as u8; // Keeps low 8 bits: 0b1110_1000 = 232!
println!("{original}_u16 as u8 = {truncated} (1000 % 256 = {})", 1000 % 256);
}
1000_u16 as u8 = 232 (1000 % 256 = 232)
2. Widening (Smaller Width -> Larger Width): Zero-Extension vs Sign-Extension
- If the source integer is unsigned (
u8), widening fills the new high bits with0(zero-extension). - If the source integer is signed (
i8), widening fills the new high bits with copies of the sign bit (sign-extension), preserving negative values (or turning-1_i8into65535_u16if cast directly tou16!).
fn main() {
let neg_one: i8 = -1; // 0b1111_1111 (0xFF)
// Sign-extends `-1_i8` to 16 bits (`0xFFFF`), which as `i16` is `-1`, and as `u16` is `65535`!
let as_i16: i16 = neg_one as i16;
let as_u16: u16 = neg_one as u16;
// To zero-extend `neg_one`'s 8 bits into `u16` (`0x00FF` = 255), cast to `u8` first:
let zero_extended: u16 = (neg_one as u8) as u16;
println!("neg_one as i16: {as_i16}");
println!("neg_one as u16: {as_u16} (0x{as_u16:04X} - sign-extended!)");
println!("(neg_one as u8) as u16: {zero_extended} (0x{zero_extended:04X} - zero-extended!)");
}
neg_one as i16: -1
neg_one as u16: 65535 (0xFFFF - sign-extended!)
(neg_one as u8) as u16: 255 (0x00FF - zero-extended!)
Float-to-Integer as Cast Semantics (Truncating & Saturating)
Since Rust 1.45, casting a floating-point number (f32 or f64) to any integer type with as is 100% deterministic and saturating (in C and C++, casting an out-of-range float to int is Undefined Behavior):
- Fractional parts are truncated toward zero (
4.99 as i32 == 4and-4.99 as i32 == -4). - Values larger than
T::MAX(including+INFINITY) saturate toT::MAX. - Values smaller than
T::MIN(including-INFINITY) saturate toT::MIN. NaNconverts to0.
fn main() {
println!("3.99 as i32 = {}", 3.99_f64 as i32);
println!("-3.99 as i32 = {}", -3.99_f64 as i32);
println!("9999.0 as u8 = {}", 9999.0_f64 as u8);
println!("-50.0 as u8 = {}", -50.0_f64 as u8);
println!("f64::INFINITY as u8 = {}", f64::INFINITY as u8);
println!("f64::NAN as u8 = {}", f64::NAN as u8);
}
3.99 as i32 = 3
-3.99 as i32 = -3
9999.0 as u8 = 255
-50.0 as u8 = 0
f64::INFINITY as u8 = 255
f64::NAN as u8 = 0
Limitations and Forbidden as Casts
1. Precision Loss When Casting Large i64/u64 to f32/f64
Because an f32 has only 24 bits of mantissa precision (and an f64 has 53 bits), casting a large i32/u32 to f32 (or a large i64/u64 above $2^{53}$ to f64) silently rounds the lowest digits:
fn main() {
let exact_int: i32 = 16_777_217; // 2^24 + 1
let as_float: f32 = exact_int as f32;
let back_to_int: i32 = as_float as i32;
println!("Original i32: {exact_int}");
println!("After `as f32` and back: {back_to_int} (lost 1 due to 24-bit f32 mantissa!)");
}
Original i32: 16777217
After `as f32` and back: 16777216 (lost 1 due to 24-bit f32 mantissa!)
2. Forbidden as Casts (E0054 / E0604)
Rust rejects as casts when a safer or multi-step conversion is required:
- Integer
as boolis forbidden (E0054): Writex != 0instead. u32(or any integer other thanu8)as charis forbidden (E0604): Because not everyu32is a valid Unicode scalar value, you must usechar::from_u32(x)(which returnsOption<char>).- Non-primitive types (
String,Vec, structs):asnever invokes custom conversion methods; useFrom/IntoorTryFrom/TryInto.
// ❌ does not compile
fn main() {
let code: u32 = 0x1F980;
let ch = code as char;
}
error[E0604]: only `u8` can be cast as `char`, not `u32`
--> src/main.rs:3:14
|
3 | let ch = code as char;
| ^^^^^^^^^^^^ invalid cast
|
help: try `char::from_u32` instead
|
3 | let ch = char::from_u32(code);
| ~~~~~~~~~~~~~~~ ~
error: aborting due to 1 previous error
Clippy Lints for Catching Risky as Casts
In safety-critical codebases, enable Clippy's pedantic cast lints—#[warn(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap, clippy::cast_lossless)]—to flag implicit lossy as casts and replace them with From::from, TryFrom::try_from, or .cast_signed() / .cast_unsigned().