Type Casts in Rust - The as Keyword
Understanding explicit type casts using the as keyword, including numeric conversions, pointer casts, and safety rules
The as keyword is Rust's built‑in mechanism for performing an explicit type cast when the compiler will not do it for you. It converts a value of one type into a value of another type — but only for a restricted set of type pairs that the language defines. Unlike many other languages, Rust has almost no implicit numeric conversions. An i32 never silently becomes an i64 or a f64. You tell the compiler exactly what you want with as.
Why Explicit Casts Exist
Languages that convert numbers automatically hide costs. Widening an integer is cheap, but narrowing one can lose information. Converting a float to an integer discards the fractional part, and out‑of‑range values cause real trouble. Rust makes these costs visible by requiring an as cast everywhere a type change happens. The keyword acts as a marker: “I understand the consequences, and I want this conversion anyway.”
No Implicit Conversions:
Rust’s refusal to perform implicit numeric casts is a deliberate design choice. An expression like let x: i32 = 5u8; will not compile. You must write let x: i32 = 5u8 as i32;. This forces you to think about precision loss and range mismatch before the code runs.
How the as Keyword Works
The syntax is simple:
value as TargetType
The compiler checks that a cast from the source type to the target type is allowed. If it is not — for example, casting a String to an i32 — you get a compile‑time error. When the cast is permitted, the compiler inserts the necessary low‑level instructions: truncation, bit‑pattern reinterpretation, sign extension, or pointer recalculation, depending on the types involved.
as is an expression. It produces a value of the target type, so you can use it anywhere an expression is expected.
Numeric Casts
Numeric casts are the most common use of as. The rules vary depending on whether the source and target are integers or floating‑point numbers.
Integer to Integer
Casting between integer types of different sizes or signedness follows bit‑level rules, not mathematical rounding or saturation.
- Widening (smaller to larger): The value is sign‑extended if the source is signed and the target is also signed. If the source is unsigned, it is zero‑extended. No information is lost.
- Narrowing (larger to smaller): The lower bits are kept; the higher bits are discarded. This can change the numeric value drastically, but it is well‑defined and never undefined behaviour.
let a: u32 = 0x1234_5678;
let b: u16 = a as u16;
println!("0x{:x}", b); // prints "0x5678"
The top 16 bits are thrown away. The result is always the original value modulo 2^N, where N is the bit width of the target type. For signed narrowing, the behaviour is the same — the compiler does not preserve the sign.
Silent Truncation:
Narrowing with as never panics, never saturates, and never warns at runtime. A value that is too large for the target type wraps around silently, which is a frequent source of logic bugs.
Floating‑Point to Integer
Converting a float to an integer truncates toward zero. The fractional part is dropped. This is safe as long as the truncated value fits in the integer’s range.
let x: f64 = 3.9;
let y: i32 = x as i32;
println!("{}", y); // prints 3
Out‑of‑Range Float‑to‑Int is Undefined Behaviour:
If the floating‑point value, after truncation, does not fit in the target integer type — for example, f64::MAX as i32 or f64::NEG_INFINITY as i32 — the result is undefined behaviour. The program may crash, produce garbage, or behave unpredictably. Always guard such casts with range checks or use safe conversion traits like TryFrom.
Integer to Floating‑Point
An integer becomes the nearest representable floating‑point number. Large integers may lose precision because a float cannot represent every integer beyond a certain magnitude, but the conversion is always safe.
Floating‑Point to Floating‑Point
Casting f64 to f32 rounds to the nearest representable value (with ties to even). Overflow results in infinity, and underflow in a subnormal or zero. Casting f32 to f64 is exact — every f32 value fits perfectly in f64.
Widening is Always Safe:
Any integer can be widened to a larger integer of the same signedness without data loss. Any f32 can be widened to f64 without loss. These casts are predictable and safe to use without additional checks.
Pointer and Integer Casts
as can convert between pointers and integers, and between different pointer types. These conversions are deeply tied to unsafe code.
- Reference to raw pointer:
&Tcan be cast to*const T, and&mut Tto*mut T. This is allowed outsideunsafeblocks because the operation itself does not dereference memory. - Raw pointer to raw pointer: Any pointer type can be cast to any other pointer type, including changing mutability, but this requires an
unsafeblock because you are circumventing the type system’s guarantees about what kind of data lives at that address. - Pointer to integer:
*const Tor*mut Tcan be cast tousize(or any integer type that fits the pointer width). This gives you the raw address as a number. - Integer to pointer: An integer can be cast to a raw pointer. This is always
unsafeand is needed for memory‑mapped I/O or interacting with hardware.
let x: i32 = 42;
let ptr: *const i32 = &x as *const i32;
let addr: usize = ptr as usize;
let reconstructed: *const i32 = addr as *const i32;
// Dereferencing reconstructed would require unsafe.
Requires unsafe Context:
Creating a raw pointer from a reference does not require unsafe, but most uses of the resulting pointer (dereferencing, cast to a different pointee type) do. The as keyword itself does not change safety requirements; you still need an unsafe block to do anything dangerous with the pointer.
Enum and Boolean Casts
as can convert C‑like enums (enums without fields) and boolean values to integers.
Enum to Integer
A fieldless enum can be cast to any integer type. The variant’s discriminant value — the number assigned by the compiler (starting at 0 by default) — becomes the integer.
enum Color {
Red,
Green,
Blue,
}
let n: u8 = Color::Green as u8;
println!("{}", n); // prints 1
You can also assign explicit discriminants, and the cast uses those values.
Boolean to Integer
true becomes 1, and false becomes 0 for any integer type.
let one: i32 = true as i32;
let zero: i32 = false as i32;
Character to Integer
A char can be cast to any integer type, giving the Unicode scalar value. If the value is too large for the target type, the high bits are truncated — this can produce a meaningless number, but it is not undefined behaviour.
let a: u8 = 'A' as u8;
println!("{}", a); // prints 65
What as Cannot Do
The as keyword only works for a fixed set of conversions defined by the language. It is not a general “convert anything” tool.
- You cannot cast between types that are not related by the standard rules, such as
Stringtoi32or a struct to another struct. - You cannot use
asto cast array to slice (use&arr[..]instead). - You cannot cast a trait object to a concrete type with
as. asdoes not callFromorIntoimplementations. It always follows the primitive coercion rules described above.
as is Not a Pattern Operator:
You cannot use as inside a match arm pattern. For example, EtherType::Arp as u16 =\u003e is illegal. Patterns and expressions are separate syntactic positions. To match on a discriminant value, store it in a constant with as first, or convert the matched integer back to the enum with a from_u16 function.
When to Use as vs. From and Into
as is low‑level. It operates on bits and raw pointers. For ordinary numeric conversions that you want to be safe and obvious, prefer the From and Into traits (i32::from(5u8)) or the fallible TryFrom and TryInto traits, which return a Result and force you to handle overflow. The standard library implements these traits for many conversions, and they guarantee a panic (or an error) instead of silent truncation or undefined behaviour.
let small: u8 = 200;
// let big: i32 = small as i32; // fine, but no overflow protection
let big: i32 = i32::from(small); // also fine, and communicates intent
let large: u16 = 1000;
// let bad: u8 = large as u8; // truncates silently to 232
let checked: u8 = u8::try_from(large).expect("value too large");
The as keyword still has a place: raw pointer manipulation, performance‑critical numeric code where the wrapping semantics are exactly what you want, and casting between primitive types when you know the value is in range. For everything else, From/Into is the idiomatic Rust answer.
Summary and Next Steps
The as keyword gives you direct control over primitive conversions at the cost of requiring you to understand the underlying bit‑level behaviour. It is the only way to do certain casts — raw pointer arithmetic, enum discriminant extraction — but for everyday numeric work it is often the wrong tool. The real insight is that Rust provides a layered conversion story: as for raw, unchecked reinterpretation; From and Into for safe, infallible conversions; and TryFrom/TryInto for conversions that can fail. Which one you reach for says a great deal about the guarantees you want the compiler to enforce.