Item 6: Understand type conversions
In general, Rust does not perform automatic conversion between types. This includes integral types, even when the transformation is "safe":
let x: i32 = 42;
let y: i16 = x;
error[E0308]: mismatched types
--> use-types/src/main.rs:14:22
|
14 | let y: i16 = x;
| --- ^ expected `i16`, found `i32`
| |
| expected due to this
|
help: you can convert an `i32` to an `i16` and panic if the converted value doesn't fit
|
14 | let y: i16 = x.try_into().unwrap();
| ^^^^^^^^^^^^^^^^^^^^^
Rust type conversions fall into three categories:
- manual: user-defined type conversions provided by implementing the
From
andInto
traits - semi-automatic: explicit casts between values using the
as
keyword - automatic: implicit coercion into a new type
The latter two don't apply to conversions of user defined types (with a couple of exceptions), so the majority of this Item will focus on manual conversion. However, sections at the end will discuss casting and coercion – including the exceptions where they can apply to a user-defined type.
User-Defined Type Conversions
As with other features of the language (Item 5) the ability to perform conversions between values of different user-defined types is encapsulated as a trait – or rather, as a set of related generic traits.
The four relevant traits that express the ability to convert values of a type are:
From<T>
: Items of this type can be built from items of typeT
.TryFrom<T>
: Items of this type can sometimes be built from items of typeT
.Into<T>
: Items of this type can converted into items of typeT
.TryInto<T>
: Items of this type can sometimes be converted into items of typeT
.
Given the discussion in Item 1 about expressing things in the type system, it's no surprise to discover that the
difference with the Try...
variants is that the sole trait method returns a Result
rather than a guaranteed new
item; the trait definition also requires an associated type that provides the type of the error E
for failure
situations. You can choose to ignore the possibility of error (e.g. with .unwrap()
), but as usual it needs to be a
deliberate choice.
There's also some symmetry here: if a type T
can be transmuted into
a type U
, isn't that the same as it being
possible to create an item of type U
by transmutation from
an item of type T
?
This is indeed the case, and it leads to the first piece of advice: implement the From
trait for conversions. The
Rust standard library had to pick just one of the two possibilities (to prevent the system from spiralling around in
dizzy circles1), and came down on the side of
automatically providing Into
from a From
implementation.
If you're consuming one of these two traits, as a trait bound on a new trait of your own, then the advice is reversed:
use the Into
trait for trait bounds. That way, the bound will be satisfied both by things that directly implement
Into
, and by things that only directly implement From
.
This automatic conversion is highlighted by the documentation for From
and Into
, but it's worth reading the code
too:
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
Translating a trait specification into words can help with understanding more complex trait bounds; in this case, it's
fairly simple: "I can implement Into<U>
for a type T
whenever U
already implements From<T>
".
It's also useful in general to look over the trait implementations for a standard library type. As you'd expect, there
are From
implementations for safe integral conversions (From<u32> for u64
) and TryFrom
implementations when
the conversion isn't safe (TryFrom<u64> from u32
).
There are also various blanket trait implementations. Into
just has the one shown above, but the From
trait has many
impl<T> From<T> for ...
clauses. These are almost all for smart pointer types, allowing the smart pointer to be
automatically constructed from an instance of the type that it holds, so that methods that accept smart pointer
parameters can also be called with plain old items; more on this below and in Item 9.
The TryFrom
trait also has a blanket implementation for any type that already implements the Into
trait in the
opposite direction – which automatically includes (as above) any type that implements From
in the same
direction. This conversion will always succeed, so the associated error type is 2 the helpfully named Infallible
.
There's also one very specific generic implementation of From
that sticks out, the reflexive implementation:
impl<T> From<T> for T {
fn from(t: T) -> T {
t
}
}
Translating into words, this just says that "given a T
I can get a T
". That's such an obvious "well, doh" that it's
worth stopping to understand why this is useful.
Consider a simple struct
and a function that operates on it (ignoring that this function would be better expressed as
a method):
/// Integer value from an IANA-controlled range.
#[derive(Clone, Copy, Debug)]
pub struct IanaAllocated(pub u64);
/// Indicate whether value is reserved.
pub fn is_iana_reserved(s: IanaAllocated) -> bool {
s.0 == 0 || s.0 == 65535
}
This function can be invoked with instances of the struct
let s = IanaAllocated(1);
println!("{:?} reserved? {}", s, is_iana_reserved(s));
but even if From<u64>
is implemented
impl From<u64> for IanaAllocated {
fn from(v: u64) -> Self {
Self(v)
}
}
it can't be directly invoked for u64
values
error[E0308]: mismatched types
--> casts/src/main.rs:75:25
|
75 | if is_iana_reserved(42) {
| ^^ expected struct `IanaAllocated`, found integer
However, a generic version of the function that accepts (and explicitly converts) anything satisfying
Into<IanaAllocated>
pub fn is_iana_reserved_anything<T>(s: T) -> bool
where
T: Into<IanaAllocated>,
{
let s = s.into();
s.0 == 0 || s.0 == 65535
}
allows this use:
if is_iana_reserved_anything(42) {
The reflexive trait implementation of From<T>
means that this generic function copes with items which are already
IanaAllocated
instances, no conversion needed.
This pattern also explains why (and how) Rust code sometimes appears to be doing implicit casts between types: the
combination of From<T>
implementations and Into<T>
trait bounds leads to code that appears to magically convert at
the call site (but which is still doing safe, explicit, conversions under the covers), This pattern becomes even more
powerful when combined with reference types and their related conversion traits; more in Item 9.
Casts
Rust includes the as
keyword to perform explicit
casts between some pairs
of types.
The pairs of types that can be converted in this way is a fairly limited set, and the only user-defined types it
includes are "C-like" enum
s (those that have an associated integer value). General integral conversions are included
though, giving an alternative to into()
:
let x: u32 = 9;
let y = x as u64;
let z: u64 = x.into();
The as
version also allows lossy conversions:
let x: u32 = 9;
let y = x as u16;
which would be rejected by the from
/ into
versions:
error[E0277]: the trait bound `u16: From<u32>` is not satisfied
--> casts/src/main.rs:113:20
|
113 | let y: u16 = x.into();
| ^^^^ the trait `From<u32>` is not implemented for `u16`
|
= help: the following implementations were found:
<u16 as From<NonZeroU16>>
<u16 as From<bool>>
<u16 as From<u8>>
= note: required because of the requirements on the impl of `Into<u16>` for `u32`
For consistency and safety you should prefer from
/ into
conversions to as
casts, unless you understand and
need the precise casting semantics (e.g
for C interoperability).
Coercion
The explicit as
casts described in the previous section are a superset of the implicit
coercions that the compiler will silently perform:
any coercion can be forced with an explicit as
, but the converse is not true. (In particular, the integral
conversions performed in the previous section are not coercions, and so will always require as
.)
Most of the coercions involve silent conversions of pointer and reference types in ways that are sensible and convenient for the programmer, such as:
- converting a mutable reference to a non-mutable references (so you can use a
&mut T
as the argument to a function that takes a&T
) - converting a reference to a raw pointer (this isn't
unsafe
– the unsafety happens at the point where you're foolish enough to use a raw pointer) - converting a closure that happens not to capture any variables into a bare function pointer (Item 2)
- converting an array to a slice
- converting a concrete item to a trait object, for a trait that the concrete item implements
- converting3 an item lifetime to a "shorter" one (Item 15).
There are only two coercions whose behaviour can be affected by user-defined types. The first of these is when a
user-defined type implements the Deref
or the
DerefMut
trait. These traits indicate that the user defined
type is acting as a smart pointer of some sort (Item 9), and in this case the compiler will coerce a reference to
the smart pointer item into being a reference to an item of the type that the smart pointer contains (indicated by
its Target
).
The second coercion of a user-defined type happens when a concrete item is converted to a trait object. This operation builds a fat pointer to the item; this pointer is fat because it also includes a pointer to the vtable for the concrete type's implementation of the trait – see Item 9.
1: More properly known as the trait coherence rules.
2: For now – this is
likely to be replaced with the !
"never" type in a future
version of Rust.
3: Rust refers to these conversions as "subtyping", but it's quite different that the definition of "subtyping" used in object-oriented languages.