格式化输出

基础输出

println! 是你写下的第一行 Rust 代码就用到的工具,但它的能力远不止打印一句话。Rust 的格式化系统统一处理所有打印相关的操作,并且格式字符串的正确性在编译时就能检查——拼写错一个占位符,直接报编译错误,不会等到运行时才发现。

五个打印宏

std::fmt 模块提供了五个打印宏,记住它们的分工:

输出目标换行
print!标准输出(stdout)
println!标准输出(stdout)
format!返回 String,不输出
eprint!标准错误(stderr)
eprintln!标准错误(stderr)

stdout 与 stderr 的区别:操作系统为每个程序提供了两条独立的输出通道。print!/println! 写入 stdout(标准输出),用于程序的正常运行结果;eprint!/eprintln! 写入 stderr(标准错误),用于错误信息、警告和调试诊断。

在终端里两者看起来一样,但它们的用途不同,分开写的好处在于:用户可以把正常输出重定向到文件(./app > output.txt),而错误信息仍然显示在终端上;或者反过来只捕获错误(./app 2> error.log)。

fn main() {
    print!("没有换行");
    print!(",继续在同一行\n"); // 手动加换行

    println!("这行自动换行");

    let s = format!("拼接成字符串:{} + {} = {}", 1, 2, 3);
    println!("{}", s);

    eprintln!("这是错误信息,输出到 stderr"); // 终端通常也能看到
}

format! 是”静默”版本,不打印,只返回 String,在需要构建字符串时很有用:let msg = format!("Hello, {}!", name);

{}{:?}:两种格式化方式

Rust 的占位符有两类,对应两种格式化 trait:

占位符对应 trait设计目标
{}Display面向用户的友好展示
{:?}Debug面向开发者的调试信息
{:#?}Debug(美化版)多行缩进,结构更清晰

DisplayDebug 是两个 trait(可以理解为”能力接口”):

  • Display:定义类型”给人看”时的样子。42"hello"true 这些基本类型都实现了它,但自定义的结构体默认没有,需要手动实现。
  • Debug:定义类型”供调试用”时的样子,格式更详细,通常包含类型名和字段名。可以用 #[derive(Debug)] 让编译器自动生成,不需要手写。

简单记:开发阶段看数据用 {:?},给用户展示用 {}

trait 是 Rust 的核心概念,相当于其他语言的”接口”或”协议”。如何自定义 Display(控制 {} 输出格式)会在 第 11 章 Trait 中详细讲解。现在只需知道怎么用 {:?}{:#?} 就够了。

fn main() {
    let v = vec![1, 2, 3];

    // {} 只对实现了 Display 的类型有效
    // Vec 没有实现 Display,下面这行会编译报错:
    // println!("{}", v);

    // {:?} 对所有实现了 Debug 的类型有效,Vec 默认支持
    println!("{:?}", v);   // [1, 2, 3]

    // {:#?} 美化打印,多行缩进
    println!("{:#?}", v);
}

对于基本类型(数字、字符串、布尔值、元组等),{}{:?} 都能用。对于自定义类型(结构体、枚举、集合等),需要先告诉 Rust 如何格式化它们。

为自定义类型启用调试输出

通过 #[derive(Debug)] 属性,可以让编译器自动生成 Debug trait 的实现,不需要手写任何代码。

#[...] 这种写法叫属性(Attribute),会在本章属性一节详细讲解。现在只需要知道:把 #[derive(Debug)] 写在结构体上方,就能让它支持 {:?} 打印。

// 加上这一行,编译器自动帮你实现 {:?} 格式化
#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

#[derive(Debug)]
struct Rectangle {
    top_left: Point,
    bottom_right: Point,
}

fn main() {
    let rect = Rectangle {
        top_left: Point { x: 0.0, y: 10.0 },
        bottom_right: Point { x: 5.0, y: 0.0 },
    };

    println!("{:?}", rect);   // 单行
    println!("{:#?}", rect);  // 多行美化
}

参数引用:位置与命名

除了按顺序填充 {},还可以用位置索引命名参数更灵活地引用:

fn main() {
    // 顺序填充:按出现顺序依次替换
    println!("{} {} {}", "a", "b", "c");

    // 位置索引:可以重复使用同一个参数
    println!("{0} {1} {0}", "Alice", "Bob"); // Alice Bob Alice

    // 命名参数:更易读
    println!(
        "{subject} {verb} {object}",
        subject = "小猫",
        verb = "追",
        object = "小鱼"
    );
}

常用格式规范

{}: 后面可以加格式规范,控制数制、宽度、对齐和精度:

进制输出

fn main() {
    let n = 255;
    println!("十进制: {}",   n);      // 255
    println!("二进制: {:b}", n);      // 11111111
    println!("八进制: {:o}", n);      // 377
    println!("十六进制(小): {:x}", n); // ff
    println!("十六进制(大): {:X}", n); // FF
    println!("带前缀:  {:#x}", n);    // 0xff
    println!("带前缀:  {:#b}", n);    // 0b11111111
}

宽度与对齐

宽度规范会为输出内容分配一个指定宽度的”格子”,当内容不足这个宽度时,用空格(或指定字符)填满——对齐方式决定内容靠哪边放。

fn main() {
    // 右对齐(默认),宽度 10
    println!("{:>20}", "hello");   //                hello

    // 左对齐
    println!("{:<10}", "hello");   // hello

    // 居中
    println!("{:^10}", "hello");   //   hello

    // 用指定字符填充(这里用 '-')
    println!("{:-^10}", "hello");  // --hello---

    // 数字补零
    println!("{:0>6}", 42);        // 000042
    // 等价写法
    println!("{:06}", 42);         // 000042
}

小数精度

fn main() {
    let pi = 3.141592653589793;

    println!("{}", pi);        // 完整精度
    println!("{:.2}", pi);     // 保留 2 位小数:3.14
    println!("{:.5}", pi);     // 保留 5 位小数:3.14159
    println!("{:8.3}", pi);    // 宽度 8,3 位小数:   3.142
    println!("{:08.3}", pi);   // 宽度 8,3 位小数,补零:0003.142
}

小结

场景写法
普通打印println!("{}", val)
调试打印println!("{:?}", val) 需要 #[derive(Debug)]
美化调试println!("{:#?}", val)
构建字符串format!("...")
二进制/十六进制{:b} / {:x} / {:#x}
固定宽度{:>10} / {:<10} / {:^10}
小数位数{:.2}

实现自定义类型的 Display(控制 {} 的输出格式)属于进阶内容,会在补充内容:格式化输出进阶中详细讲解。

练习题

选择正确的宏

加载题目中…

{}{:?} 的区别

#[derive(Debug)]
struct Foo(i32);

fn main() {
    let f = Foo(42);
    println!("{:?}", f);
    // println!("{}", f); // 这行会编译报错
}
加载题目中…

#[derive(Debug)] 的作用

加载题目中…

格式规范识别

加载题目中…

stderr 与 stdout

加载题目中…

编程练习

补全下面程序,让序号用零补齐到 2 位宽度输出(0102……而不是 12……)。

fn main() {
    let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
    for (i, day) in days.iter().enumerate() {
        // TODO:序号从 1 开始,宽度 2,用零补齐
        println!("{} {}", i + 1, day);
    }
}