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.