본문으로 건너뛰기

Rust Conversion Traits

Overview

Rust uses several standard traits for conversion. The right trait depends on ownership, cost, and whether the conversion can fail.

Common groups:

  • owned infallible conversion: From and Into
  • owned fallible conversion: TryFrom and TryInto
  • borrowed view conversion: AsRef and AsMut
  • borrowed key-compatible view conversion: Borrow and BorrowMut
  • string formatting and parsing: Display, ToString, FromStr, and parse

From and Into

From<T> for U defines how to create a U from a T.

let value = String::from("hello");

Implement From when the conversion is:

  • infallible
  • lossless in the semantic sense
  • value-preserving
  • obvious

Example:

struct UserId(u64);

impl From<u64> for UserId {
fn from(value: u64) -> Self {
UserId(value)
}
}

let user_id = UserId::from(42);

When From<T> for U exists, Rust provides Into<U> for T automatically.

let user_id: UserId = 42.into();

Prefer implementing From instead of implementing Into directly. Use Into mostly in call sites or generic bounds.

fn create_user_id<T>(value: T) -> UserId
where
T: Into<UserId>,
{
value.into()
}

Use a named constructor instead of From when the conversion has multiple reasonable meanings.

struct U32Bytes([u8; 4]);

impl U32Bytes {
fn from_be_bytes(value: u32) -> Self {
Self(value.to_be_bytes())
}

fn from_le_bytes(value: u32) -> Self {
Self(value.to_le_bytes())
}
}

TryFrom and TryInto

TryFrom<T> for U defines a conversion that can fail.

use std::convert::TryFrom;

let value = u8::try_from(255_u16);
assert_eq!(value, Ok(255));

let overflow = u8::try_from(256_u16);
assert!(overflow.is_err());

For custom types, use an explicit error type.

use std::convert::TryFrom;

#[derive(Debug, PartialEq, Eq)]
struct Percent(u8);

#[derive(Debug, PartialEq, Eq)]
struct PercentError;

impl TryFrom<u8> for Percent {
type Error = PercentError;

fn try_from(value: u8) -> Result<Self, Self::Error> {
if value <= 100 {
Ok(Self(value))
} else {
Err(PercentError)
}
}
}

As with From, prefer implementing TryFrom. Rust then provides the matching TryInto implementation.

use std::convert::TryInto;

let percent: Percent = 80_u8.try_into()?;

Use TryInto in generic bounds when callers may have a type that only exposes TryInto.

fn normalize<T>(value: T) -> Result<Percent, T::Error>
where
T: TryInto<Percent>,
{
value.try_into()
}

AsRef and AsMut

AsRef<T> is for cheap shared reference conversion.

use std::path::Path;

fn print_path<P>(path: P)
where
P: AsRef<Path>,
{
println!("{}", path.as_ref().display());
}

print_path("Cargo.toml");
print_path(Path::new("src/main.rs"));

Use AsRef when the function only needs a borrowed view and should accept multiple caller-owned forms.

AsMut<T> is the mutable counterpart.

fn clear_bytes<B>(mut buffer: B)
where
B: AsMut<[u8]>,
{
for byte in buffer.as_mut() {
*byte = 0;
}
}

Do not use AsRef or AsMut for expensive conversion. Use From, TryFrom, or a named method instead.

Borrow and BorrowMut

Borrow<T> also returns a borrowed view, but it has stronger semantic expectations than AsRef<T>.

Use Borrow when generic code relies on the borrowed and owned forms behaving the same for equality, ordering, or hashing.

This is why collection lookup can use a borrowed key.

use std::collections::HashMap;

let mut users = HashMap::<String, u64>::new();
users.insert(String::from("alice"), 1);

assert_eq!(users.get("alice"), Some(&1));

Here the map stores String, but lookup can use &str because String borrows as str with compatible equality and hashing behavior.

Use AsRef instead when the code only needs access to a related reference and does not depend on identical Eq, Ord, or Hash behavior.

BorrowMut<T> is the mutable counterpart to Borrow<T>.

Display, ToString, FromStr, and parse

For text output, implement Display, not ToString.

use std::fmt;

struct UserId(u64);

impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "user-{}", self.0)
}
}

let text = UserId(42).to_string();
assert_eq!(text, "user-42");

Rust provides ToString for every type that implements Display.

For text input, implement FromStr.

use std::str::FromStr;

#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);

#[derive(Debug, PartialEq, Eq)]
struct ParseUserIdError;

impl FromStr for UserId {
type Err = ParseUserIdError;

fn from_str(value: &str) -> Result<Self, Self::Err> {
let number = value
.strip_prefix("user-")
.ok_or(ParseUserIdError)?
.parse::<u64>()
.map_err(|_| ParseUserIdError)?;

Ok(UserId(number))
}
}

let user_id: UserId = "user-42".parse()?;
assert_eq!(user_id, UserId(42));

Document whether the Display output is accepted by FromStr. If Display is intended for humans and FromStr expects a strict machine format, make that difference explicit.

Choosing a Conversion Trait

NeedPrefer
Infallible owned conversionFrom<T> for U
Call-site owned conversionvalue.into()
Fallible owned conversionTryFrom<T> for U
Call-site fallible conversionvalue.try_into()
Generic owned conversion boundT: Into<U> or T: TryInto<U>
Cheap shared reference viewAsRef<T>
Cheap mutable reference viewAsMut<T>
Collection key lookup or equivalent behaviorBorrow<T>
User-facing string outputDisplay
Convert a displayed value to Stringvalue.to_string()
Parse a value from textFromStr and str::parse
Multiple possible conversion meaningsnamed constructor or named method
Conversion can fail with domain constraintsTryFrom, FromStr, or a custom validator

Implementation Placement

For conversions such as A -> Z, B -> Z, and C -> Z, prefer placing the implementations with the type that owns the meaning and invariants of Z.

struct A {
name: String,
}

struct B {
display_name: String,
}

struct Z {
label: String,
}

impl From<A> for Z {
fn from(value: A) -> Self {
Self { label: value.name }
}
}

impl From<B> for Z {
fn from(value: B) -> Self {
Self {
label: value.display_name,
}
}
}

This keeps the conversion near the code that defines what a valid Z is.

Dependency direction can override that default:

  • if Z is local and A, B, and C are available dependencies, place the implementations with Z
  • if each source crate owns A, B, or C and depends on the crate that defines Z, place each implementation in the source crate
  • if both the trait and the involved types are foreign to the current crate, Rust's orphan rules prevent the implementation; use a local wrapper type or add the implementation in one of the owning crates

The practical rule is:

Prefer the crate that owns the target type.
If that creates the wrong dependency direction, use the crate that owns the source type.
If neither type is local, introduce a local type or move the implementation upstream.