Item 5: Understand type conversions
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 majority of this Item focuses on the first of these, manual conversions of types, because the latter two mostly don't apply to conversions of user-defined types. There are a couple of exceptions to this, so sections at the end of the Item discuss casting and coercion—including how they can apply to a user-defined type.
Note that in contrast to many older languages, Rust does not perform automatic conversion between numeric types. This even applies to "safe" transformations of integral types:
let x: u32 = 2;
let y: u64 = x;
error[E0308]: mismatched types
--> src/main.rs:70:18
|
70 | let y: u64 = x;
| --- ^ expected `u64`, found `u32`
| |
| expected due to this
|
help: you can convert a `u32` to a `u64`
|
70 | let y: u64 = x.into();
| +++++++
User-Defined Type Conversions
As with other features of the language (Item 10), the ability to perform conversions between values of different user-defined types is encapsulated as a standard 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 as follows:
From<T>
: Items of this type can be built from items of typeT
, and the conversion always succeeds.TryFrom<T>
: Items of this type can be built from items of typeT
, but the conversion might not succeed.Into<T>
: Items of this type can be converted into items of typeT
, and the conversion always succeeds.TryInto<T>
: Items of this type can be converted into items of typeT
, but the conversion might not succeed.
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 Try...
trait definitions also require an associated type that gives the type of the error E
emitted for failure situations.
The first piece of advice is therefore to implement (just) the Try...
trait if it's possible for a
conversion to fail, in line with Item 4. The alternative is to ignore the possibility of error (e.g., with
.unwrap()
), but that needs to be a deliberate choice, and in most cases it's best to leave that choice to the caller.
The type conversion traits have an obvious symmetry: if a type T
can be transformed into
a type U
(via Into<U>
),
isn't that the same as it being possible to create an item of type U
by transforming from
an item of type T
(via
From<T>
)?
This is indeed the case, and it leads to the second piece of advice: implement the From
trait for
conversions. The Rust standard library had to pick just one of the two possibilities, in order to prevent the system
from spiraling around in dizzy circles,1 and it 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 generic 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 relevant
part of the standard library code too, which is a blanket trait implementation:
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>
".
The standard library also includes various implementations of these conversion traits for standard library types. As
you'd expect, there are From
implementations for integral conversions where the destination type includes all possible
values of the source type (From<u32> for u64
), and TryFrom
implementations when the source might not fit in the
destination (TryFrom<u64> for u32
).
There are also various other blanket trait implementations in addition to the Into
version previously shown; these are
mostly for smart pointer types, allowing the smart pointer to be automatically constructed from an instance of
the type that it holds. This means that generic methods that accept smart pointer parameters can also be called with
plain old items; more on this to come and in Item 8.
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 shown previously) any type that implements From
in the same
direction. In other words, if you can infallibly convert a T
into a U
, you can also fallibly obtain a U
from a
T
; as this conversion will always succeed, the associated error type is the helpfully named
Infallible
.2
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
}
}
Translated into words, this just says that “given a T
, I can get a T
.” That's such an obvious "well,
duh" that it's worth stopping to understand why this is useful.
Consider a simple newtype struct
(Item 6) and a function that operates on it (ignoring that this function would be
better expressed as a method):
#![allow(unused)] fn main() { /// 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));
// output: "IanaAllocated(1) reserved? false"
but even if From<u64>
is implemented for the newtype wrapper:
impl From<u64> for IanaAllocated {
fn from(v: u64) -> Self {
Self(v)
}
}
the function can't be directly invoked for u64
values:
if is_iana_reserved(42) {
// ...
}
error[E0308]: mismatched types
--> src/main.rs:77:25
|
77 | if is_iana_reserved(42) {
| ---------------- ^^ expected `IanaAllocated`, found integer
| |
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:7:8
|
7 | pub fn is_iana_reserved(s: IanaAllocated) -> bool {
| ^^^^^^^^^^^^^^^^ ----------------
help: try wrapping the expression in `IanaAllocated`
|
77 | if is_iana_reserved(IanaAllocated(42)) {
| ++++++++++++++ +
However, a generic version of the function that accepts (and explicitly converts) anything
satisfying Into<IanaAllocated>
:
pub fn is_iana_reserved<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(42) {
// ...
}
With this trait bound in place, the reflexive trait implementation of From<T>
makes more sense: it means that the
generic function copes with items that 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 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 8.
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 constitute a fairly limited set, and the only user-defined types it
includes are "C-like" enum
s (those that have just an associated integer value). General integral conversions are
included, though, giving an alternative to into()
:
#![allow(unused)] fn main() { let x: u32 = 9; let y = x as u64; let z: u64 = x.into(); }
The as
version also allows lossy conversions:3
#![allow(unused)] fn main() { 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
--> src/main.rs:136:20
|
136 | let y: u16 = x.into();
| ^^^^ the trait `From<u32>` is not implemented for `u16`
|
= help: the following other types implement trait `From<T>`:
<u16 as From<NonZeroU16>>
<u16 as From<bool>>
<u16 as From<u8>>
= note: required for `u32` to implement `Into<u16>`
For consistency and safety, you should prefer from
/into
conversions over as
casts, unless you
understand and need the precise casting
semantics (e.g., for C interoperability).
This advice can be reinforced by Clippy (Item 29), which includes several lints about as
conversions; however, these lints are
disabled by default.
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 coercions involve silent conversions of pointer and reference types in ways that are sensible and convenient for the programmer, such as converting the following:
- A mutable reference to an immutable reference (so you can use a
&mut T
as the argument to a function that takes a&T
) - A reference to a raw pointer (this isn't
unsafe
—the unsafety happens at the point where you're foolish enough to dereference a raw pointer) - A closure that happens to not capture any variables into a bare function pointer (Item 2)
- An array to a slice
- A concrete item to a trait object, for a trait that the concrete item implements
- An item lifetime to a "shorter" one (Item 14)4
There are only two coercions whose behavior can be affected by user-defined types. The first happens 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 8), 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 includes both a pointer to the item's location in memory and a pointer to the vtable for the concrete type's implementation of the trait—see Item 8.
For now—this is
likely to be replaced with the !
"never" type in a future
version of Rust.
Allowing lossy conversions in Rust was probably a mistake, and there have been discussions around trying to remove this behavior.
Rust refers to these conversions as "subtyping", but it's quite different from the definition of "subtyping" used in object-oriented languages.