Step 4 of 7
Conversions and flexible parameters: From, Into, AsRef
Good Rust APIs are pleasant to call: they accept what callers naturally have and convert it without fuss. Three traits do most of the work.
From and Into
Implement From<A> for B and you get both B::from(a) and a.into() (the standard library has a blanket impl Into<B> for A whenever B: From<A>). So you always implement From, never Into (clippy's from_over_into says so). Use it for conversions that can't fail and are unsurprising; for fallible ones, implement TryFrom (and get try_into()).
impl Into<String> for values you store
A constructor that stores a String should let callers pass a &str or a String they already own (without a forced extra copy):
struct Player {
name: String,
tags: Vec<String>,
}
impl Player {
fn new(name: impl Into<String>) -> Self {
Player { name: name.into(), tags: Vec::new() }
}
fn tag(mut self, t: impl Into<String>) -> Self {
self.tags.push(t.into());
self
}
}
fn longest<S: AsRef<str>>(words: &[S]) -> Option<&str> {
words.iter().map(|w| w.as_ref()).max_by_key(|w| w.len())
}
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let owned = String::from("Ferris");
let a = Player::new("Corro").tag("mod");
let b = Player::new(owned).tag(String::from("crab"));
println!("{} {:?} / {} {:?}", a.name, a.tags, b.name, b.tags);
println!("{:?}", longest(&["a", "abc", "ab"]));
println!("{:?}", longest(&[String::from("crab"), String::from("lobster")]));
let f: Fahrenheit = Celsius(100.0).into();
println!("{}", f.0);
}
Corro ["mod"] / Ferris ["crab"]
Some("abc")
Some("lobster")
212
Piece by piece:
name: impl Into<String>accepts anything convertible into aString. With a&str,.into()allocates the copy it must; with an ownedString, it's just moved in, no copy at all.S: AsRef<str>accepts anything that can be viewed as a&strwithout converting:&[&str]and&[String]both work, as you see.AsRefborrows;Intoconverts (and may consume).Celsius(100.0).into()works because ofFrom<Celsius> for Fahrenheit; the type annotationlet f: Fahrenheittellsintowhich target to use.- You've met
AsRef<Path>already without noticing:std::fs::read_to_string(path)takesP: AsRef<Path>, which is why a&str, aStringor aPathBufall work.
Which parameter type?
| You need to | Take |
|---|---|
| read text | &str (a &String coerces to it automatically) |
| read a list | &[T], not &Vec<T> |
| store a string | impl Into<String> (or just String) |
| read several strings of either kind | &[S] with S: AsRef<str> |
| open a file path | impl AsRef<Path> |
| consume any sequence | impl IntoIterator<Item = T> (next step) |
Don't over-generalize: &str is the right answer most of the time, and every generic parameter makes signatures harder to read and compile times longer. Reach for these when callers really have different types.
Common mistakes
&String parameters are a classic review comment (clippy: ptr_arg): they force callers to have a String, while &str accepts both.
fn greet(name: &String) -> String {
format!("Hello, {name}!")
}
fn main() {
println!("{}", greet("Ana"));
}
error[E0308]: mismatched types
impl Into<String>on a parameter you only read: it allocates for nothing. Take&str.let x = value.into();with no type to aim at:error[E0283]: type annotations needed. Annotate the variable or callTarget::from(value).
Your task
Tags on articles. Implement From<&str> for Tag (trim and lowercase the text) and From<Tag> for String. Write fn normalize_all<S: AsRef<str>>(items: &[S]) -> Vec<Tag> (works for &[&str] and &[String]), and give Article a constructor new(title: impl Into<String>) and a chainable with_tag(mut self, tag: impl Into<Tag>) -> Self.
Challenge 2
Colors. Implement From<(u8, u8, u8)> for Rgb and From<Rgb> for String (lowercase hex, #ff8000), and TryFrom<&str> for Rgb that parses #rrggbb (exactly 7 characters, a #, then hex digits) with type Error = String and the error invalid color "red".
Challenge 3
Write fn total_size<P: AsRef<Path>>(paths: &[P]) -> std::io::Result<u64> that adds up the sizes in bytes of the given files (std::fs::metadata(path)?.len()). Thanks to AsRef<Path> it must accept &[&str], Vec<String> and Vec<PathBuf>. A missing file returns the I/O error. (The tests run in a folder containing a.txt with 5 bytes and b.txt with 11.)
Previous: Make invalid states unrepresentable Next: RAII guards and Drop