目录
- Rust std::fmt::Display 有什么作用?
- 二、Display trait 的定义
- 三、为什么最后不加分号?
- 四、实现 Display 后能做什么?
- 五、Display 和 Debug 的区别
- 六、为什么 Debug 能派生,Display 通常不能?
- 七、为枚举实现 Display
- 八、根据枚举中的数据进行格式化
- 九、在自定义错误类型中使用 Display
- 十、格式化结构体中的其他字段
- 十一、多个 write!() 操作
- 十二、不要在 fmt() 中调用自身的 to_string()
- 十三、不要先创建 String,再写入 Formatter
- 十四、Display 只能有一种默认展示方式
- 十五、使用包装类型提供多种显示格式
- 十六、格式化数字
- 十七、为什么 Vec<T> 常常不能直接用 {}?
- 十八、给列表创建自定义 Display 包装器
- 十九、在泛型函数中要求类型实现 Display
- 二十、返回实现了 Display 的值
- 二十一、Display 与 FromStr 的关系
- 二十二、一个更完整的工单系统示例
- 二十三、常见错误总结
- 二十四、什么时候应该实现 Display?
- 二十五、一句话理解
Rust std::fmt::Display 有什么作用?
std::fmt::Display 是 Rust 标准库提供的一个格式化输出 trait。
它的核心作用是:
告诉 Rust:一个类型应该以什么样的、适合用户阅读的文本形式显示。
实现 Display 后,这个类型就可以使用:
println!("{}", value);
format!("{}", value);
value.to_string();
其中 {} 对应的正是 Display trait。Rust 官方将 Display 定义为面向用户的输出格式,而 Debug 主要用于开发调试。(Rust 文档)
一、最简单的例子
假设我们有一个 User 结构体:
struct User {
id: u64,
name: String,
}
直接使用 {} 输出:
fn main() {
let user = User {
id: 1001,
name: String::from("张三"),
};
println!("{}", user);
}
编译会报错,因为 Rust 不知道应该如何将 User 转换成用户可读的字符串。
我们需要为 User 实现 Display:
use std::fmt;
struct User {
id: u64,
name: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户ID:{},用户名:{}", self.id, self.name)
}
}
fn main() {
let user = User {
id: 1001,
name: String::from("张三"),
};
println!("{}", user);
}
输出:
用户ID:1001,用户名:张三
官方文档中的典型写法也是为结构体实现 fmt(),然后通过 write!() 把格式化内容写入 Formatter。(Rust 文档)
二、Display trait 的定义
它的定义大致如下:
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
通常写成:
impl fmt::Display for 类型 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 格式化逻辑
}
}
参数分别是什么意思?
1. &self
表示当前需要格式化的对象。
例如:
self.id self.name
就是访问当前对象的字段。
2. f: &mut fmt::Formatter<'_>
Formatter 是 Rust 格式化系统提供的写入目标。
你一般不需要自己创建它。当执行:
println!("{}", user);
Rust 会自动创建和传入 Formatter。官方文档也说明,使用者通常不会直接构造 Formatter,它会由格式化系统传给 fmt()。(Rust 文档)
3. fmt::Result
fmt::Result 本质上类似于:
Result<(), fmt::Error>
格式化成功返回:
Ok(())
格式化失败返回:
Err(fmt::Error)
不过一般不需要手动返回,直接使用:
write!(f, "...", ...)
即可,因为 write!() 自己会返回 fmt::Result。
官方建议:只有底层 Formatter 写入失败时,fmt() 才应该返回错误,不要在这里设计自己的业务错误。(Rust 文档)
三、为什么最后不加分号?
推荐写法:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户:{}", self.name)
}
}
注意最后的 write!() 没有分号。
因为 write!() 返回的是:
fmt::Result
而 fmt() 也要求返回:
fmt::Result
因此可以直接把 write!() 的结果作为函数返回值。
下面两种写法等价。
简洁写法
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户:{}", self.name)
}
完整写法
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户:{}", self.name)?;
Ok(())
}
当只有一次写入操作时,通常使用第一种。
四、实现 Display 后能做什么?
1. 使用 println!("{}", value)
println!("{}", user);
2. 使用 format!()
let message = format!("当前登录用户:{}", user);
println!("{}", message);
结果:
当前登录用户:用户ID:1001,用户名:张三
3. 使用 to_string()
let user_text = user.to_string();
println!("{}", user_text);
当一个类型实现了 Display 后,Rust 会让它自动获得 ToString 的相关能力,因此通常应该实现 Display,而不是手动实现 ToString。(Rust 文档)
例如:
use std::fmt;
struct User {
id: u64,
name: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.name, self.id)
}
}
fn main() {
let user = User {
id: 1001,
name: String::from("张三"),
};
let text: String = user.to_string();
println!("{}", text);
}
输出:
张三(1001)
五、Display 和 Debug 的区别
这是使用 Display 时最重要的知识点。
Display
面向普通用户,强调:
- 简洁;
- 易读;
- 业务友好;
- 不一定展示全部内部字段。
使用:
println!("{}", value);
格式占位符:
{}
Debug
面向程序员,强调:
- 调试;
- 查看内部结构;
- 查看字段和值;
- 快速定位程序问题。
使用:
println!("{:?}", value);
格式占位符:
{:?}
Rust 官方明确区分了两者:Display 用于用户可见输出,Debug 用于呈现内部状态、辅助程序调试。(Rust 文档)
对比示例
use std::fmt;
#[derive(Debug)]
struct User {
id: u64,
name: String,
password_hash: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}(用户ID:{})", self.name, self.id)
}
}
fn main() {
let user = User {
id: 1001,
name: String::from("张三"),
password_hash: String::from("$argon2id$xxxxx"),
};
println!("Display:{}", user);
println!("Debug:{:?}", user);
}
输出类似:
Display:张三(用户ID:1001)
Debug:User { id: 1001, name: "张三", password_hash: "$argon2id$xxxxx" }
可以看到:
Display只显示适合用户查看的信息;Debug展示了结构体内部字段;Display可以主动隐藏密码、令牌等敏感字段。
需要注意,即使 Debug 方便,也不应该在生产日志中随意输出包含密码、令牌、密钥等敏感信息的完整对象。
六、为什么 Debug 能派生,Display 通常不能?
Debug 可以直接写:
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
但是不能写:
#[derive(Display)]
struct User {
id: u64,
name: String,
}
标准库没有提供通用的 Display 派生,因为 Rust 不知道你希望用户看到什么。
例如一个用户对象可能有很多合理的展示方式:
张三
或者:
张三(1001)
或者:
用户ID:1001,用户名:张三
或者:
1001,张三
这些都是业务决定,编译器无法自动选择。官方文档也指出,Display 是用户输出格式,因此不能像 Debug 那样直接由标准库派生。(Rust 文档)
七、为枚举实现 Display
Display 特别适合将枚举转换成中文业务文案。
use std::fmt;
enum TicketStatus {
Pending,
Processing,
Completed,
Closed,
}
impl fmt::Display for TicketStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
TicketStatus::Pending => "待处理",
TicketStatus::Processing => "处理中",
TicketStatus::Completed => "已完成",
TicketStatus::Closed => "已关闭",
};
write!(f, "{}", text)
}
}
fn main() {
let status = TicketStatus::Processing;
println!("工单状态:{}", status);
}
输出:
工单状态:处理中
这比业务代码中到处写 match 更统一。
例如原来可能是:
let text = match status {
TicketStatus::Pending => "待处理",
TicketStatus::Processing => "处理中",
TicketStatus::Completed => "已完成",
TicketStatus::Closed => "已关闭",
};
实现 Display 后,可以直接:
let text = status.to_string();
八、根据枚举中的数据进行格式化
枚举变体也可以携带数据:
use std::fmt;
enum ApiError {
NotFound(String),
Unauthorized,
InternalError(String),
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiError::NotFound(resource) => {
write!(f, "资源不存在:{}", resource)
}
ApiError::Unauthorized => {
write!(f, "当前用户没有访问权限")
}
ApiError::InternalError(message) => {
write!(f, "服务器内部错误:{}", message)
}
}
}
}
fn main() {
let error = ApiError::NotFound(String::from("工单 #1001"));
println!("{}", error);
}
输出:
资源不存在:工单 #1001
这种写法在自定义错误类型中很常见。
九、在自定义错误类型中使用 Display
Rust 的错误类型通常需要:
Debug:供程序员调试;Display:供用户查看错误信息;Error:表明它是一个标准错误类型。
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct UserNotFoundError {
user_id: u64,
}
impl fmt::Display for UserNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "找不到ID为 {} 的用户", self.user_id)
}
}
impl Error for UserNotFoundError {}
fn find_user(user_id: u64) -> Result<(), UserNotFoundError> {
Err(UserNotFoundError { user_id })
}
fn main() {
match find_user(1001) {
Ok(_) => println!("查询成功"),
Err(error) => {
println!("错误:{}", error);
println!("调试信息:{:?}", error);
}
}
}
输出:
错误:找不到ID为 1001 的用户
调试信息:UserNotFoundError { user_id: 1001 }
这里:
println!("{}", error);
调用 Display。
而:
println!("{:?}", error);
调用 Debug。
十、格式化结构体中的其他字段
假设结构体中包含另一个自定义类型:
use std::fmt;
struct Department {
name: String,
}
impl fmt::Display for Department {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
struct User {
name: String,
department: Department,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{},所属部门:{}",
self.name,
self.department
)
}
}
fn main() {
let user = User {
name: String::from("张三"),
department: Department {
name: String::from("IT运维部"),
},
};
println!("{}", user);
}
输出:
张三,所属部门:IT运维部
当执行:
write!(f, "{}", self.department)
Rust 会继续调用 Department 的 Display::fmt()。
也就是说,多个实现了 Display 的类型可以嵌套组合。
十一、多个 write!() 操作
有时格式化逻辑比较复杂,可以执行多次写入:
use std::fmt;
struct User {
id: u64,
name: String,
enabled: bool,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户:{}", self.name)?;
write!(f, ",ID:{}", self.id)?;
if self.enabled {
write!(f, ",状态:启用")?;
} else {
write!(f, ",状态:停用")?;
}
Ok(())
}
}
这里的 ? 表示:
- 如果写入成功,继续执行;
- 如果写入失败,立即返回
fmt::Error。
也可以简化为一次写入:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.enabled {
"启用"
} else {
"停用"
};
write!(
f,
"用户:{},ID:{},状态:{}",
self.name,
self.id,
status
)
}
}
通常第二种更简洁。
十二、不要在 fmt() 中调用自身的 to_string()
下面是错误写法:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
原因是:
self.to_string()
依赖 Display。
而当前正在执行的又是:
Display::fmt()
于是形成递归调用:
fmt()
→ to_string()
→ fmt()
→ to_string()
→ fmt()
最终可能导致栈溢出。
正确做法是直接格式化字段:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.name, self.id)
}
}
十三、不要先创建 String,再写入 Formatter
下面的代码虽然能工作,但会额外创建一个 String:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = format!("{}({})", self.name, self.id);
write!(f, "{}", text)
}
}
更推荐直接写入:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.name, self.id)
}
}
因为 write!(f, ...) 会直接把格式化结果写入提供的 Formatter,不需要先创建中间字符串。
十四、Display 只能有一种默认展示方式
对于同一个类型,只能实现一次:
impl fmt::Display for User {
// ...
}
不能同时写两个:
impl fmt::Display for User {
// 中文格式
}
impl fmt::Display for User {
// CSV 格式
}
这会产生 trait 实现冲突。
因此,Display 应该用于这个类型最自然、最常用的默认显示格式。
例如:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.name, self.id)
}
}
如果还需要 JSON、CSV、完整信息等其他格式,可以提供普通方法:
impl User {
fn to_csv(&self) -> String {
format!("{},{}", self.id, self.name)
}
fn to_full_text(&self) -> String {
format!(
"用户ID:{},用户名:{}",
self.id,
self.name
)
}
}
使用:
println!("{}", user);
println!("{}", user.to_csv());
println!("{}", user.to_full_text());
官方文档也建议:当一个类型存在多种显示格式时,可以通过专门的显示适配器或包装类型提供其他格式,而不是给一个类型提供多个 Display 实现。(Rust 文档)
十五、使用包装类型提供多种显示格式
例如,同一个日期可能需要:
2026-07-19
也可能需要:
2026年07月19日
可以定义包装类型:
use std::fmt;
struct Date {
year: u32,
month: u32,
day: u32,
}
struct ChineseDate<'a>(&'a Date);
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02}",
self.year,
self.month,
self.day
)
}
}
impl fmt::Display for ChineseDate<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let date = self.0;
write!(
f,
"{}年{:02}月{:02}日",
date.year,
date.month,
date.day
)
}
}
fn main() {
let date = Date {
year: 2026,
month: 7,
day: 19,
};
println!("默认格式:{}", date);
println!("中文格式:{}", ChineseDate(&date));
}
输出:
默认格式:2026-07-19 中文格式:2026年07月19日
这就是“显示适配器”的思路。
十六、格式化数字
Display 内部也可以继续使用 Rust 的格式化语法。
例如金额:
use std::fmt;
struct Money {
amount: f64,
currency: String,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.2} {}", self.amount, self.currency)
}
}
fn main() {
let money = Money {
amount: 1288.5,
currency: String::from("CNY"),
};
println!("{}", money);
}
输出:
1288.50 CNY
其中:
{:.2}
表示保留两位小数。
补零
write!(f, "{:04}-{:02}-{:02}", year, month, day)
例如:
2026-07-09
十六进制
write!(f, "0x{:X}", self.value)
例如:
0xFF
标准格式化模块还提供十六进制、八进制、二进制、科学计数法等对应的格式化 trait。(Rust 文档)
十七、为什么 Vec<T> 常常不能直接用 {}?
下面通常不能编译:
let numbers = vec![1, 2, 3];
println!("{}", numbers);
因为 Vec<T> 没有一种唯一且明确的用户展示格式。
它可能被展示为:
1,2,3
也可能是:
[1, 2, 3]
也可能是:
1 2 3
因此调试时一般使用:
println!("{:?}", numbers);
输出:
[1, 2, 3]
也可以手动格式化:
let numbers = vec![1, 2, 3];
let text = numbers
.iter()
.map(|number| number.to_string())
.collect::<Vec<_>>()
.join(", ");
println!("{}", text);
输出:
1, 2, 3
十八、给列表创建自定义 Display 包装器
use std::fmt;
struct DisplayList<'a, T>(&'a [T]);
impl<T> fmt::Display for DisplayList<'_, T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, item) in self.0.iter().enumerate() {
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
Ok(())
}
}
fn main() {
let numbers = vec![10, 20, 30];
println!("{}", DisplayList(&numbers));
}
输出:
10, 20, 30
这里的约束:
T: fmt::Display
表示列表中的元素必须实现 Display。
十九、在泛型函数中要求类型实现 Display
可以通过 trait bound 要求参数支持 {} 输出:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("值为:{}", value);
}
fn main() {
print_value(100);
print_value("hello");
print_value(3.14);
}
等价写法:
fn print_value<T>(value: T)
where
T: Display,
{
println!("值为:{}", value);
}
也可以使用 impl Trait:
fn print_value(value: impl Display) {
println!("值为:{}", value);
}
二十、返回实现了 Display 的值
例如:
use std::fmt::Display;
fn get_message() -> impl Display {
"操作成功"
}
fn main() {
println!("{}", get_message());
}
这里返回的具体类型是:
&'static str
但函数签名只承诺:
它实现了 Display
二十一、Display 与 FromStr 的关系
有时我们希望一个类型:
- 可以转换为字符串;
- 也可以从字符串解析回来。
例如:
let text = value.to_string(); let value = text.parse::<MyType>()?;
这通常意味着:
Display:对象转字符串;FromStr:字符串转对象。
但实现 Display 并不意味着必须实现 FromStr,也不意味着输出结果一定可以无损解析回来。官方文档建议:当 Display 输出是无损且适合机器解析的格式时,可以考虑同时实现 FromStr,并清楚记录往返转换行为。(Rust 文档)
例如:
use std::fmt;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct UserId(u64);
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "USER-{}", self.0)
}
}
impl FromStr for UserId {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let id = value
.strip_prefix("USER-")
.ok_or("格式错误")?
.parse::<u64>()
.map_err(|_| "用户ID不是有效数字")?;
Ok(UserId(id))
}
}
fn main() {
let user_id = UserId(1001);
let text = user_id.to_string();
println!("{}", text);
let parsed: UserId = text.parse().unwrap();
println!("{:?}", parsed);
assert_eq!(user_id, parsed);
}
输出:
USER-1001 UserId(1001)
二十二、一个更完整的工单系统示例
use std::fmt;
#[derive(Debug)]
enum TicketType {
Bug,
Change,
Permission,
}
impl fmt::Display for TicketType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
TicketType::Bug => "故障处理",
TicketType::Change => "系统发布与变更",
TicketType::Permission => "账号与权限",
};
write!(f, "{}", text)
}
}
#[derive(Debug)]
enum TicketStatus {
Pending,
Processing,
Completed,
}
impl fmt::Display for TicketStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
TicketStatus::Pending => "待处理",
TicketStatus::Processing => "处理中",
TicketStatus::Completed => "已完成",
};
write!(f, "{}", text)
}
}
#[derive(Debug)]
struct Ticket {
id: u64,
title: String,
ticket_type: TicketType,
status: TicketStatus,
}
impl fmt::Display for Ticket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"工单 #{}:{}|类型:{}|状态:{}",
self.id,
self.title,
self.ticket_type,
self.status
)
}
}
fn main() {
let ticket = Ticket {
id: 1001,
title: String::from("申请生产环境发布"),
ticket_type: TicketType::Change,
status: TicketStatus::Pending,
};
println!("{}", ticket);
println!("{:?}", ticket);
}
Display 输出:
工单 #1001:申请生产环境发布|类型:系统发布与变更|状态:待处理
Debug 输出类似:
Ticket {
id: 1001,
title: "申请生产环境发布",
ticket_type: Change,
status: Pending
}
二十三、常见错误总结
错误一:使用 {},但没有实现 Display
struct User {
name: String,
}
println!("{}", user);
解决:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
或者调试时使用:
#[derive(Debug)]
struct User {
name: String,
}
println!("{:?}", user);
错误二:忘记返回 fmt::Result
错误:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name);
}
因为加了分号后,函数最后返回的是:
()
正确:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
或者:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)?;
Ok(())
}
错误三:在 fmt() 中调用 self.to_string()
错误:
write!(f, "{}", self.to_string())
会导致递归调用。
正确:
write!(f, "{}({})", self.name, self.id)
错误四:把 Display 当序列化工具使用
虽然 Display 可以生成字符串,但它不应该代替 JSON 序列化。
不建议:
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
r#"{{"id":{},"name":"{}"}}"#,
self.id,
self.name
)
}
}
对于 JSON,应使用 serde_json:
let json = serde_json::to_string(&user)?;
通常可以这样区分:
Display:给人看;Debug:给开发者调试;serde_json:结构化数据传输和存储。
二十四、什么时候应该实现 Display?
适合实现的场景:
- 用户名、订单号、工单号等业务对象;
- 状态枚举的中文展示;
- 错误信息;
- 金额、日期、版本号;
- 日志中需要简洁展示的对象;
- 命令行程序中的输出内容;
- 需要调用
.to_string()的自定义类型。
不一定适合实现的场景:
- 类型没有明显、唯一的用户展示格式;
- 只需要临时调试;
- 需要输出 JSON、XML、CSV 等严格数据格式;
- 可能意外暴露密码、令牌、密钥等敏感字段。
二十五、一句话理解
可以把 Display 理解为:
这个对象给普通用户看时,应该显示成什么文本?
而 Debug 可以理解为:
这个对象给程序员排查问题时,内部结构是什么?
最典型的实现模板是:
use std::fmt;
impl fmt::Display for MyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "需要显示的内容")
}
}
实现以后即可使用:
println!("{}", value);
format!("{}", value);
value.to_string();