How to perform type conversion in Rust

Rust does not support implicit type conversion as many other languages do. However, type conversion in Rust (coercion) can still be performed explicitly in one of these ways:

  1. It can be done using the dot operator (.) and a very limited type of coercion.

  2. Primitive types conversion can be performed through the as keyword.

  3. Many standard types and types found in third-party crates contain methods (parse(), from()) that can be used for type conversion.

  4. Casting C-style and pointers casting can still be performed through the as keyword (or specific methods) and completed through unsafe Rust.

Shortly, we will look into the working examples of the above four ways.

Rust is a strongly typed language

In Rust programming, a significant amount of code is often required to convert values from one type to another. The Rust language and standard library provide features such as type inference and methods like unwrap(), unwrap_or(), ok(), and err() to make it easier to perform these conversions. Rust encourages the use of these tools through its design and conventions, which aim to make it more efficient and convenient to convert between types in Rust code.

However, since Rust is a strongly typed language, implicit type conversion is not possible, at least not to the extent that it is done in loosely typed languages. In JavaScript, for example, the following behavior does not raise any error:

let x = "3"; // x is a string '3'
let y = - x; // y is a number -3

JavaScript, in fact, implicitly converts strings to numbers. However, in Rust, the same code would not work:

let x = "3"; // x is a &str '3'
let y = - x; // y gives ERROR
println!("{}, {}", x, y);

As we can see, it returns a compilation error.

However, Rust can achieve the same result as the JavaScript code with an explicit conversion through the parse() method.

let x = "3"; // x is a &str '3'
let y = - x.parse::<i32>().unwrap(); // y is now a number -3
println!("{}, {}", x, y);

The parse() method can convert the str type to any type that implements the std:str:FromStr trait.

In line 2 of the above code, we use parse() to convert an &str type to an i32, with the turbofish notation (::<i32>). This gives a Result type which we unwrap to obtain the number we were searching for; with the minus sign (-) at the beginning, we make it negative, as in the JavaScript example.

The parse() method is just one of the many ways in which we can perform type conversion in Rust. Let’s see some other ways.

Limited coercion and the dot operator

In Rust, a coercion (implicit conversion) does exist, but it is a local one and very limited. Limited coercion is used, for example, to take a reference to a mut type (&mut T) and convert it to a reference of the same type but non-mutable (&T). What changes here is just the mutability, and it is done seamlessly without incurring errors.

This type of coercion happens in specific places (mainly in function results, arguments in function calls, and the let, const, and static declarations). It is very limited and is needed for the thing to simply work. In most cases, it is tied to the (de)referencing Rust mechanisms.

Note: Coercion is not a method for type conversion in a strict sense here.

For example, in this case &mut 42 is coerced to &i8:

let x: &i8 = &mut 42;

In the same way, the dot operator can perform auto-referencing, auto-dereferencing, and coercion in order to make types match.

For example, when working with boxed arrays (arrays wrapped in a Box type, which allocates the array to the heap memory) we have the following:

use std::ops::Index;
fn main() {
let array: Box<[char; 1]> = Box::new(['A']);
let element = array.index(0);
println!("{}", element);
}

In the above code, the index() method in line 5 is not implemented for Box<T>; however, it is implemented for an array. Thus, the dot operator in line 5 derferences the boxed array to the contained array and accesses the element at index 0.

As you can see, coercion and the use of the dot operator, are not really methods for type conversion in a strict sense. This means you can’t use them to convert between different, unrelated types, as when converting an integer to a float number. For that, we need the next method.

Casting through the as keyword

Primitive types can be converted to other primitive types using the as keyword. Take a look at the example below.

Code example

let number = 3.1416;
let integer = number as i64;
let again_float = integer as f64;
println!("{}, {}, {}", number, integer, again_float);

Code explanation

  • Line 1: We define a number of type float.

  • Line 2: We cast it to an integer, that is, we explicitly declare a conversion with the as i64 notation.

  • Line 3: We cast it again to a float type with the as f64 notation.

  • Line 4: We print out all three values.

The as keyword can be used to convert between most of the primitive numbers, particularly the numeric ones.

The as keyword is not limited to converting numeric types. See the type cast expression documentation for more use cases.

Converting between types with provided methods

We have already seen that the parse() method is available to &str types to convert between string types and numbers. The parse() method can also convert strings into any type that implements the std:str:FromStr trait.

We can actually implement the FromStr trait for any of our own types, and most crates found in crates.io actually do it. Let’s see how to implement our own FromStr trait with an example.

