打印和格式化
官方教程到这里会给你讲一个"猜数字"的小游戏,虽然很有趣,但不建议初学者直接阅读。因为这个游戏涉及到很多概念(随机数、输入输出、循环、条件判断等),如果你还不熟悉 Rust 的基础语法,只会增加你的负担,让你更容易放弃。
本章提前介绍打印功能,是因为后续所有章节都会用到它来观察变量的值和程序运行结果。掌握好这一章,可以让后续的学习和调试更加顺畅。如果实在看不懂没关系,先有个印象.
先看一个最简单的 Rust 程序:
fn main() {
println!("Hello, world!");
}println! 后面带 !,说明它是一个宏,不是普通函数。Rust 中的打印功能都是通过宏来实现的。
打印宏概览
Rust 提供了两组打印宏,分别输出到不同的流:
标准输出(stdout) —— 用于正常的程序输出:
| 宏 | 换行 | 说明 |
|---|---|---|
print! | ❌ | 输出内容,不换行 |
println! | ✅ | 输出内容并换行,最常用 |
dbg! | ✅ | 打印文件名、行号和表达式的值,专用于调试 |
标准错误(stderr) —— 用于错误信息和日志:
| 宏 | 换行 | 说明 |
|---|---|---|
eprint! | ❌ | 输出到标准错误,不换行 |
eprintln! | ✅ | 输出到标准错误并换行 |
为什么要分两个流?在终端中,用户经常会将程序输出重定向到文件(
cargo run > output.txt)。如果只用println!,错误信息也会进文件,屏幕上什么都看不到。使用eprintln!的错误信息不受重定向影响,始终显示在屏幕上。
stdout 和 stderr 的重定向
- stdout(标准输出) 默认指向屏幕,但可以通过
>符号重定向到文件 - stderr(标准错误) 默认也指向屏幕,但可以通过
2>符号重定向到文件
cargo run > output.txt # stdout 进文件,stderr 仍显示在屏幕
cargo run 2> error.txt # stderr 进文件,stdout 仍显示在屏幕
cargo run &> all.txt # stdout 和 stderr 都进文件| 符号 | 对应流 | 典型用途 |
|---|---|---|
> / 1> | 标准输出 stdout | 存储程序的正常结果 |
2> | 标准错误 stderr | 存储报错和日志 |
&> | 全部 | 完整记录所有输出 |
使用建议:
println!:只打印用户请求的数据(搜索结果、计算结果等)eprintln!:打印"正在处理..."、"网络错误"、"缺少参数"等非业务信息
占位符与格式化
println! 使用 {} 作为占位符,将后面的参数填入对应位置:
println!("Hello, {}!", "world"); // Hello, world!
println!("{} + {} = {}", 1, 2, 1 + 2); // 1 + 2 = 3三种参数写法:
// 1. 按顺序填入
println!("{}, {}", "hello", "world");
// 2. 按位置填入
println!("{1}, {0}", "world", "hello"); // hello, world
// 3. 按名称填入
println!("{name} is {age}", name = "Alice", age = 30);显示格式(最常用)
| 占位符 | 对应 Trait | 说明 |
|---|---|---|
{} | Display | 面向用户的输出,数字和字符串默认支持 |
{:?} | Debug | 面向开发者的紧凑输出 |
{:#?} | Debug | 美化输出,带缩进换行,适合查看结构体 |
{:p} | Pointer | 显示内存地址(十六进制) |
let v = vec![1, 2, 3];
println!("{:?}", v); // [1, 2, 3]
println!("{:#?}", v); // 带缩进的多行格式自定义结构体默认不支持
{}和{:?}。添加#[derive(Debug)]注解后可使用{:?}和{:#?}。实现Displaytrait 后才能使用{}。
数值进制
let n = 255;
println!("{:b}", n); // 11111111 (二进制)
println!("{:o}", n); // 377 (八进制)
println!("{:x}", n); // ff (十六进制小写)
println!("{:X}", n); // FF (十六进制大写)
println!("{:#x}", n); // 0xff (带前缀)
println!("{:e}", n); // 2.55e2 (科学计数法)对齐与填充
println!("|{:10}|", "left"); // |left | 默认左对齐
println!("|{:<10}|", "left"); // |left | 左对齐
println!("|{:>10}|", "right"); // | right| 右对齐
println!("|{:^10}|", "center"); // | center | 居中
println!("{:0>5}", 42); // 00042 (用 0 填充左侧)
println!("{:*^10}", "Hi"); // ****Hi**** (用 * 填充两侧)浮点数精度
println!("{:.2}", 3.14159); // 3.14
println!("{:8.2}", 3.14159); // " 3.14" (宽度 8,保留 2 位小数)
println!("{:+}", 42); // +42 (强制显示正负号)println! 和 dbg! 的区别
这是两种最常用的调试手段,它们的设计理念完全不同:
| 特性 | println! | dbg! |
|---|---|---|
| 第一个参数 | 必须是字符串字面量(格式模板) | 可以是任意表达式,也可以留空 |
| 输出内容 | 完全由你控制 | 自动带上文件名、行号和变量名 |
| 所有权 | 借用参数,不转移 | 转移所有权,但返回值本身 |
| 输出流 | stdout | stderr |
| 空括号 | println!() 仅打印换行 | dbg!() 打印当前代码位置 |
let a = 10;
let b = dbg!(a * 2) + 1; // 打印 [src/main.rs:2] a * 2 = 20,同时 b = 21
println!("b = {b}"); // b = 21dbg! 的"转移所有权并返回"特性让它可以嵌入表达式中间使用,不打断代码逻辑:
// 调试链式调用时非常有用
let result = dbg!(vec![1, 2, 3])
.iter()
.map(|x| dbg!(x * 2))
.collect::<Vec<_>>();调试时推荐优先使用
dbg!,它自动带文件名和行号,信息量更大,用完也更容易清理。
format! 格式化到字符串
format! 使用和 println! 完全相同的格式语法,但不打印,而是返回一个 String,主要用于字符串的拼接:
let s = format!("Hello, {}!", "world"); // 返回 String 类型 "Hello, world!"
let info = format!("{:.2}%", 99.9); // 返回 String 类型 "99.90%"输出流对比:
| 宏 | 输出目标 | 返回值 |
|---|---|---|
println! | 屏幕(stdout) | 无 |
eprintln! | 屏幕(stderr) | 无 |
format! | 字符串(内存) | String |
write! | 任意缓冲区 | Result |
println!、format!、write!、panic!等宏底层都使用同一个格式化引擎std::fmt,格式语法完全通用。
内联变量捕获
Rust 2021 版本起,可以直接在占位符中写变量名:
let name = "Alice";
let age = 30;
println!("{name} is {age}"); // Alice is 30注意:内联语法只支持本地变量名,不支持表达式或字段访问:
let x = Point { val: 42 };
println!("{x.val}"); // ❌ 编译错误
println!("{}", x.val); // ✅ 正确写法
println!("{}", 1 + 2); // ✅ 参数位置支持任意表达式