format! 可以说是我们日常格式化字符串过程中最常用的宏了,但每次想要使用相对复杂一点的功能时总要搜索一番,不大方便。因此便有了这篇文章,在学习的同时方便今后的查阅。
本文假定读者已经使用并了解过诸如 println!、format! 这些宏,在有一定基础的条件下从语法层面往下推,介绍 std::fmt 目前支持的格式语法。
ToC
格式语法
目前(2021-02-24)的语法如下所示[1]:
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 := characteralign := '<' | '^' | '>'sign := '+' | '-'width := countprecision := count | '*'type := identifier | '?' | ''count := parameter | integerparameter := argument '$'从上面的语法中,我们可以总结出一些基本的规律:
- 格式化部分通过
{}包裹 - 大括号本身通过重复两次进行转义
- 格式化部分由
argument和format_spec组成,中间由:分隔
在这些基本元素的基础上,我们慢慢向下挖掘。
位置参数与命名参数
argument 中的 integer 和 identifier。
位置参数(Positional parameters)
通过指定参数的位置,我们可以向某一个位置指定填充第 [n] 个参数。如下所示:
fn main() { println!("{}{}{} {1}{1}{4}{5}{1}{4} {}{}{}", 0, 1, 2, 3, 4, 5);}输出如下:
012 114514 345可以看到,中间正确地输出了 114514;而两边没有指定位置的输出项则是按照参数的原本顺序输出了对应的内容。
命名参数(Named parameters)
通过指定参数的名称,我们可以更方便地理解格式化输出的结构,如下所示:
fn main() { println!("Hello, {username}! Welcome to {app_name}!", username = "Yesterday17", app_name = "std::fmt", );}毫无疑问,输出如下所示:
Hello, Yesterday17! Welcome to std::fmt!有无 argument 之间的关系
从上面的例子中,可以看出:无论是哪一种参数,其本质都只是非顺序格式化,与无 argument 的顺序格式化相对。有无 argument 的格式化过程不会互相干扰。
格式参数(Formatting Parameters)
格式参数对应的就是上文语法中的 format_spec 了。方便起见,我们把对应的语法再贴一遍:
format_spec := [[fill]align][sign]['#']['0'][width]['.' precision][type]可以看到,format_spec 中又分为了好几个部分,我们一个个来看:
宽度(Width)
宽度用于指定参数输出时的(字符串)长度(或者直接叫宽度),使用了如下语法:
width := countcount := parameter | integerparameter := argument '$'可以看到,count 有 integer 和 paramater 两种选项。前者用于直接在格式化字符串中指定宽度,而后者则可以通过 argument(在后面增加一个 **$**),在运行时指定宽度。
简单示例如下:
fn main() { println!("Hello, {username:5}! Welcome to {app_name:width$}!", username = "mmf", app_name = "std::fmt", width = 10, );}输出如下:
Hello, mmf ! Welcome to std::fmt !填充与对齐(Fill/Alignment)
填充与对齐增加了使用了如下语法:
format_spec := [[fill]align] ...fill := characteralign := '<' | '^' | '>'可以看到,fill 使用的是单个字符,而 align 使用的则是 **<^>** 三个字符中的任意一个。
fill 就是填充的字符;而 align 则是字符填充后原内容的对齐位置,对应左对齐、居中和右对齐。
修改一下上面的例子:
fn main() { println!("Hello, {username:-^5}! Welcome to {app_name:>>width$}!", username = "mmf", app_name = "std::fmt", width = 10, );}注意,区别在于在 username:5 的 : 后面增加了 -^,以 - 为 fill,^ 为 align;以及在 app_name:width$ 的 : 后面增加了 >>,以 > 为 fill,以 > 为 align。输出如下:
Hello, -mmf-! Welcome to >>std::fmt!符号(Sign)
在默认情况下,正数的正号(+)并不会随数字输出。如果想要输出,就需要通过符号来指定了。
符号位于 [[fill] align] 正后方,用于表示输出数字时需要强制输出符号。使用的语法如下:
sign := '+' | '-'可以看到,sign 可以选择 + 或者 -,但 - 目前只是保留,并没有实际效果。
值得一提的是,正负是基于 Signed trait 判断的。
精度(Precision)
在输出小数时,我们常常希望限定输出的精度。精度使用如下语法:
format_spec := ... ['.' precision] ...precision := count | '*'精度可以像宽度一样指定 count,即直接指定或参数指定;或者使用 * 符号,意为以下一个 {} 的参数作为精度。如下例所示:
fn main() { println!("number: {num:.prec$}", num = 114514.1919810, prec = 2); println!("number: {num:.*}", 5, num = 114514.1919810);}输出如下:
number: 114514.19number: 114514.19198数字 0
数字 0 位于 [width] 之前,表示输出项目为数字,且以 0 为 fill。填充的 0 只会出现在数字之前。如下例所示:
fn main() { println!("number: {positive:<+0width$}", positive = 114514, width = 10); println!("number: {positive:^+0width$}", positive = 114514, width = 10); println!("number: {positive:>+0width$}", positive = 114514, width = 10);
println!("number: {positive:<0width$}", positive = 114514.1919, width = 20); println!("number: {positive:^0width$.prec$}", positive = 114514.1919, width = 20, prec = 2); println!("number: {positive:>0width$.*}", 1, positive = 114514.1919, width = 20);}输出结果为:
number: +000114514number: +000114514number: +000114514number: 000000000114514.1919number: 00000000000114514.19number: 000000000000114514.2输出类型(Type)
输出类型位于整个格式参数的末尾,使用的语法如下:
format_spec := ... [type]type := identifier | '?' | ''可以看到,其规则规定了 **identifier**、**?** 或空,但实际的选项是下面这些:
| 内容 | 表示 |
|---|---|
| 空 | Diaplay trait |
? | Debug trait |
x? | Debug trait + 小写 16 进制数 |
X? | Debug trait + 大写 16 进制数 |
o | Octal trait |
x | LowerHex trait |
X | UpperHex trait |
p | Pointer trait |
b | Binary trait |
e | LowerExp trait |
E | UpperExp trait |
另一种输出格式(# )
在符号(Sign)和数字 0 之间增加一个 # 号,即可改变部分输出类型(Type)的输出格式。如下表所示:
| 内容 | 表示 |
|---|---|
#? | 格式化的 Debug trait 输出内容 |
#x、#X | 在十六进制数前增加 0x |
#b | 在二进制数前增加 0b |
#o | 在八进制数前增加 0o |