What is the matter with `AsRef`?
the AsRef
trait is everywhere in the std lib and very handy. However, the benefit using it is maybe not too obvious.
But read on, and you will see some useful examples and where to apply it.
This is the 2nd post on the series "practical rust bites" that shows very tiny pieces of rust taken out of practical real projects. It aims to illustrate only one rust idiom or concept at a time with very practical examples.
Background
AsRef
can be found in the std::convert
module and is used
for cheap reference-to-reference conversion.
It's a very essential trait widely used in the std library and helps with seamless reference conversions from one type to another. Often it is at play, and you do not even realize it is there.
Example: String
or &str
Did you ever have had a function where you wanted to accept a string as a parameter?
You might then also have asked yourself should you accept a string reference (as in &str
) or an owned string (as in String
)?
So you would have something like this in mind:
fn take_a_str(some: &str) {}
and the other one:
fn take_a_string(some: String) {}
So why not have both?!
At this very point AsRef<str>
comes in very handy,
because both types str
and String
implement this trait.
fn take_a_str(some: impl AsRef<str>) {
let some = some.as_ref();
println!("{some}");
}
fn main() {
take_a_str("str");
take_a_str("String".to_string());
// also `&String` is supported:
let string_ref = "StringRef".to_string();
take_a_str(&string_ref);
}
check out the code on the playground
As you can see this version of take_a_str
accepts a type that just implements AsRef<str>
.
By doing so some: impl AsRef<str>
does not make any concrete assumptions on the type that is provided.
Instead, it just insists that any given type only implements this trait.
Example: wrapper type
In this example we want to focus on how can you implement AsRef
yourself for any arbitrary struct.
Let's look at this tiny program:
pub struct Envelope {
letter: String
}
fn main() {
let a_letter = Envelope {
letter: "a poem".to_string()
};
println!("this is a letter: {}", &a_letter);
}
But of course you'd say now, well, it does not implement Display
and so does the rust compiler agree with you:
error[E0277]: `Envelope` doesn't implement `std::fmt::Display`
--> src/main.rs:10:38
|
10 | println!("this is a letter: {}", &a_letter);
| ^^^^^^^^^ `Envelope` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Envelope`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
But we ignore Display
for now and focus on another use case. That is we want to access the inner value of Envelope
.
Solving this problem with implementing AsRef<str>
we would do the following:
impl AsRef<str> for Envelope {
fn as_ref(&self) -> &str {
// here we up-call to the `AsRef<str>` implementation for String
self.letter.as_ref()
}
}
and with this we need just one little adjustment:
fn main() {
let a_letter = Envelope {
letter: "a poem".to_string()
};
println!("this is a letter: {}", a_letter.as_ref());
}
Now the rust compiler does not complain anymore.
Again, of course it would be appropriate to implement Display
for this very println!
case, but the point is here that we would want to access the inner data as reference.
check out the full example on the playground
Example: a composed type
I have to admit, take this example with a grain of salt. It's very likely that this kind of usage would lead to problems with bigger structs. So as a rule of thumb if a struct has more than 2 fields, better not go down this path.
let's look at a simple struct that represents a weight:
struct Weight {
weight: f32,
unit: String
}
impl Weight {
/// Weight in Tons that is 157.47 stones
pub fn from_tons(weight: f32) -> Self {
Self { weight, unit: "t".to_string() }
}
/// Weight in Stones
pub fn from_stones(weight: f32) -> Self {
Self { weight, unit: "st".to_string() }
}
}
As you can see we have not given pub
fields and also no getter accessor functions.
So how we can actually get our hand on the data inside?
You've guessed it, AsRef
is our friend here as well.
impl AsRef<str> for Weight {
fn as_ref(&self) -> &str {
&self.unit
}
}
impl AsRef<f32> for Weight {
fn as_ref(&self) -> &f32 {
&self.weight
}
}
So here we use the AsRef
trait for the types f32
and str
to get access to the weight and unit inside the struct.
fn main() {
let a_ton = Weight::from_tons(1.3);
let tons: &f32 = a_ton.as_ref();
let unit: &str = a_ton.as_ref();
println!("a weight of {tons} {unit}");
}
you could also skip the variable bindings and make it less verbose:
fn main() {
let a_ton = Weight::from_tons(1.3);
println!(
"a weight of {} {}",
a_ton.as_ref() as &f32,
a_ton.as_ref() as &str
);
}
checkout the full example on the playground
Wrap up
AsRef
is a very handy tool and if you go through the standard library, you will find it implemented for a lot of types.
By this a lot of reference types become interchangeable and this increases the overall ergonomics a lot.
As you also have seen how AsRef
can be useful to get access to inner data of structs without having to provide accessory methods or public fields.
A very related function to get the inner value of a struct, but as value (not as reference) is the .into_inner().
As you can see it is used a lot in the std lib, but it is not bound to a trait. So this is more of a convention: Whenever you need to consume a struct and unfold the inner wrapped type, then .into_inner()
is the common schema to go for.
Versions used for this post:
$ cargo --version && rustc --version
cargo 1.61.0 (a028ae42f 2022-04-29)
rustc 1.61.0 (fe5b13d68 2022-05-18)
To receive updates about new blog post and other rust related news you can follow me on twitter.