Rust Data Types

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 with 0 (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_i8 into 65535_u16 if cast directly to u16!).
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):

  1. Fractional parts are truncated toward zero (4.99 as i32 == 4 and -4.99 as i32 == -4).
  2. Values larger than T::MAX (including +INFINITY) saturate to T::MAX.
  3. Values smaller than T::MIN (including -INFINITY) saturate to T::MIN.
  4. NaN converts to 0.
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 bool is forbidden (E0054): Write x != 0 instead.
  • u32 (or any integer other than u8) as char is forbidden (E0604): Because not every u32 is a valid Unicode scalar value, you must use char::from_u32(x) (which returns Option<char>).
  • Non-primitive types (String, Vec, structs): as never invokes custom conversion methods; use From/Into or TryFrom/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().