Background
Rust is a strongly-typed programming language with static typing. It’s also quite pedantic. For example, it’s impossible to compare two integers with each other if they are of the different types. To avoid any extra work at runtime, such as reflection, Rust requires us to be precise with the types that our programs use.
This small program won't compile:
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4a0ad78e651e74bc52d986037eb21b0c
fn main() {
let a: i32 = 10;
let b: u32 = 10;
println!("{}", a + b);
}
Instead creating a program that prints 20 to the console, we receive an error message with two error codes:
error[E0308]: mismatched types
--> src/main.rs:4:22
|
4 | println!("{}", a + b);
| ^ expected `i32`, found `u32`
error[E0277]: cannot add `u32` to `i32`
--> src/main.rs:4:20
|
4 | println!("{}", a + b);
| ^ no implementation for `i32 + u32`
|
= help: the trait `Add<u32>` is not implemented for `i32`
For the curious, here is some information about how to interpret those error codes. The first (E0308) is caused because the addition operator (+) expects the same type for both of its operands. The second error (E0277) is subtly different. It’s saying that the trait bounds (the interface) are not satisfied. Addition is provided by the std::ops::Add trait and has been implicitly parameterized to i32 type by its left operand.
Explicitly changing types with the as keyword
The most common method you’ll encounter to convert one type to another is via the as keyword. It’s particularly common when converting between usize (which may be 32 or 64 bits wide, depending on CPU architecture) and fixed-width integers, such as u32
.
One option that's available to us for fixing the earlier example is to promote both values to the type i64
, which can represent all possible values within the u32
and i32
types:
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b55bd9b4447b9b01d4f98649ed54edf6
fn main() {
let a: i32 = 10;
let b: u32 = 10;
println!("{}", a as i64 + b as i64);
}
If the value cannot fit within the bounds of the type being converted to — a negative number cannot be represented by an unsigned integer type, for example — then the program will exhibit very strange behaviour (see the "Rustnomicon" reference at the bottom of the article for details). We'll work through other options for handling cases where type conversions might fail later in the series.
The as
keyword asks Rust to do the minimal amount of work possible to treat a value of one type as a value of another. Very often, such as converting between integer types of equal width, the internal representation does not change. This efficiency comes at a cost.
As is only available for primitive types. When you require conversions of types that you’ve designed yourself, you need to use the std::convert::{From,Into}
traits.
The Rust reference provides a good overview of the details in the type cast expression page. The Rustnomicon also has a detailed section about casting between data types.
Posts to come in this series. Please follow me to get notified when they are posted!
- Using the
From
andInto
traits - Using the
From
andInto
traits - Hidden type conversion via the
Deref
trait - Trait objects
- Downcasting
Thanks to @vorfeedcanal for comments on a previous version of this post.
Top comments (4)
Hey, great article!
I had a couple of questions :
Looking forward to the series, I found out about the book from your last post, and the topics covered seem very interesting 😄😄
Thanks :)
I'm not sure about what happens at the machine code level. I could imagine that the optimizer might avoid promoting values to a wider type if it's not necessary, but I don't think that there would be a difference between the data type on the stack and the data type held in registers.
Actually, I was wrong about this. Programs don't crash, they just silently continue with the wrong value. I have updated the article.
doc.rust-lang.org/nomicon/casts.html
casting from a larger integer to a smaller integer (e.g. u32 -> u8) will truncate
casting from a smaller integer to a larger integer (e.g. u8 -> u32) will
zero-extend if the source is unsigned
sign-extend if the source is signed
Someone is wrong. Either nomicon or this article.
I'm wrong. I'll make some changes to the post.