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, andFromStrin real service code.
本章将学习: Rust 的转换 trait 如何对应 Java 的构造器、静态工厂、DTO 映射器与解析 API,以及在真实服务代码里什么时候该用From、Into、TryFrom、FromStr。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 failFrom<T>表示转换一定成功。TryFrom<T>means validation is requiredTryFrom<T>表示转换前必须校验。FromStrmeans parse text into a valueFromStr表示把文本解析成值。Into<T>is what callers usually use afterFrom<T>existsInto<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.fromString、Integer.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 parservalueOf(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
Stringeverywhere instead of introducing value objects
到处乱传裸String,而不是建立小型值对象。