Rust语言的打印操作主要是通过在std::fmt
里面定义的一系列宏来处理。主要包括:
format!
: 将格式化文本存入字符串。print!
: 与format!
类似,但是把文本输出到控制台(io::stdout
)println!
: 与print!
类似,但是输出结果末尾会追加换行符。eprint!
: 与format!
类似,但是把文本输出到标准错误(std::stderr
)。eprintln!
: 与eprint!
类似,但是输出结果末尾会追加换行符。write!
: 与format!
类似,但是把文本输出到&mut io::Write
writeln!
: 与write!
,但是输出结果末尾会追加换行符。
这些宏都会以一致的规则对文本进行解析,格式化的正确性会在编译器进行检查。
格式化规则
位置参数
每个格式化参数可以按照顺序进行格式化,也可以按照指定的顺序对其进行格式化。
{}
: 没有指定索引位置,会按照参数迭代逐个使用{n}
: 指定了索引位置,就会使用指定索引的参数
fn main() {
// 按照顺序进行格式化
// 输出: 10 is a number, 'hello world' is a string
println!("{} is a number, '{}' is a string", 10, "hello world");
// 按照索引进行格式化
// 输出: 10 is a number, 'hello world' is a string
println!("{1} is a number, '{0}' is a string", "hello world", 10);
// 按照顺序与索引混合进行格式化
// 没有索引的{}会按照迭代顺序进行格式化
// 输出: 2 1 1 2
println!("{1} {} {0} {}", 1, 2)
}
具名参数
对于已经存在的变量,可以按照变量名字输出。
如果不存在,也可以进行指定名称。
fn main() {
// 已存在的变量
// 输出:My name is Bob.
let name = "Bob";
println!("My name is {name}.");
// 格式化是指定名称,此处并不会覆盖原有的定义
// 输出:My name is Alice, his name is Jeff.
println!(
"My name is {name}, his name is {name1}.",
name = "Alice",
name1 = "Jeff"
);
// 输出:name: Bob
println!("name: {name}");
}
格式化参数
我们可以指定一些格式化的参数,来对格式化的输出进行个性化的定制。
指定宽度
需要注意的是,这里指定的是最小宽度,如果字符串不足宽度会进行填充,如果超出并不会截断。
{:n}
: 通过数字直接指定宽度,比如{:5}
{:n$}
: 通过参数索引指定宽度,比如{:1$}
{:name$}
: 通过具名参数指定宽度,比如{:width$}
fn main() {
// 不足长度5,会填充空格
// 输出: Hello a !
println!("Hello {:5}!", "a");
// 超出长度1,仍然完整输出
// 输出: Hello abc!
println!("Hello {:1}!", "abc");
// 因为未指定索引,所以按照顺序位置上引用的是"abc"
// 通过$符指定了宽度的索引为1,即宽度为5
// 输出: Hello abc !
println!("Hello {:1$}!", "abc", 5);
// 指定了位置索引为1,使用$符指定了宽度的索引为0,即宽度为5
// 输出: Hello abc !
println!("Hello {1:0$}!", 5, "abc");
// 通过具名参数指定宽度为5
// 输出:Hello abc !
println!("Hello {:width$}!", "abc", width = 5);
let width = 5;
println!("Hello {:width$}!", "abc");
}
填充与对齐
在格式化字符串内,一般按照 : + 填充字符 + 对齐方式
来排列。
[fill]<
: 左对齐[fill]^
: 居中对齐[fill]>
:右对齐
非数字时,默认情况下填充使用空格+左对齐。
数字时,默认情况下填充使用空格+右对齐。
特别要注意的,某些类型可能不会实现对齐。特别是对于Debug
trait,通常不会实现该功能。确保应用填充的一种好方法是格式化输入,再填充此结果字符串以获得输出。
fn main() {
// 非数字默认左对齐,空格填充
assert_eq!(format!("Hello {:7}!", "abc"), "Hello abc !");
// 左对齐
assert_eq!(format!("Hello {:<7}!", "abc"), "Hello abc !");
// 左对齐,使用-填充
assert_eq!(format!("Hello {:-<7}!", "abc"), "Hello abc----!");
// 右对齐,使用-填充
assert_eq!(format!("Hello {:->7}!", "abc"), "Hello ----abc!");
// 中间对齐,使用-填充
assert_eq!(format!("Hello {:-^7}!", "abc"), "Hello --abc--!");
// 数字默认右对齐,空格填充
assert_eq!(format!("Hello {:7}!", 7), "Hello 7!");
// 左对齐
assert_eq!(format!("Hello {:<7}!", 7), "Hello 7 !");
// 居中对齐
assert_eq!(format!("Hello {:^7}!", 7), "Hello 7 !");
// 填充0
assert_eq!(format!("Hello {:07}!", 7), "Hello 0000007!");
// 负数填充0,负号会占用一位
assert_eq!(format!("Hello {:07}!", -7), "Hello -000007!");
}
格式化标识
我们还可以使用一些格式化标识对字符串进行格式化。
+
: 数字打印符号,默认情况下数字不会打印正号,加了该标识符可以强制打印正负号。#?
: 打印Debug格式,同时会添加换行符和缩进。#x
: 以十六进制小写形式打印#X
: 以十六进制大写形式打印#b
: 以二进制形式打印#o
: 以八进制形式打印
#[allow(dead_code)]
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
// 打印正负号
assert_eq!(format!("Hello {:+7}!", 5), "Hello +5!");
assert_eq!(format!("Hello {:+7}!", -5), "Hello -5!");
let user: User = User {
name: "John".to_string(),
age: 30,
};
// 输出Debug格式
assert_eq!(
format!("Hello {:?}!", user),
"Hello User { name: \"John\", age: 30 }!"
);
// 输出美化后的Debug格式
assert_eq!(
format!("Hello {:#?}!", user),
"Hello User {\n name: \"John\",\n age: 30,\n}!"
);
// 以16进制输出
assert_eq!(format!("Hello {:#x}!", 10251), "Hello 0x280b!");
assert_eq!(format!("Hello {:#X}!", 10251), "Hello 0x280B!");
// 以二进制输出
assert_eq!(format!("Hello {:#b}!", 10251), "Hello 0b10100000001011!");
// 以八进制输出
assert_eq!(format!("Hello {:#o}!", 10251), "Hello 0o24013!");
}
精度控制
这里的精度控制其实就是最大宽度,超出将会进行截断。
对于整数类型,这里会被忽略。
对于浮点类型,指示小数点后打印多少位。
.n
: 整数n就是精度.n$
: 使用参数n作为精度,这里也可以是具名参数。.*
: 与两个格式输入关联,第一个输入是要保存的精度,第二个是要打印的值。
fn main() {
// 直接指定精度
assert_eq!(
format!("Hello {} is {:.5}", "abc", 0.01),
"Hello abc is 0.01000"
);
// 直接指定精度
assert_eq!(
format!("Hello {0} is {1:.5}", "abc", 0.01),
"Hello abc is 0.01000"
);
// 通过参数索引指定精度
assert_eq!(
format!("Hello {0} is {1:.2$}", "abc", 0.01, 5),
"Hello abc is 0.01000"
);
// 通过参数名称指定精度
assert_eq!(
format!("Hello {0} is {1:.precision$}", "abc", 0.01, precision = 5),
"Hello abc is 0.01000"
);
assert_eq!(
format!(
"Hello {} is {number:.precision$}",
"abc",
number = 0.01,
precision = 5
),
"Hello abc is 0.01000"
);
// 通过星号,从参数内读取精度和要处理的数字
assert_eq!(
format!("Hello {} is {:.*}", "abc", 5, 0.01),
"Hello abc is 0.01000"
);
// 通过星号,从参数内读取精度,数字指定了索引
assert_eq!(
format!("Hello {} is {2:.*}", "abc", 5, 0.01),
"Hello abc is 0.01000"
);
// 通过星号,从参数内读取精度,通过名称指定数字
assert_eq!(
format!("Hello, {number:.*}", 3, number = 123.45),
"Hello, 123.450"
);
// 数字为字符串,所以直接截断
assert_eq!(
format!("Hello, {number:.*}", 3, number = "123.45"),
"Hello, 123"
);
// 配合最小宽度做填充
assert_eq!(
format!("Hello, {number:>8.*}", 3, number = 123.45),
"Hello, 123.450"
);
}
转义
字面量字符{
和}
可以通过添加相同字符进行转义,即{
字符使用{{
进行转义,而}
字符使用}}
进行转义。
assert_eq!(format!("Hello {{}}"), "Hello {}");
assert_eq!(format!("{{ Hello"), "{ Hello");
语法顺序
format_string := text [ maybe_format text ] *
maybe_format := '{' '{' | '}' '}' | format
format := '{' [ argument ] [ ':' format_spec ] '}'
argument := integer | identifier
format_spec := [[fill]align][sign]['#']['0'][width]['.' precision]type
# 填充字符串
fill := character
# 对齐方式
align := '<' | '^' | '>'
# 正负号
sign := '+' | '-'
# 宽度
width := count
# 精度
precision := count | '*'
# 输出类型
type := '' | '?' | 'x?' | 'X?' | identifier
count := parameter | integer
parameter := argument '$'
格式化的trait
- nothing =>
Display
?
=>Debug
x?
=>Debug
带有小写十六进制整数X?
=>Debug
带有大写十六进制整数o
=>Octal
, 八进制x
=>LowerHex
, 小写十六进制X
=>UpperHex
, 大写十六进制p
=>Pointer
, 指针b
=>Binary
, 二进制e
=>LowerExp
,小写e的科学计数法E
=>UpperExp
,大写E的科学计数法
Debug trait
Debug
是为开发者调试打印数据结构所设计的,在使用的时候,Debug 用 {:?}
来打印。在大多数情况下,建议使用 #[derive(Debug)]
派生宏直接生成就足够了。
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
Display trait
Display
是给用户显示数据结构所设计的,在使用的时候,Display 用 {}
打印。自定义类型的Display
必须自己实现。
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
实现示例:
#[allow(dead_code)]
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "name: {}, age: {}", self.name, self.age)
}
}
fn main() {
let user = User {
name: "John".to_string(),
age: 20,
};
// 输出: name: John, age: 20
println!("{}", user);
}