16.2 Casting with as

The as keyword is Rust’s simplest way to convert between types. It is often used for numeric conversions, pointer casts, and other low-level operations. While as is versatile, its behavior is not always intuitive and requires careful attention to potential pitfalls.

16.2.1 Overview of as

The as keyword works for:

  • Primitive Types: Casting between integers, floating-point types, and pointers.
  • Enums to Integers: Converting an enum variant into its discriminant value.
  • Booleans to Integers: Converting booleans to integers, resulting in 0 for false and 1 for true.
  • Pointers: Casting between raw pointer types, such as *const T to *mut T.
  • Type Inference: as can also be used with the _ placeholder when the destination type can be inferred. Note that this can cause inference breakage and usually such code should use an explicit type for both clarity and stability.

16.2.2 Casting Between Numeric Types

The as keyword can convert between numeric types, such as i32 to f64 or u16 to u8. However, as does not perform runtime checks for overflow or truncation.

When casting between signed and unsigned types, as interprets the bit pattern of the value without modification. This can lead to surprising results.

Example:

fn main() {
    let x: u16 = 500;
    let y: u8 = x as u8; // Truncates to fit within u8 range
    println!("x: {}, y: {}", x, y); // Outputs: x: 500, y: 244

    let x: u8 = 255;
    let y: i8 = x as i8; // Interpreted as -1 due to two's complement
    println!("x: {}, y: {}", x, y); // Outputs: x: 255, y: -1
}

16.2.3 Overflow and Precision Loss

When casting from a larger type to a smaller type, as truncates the value to fit the target type. For floating-point to integer conversions, the fractional part is discarded. Converting from an integer to a floating-point type may lose precision.

Example:

fn main() {
    let i: i64 = i64::MAX;
    let x: f64 = i as f64; // Precision loss
    println!("i: {}, x: {}", i, x); // i: 9223372036854775807, x: 9223372036854776000

    let x: f64 = 1e19;
    let i: i64 = x as i64; // Saturated at i64::MAX
    println!("x: {}, i: {}", x, i); // x: 10000000000000000000, i: 9223372036854775807
}

16.2.4 Casting Enums to Integer Values

You can cast enum variants to their underlying integer values using as.

Example:

#[derive(Debug, Copy, Clone)]
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 2,
    Blue = 3,
}

fn main() {
    let color = Color::Green;
    let value = color as u8; // Cast the enum to its underlying u8 representation
    println!("The value of {:?} is {}", color, value); // The value of Green is 2
}

Explanation:

  • The #[repr(u8)] attribute ensures that the Color enum is represented as a u8 in memory. Without this attribute, the default representation may vary.
  • The as keyword casts the Color::Green variant to its underlying discriminant value (2 in this case).

This approach is commonly used when working with enums that need to interface with external systems or protocols where numeric values are expected.

16.2.5 Performance Considerations

Most as casts, such as between integers of the same size, enums to integers, or pointer types, are no-ops with no additional performance cost. Truncation during casts to narrower integer types is also highly efficient, typically involving a single instruction.

In contrast, casting between integers and floating-point types (e.g., i32 to f32 or f64 to u32) incurs a small performance cost due to the need for bit pattern transformations, as these operations are not simple reinterpretations.

16.2.6 Limitations of as

The as keyword is limited to primitive types and does not work for more complex conversions like those between structs or custom data types. Additionally, as does not provide error handling, so it may silently produce incorrect results if not used carefully.