🎓 A1Learner
🔍

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 written

Fallible 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 String

Collections: 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?