std1.97.1——fmt模块总览

目录

  • [0. 准备](#0. 准备)
    • [0.1 查看当前toolchain的std文档](#0.1 查看当前toolchain的std文档)
    • [0.2 语法和词法结构](#0.2 语法和词法结构)
  • [1. std::fmt](#1. std::fmt)
  • [2. 格式化字符串](#2. 格式化字符串)
    • [2.1 位置参数](#2.1 位置参数)
    • [2.2 命名参数](#2.2 命名参数)
    • [2.3 格式化参数](#2.3 格式化参数)
      • [2.3.1 宽度](#2.3.1 宽度)
      • [2.3.2 填充/对齐](#2.3.2 填充/对齐)
      • [2.3.3 标志](#2.3.3 标志)
      • [2.3.4 精度](#2.3.4 精度)
      • [2.3.5 本地化](#2.3.5 本地化)
      • [2.3.6 转义](#2.3.6 转义)
  • [3. 语法](#3. 语法)
  • [4. Trait](#4. Trait)
    • [4.1 实现Display trait](#4.1 实现Display trait)
    • [4.2 Display与Debug的区别](#4.2 Display与Debug的区别)
  • [5. 宏](#5. 宏)
    • [5.1 write!宏](#5.1 write!宏)
    • [5.2 print!宏](#5.2 print!宏)
    • [5.3 eprint!](#5.3 eprint!)
    • [5.4 format_args!](#5.4 format_args!)
  • 参考

0. 准备

0.1 查看当前toolchain的std文档

  命令:rustup doc --std

0.2 语法和词法结构

1. std::fmt

  地位:用于格式化与打印字符串的工具集。

  本模块包含了对format!语法扩展的运行时支持。该宏由编译器实现,会生成对本模块的调用,以便在运行时将参数格式化为字符串。

2. 格式化字符串

  格式化字符串语法,适用于这些宏:format!、write!、writeln!、print!、println!、eprint!、eprintln!、format_args!。

  format!宏旨在让熟悉C语言的printf和fprintf函数或Python的str.format函数的开发者感到易于上手。

  format!宏返回一个String,下面仅是为了将format!构建的String输出到控制台才调用println!宏:

rust 复制代码
fn main() {
	println!("{}", format!("Hello"));
	println!("{}", format!("Hello, {}!", "world"));
	println!("{}", format!("The number is {}", 1));
	println!("{}", format!("{:?}", (3, 4)));
	println!("{}", format!("{value}", value=4));
	let people = "Rustaceans";
	println!("{}", format!("Hello {people}!"));
	println!("{}", format!("{} {}", 1, 2));
	println!("{}", format!("{:04}", 42));
	println!("{}", format!("{:#?}", (100, 200)));
}

  format!宏的第一个参数是格式化字符串,它是字符串字面量,不能是传入的变量(以便编译器进行有效性检查)。随后编译器会解析格式字符串,并判断所提供的参数列表是否符合传递给该格式字符串。

  若要将单个值转换为字符串,请使用to_string方法。要使用to_string,该值必须实现当前模块的Display trait。

2.1 位置参数

  每个格式化占位符都可以指明它引用的是参数列表中的第几个参数,如果省略,则认为它引用参数列表中的下一个参数。例如,格式化字符串{} {} {}会使用参数列表中的三个参数,并按照给出的顺序格式化它们。而格式化字符串{2} {1} {0}则会按相反顺序格式化它们。

  一旦开始混用显式编号和隐式顺序这两种位置指定方式,事情就完全不一样了:可以下一个值这种指定方式看作一个在值列表上移动的迭代器。每次遇到一个{},迭代器在值列表上向后移动一步(索引增1)。具体例子如下:

rust 复制代码
fn main() {
	println!("{}", format!("{1} {} {0} {}", 1, 2));
}

  对上面这个例子的处理:

  1. 遇到{1}时,直接取参数列表索引为1处的值2。

  2. 遇到{}时,直接获取内部隐式迭代器当前索引位置0处的值1,向后移动一步,内部隐式迭代器索引变为1。

  3. 遇到{0}时,直接取参数列表索引为0处的值1。

  4. 遇到{}时,直接获取内部隐式迭代器当前索引位置1处的值2,向后移动一步,内部隐式器此时已指向参数列表末尾,之后无法再获取值(若无更多格式占位符则无问题)。

  格式化字符串必须使用参数列表中的所有参数,否则编译会报错。可以在格式化字符串中多次引用参数列表中的同一个参数。

2.2 命名参数

  Rust本身并不像Python一样,函数中拥有关键字参数。format!宏作为语法扩展,可以利用命名参数。参数列表末尾可以给出命名参数对应的值,以identifier = expression的形式。

  以下是format!宏使用命名参数的例子:

rust 复制代码
fn main() {
	println!("{}", format!("{argument}", argument = "test"));
	println!("{}", format!("{name} {}", 1, name = 2));
	println!("{}", format!("{a} {c} {b}", a="a", b='b', c=3));
}

  如果命名参数没有出现在参数列表中,format!会引用当前作用域中同名的变量:

rust 复制代码
fn make_string(a: u32, b: &str) -> String {
	format!("{b} {a}")
}

fn main() {
	let argument = 2 + 2;
	println!("{}", format!("{argument}"));
	println!("{}", make_string(927, "label"));
}

  提供的命名参数若未被格式化字符串使用,会导致编译错误。

2.3 格式化参数

  每个被格式化的参数可以通过若干格式化参数进行转换(对应format_spec格式说明)。这些格式化参数会影响被格式化内容最终呈现的字符串形式。

  格式语法中的冒号:用于分隔输入数据的标识符与格式化参数,冒号本身不改变任何内容,仅用于引入后续的格式化参数。

rust 复制代码
fn main() {
	let a = 5;
	let b = &a;
	println!("{a:e} {b:p}");
}

2.3.1 宽度

rust 复制代码
fn main() {
	println!("Hello {:5}!", "x");
	println!("Hello {:1$}!", "x", 5);
	println!("Hello {1:0$}!", 5, "x");
	println!("Hello {:width$}!", "x", width = 5);
	let width = 5;
	println!("Hello {:width$}!", "x");
}

  这是一个用于指定格式化内容所占最小宽度的参数。如果值的字符串长度不足以填满这么多字符,则会使用下一节填充/对齐指定的填充字符来补足所需空间。

  宽度也可以通过引用参数列表中的另一个参数来动态提供,只需在参数名后面加后缀。使用{:N}可以引用参数列表中第N个位置的参数,N为从0开始的整数,或使用{:name$}来引用参数列表中的命名参数。参数列表被宽度引用的参数类型必须是usize。

  使用带$的语法引用参数列表中的参数不会影响下一个参数计数器,因此通常建议按位置引用参数列表中的参数,或者直接使用命名参数。

2.3.2 填充/对齐

rust 复制代码
fn main() {
	println!("Hello {:<5}!", "x");
	println!("Hello {:-<5}!", "x");
	println!("Hello {:^5}!", "x");
	println!("Hello {:>5}!", "x");
}

  可选的填充字符和对齐方式通常与宽度参数配合使用。它们必须定义在宽度之前,紧跟在冒号:之后。这表示当被格式化的值宽度小于指定宽度时,会在其周围额外填充一些字符。

  填充支持以下不同的对齐方式:

  fill<:参数在宽度列中左对齐。

  fill^:参数在宽度列中居中对齐。

  fill>:参数在宽度列中右对齐。

  对于居中对齐来说,宽度 N N N为奇数,左右填充字符的数量均为 N − 1 2 \frac{N-1}{2} 2N−1;宽度 N N N为偶数,左边填充字符数量为 N 2 − 1 \frac{N}{2}-1 2N−1,右边填充字符数量为 N 2 \frac{N}{2} 2N。

rust 复制代码
fn main() {
	println!("Hello {:-^5}!", "x");
	println!("Hello {:-^6}!", "x");
}

  非数值类型的默认填充字符为空格,默认对齐方式为左对齐。数值类型的默认填充字符同样为空格,默认对齐方式为右对齐。如果为数值类型指定了0标志,则隐式填充字符变为0。

rust 复制代码
fn main() {
	println!("Hello {:5}!", "x");
	println!("Hello {:5}!", 1);
}

  某些类型可能并未实现对齐功能,尤其是Debug trait通常不支持对清。确保正确应用填充的一个好方法是:先格式化输入内容,然后对得到的字符串进行填充,以获得最终输出:

rust 复制代码
fn main() {
	println!("Hello {:^15}!", format!("{:?}", Some("hi")));
}

2.3.3 标志

rust 复制代码
fn main() {
	println!("Hello {:+}!", 5);
	println!("{:#x}!", 27);
	println!("Hello {:05}!", 5);
	println!("Hello {:05}!", -5);
	println!("{:#010x}!", 27);
}

  这些标志用于改变格式化器的行为。

  +:用于数值类型,表示始终打印正负号。有符号数值的正号和符号都会被打印,无符号数值打印正号。

  #:此标志应使用"替代"形式的打印。替代形式包括:

    #?:美化Debug的格式化打印(添加换行和缩进)。

    #x:在参数前加0x。

    #X:在参数前加0x。

    #b:在参数前加0b。

    #o:在参数前加0o。

rust 复制代码
fn main() {
    println!("{:#?}", (100, 200));
    println!("{:#x}", 255);
    println!("{:#X}", 255);
    println!("{:#b}", 10);
    println!("{:#o}", 64);
}

  0:用于指示数值格式化时,应使用0字符填充至width,且填充需要感知符号。格式{:08}对于整数1会产生00000001,对于整数-1会产生-0000001。填充零始终放在符号(如果有)之后、数字(宽度的数字)之前。与#标志一起使用的时:填充0在前缀之后、数字之前。前缀包含在总宽度中。此标志会覆盖前面的填充字符和对齐,使其不产生效果。

rust 复制代码
fn main() {
    println!("{:+05}", 5);
    println!("{:+05}", -2);
    println!("{:+06}", 5.1);
}

2.3.4 精度

  对于非数字类型,这可以被视为最大宽度。如果字符串长于此宽度,则会被截断到这么多字符,并且在设置了填充、对齐和宽度参数时,截断后的值会以正确的填充、对齐和宽度输出。

rust 复制代码
fn main() {
    println!("{:+>10.7}", "Hello world from Rust");
}

  对于整数类型,精度被忽略。

  对于浮点类型,精度指示应打印小数点后多少位数字。

  有三种方式可以指定所需的精度:

  1. 整数.N:N本身为精度。

  2. .N/name$:N引用参数列表中对应位置的参数,name引用参数列表中的命名参数。

  3. .*:

  使用形如{:<spec>.*}的格式字符串,其中<spec>指的是其他参数,会消耗隐式迭代器当前及其后的两个输入:当前位置的输入作为usize精度,下一个输入才是要打印的值。

  使用形如{<arg>:<spec>.*}的格式字符串,则<arg>显式指明要打印的值,精度则取自隐式迭代器当前位置的输入。

rust 复制代码
fn main() {
    // 遇到{0},位置参数对应位置0处的"x"
    // 遇到{1:.5},1在冒号之前是位置参数,对应位置1处的0.01,.5对应精度5
    println!("Hello {0} is {1:.5}", "x", 0.01);

    // 遇到{1},位置参数对应位置1处的"x"
    // 遇到{2:.0$},2在冒号之前是位置参数,对应位置2处的0.01,.0$表示精度取位置0处的5
    println!("Hello {1} is {2:.0$}", 5, "x", 0.01);

    // 遇到{0},位置参数对应位置0处的"x"
    // 遇到{2:.1$},2在冒号之前是位置参数,对应位置2处的0.01,.1$表示精度取位置1处的5
    println!("Hello {0} is {2:.1$}", "x", 5, 0.01);

    // 遇到{},隐式迭代器当前位置0,对应"x"
    // 遇到{:.*},符合{:<spec>.*},隐式迭代器当前位置1,精度对应5,
    //                           下一个位置2对应的0.01是打印的值
    println!("Hello {} is {:.*}",    "x", 5, 0.01);

    // 遇到{1},位置参数对应位置1处的"x"
    // 遇到{2:.*},符合{<arg>:<spec>.*},2在冒号前指明打印的值为位置2处的0.01
    //                                 隐式迭代器当前位置0,精度取位置0处的5
    println!("Hello {1} is {2:.*}",  5, "x", 0.01);

    // 遇到{},隐式迭代器当前位置0,对应"x"
    // 遇到{2:.*},符合{<arg>:<spec>.*},2在冒号前指明打印的值为位置2处的0.01,
    //                                 隐式迭代器当前位置1,精度取位置1处的5
    println!("Hello {} is {2:.*}",   "x", 5, 0.01);

    // 遇到{},隐式迭代器当前位置0,对应"x"
    // 遇到{number:.prec$},number在冒号前是命名参数对应0.01,.prec$表示精度取命名参数prec的值5
    println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01);
}
rust 复制代码
fn main() {
    // 遇到{}, 隐式迭代器当前位置0,对应"Hello"
    // 遇到{name:.*},匹配{<arg>:<spec>.*},name在冒号之前是命名参数对应1234.56
    //                                    隐式迭代器当前位置1,精度取位置1处的3
    println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56);
    // 遇到{},隐式迭代器当前位置0,对应"Hello"
    // 遇到{name:.*},匹配{<arg>:<spec>.*},name在冒号之前是命名参数对应"1234.56"
    //                                    隐式迭代器当前位置1,精度取位置1处的3
    // 精度对应打印值的类型是非数值类型,产生截断,截断后打印的值是"123"
    println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56");
    // 遇到{},隐式迭代器当前位置0,对应"Hello"
    // 遇到{name:>8.*},匹配{<arg>:<spec>.*},name在冒号之前是命名参数对应"1234.56"
    //                                      >8指明右对齐,宽度为8
    //                                      隐式迭代器当前位置1,精度取位置1处的3
    // 精度对应打印值的类型是非数值类型,产生截断,截断后打印的值是"123"
    // 最终{name:>8.*}为     123
    println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56");
}

  在截断浮点数时,Rust使用四舍六入五成双,这是IEEE 754中的默认舍入模式。

  四舍六入五成双:

  1. 末位<5:舍去。

  2. 末位>5:进一。

  3. 末尾=5:看前一位,奇数进一,偶数舍去。

rust 复制代码
fn main() {
    println!("{0:.1$e}", 12345, 3);
    println!("{0:.1$e}", 12355, 3);
}

2.3.5 本地化

  Rust标准库提供的格式化函数没有任何区域概念,无论用户配置如何,在所有系统上都会产生相同的结果。

2.3.6 转义

  字面字符{和}可以通过在前面加上相同的字符来包含在字符串中。

rust 复制代码
fn main() {
	println!("Hello {{}}");
	println!("{{ Hello");
}

3. 语法

  格式化语法的文法:

复制代码
format_string := text [ maybe_format text ] *
maybe_format := '{' '{' | '}' '}' | format
format := '{' [ argument ] [ ':' format_spec ] [ ws ] * '}'
argument := integer | identifier

format_spec := [[fill]align][sign]['#']['0'][width]['.'precision][type]
fill := character
align := '<' | '^' | '>'
sign := '+' | '-'
width := count
precision := count | '*'
type := '?' | 'x?' | 'X?' | 'o' | 'x' | 'X' | 'p' | 'b' | 'e' | 'E'
count := parameter | integer
parameter := argument '$'

  text蹦年包含任何{或}。

  ws是Rust定义的空白字符。

  integer是一个十进制整数,可包含前导零且必须能放入usize类型。

  identifier是Rust定义的IDENTIFIER_OR_KEYWORD,不能是单个下划线。

rust 复制代码
fn main() {
    // {{ -> {
    // {name:-<0$.pre$},name在冒号前是命名参数对应"test",
    //                   -是填充字符,<是左对齐,0$是宽度对应位置0处的值10
    //                   .pre$是精度,pre是精度引用的命名参数,对应值3
    //                   name不是数值类型,会因精度产生截断,截断后的值为tes
    // }} -> }
    println!("{{ {name:-<0$.pre$} }}", 10, name="test", pre=3);
}

4. Trait

  格式占位符中的每个type对应一个特定的格式化Trait。type到trait的映射如下:

  未指定 => Display

  ? => Debug

  x? => Debug(仅对整数触发,十六进制,小写)

  X? => Debug(仅对整数触发,十六进制,大写)

  o => Octal

  x => LowerHex

  X => UpperHex

  p => Pointer

  b => binary

  e => LowerExp

  E => UpperExp

  这意味着,任何实现了fmt::Binary trait的类型都可以用{:b}格式化。标准库为许多原始类型实现了这些trait。当未指定type时,使用的trait是Display。

  为自己的类型实现格式化trait时,你需要实现以下签名的方法:

rust 复制代码
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result (

  你的类型将按引用作为self传入,然后函数应向实现了fmt::Write的Formatter f输出内容。每个格式化trait的实现都有责任正确遵守所请求的格式化参数。这些参数的值可以通过Formatter结构体的方法访问。为了帮助完成此工作,Fromatter结构体还提供了一些辅助方法。

  此外,此函数的返回值是fmt::Result,它是Result<(),std::fmt::Error>的别名。格式化本身不会失败,类型不匹配、精度写错在编译时就解决了。返回Result的唯一原因是最终要向底层输出流写入,而写入可能失败,fmt::Error就是用来传递这个失败的。所以你的实现只需要做一件事:调用Formatter的方法时它返回Err就原样转发,其余情况返回Ok,不要自行凭空返回Err。

4.1 实现Display trait

rust 复制代码
use std::fmt;

#[derive(Debug)]
struct Vector2D {
    x: isize,
    y: isize,
}

impl fmt::Display for Vector2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // `f` 实现了 `Write` trait,这正是 write! 宏所期望的。
        // 请注意,此格式化忽略了格式字符串提供的各种标志。
        write!(f, "({}, {})", self.x, self.y)
    }
}

// 不同的 trait 允许类型的不同输出形式。
// 此格式的含义是打印向量的模。
impl fmt::Binary for Vector2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let magnitude = (self.x * self.x + self.y * self.y) as f64;
        let magnitude = magnitude.sqrt();

        // 使用 Formatter 对象上的辅助方法 `pad_integral`
        // 来遵守格式化标志。详见方法文档,
        // `pad` 函数可用于填充字符串。
        let decimals = f.precision().unwrap_or(3);
        let string = format!("{magnitude:.decimals$}");
        f.pad_integral(true, "", &string)
    }
}

fn main() {
    let myvector = Vector2D { x: 3, y: 4 };

    println!("{myvector}");       // => "(3, 4)"
    println!("{myvector:?}");     // => "Vector2D {x: 3, y:4}"
    println!("{myvector:10.3b}"); // => "     5.000"
}

4.2 Display与Debug的区别

  Display实现断言该类型始终可以忠实地表示为UTF-8字符串。并非期望所有类型都实现Display trait。

  Debug实现应为所有公开类型实现。输出通常应尽可能忠实地表示内部状态。Debug trait的目的是方便调试Rust代码。在大多数情况下,使用#derive(Debug)就足够了,也是推荐的做法。

5. 宏

5.1 write!宏

  write!和writeln!是两个用于将格式化字符串输出到指定流的宏。用于避免格式字符串的中间分配,直接写入输出。在底层,此函数实际调用的是std::io::Write和std::fmt::Write trait上定义的write_fmt函数。

rust 复制代码
use std::fmt::Write;

fn main() {
    let mut w = String::new();
    write!(&mut w, "Hello {}!", "world").unwrap();
    println!("{:?}", w);
}

5.2 print!宏

  此宏和println!将输出发送到stdout。与write!宏类似,这些宏的目标是在打印输出时避免中间分配。

rust 复制代码
fn main() {
	print!("Hello {}!", "world");
	println!("I have a newline {}", "character at the end");
}

5.3 eprint!

  eprint!和eprintln!宏分别与print!和println!相同,区别在于它们将输出发送到stderr。

5.4 format_args!

  format_args!是一个奇特的宏,用于安全地传递一个描述格式字符串的不透明对象。此对象不需要任何堆分配即可创建,且引用栈上的信息。在底层,所有宏都是基于它实现的。

rust 复制代码
use std::fmt;
use std::io::{self, Write};

fn my_fmt_fn(args: fmt::Arguments<'_>) {
    write!(&mut io::stdout(), "{args}").unwrap();
}

fn main() {
    let mut some_writer = io::stdout();
    write!(&mut some_writer, "{}", format_args!("print with a {}", "macro")).unwrap();
    my_fmt_fn(format_args!(", or a {} too", "function"));
}

  format_args!宏的结果是fmt::Arguments类型的值。然后可以将此结构体传递给本模块的write和format函数以处理格式字符串。此宏的目标是在处理格式化字符串时进一步避免中间分配。

参考

1、Rust std::fmt

相关推荐
SomeB1oody3 小时前
【RustyML入门】5.3. 聚类指标
开发语言·后端·机器学习·rust·教程
雾沉川4 小时前
Rust IDE 选型:除 VS Code 之外,RustRover 非商业场景可免费使用
ide·vscode·rust
程序员爱钓鱼5 小时前
Go 编程实战:Map——使用 Key-Value 管理键值数据
后端·rust·go
程序员爱钓鱼6 小时前
Rust Trait Object详解:dyn Trait与动态分发
后端·面试·rust
蒜香可达鸭18 小时前
Grok-build: Rust 工程实践分析
rust
SomeB1oody1 天前
【RustyML入门】5.2. 分类指标
开发语言·后端·机器学习·rust·教程
星栈1 天前
被 Rust async 纠正的三个异步认知
前端·后端·rust
smallswan1 天前
《Rust七十二变》开源了
开发语言·后端·rust·ai编程
深海呐1 天前
Rust 语言完整解读
java·开发语言·rust