1.3 · AsRef, AsMut, Into, From: The Conversion Matrix
Domain 1 — Ownership Mechanics in the Standard Library Duration: ~15 minutes Library components:
std::convert::AsRef,std::convert::AsMut,std::convert::From,std::convert::Into,std::convert::TryFrom,std::convert::TryInto,std::convert::Infallible,std::convert::identity
Introduction
The type system of Rust is strict, and this is intentional. But a strict type system without conversions makes APIs difficult to use. The std::convert module has a family of traits that convert values between types safely. Each trait has clear semantics:
| Trait | Direction | Cost | Fallible? |
|---|---|---|---|
AsRef<T> | &Self → &T | Cheap (reference cast) | No |
AsMut<T> | &mut Self → &mut T | Cheap (reference cast) | No |
From<T> | T → Self | May allocate | No |
Into<T> | Self → T | May allocate | No |
TryFrom<T> | T → Result<Self, Err> | May allocate | Yes |
TryInto<T> | Self → Result<T, Err> | May allocate | Yes |
This tutorial explains:
- When to use each trait.
- The blanket implementations that connect the traits.
- How to design APIs that combine the traits and are easy to call.
AsRef: Cheap Reference Conversion
AsRef<T> means that a type can give you a &T at low cost. There is no allocation and no copy of the data. It is only a conversion between references.
// `?Sized` permits unsized targets such as `str`, `[T]`, and `Path`.
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
The standard library implements AsRef<Path> for &str, String, PathBuf, OsStr, and OsString. Thus a function that accepts impl AsRef<Path> accepts all of them.
When to use AsRef vs. Borrow
This selection confuses most Rust developers. The rule is:
AsRef<T>: Use it when you need a cheap reference conversion with no contract about hashing or equality. This is the common case for function parameters.Borrow<T>: Use it when the borrowed form must hash and compare identically to the original. This is the case forHashMapandHashSet.
An example of a violation is a string wrapper that compares without case sensitivity. The wrapper could implement AsRef<str> to give access to the underlying bytes. But it must not implement Borrow<str>. Its Eq implementation (case-insensitive) is different from the Eq implementation of str (case-sensitive). If you used the wrapper as a HashMap key through Borrow<str>, lookups would not operate correctly.
Use case: A path utility function that accepts anything path-like
use std::path::Path;
fn main() {
// Accepts each type that can give a &Path.
fn file_exists(path: impl AsRef<Path>) -> bool {
let p: &Path = path.as_ref(); // a reference conversion: no allocation
println!(" Checking path: {}", p.display());
p.exists()
}
// All of these calls compile:
println!("--- AsRef<Path> in action ---");
let _ = file_exists("/tmp"); // &str
let _ = file_exists(String::from("/tmp")); // String
let _ = file_exists(std::path::PathBuf::from("/tmp")); // PathBuf
// AsRef<str>: accepts each string-like type.
fn shout(text: impl AsRef<str>) {
println!(" {}", text.as_ref().to_uppercase());
}
println!("\n--- AsRef<str> ---");
shout("hello"); // &str
shout(String::from("world")); // String
shout(std::borrow::Cow::Borrowed("cow")); // Cow<str>
// A custom AsRef implementation for a wrapper type.
struct EmailAddress(String);
impl AsRef<str> for EmailAddress {
fn as_ref(&self) -> &str { &self.0 } // borrows the inner String as &str
}
println!("\n--- Custom AsRef implementation ---");
let email = EmailAddress("user@example.com".to_string());
shout(email.as_ref()); // passes the &str
}
01_12_asref_vs_borrow.rs prints these lines for the code above:
--- AsRef<Path> in action ---
Checking path: /tmp
Checking path: /tmp
Checking path: /tmp
--- AsRef<str> ---
HELLO
WORLD
COW
--- Custom AsRef implementation ---
USER@EXAMPLE.COM
AsMut
AsMut<T> is the mutable equivalent. It is less common, because conversions between mutable references are rarer. But it has the same pattern:
// Accepts each type that can give a &mut [u8], for example Vec<u8> or [u8; 4].
fn clear_buffer(buf: &mut impl AsMut<[u8]>) {
buf.as_mut().fill(0); // sets each byte to 0
}
From and Into: Infallible Value Conversions
From<T> defines how to make a Self from a value of type T. It is the conversion trait that you implement.
pub trait From<T>: Sized {
fn from(value: T) -> Self; // takes ownership of `value`
}
Into<T> is the opposite direction: it converts self into a T. You almost never implement Into directly, because the standard library has a blanket implementation:
// For each pair of types where U: From<T>, the type T gets Into<U>.
impl<T, U> Into<U> for T where U: From<T> {
fn into(self) -> U {
U::from(self)
}
}
Implement From. You get Into at no cost.
Use case: Custom type conversions and ergonomic APIs
fn main() {
// From implementations of the standard library.
println!("=== From conversions ===");
let s: String = String::from("hello"); // From<&str> for String
println!("String::from(\"hello\") = {s:?}");
let f: f64 = f64::from(42_i32); // From<i32> for f64
println!("f64::from(42i32) = {f}");
let wide: u32 = u32::from(255_u8); // From<u8> for u32 (lossless widening)
println!("u32::from(255u8) = {wide}");
// Into: the blanket implementation.
println!("\n=== Into (blanket impl) ===");
let s: String = "hello".into(); // the type annotation selects Into<String>
println!("\"hello\".into() = {s:?}");
let f: f64 = 42_i32.into(); // the type annotation selects Into<f64>
println!("42i32.into() = {f}");
// A custom From implementation in each direction.
#[derive(Debug)]
struct Celsius(f64);
#[derive(Debug)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
println!("\n=== Custom From implementation ===");
let boiling = Celsius(100.0);
let boiling_f: Fahrenheit = boiling.into(); // blanket Into: moves `boiling`
println!("Celsius(100.0) = {boiling_f:?}");
assert_eq!(boiling_f.0, 212.0);
let freezing_c = Celsius::from(Fahrenheit(32.0));
println!("Fahrenheit(32.0) = {freezing_c:?}");
assert_eq!(freezing_c.0, 0.0);
// Into in a function parameter: the caller passes a Celsius or a Fahrenheit.
fn set_temperature(temp: impl Into<Celsius>) {
let celsius: Celsius = temp.into();
println!(" Temperature set to {celsius:?}");
}
println!("\n=== Into in function parameters ===");
set_temperature(Celsius(25.0)); // already a Celsius: no conversion
set_temperature(Fahrenheit(98.6)); // converts through From<Fahrenheit>
}
01_13_from_into.rs prints these lines for the code above:
=== From conversions ===
String::from("hello") = "hello"
f64::from(42i32) = 42
u32::from(255u8) = 255
=== Into (blanket impl) ===
"hello".into() = "hello"
42i32.into() = 42
=== Custom From implementation ===
Celsius(100.0) = Fahrenheit(212.0)
Fahrenheit(32.0) = Celsius(0.0)
=== Into in function parameters ===
Temperature set to Celsius(25.0)
Temperature set to Celsius(37.0)
From for error conversion
One of the most important uses of From in real Rust code is the conversion of error types. The ? operator calls From::from() on the error. This call converts the error to the error type that the function returns.
fn main() {
// One application error type that contains two library error types.
#[derive(Debug)]
#[allow(dead_code)] // this example never makes an `Io` value
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
fn parse_port(s: &str) -> Result<u16, AppError> {
// `parse` returns Result<u16, ParseIntError>.
// On Err(e), `?` returns Err(AppError::from(e)) from the function.
let port: u16 = s.parse()?;
Ok(port)
}
println!("=== From for error conversion ===");
match parse_port("8080") {
Ok(p) => println!(" Parsed port: {p}"),
Err(e) => println!(" Error: {e:?}"),
}
match parse_port("not_a_number") {
Ok(p) => println!(" Parsed port: {p}"),
Err(e) => println!(" Error: {e:?}"),
}
}
01_13_from_into.rs prints these lines for the code above:
=== From for error conversion ===
Parsed port: 8080
Error: Parse(ParseIntError { kind: InvalidDigit })
This pattern is very common. Most Rust applications define a central error enum, with a From implementation for each error type that they get.
TryFrom and TryInto: Fallible Conversions
From and Into are for conversions that always succeed. When a conversion can fail (numeric narrowing, parsing, validation), use TryFrom and TryInto:
pub trait TryFrom<T>: Sized {
type Error; // the type of the value in the Err result
fn try_from(value: T) -> Result<Self, Self::Error>;
}
As for From and Into, there is a blanket implementation. Implement TryFrom, and you get TryInto at no cost.
Use case: Safe numeric narrowing
// The prelude of edition 2021 and later has these two traits. The imports are optional.
use std::convert::TryFrom;
use std::convert::TryInto;
fn main() {
println!("=== Numeric narrowing with TryFrom ===");
let big: i32 = 300;
let result: Result<u8, _> = u8::try_from(big); // 300 is larger than u8::MAX (255)
println!("u8::try_from(300i32) = {result:?}");
assert!(result.is_err());
let small: i32 = 42;
let result: Result<u8, _> = u8::try_from(small); // 42 fits in a u8
println!("u8::try_from(42i32) = {result:?}");
assert_eq!(result, Ok(42));
// TryInto comes from the blanket implementation, as Into does.
println!("\n=== TryInto ===");
let val: i64 = 1_000_000;
let narrowed: Result<u16, _> = val.try_into(); // the type annotation selects u16
println!("1_000_000i64.try_into::<u16>() = {narrowed:?}");
assert!(narrowed.is_err());
}
01_14_tryfrom_tryinto.rs prints these lines for the code above:
=== Numeric narrowing with TryFrom ===
u8::try_from(300i32) = Err(TryFromIntError(PosOverflow))
u8::try_from(42i32) = Ok(42)
=== TryInto ===
1_000_000i64.try_into::<u16>() = Err(TryFromIntError(PosOverflow))
Use case: Validated domain types
TryFrom is the idiomatic method to make types that enforce their invariants at construction time.
use std::convert::TryFrom;
fn main() {
// Invariant: the number in a Port is always 1024 or larger.
#[derive(Debug, PartialEq)]
struct Port(u16);
#[derive(Debug, PartialEq)]
enum PortError { Zero, SystemPort(u16) }
impl std::fmt::Display for PortError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PortError::Zero => write!(f, "port cannot be zero"),
PortError::SystemPort(p) => write!(f, "port {p} is a system port (< 1024)"),
}
}
}
impl TryFrom<u16> for Port {
type Error = PortError;
// The only code that makes a Port. It rejects each invalid number.
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
0 => Err(PortError::Zero),
1..=1023 => Err(PortError::SystemPort(value)),
_ => Ok(Port(value)), // 1024 to 65535
}
}
}
for &val in &[0_u16, 80, 443, 8080, 65535] {
let port = Port::try_from(val); // Result<Port, PortError>
println!(" Port::try_from({val:5}) = {port:?}"); // {val:5} pads val to 5 columns
}
assert_eq!(Port::try_from(0), Err(PortError::Zero));
assert_eq!(Port::try_from(80), Err(PortError::SystemPort(80)));
assert_eq!(Port::try_from(8080), Ok(Port(8080)));
}
01_14_tryfrom_tryinto.rs prints these lines for the code above:
Port::try_from( 0) = Err(Zero)
Port::try_from( 80) = Err(SystemPort(80))
Port::try_from( 443) = Err(SystemPort(443))
Port::try_from( 8080) = Ok(Port(8080))
Port::try_from(65535) = Ok(Port(65535))
With this pattern, when you have a Port, you know that it is valid. The type system enforces the invariant. You do not need to examine the number again at runtime in many locations of your code.
Infallible: The "Cannot Fail" Error Type
std::convert::Infallible is a type with no possible values: an empty enum. It is the error type for conversions that can never fail:
impl From<&str> for String {
// This conversion can never fail. Thus, as a TryFrom conversion,
// its error type is Infallible.
}
Each From implementation also gives a TryFrom implementation with Error = Infallible. The standard library has a blanket implementation for this:
// Each From impl gives an Into impl, so this impl applies to each From conversion.
impl<T, U> TryFrom<U> for T where U: Into<T> {
type Error = Infallible;
fn try_from(value: U) -> Result<Self, Self::Error> {
Ok(U::into(value)) // never returns Err
}
}
Thus you can always call .try_into(), also for an infallible conversion, and the result is always Ok. Infallible lets generic code with a TryFrom bound operate the same for fallible and infallible conversions.
The never type (!) is not fully stable in Rust 1.99. The standard library documentation states a plan to make Infallible an alias for ! when ! becomes stable.
as Casting: The Low-Level Conversion
Before TryFrom and Into existed, the as keyword was the only method to convert between primitive types. as is still useful, but it has risks.
as always succeeds at compile time. It never returns a Result. When a value does not fit in the target type, as silently does one of these conversions:
| Conversion | Behaviour |
|---|---|
Integer widening (u8 → u32) | Zero-extends (unsigned) or sign-extends (signed). The result is always exact. |
Integer narrowing (u32 → u8) | Truncates: 300u32 as u8 == 44 (300 % 256) |
| Signed to unsigned | Reinterprets the bits: -1i8 as u8 == 255 |
| Float to integer | Truncates toward zero: 3.9f64 as i32 == 3. NaN gives 0, and ∞ gives T::MAX. |
| Integer to float | Gives the nearest representable value (may lose precision for large integers) |
Use case: When as gives a wrong value and try_into() prevents it
fn main() {
println!("=== Integer truncation with `as` ===");
let big: u32 = 300;
let truncated = big as u8; // keeps the low 8 bits: 300 % 256 = 44
println!("300u32 as u8 = {truncated}");
assert_eq!(truncated, 44); // NOT 300 and NOT an error
// Safe alternative: returns Err and does not truncate silently.
let safe = u8::try_from(300u32);
println!("u8::try_from(300u32) = {safe:?}");
assert!(safe.is_err());
println!("\n=== Sign semantics with `as` ===");
let neg: i8 = -1; // bit pattern 0xFF
let as_u8 = neg as u8; // reads the same bits as an unsigned value: 255
println!("-1i8 as u8 = {as_u8}");
assert_eq!(as_u8, 255);
println!("\n=== Float to integer: truncation, not rounding ===");
let f: f64 = 3.9;
let i = f as i32; // truncates toward zero: gives 3, not 4
println!("3.9f64 as i32 = {i}");
assert_eq!(i, 3);
// NaN and infinity saturate in Rust 1.45+ (before 1.45, the cast was UB).
let nan = f64::NAN as i32; // NaN becomes 0
let inf = f64::INFINITY as i32; // +infinity becomes i32::MAX
println!("NAN as i32 = {nan}");
println!("INFINITY as i32 = {inf}");
assert_eq!(nan, 0);
assert_eq!(inf, i32::MAX);
println!("\n=== When to use `as` vs `try_into()` ===");
// GOOD: `as` for a widening conversion (a u8 always fits in a u32).
let byte: u8 = 200;
let wide: u32 = byte as u32;
println!("u8 as u32 (safe widen): {wide}");
assert_eq!(wide, 200);
// GOOD: `as` when you want the truncated bits on purpose.
let hash: u64 = 0xDEAD_BEEF_CAFE_1234;
let low_byte = hash as u8; // keeps only the low byte: 0x34
println!("hash as u8 (intentional mask): 0x{low_byte:02X}");
// PREFER try_into(): when the value may be out of range at runtime.
fn safe_index(v: &[i32], pos: i64) -> Option<i32> {
// A negative pos gives Err. Then `.ok()?` returns None from the function.
let idx: usize = pos.try_into().ok()?;
v.get(idx).copied() // None if idx is out of bounds
}
let data = vec![10, 20, 30];
println!("safe_index(data, 1) = {:?}", safe_index(&data, 1));
println!("safe_index(data, -1) = {:?}", safe_index(&data, -1));
}
01_16_as_casting.rs prints these lines for the code above:
=== Integer truncation with `as` ===
300u32 as u8 = 44
u8::try_from(300u32) = Err(TryFromIntError(PosOverflow))
=== Sign semantics with `as` ===
-1i8 as u8 = 255
=== Float to integer: truncation, not rounding ===
3.9f64 as i32 = 3
NAN as i32 = 0
INFINITY as i32 = 2147483647
=== When to use `as` vs `try_into()` ===
u8 as u32 (safe widen): 200
hash as u8 (intentional mask): 0x34
safe_index(data, 1) = Some(20)
safe_index(data, -1) = None
The rule: use as when you can prove at the call site that the value fits (widening conversions, intentional masking). Use try_into() when there is any doubt.
Recent TryFrom Stabilizations: char and bool
The standard library got two TryFrom implementations recently. They are useful to know.
TryFrom<char> for usize (stabilized in 1.94)
Rust 1.94 stabilized impl TryFrom<char> for usize. It converts a char to its Unicode scalar value (code point) as a usize. This is the fallible equivalent of c as u32. On a 16-bit platform, usize has the size of u16, and the conversion fails if the code point is larger than 65535. On 32-bit and 64-bit platforms, the conversion always succeeds.
fn main() {
let c = '7';
let code_point = usize::try_from(c); // Result<usize, _>
println!("usize::try_from('7') = {code_point:?}"); // Ok(55): U+0037, not 7
assert_eq!(code_point, Ok(55));
let emoji = '🦀'; // U+1F980 = 129_408
let crab_cp = usize::try_from(emoji);
println!("usize::try_from('🦀') = {crab_cp:?}");
assert_eq!(crab_cp, Ok(129_408));
// Practical use: a char as a hash bucket index.
fn char_bucket(c: char, buckets: usize) -> usize {
// unwrap_or(0) gives 0 if the code point does not fit in a usize.
usize::try_from(c).unwrap_or(0) % buckets
}
println!("bucket('A', 8) = {}", char_bucket('A', 8)); // 65 % 8 = 1
println!("bucket('a', 8) = {}", char_bucket('a', 8)); // 97 % 8 = 1
println!("bucket('Z', 8) = {}", char_bucket('Z', 8)); // 90 % 8 = 2
assert_eq!(char_bucket('🦀', 8), 129_408 % 8); // 0
}
01_14_tryfrom_tryinto.rs prints these lines for the code above:
usize::try_from('7') = Ok(55)
usize::try_from('🦀') = Ok(129408)
bucket('A', 8) = 1
bucket('a', 8) = 1
bucket('Z', 8) = 2
Note: '7' gives the code point 55 (its Unicode value), not 7. To convert a digit character to its numeric value, use c.to_digit(10), or subtract '0' as usize from the code point.
TryFrom<{integer}> for bool (stabilized in 1.95)
Rust 1.95 stabilized TryFrom implementations from each fixed-width integer type (i8–i128, u8–u128) to bool:
0converts tofalse.1converts totrue.- All other values fail with a
TryFromIntError.
These implementations do not include usize and isize. This conversion is the fallible equivalent of the lossy x != 0 idiom. It is useful when you parse binary formats or FFI flags. There, a value other than 0 or 1 indicates corrupted input, not a "true" value.
fn main() {
assert_eq!(bool::try_from(0_u8), Ok(false));
assert_eq!(bool::try_from(1_i32), Ok(true));
println!("bool::try_from(0u8) = {:?}", bool::try_from(0_u8));
println!("bool::try_from(1i32) = {:?}", bool::try_from(1_i32));
let invalid = bool::try_from(2_u64); // the error type is std::num::TryFromIntError
println!("bool::try_from(2u64) = {invalid:?}");
assert!(invalid.is_err());
// Check the flag bytes of a binary header: only 0 and 1 are valid.
let header = [0_u8, 1, 255];
for byte in header {
match bool::try_from(byte) {
Ok(flag) => println!("byte {byte:3} → flag {flag}"),
Err(e) => println!("byte {byte:3} → error: {e}"), // {e} prints the Display text
}
}
}
01_14_tryfrom_tryinto.rs prints these lines for the code above:
bool::try_from(0u8) = Ok(false)
bool::try_from(1i32) = Ok(true)
bool::try_from(2u64) = Err(TryFromIntError(PosOverflow))
byte 0 → flag false
byte 1 → flag true
byte 255 → error: number too large to fit in target type
Since Rust 1.99, the Display text of TryFromIntError tells you if the value is too large or too small. The text is "number too large to fit in target type" or "number too small to fit in target type". Earlier releases printed one generic message for the two cases.
convert::identity: The Typed No-Op
std::convert::identity<T>(x: T) -> T is a function that returns its argument unchanged. Its body is only { x }.
This trivial function is in the standard library because its type signature is useful. Some contexts need a fn(T) -> T:
Use case: Filtering out None values from an iterator
use std::convert::identity;
// identity is a const fn, so you can call it in a constant.
const ANSWER: i32 = identity(42);
const ZERO: usize = identity(0);
const MAX_RETRIES: usize = identity(3);
fn main() {
println!("=== Filtering None values with filter_map + identity ===");
let mixed: Vec<Option<i32>> = vec![Some(1), None, Some(3), None, Some(5)];
// filter_map calls the function on each item. It keeps x for Some(x) and drops None.
// The items are already Options, so identity is the function.
// (`.flatten()` gives the same result and is the form that Clippy recommends.)
let present: Vec<i32> = mixed.into_iter().filter_map(identity).collect();
println!("filter_map(identity): {present:?}");
assert_eq!(present, [1, 3, 5]);
// A higher-order function that needs a fn(i32) -> i32.
fn apply_transform(values: Vec<i32>, transform: fn(i32) -> i32) -> Vec<i32> {
values.into_iter().map(transform).collect()
}
println!("\n=== identity as a typed function pointer ===");
let numbers = vec![1, 2, 3, 4, 5];
let doubled = apply_transform(numbers.clone(), |n| n * 2); // a real transform
println!("doubled: {doubled:?}");
let unchanged = apply_transform(numbers.clone(), identity); // no transform
println!("unchanged: {unchanged:?}");
assert_eq!(doubled, [2, 4, 6, 8, 10]);
assert_eq!(unchanged, numbers);
println!("\n=== identity is a const fn ===");
println!("const ANSWER = {ANSWER}");
println!("const ZERO = {ZERO}, const MAX_RETRIES = {MAX_RETRIES}");
}
01_17_convert_identity.rs prints these lines for the code above:
=== Filtering None values with filter_map + identity ===
filter_map(identity): [1, 3, 5]
=== identity as a typed function pointer ===
doubled: [2, 4, 6, 8, 10]
unchanged: [1, 2, 3, 4, 5]
=== identity is a const fn ===
const ANSWER = 42
const ZERO = 0, const MAX_RETRIES = 3
You will not use identity frequently. But when an API takes a fn(T) -> T and you need a function that returns its argument unchanged, identity is the function to use. Its name also documents the intent.
Designing APIs with Conversion Traits
These are the patterns, in the order of how frequently you should use them:
Pattern 1: impl AsRef<T> for read-only access
Use this pattern when you only read the data and do not need ownership. This is the cheapest option: it never allocates.
// Accepts each type that implements AsRef<Path>: &str, String, PathBuf, &Path.
fn print_extension(path: impl AsRef<std::path::Path>) {
let p = path.as_ref(); // &Path
match p.extension() { // Option<&OsStr>
Some(ext) => println!(" {}: extension = {}", p.display(), ext.to_string_lossy()),
None => println!(" {}: no extension", p.display()),
}
}
// print_extension("photo.jpg") prints " photo.jpg: extension = jpg".
// print_extension("Makefile") prints " Makefile: no extension".
Pattern 2: impl Into<T> for taking ownership
Use this pattern when you must store the data. The caller can pass borrowed data, which the function converts. Or the caller can pass owned data, which moves into the function at low cost.
struct LogEntry {
level: String,
message: String,
}
impl LogEntry {
// Accepts each type that converts to a String, for example &str and String.
fn new(level: impl Into<String>, message: impl Into<String>) -> Self {
LogEntry {
level: level.into(), // a &str allocates here, a String moves
message: message.into(),
}
}
}
// &str arguments: each `into()` call allocates a String.
let e1 = LogEntry::new("INFO", "server started");
// String arguments: each String moves into the struct.
let e2 = LogEntry::new(String::from("ERROR"), format!("port {} in use", 8080));
Pattern 3: Do not over-generalize
A common mistake is to use impl Into<T> on many parameters. This increases monomorphization and compile times. The recommendation:
- 1–2 parameters:
impl Into<T>is acceptable. - 3+ parameters: Consider concrete types or a builder pattern.
- Read-only access: Use
AsRef<T>, notInto<T>.
fn main() {
// Good: AsRef to read, Into to store.
fn process(
input: impl AsRef<str>, // read-only: no allocation
label: impl Into<String>, // needs ownership: the type of the argument sets the cost
) {
let _text = input.as_ref(); // &str
let _label = label.into(); // String
}
process("hello", "label"); // &str for the two parameters: label allocates
process("hello", String::from("x")); // label moves: `process` does not allocate
}
Figure: The Conversion Trait Matrix
Overview: How the Blanket Implementations Connect
Three blanket implementations connect the conversion traits:
From<T>→Into<T>: ImplementFrom, and you getIntoat no cost.TryFrom<T>→TryInto<T>: ImplementTryFrom, and you getTryIntoat no cost.From<T>→TryFrom<T>: Each infallible conversion is also a fallible conversion that always returnsOk.
Thus you only need to implement From (for an infallible conversion) or TryFrom (for a fallible conversion). The blanket implementations supply the other traits automatically.
Figure: Blanket Implementation Chain — Implement From, Get Everything
There is one more blanket implementation: each type implements From<T> for T (the identity conversion). Thus String::from(already_a_string) compiles, and let s: &str = "hello".into(); compiles too. They are no-ops, and the compiler removes them during optimization.
Summary
| When you need... | Use... | You implement... |
|---|---|---|
A cheap &T from &Self | AsRef<T> | AsRef<T> |
A cheap &mut T from &mut Self | AsMut<T> | AsMut<T> |
| An infallible owned conversion | Into<T> | From<T> (the blanket implementation gives you Into) |
| A fallible owned conversion | TryInto<T> | TryFrom<T> (the blanket implementation gives you TryInto) |
Key lookups in a HashMap or HashSet | Borrow<T> | Borrow<T> (with the hash/eq contract) |
The conversion traits are the Rust alternative to function overloading. You do not write many function signatures. You write one function that accepts impl Into<T> or impl AsRef<T>. The type of the caller's argument selects the conversion, if a conversion is necessary. The cost is always visible:
AsRefhas no cost.FromandIntomay allocate.TryFromandTryIntomay fail.
Code Examples
| File | Description |
|---|---|
01_12_asref_vs_borrow.rs | AsRef and Borrow: when to use each one, and a custom AsRef |
01_13_from_into.rs | From and Into: the blanket implementation, custom types, and error conversion |
01_14_tryfrom_tryinto.rs | TryFrom and TryInto: numeric narrowing, validated types, TryFrom<char> (1.94), and TryFrom<{integer}> for bool (1.95) |
01_15_conversion_api_design.rs | How to combine conversion traits in the design of a public API |
01_16_as_casting.rs | as casting: truncation, sign semantics, float to integer, and when to use try_into() |
01_17_convert_identity.rs | convert::identity: filter_map, a typed fn(T) -> T no-op, and const contexts |