Conversion traits
Several traits you have already been using without naming them are just ordinary traits in std. Converting between types is done with traits, not with cast operators or constructors-by-convention. The big four are From/Into, FromStr, TryFrom/TryInto, and AsRef.
From<T> and Into<T>
From<T> defines "build a Self from a T". You implement From, and you get Into for free — the standard library has a blanket impl impl<T, U> Into<U> for T where U: From<T>. So implement one direction, get both call styles.
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)
}
}
let f = Fahrenheit::from(Celsius(100.0)); // From
let f: Fahrenheit = Celsius(100.0).into(); // Into, no extra impl neededYou have already seen this everywhere:
let s = String::from("x"); // From<&str> for String
let s: String = "x".into(); // the same conversion, Into side"x".into() is not magic syntax; it is a call to Into::into, resolved to the From<&str> for String impl. The reason .into() sometimes "knows" what to build is type inference from the target (let s: String = ..., a function parameter, a struct field).
The ? operator also leans on From. When you write expr? and the error types differ, ? calls From::from on the error to convert it into your function's declared error type. That is why Box<dyn Error> works as a catch-all return type (module 10): std provides From<E> for Box<dyn Error> for every error type, so every ? site converts automatically.
In C#: There is no direct From/Into analog. The closest is a user-defined implicit conversion operator (public static implicit operator Fahrenheit(Celsius c)), but that is per-type ad-hoc and not a shared interface you can constrain on. From/Into are real traits, so generic code can demand T: Into<String> the way C# code would demand an interface.
FromStr and .parse()
FromStr defines "parse a Self out of a &str". You rarely call FromStr::from_str directly; you call .parse(), which is the ergonomic wrapper, and the ::<T> turbofish (or a type annotation) tells it which type to produce.
let n: i32 = "42".parse().unwrap(); // FromStr for i32
let n = "42".parse::<i32>().unwrap(); // same, turbofish chooses the typeThe key difference from C#: parsing returns a Result, never an out-parameter. There is no two-method split like Parse vs TryParse.
In C#: int.Parse("42") throws on failure; int.TryParse("42", out var n) returns a bool and writes through an out parameter. Rust collapses both into one method whose Result carries either the value or the parse error — handle it with match, ?, .unwrap(), or .ok(). The error type lives in the impl (impl FromStr for i32 { type Err = ParseIntError; ... }).
TryFrom<T> and TryInto<T>
From is for conversions that cannot fail. When a conversion can fail, use TryFrom<T>, which returns Result<Self, Self::Error>. Implement TryFrom and you get TryInto for free, exactly like the From/Into pairing.
struct Percentage(u8);
impl TryFrom<i32> for Percentage {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if (0..=100).contains(&value) {
Ok(Percentage(value as u8))
} else {
Err(format!("{value} is out of range 0..=100"))
}
}
}
let p = Percentage::try_from(50)?; // TryFrom + ? (early-returns the Err)
let p: Percentage = 50i32.try_into()?; // TryInto side, also `?`-friendlyThis is the deeper version of the ?-uses-From insight. ? lifts one error type into another by calling a fallible-ish conversion, and TryFrom is the trait that models "this conversion might fail, here is the error." When module 27 uses thiserror's #[from] attribute, it is generating exactly a From<SourceError> for YourError impl so that ? can do the lift for you — #[from] is sugar over the conversion trait, nothing more.
AsRef<T>
AsRef<T> is a cheap, infallible reference-to-reference view: given &self, hand back a &T without allocating or copying. The classic use is accepting "anything that can be viewed as a &str" so callers can pass &str, String, or &String interchangeably.
fn shout(text: impl AsRef<str>) {
println!("{}!", text.as_ref().to_uppercase());
}
shout("hi"); // &str
shout(String::from("hi")); // String
shout(&String::from("hi")); // &Stringimpl AsRef<str> in the signature is the idiomatic way to make a function flexible about its string-ish input without forcing the caller to convert first.
There is also Borrow for the related but stricter case of hashing/ordering equivalence (e.g. looking up a String key in a map with a &str). We will not cover it here — just know the name when you meet it.
In C#: There is no single equivalent. IConvertible (Convert.ToInt32, etc.) is the nearest "convert between built-ins" interface, but it is runtime-dispatched and unrelated to references. AsRef overlaps with overloading a method on string/ReadOnlySpan<char>, or accepting an interface — but AsRef is a zero-cost view, not a conversion or copy.
Key takeaway
Conversions are traits. Implement From for infallible conversions and get Into (and ?-friendly error lifting) for free; the String::from("x") / "x".into() pair you have used all along is exactly this. Use TryFrom (returns Result) for fallible conversions — the same mechanism behind thiserror's #[from]. Use .parse() (backed by FromStr) instead of C#'s Parse/TryParse split, and take impl AsRef<str> to accept any string-like argument cheaply.