We have the struct Point defined as follows:

#[derive(Debug)]
struct Point {
x: i64,
y: i64,
z: i64,
}
let my_point = Point {x: 3, y:3, z: 1};
println!("{:#?}", my_point);

Code example

We can implement the FromStr trait for it in this way:

use std::str::FromStr;
use std::num::ParseIntError;
#[derive(Debug)]
struct Point {
x: i64,
y: i64,
z: i64,
}
impl FromStr for Point {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s_iter = s
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| Some(s.split(',')))
.unwrap();
let x = s_iter.next().unwrap().parse::<i64>()?;
let y = s_iter.next().unwrap().parse::<i64>()?;
let z = s_iter.next().unwrap().parse::<i64>()?;
Ok(Point { x, y, z })
}
}
let string_point = "(2,3,1)";
let my_point = string_point.parse::<Point>().unwrap();
println!("{:#?}", my_point);

Code explanation

  • Line 10: We start implementing FromStr for our Point struct.

  • Line 11: To do so, we provide an Err type because parse() must return a Result to signal if the conversion went wrong. The Err type will manifest the explanation of why the conversion did not succeed.

  • Lines 13–25: We provide the from_str() function that is internally called by parse().

  • Lines 14–18: Since we want to parse strings in the format "(x, y, z)", we create an iterator over comma-separated values using split(','), after having stripped the prefix ( and the suffix ).

  • Lines 20–22: We iterate over the string iterator we created, extracting each block as i64; we use the same parse() function from the standard str type.

  • Line 24: We construct and return a Point with the x, y, and z we just extracted.

  • Lines 27–29: We can see the usage of the parse() function. We declare a point in string format and then parse it as a Point; finally, we print it with the Debug notation to show that it is indeed a Point.

Besides the parse() method, there are also the from() and into() methods, which are implemented for most of the primitive types and the std library types.

Moreover, there are many more methods for type conversion found both in the std library and third-party crates on crates.io, which do not have standard names such as parse(), from(), and into(). They are usually in the form to_<type> or from_<type>; think, for example, about the str method to_string() or the from_str() method for String.

Usually, all these conversion methods are clearly stated and documented in each crate’s documentation.

Casting pointers

We can use the as keyword or other standard methods (for example, as_ptr() or as_mut_ptr() for vectors) to convert between pointer types, much like C does. We can even play with some pointer math. However, in order to dereference these raw pointers, we must resort to unsafe Rust.

Code example

For example, this code gives an error:

let mut array: [i32; 2] = [0, 1];
let pointer: *mut i32 = array.as_mut_ptr();
println!("{:?}", *pointer); // Error: dereferencing a raw pointer!

Code explanation

  • Line 1: We create a two-element array.

  • Line 2: We get a *mut (raw) pointer to that array through the as_mut_ptr() method.

  • Line 3: We try to dereference that raw pointer, but the compiler won’t allow us.

The compiler gives us the reason why we can’t dereference raw pointers.

Note: Raw pointers may be null, dangling, or unaligned; they can also violate aliasing rules and cause data races. All of these are undefined behaviors.

We can solve the problem in the above code in this way:

let mut array: [i32; 2] = [0, 1];
let pointer: *mut i32 = array.as_mut_ptr();
unsafe { println!("{:?}", *pointer); }

In line 3, we use the unsafe keyword, that is, we tell the compiler that we take responsibility to deal with a pointer that could be dangling, null, or unaligned.

We basically tell the Rust compiler we will do all the necessary checks, or that we can guarantee there won’t be problems with the use of that pointer. This is the meaning of unsafe Rust in a nutshell.

Code example

In Rust, we can still do some pointers' math, but we always need unsafe to access (dereference) the pointers:

use std::mem::size_of;
fn main() {
let mut array: [i32; 2] = [0, 1];
let pointer: *mut i32 = array.as_mut_ptr();
let first_element = pointer as usize;
let second_element = first_element + size_of::<i32>();
let new_pointer = second_element as *mut i32;
unsafe { println!("the second element is: {}", *new_pointer); }
}

Code explanation

  • Line 4: We define an array.

  • Line 5: We get a raw pointer to the array (it points to the first element of the array).

  • Line 6: We cast the pointer to usize.

  • Line 7: We add to that address the size of an i32. In this way, we are effectively pointing to the second element of the array.

  • Line 8: We cast the pointer to the second element back to a raw pointer with as *mut i32.

  • Line 9: Using unsafe Rust, we dereference the pointer we got, showing that it indeed points to the second element of the array.

Free Resources