Rust Conversion Traits
Conversion traits are the traits behind spellings like .into() and String::from(...). A library author implements the right one, and every caller then gets the friendly spelling for free.
Implement From; Into arrives free
impl From<Minutes> for Seconds {
fn from(m: Minutes) -> Seconds { Seconds(m.0 * 60) }
}
let s: Seconds = Minutes(2).into(); // no Into impl was writtenFallible conversions use TryFrom
impl TryFrom<u32> for Percent {
type Error = TooBig;
// try_from returns Err(TooBig) for any value above 100
}AsRef: one function, many inputs
File::open accepts AsRef<Path> for the same reason: &str, String and PathBuf all qualify.
fn greet(name: impl AsRef<str>) {
println!("hello, {}", name.as_ref());
}
greet("Ferris"); // a &str works
greet(String::from("Ferris")); // and so does a StringCollections: FromIterator and Extend
With these two, callers can collect() into your collection. They can add to it later. These are the two moves every std collection offers.
impl FromIterator<u32> for Bag {
// from_iter builds a Bag from any iterator of u32
}
// callers: let bag: Bag = items.into_iter().collect();Serde support hides behind a feature
If a type can be serialized, gate it. The feature is named exactly serde. Users who do not need it pay nothing.
// Cargo.toml: serde = { version = "1", optional = true }
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct Point { x: i32, y: i32 }AsMut: the mutable twin
Give one type both traits. Generic code can then read it through AsRef. It can rewrite it through AsMut. Here a Label lets other code shout without ever touching the String inside:
struct Label(String);
impl AsRef<str> for Label {
fn as_ref(&self) -> &str { &self.0 }
}
impl AsMut<str> for Label {
fn as_mut(&mut self) -> &mut str { &mut self.0 }
}
let mut label = Label(String::from("fragile"));
label.as_mut().make_ascii_uppercase();
println!("label = {}", label.as_ref());Example
use std::convert::TryFrom;
struct Minutes(u32);
struct Seconds(u32);
impl From<Minutes> for Seconds {
fn from(m: Minutes) -> Seconds {
Seconds(m.0 * 60)
}
}
#[derive(Debug)]
struct Percent(u8);
#[derive(Debug)]
struct TooBig;
impl TryFrom<u32> for Percent {
type Error = TooBig;
fn try_from(v: u32) -> Result<Percent, TooBig> {
if v <= 100 {
Ok(Percent(v as u8))
} else {
Err(TooBig)
}
}
}
fn main() {
let s: Seconds = Minutes(3).into();
println!("3 minutes = {} seconds", s.0);
let p = Percent::try_from(80);
println!("80 -> {:?}", p);
let q = Percent::try_from(140);
println!("140 -> {:?}", q);
}Key points
- One implemented trait gives every caller the short spelling: .into() works because From exists.
- Never implement Into or TryInto yourself.
- std blanket-implements them from From and TryFrom.
- A hand-written Into would collide with the blanket one.
- From is only for conversions that always succeed.
- It cannot return an error.
- A conversion that might fail belongs in TryFrom, like u32 into u16, which can overflow.
- A public error type should also be Send and Sync.
- Then it can cross threads.
- It can also be stored inside other errors.
- Generic reader and writer functions take R: Read and W: Write by value.
- A caller who must keep their file passes &mut file instead.
- That reference also implements Read.
- AsMut is AsRef's mutable twin.
- Implement it when generic code should be allowed to change what is inside.
- Read-only access is not enough.
- Reach for the From trait when the conversion should work everywhere — with .into(), with ?, and in other people's generic code.
In this example, the two traits split the work by honesty. Minutes into Seconds can never fail, so it is a From — and because From exists, main can write Minutes(3).into() without anyone implementing Into. Percent is different. A u32 above 100 is not a percent. So the conversion is a TryFrom with its own error type, TooBig. 80 converts and prints Ok(Percent(80)). 140 is refused and prints Err(TooBig). The caller sees the split in the return type before running anything.
Note
Give your own type a to_/as_/into_ method for a conversion only that type needs.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Your conversion works for most inputs but must refuse a few. Which trait do you implement?