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 as
keyword.
Many standard types and types found in third-party crates contain methods (parse()
, from()
) that can be used for type conversion.
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.
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.
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.
as
keywordPrimitive types can be converted to other primitive types using the as
keyword. Take a look at the example below.
let number = 3.1416;let integer = number as i64;let again_float = integer as f64;println!("{}, {}, {}", number, integer, again_float);
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.
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);
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);
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.
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.
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!
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.
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); }}
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.