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:
-
It can be done using the dot operator (
.) and a very limited type of coercion. -
Primitive types conversion can be performed through the
askeyword. -
Many standard types and types found in third-party crates contain methods (
parse(),from()) that can be used for type conversion. -
Casting
C-styleand pointers casting can still be performed through theaskeyword (or specific methods) and completed throughunsafeRust.
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 ERRORprintln!("{}, {}", 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 -3println!("{}, {}", 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
numberof type float. -
Line 2: We cast it to an integer, that is, we explicitly declare a conversion with the
as i64notation. -
Line 3: We cast it again to a float type with the
as f64notation. -
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
FromStrfor ourPointstruct. -
Line 11: To do so, we provide an
Errtype becauseparse()must return aResultto signal if the conversion went wrong. TheErrtype will manifest the explanation of why the conversion did not succeed. -
Lines 13–25: We provide the
from_str()function that is internally called byparse(). -
Lines 14–18: Since we want to parse strings in the format
"(x, y, z)", we create an iterator over comma-separated values usingsplit(','), 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 sameparse()function from the standardstrtype. -
Line 24: We construct and return a
Pointwith thex,y, andzwe 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 aPoint; finally, we print it with theDebugnotation to show that it is indeed aPoint.
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 theas_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
pointerto the array (it points to the first element of the array). -
Line 6: We cast the
pointertousize. -
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
pointerto the second element back to a rawpointerwithas *mut i32. -
Line 9: Using
unsafeRust, we dereference the pointer we got, showing that it indeed points to the second element of the array.