不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-19 15:58:22  修改时间:2026-07-27 00:59:18  分类:01.Rust编程  编辑

目录

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)

五、DisplayDebug 的区别

这是使用 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 会继续调用 DepartmentDisplay::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

二十一、DisplayFromStr 的关系

有时我们希望一个类型:

  • 可以转换为字符串;
  • 也可以从字符串解析回来。

例如:

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();