As Rust is a strongly typed language, all type conversions must be performed explicitly in the code. As Rust has a rich type system (programming logic and semantics are mostly expressed in types rather than in values), type conversions are inevitable in almost every single line of code. Fortunately, Rust offers well-designed type conversion capabilities, which are quite ergonomic, intuitive and are pleasant to use.
Value-to-value conversion in Rust is done with From
and Into
mirrored traits (implementing one automatically implements another one). These traits provide non-fallible conversion.
If your conversion may fail, then you should use TryFrom
/TryInto
analogues, which allow failing in a controlled way.
let num: u32 = 5;
let big_num: u64 = num.into();
let small_num: u16 = big_num.try_into().expect("Value is too big");
Note, that all these traits consume ownership of a passed value. However, they can be implemented for references too if you're treating a reference as a value.
For better understanding From
/Into
and TryFrom
/TryInto
purpose, design, limitations and use cases read through:
- Rust By Example: 6.1. From and Into
- Official
From
docs - Official
Into
docs - Official
TryFrom
docs - Official
TryInto
docs
Quite often you don't want to consume ownership of a value for conversion, but rather to refer it as another type. In such case AsRef
/AsMut
should be used. They allow to do a cheap non-fallible reference-to-reference conversion.
let string: String = "some text".into();
let bytes: &[u8] = string.as_ref();
AsRef
/AsMut
are commonly implemented for smart pointers to allow referring a data behind it via regular Rust references.
For better understanding AsRef
/AsMut
purpose, design, limitations and use cases read through:
- Official
AsRef
docs - Official
AsMut
docs - Ricardo Martins: Convenient and idiomatic conversions in Rust
Difference from Borrow
Novices in Rust are often confused with the fact that AsRef
/AsMut
and Borrow
/BorrowMut
traits have the same signatures, because it may not be clear which trait to use or implement for their needs.
See explanation in Borrow
trait docs:
Further, when providing implementations for additional traits, it needs to be considered whether they should behave identical to those of the underlying type as a consequence of acting as a representation of that underlying type. Generic code typically uses
Borrow<T>
when it relies on the identical behavior of these additional trait implementations. These traits will likely appear as additional trait bounds.In particular
Eq
,Ord
andHash
must be equivalent for borrowed and owned values:x.borrow() == y.borrow()
should give the same result asx == y
.If generic code merely needs to work for all types that can provide a reference to related type
T
, it is often better to useAsRef<T>
as more types can safely implement it.
And another one in AsRef
trait docs:
- Unlike
AsRef
,Borrow
has a blanket impl for anyT
, and can be used to accept either a reference or a value.Borrow
also requires thatHash
,Eq
andOrd
for a borrowed value are equivalent to those of the owned value. For this reason, if you want to borrow only a single field of a struct you can implementAsRef
, but notBorrow
.
So, as a conclusion:
AsRef
/AsMut
means that the implementor type may be represented as a reference to the implemented type. More like one type contains another one, or is just generally reference-convertible to the one.Borrow
/BorrowMut
means that the implementor type is equivalent to the implemented type in its semantics, differing only in how its data is stored. More like one type is just a pointer to another one.
For example, it's natural for an UserEmail
type to implement Borrow<str>
, so it may be easily consumed in the code accepting &str
(converted to &str
), as they're semantically equivalent regarding Hash
, Eq
and Ord
. And it's good for some execution Context
to implement AsRef<dyn Repository>
, so it can be extracted and used where needed, without using the whole Context
.
For better understanding AsRef
/Borrow
differences, read through:
AsRef
/AsMut
are able to do only outer-to-inner reference conversion, but obviously not the opposite.
struct Id(u8);
impl AsRef<u8> for Id {
fn as_ref(&self) -> &u8 {
&self.0
}
}
impl AsRef<Id> for u8 {
fn as_ref(&self) -> &Id {
&Id(*self)
}
}
error[E0515]: cannot return reference to temporary value
--> src/lib.rs:11:9
|
11 | &Id(*self)
| ^---------
| ||
| |temporary value created here
| returns a reference to data owned by the current function
However, there is nothing wrong with such conversion as long as memory layout of the inner type is the same for the outer type.
#[repr(transparent)]
struct Id(u8);
impl AsRef<Id> for u8 {
fn as_ref(&self) -> &Id {
unsafe { mem::transmute(self) }
}
}
That's exactly what ref-cast
crate checks and does, without necessity of writing unsafe
explicitly. See crate's documentation for more explanations.
Deref
/DerefMut
standard library trait allows to implicitly coerce from a custom type to a reference when dereferencing (operator *v
) is used. The most common example of this is using Box<T>
where &T
is expected.
fn hello(name: &str) {
println!("Hello, {}!", name);
}
let m = Box::new(String::from("Rust"));
hello(&m);
For better understanding Deref
purpose, design, limitations and use cases read through:
- Rust Book: 15.2. Treating Smart Pointers Like Regular References with the Deref Trait
- Official
Deref
docs - Deref vs AsRef vs Borrow vs Cow
The implicit coercion that Rust implements for Deref
is a sweet honey pot which may lead you to misuse of this feature.
The common temptation is to use Deref
in a combination with newtype pattern, so you can use your inner type via outer type without any explicit requirements. However, this is considered to be a bad practice, and official Deref
docs clearly states:
Deref
should only be implemented for smart pointers.
The wider explanation of this bad practice is given in this SO answer and Deref
polymorphism anti-pattern description.
For casting between types the as
keyword is used in Rust.
fn average(values: &[f64]) -> f64 {
let sum: f64 = sum(values);
let size: f64 = len(values) as f64;
sum / size
}
However, it supports only a small, fixed set of transformations, and is not idiomatic to use when other conversion possibilities are available (like From
, TryFrom
, AsRef
).
See also:
Estimated time: 1 day
Implement the following types:
EmailString
- a type, which value can be only a valid email address string.Random<T>
- a smart pointer, which takes 3 values of the pointed-to type on creation and points to one of them randomly every time is used.
Provide conversion and Deref
implementations for these types on your choice, to make their usage and interoperability with std
types easy and ergonomic.
Write simple tests for the task.
After completing everything above, you should be able to answer (and understand why) the following questions:
- How value-to-value conversion is represented in Rust? What is relation between fallible and infallible one?
- How reference-to-reference conversion is represented in Rust? How its traits differ? When and which one should be used?
- How can inner-to-outer reference conversion be achieved in Rust? Which prerequisites does it have?
- What is dereferencing in Rust? How it can be abused? Why it shouldn't be abused?
- Why using
as
keyword is not a good practice in Rust? Why do we still use it?