Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Type Conversions in Rust
Rust 中的类型转换

What you’ll learn: How Rust conversion traits map to Java constructors, static factories, DTO mappers, and parsing APIs, plus when to use From, Into, TryFrom, and FromStr in real service code.
本章将学习: Rust 的转换 trait 如何对应 Java 的构造器、静态工厂、DTO 映射器与解析 API,以及在真实服务代码里什么时候该用 FromIntoTryFromFromStr

Difficulty: 🟡 Intermediate
难度: 🟡 中级

Java projects often express conversions through constructors, of(...), valueOf(...), or mapper classes.
Java 项目通常会用构造器、of(...)valueOf(...),或者 mapper 类来表达各种转换。

Rust pulls that intent into a small set of standard traits.
Rust 会把这些意图收束到一小组标准 trait 里。

The Core Distinction
核心区别

  • From<T> means the conversion cannot fail
    From<T> 表示转换一定成功。
  • TryFrom<T> means validation is required
    TryFrom<T> 表示转换前必须校验。
  • FromStr means parse text into a value
    FromStr 表示把文本解析成值。
  • Into<T> is what callers usually use after From<T> exists
    Into<T> 往往是调用方在 From<T> 存在之后更方便使用的入口。

Java Mapper vs Rust Trait Impl
Java Mapper 和 Rust Trait 实现的对照

public record UserDto(String id, String email) { }

public final class User {
    private final UUID id;
    private final String email;

    private User(UUID id, String email) {
        this.id = id;
        this.email = email;
    }

    public static User fromDto(UserDto dto) {
        return new User(UUID.fromString(dto.id()), dto.email());
    }
}
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct UserDto {
    id: String,
    email: String,
}

#[derive(Debug)]
struct User {
    id: uuid::Uuid,
    email: String,
}

impl TryFrom<UserDto> for User {
    type Error = String;

    fn try_from(dto: UserDto) -> Result<Self, Self::Error> {
        let id = dto.id.parse().map_err(|_| "invalid UUID".to_string())?;
        if !dto.email.contains('@') {
            return Err("invalid email".into());
        }

        Ok(User {
            id,
            email: dto.email,
        })
    }
}
}

The business purpose is the same, but Rust turns the conversion rule into part of the type system.
业务目的其实是一样的,但 Rust 会把这条转换规则直接写进类型系统里。

From for Infallible Conversions
From 表示必然成功的转换

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct UserId(uuid::Uuid);

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

impl From<UserId> for uuid::Uuid {
    fn from(value: UserId) -> Self {
        value.0
    }
}
}

If no validation is needed, From is the most direct expression.
如果完全不需要校验,From 就是最直接的表达方式。

Into for Convenient Call Sites
Into 让调用点更顺手

#![allow(unused)]
fn main() {
fn load_user(id: impl Into<UserId>) {
    let id = id.into();
    println!("loading {:?}", id);
}

let uuid = uuid::Uuid::new_v4();
load_user(UserId::from(uuid));
}

APIs often accept Into<T> so callers can pass either the wrapped type or something easy to wrap.
很多 API 会选择接收 Into<T>,这样调用方既可以传已经包装好的值,也可以传容易包装进去的值。

TryFrom for DTO-to-Domain Boundaries
DTO 到领域对象边界上用 TryFrom

This is one of the most useful patterns for Java backend teams.
这其实是 Java 后端团队最该尽快掌握的模式之一。

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct CreateUserRequest {
    email: String,
    age: u8,
}

#[derive(Debug)]
struct NewUser {
    email: String,
    age: u8,
}

impl TryFrom<CreateUserRequest> for NewUser {
    type Error = String;

    fn try_from(value: CreateUserRequest) -> Result<Self, Self::Error> {
        if !value.email.contains('@') {
            return Err("email must contain @".into());
        }

        if value.age < 18 {
            return Err("user must be an adult".into());
        }

        Ok(Self {
            email: value.email.trim().to_lowercase(),
            age: value.age,
        })
    }
}
}

This is the Rust version of validating request data before creating a stronger domain object.
这就是 Rust 版本的“先验证请求数据,再创建更强约束的领域对象”。

FromStr for Parsing
FromStr 表示解析

#![allow(unused)]
fn main() {
use std::str::FromStr;

#[derive(Debug, Clone, Copy)]
enum Environment {
    Local,
    Staging,
    Production,
}

impl FromStr for Environment {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_ascii_lowercase().as_str() {
            "local" => Ok(Self::Local),
            "staging" => Ok(Self::Staging),
            "production" => Ok(Self::Production),
            other => Err(format!("unknown environment: {other}")),
        }
    }
}
}

This is the natural Rust equivalent of UUID.fromString, Integer.parseInt, or Spring binder conversion.
这就是 Rust 对 UUID.fromStringInteger.parseInt、Spring 绑定器转换的自然对应物。

Display for Rendering
Display 负责展示

#![allow(unused)]
fn main() {
use std::fmt;

struct AccountNumber(String);

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

Parsing and rendering stay separate in Rust, which usually makes formatting code easier to reason about.
Rust 会把“解析”和“展示”分开处理,这通常会让格式化代码更清晰。

Quick Rules
快速规则

Java habit
Java 习惯
Better Rust choice
更合适的 Rust 选择
constructor that always succeeds
一定成功的构造器
From<T>
static factory that may reject input
可能拒绝输入的静态工厂
TryFrom<T>
valueOf(String) or parser
valueOf(String) 或解析器
FromStr
standalone mapper service everywhere
到处都是独立 mapper 服务
put trait impls near the types
把 trait 实现贴近类型本身

Common Mistakes
常见误区

  • using From<T> for a conversion that can fail
    明明可能失败,却硬写成 From<T>
  • spreading validation across controllers and services
    把校验逻辑拆散到 controller 和 service 各处。
  • passing raw String everywhere instead of introducing value objects
    到处乱传裸 String,而不是建立小型值对象。