ex11: 打印与 Debug

恭喜各位坚持到这里的人,我们现在已经学习完了所有基本的数据类型了。虽然可以有很多的内容还不是很理解,不是很明白,不过好不容易已经看到这里了,我们就在中途先休息一下。让我们把之前的知识好好消化一下,讲一些略微轻松愉快的事情:打印。

我们在 ex3 就已经提到过打印的方法,在之前的练习中也大量出现了使用println!宏将数据打印到命令行的方法,练习中还出现了使用"{:?}"的形式进行格式化的 Debug 输出形式。我们在当时没有进行深入的说明。在这一章中,有了之前的变量和类型的基础,我们可以使用更多更好的方法进行打印。

打印的来源

我们现在使用的打印宏println!来自std crate(Rust 标准库)的 std::fmt module。Rust 默认可以使用标准库的各种函数、类型以及宏,这使得我们可以比较方便的进行编辑和输出。

我们现在看到的所有格式化输出都需要归功于std::fmt::format!宏,这个宏可以将我们给出的格式化语句转化为一个String类型的变量,或者说一个可以变化的字符串。其余的宏利用相关的特性将格式化的字符串输出到特定的终端上。

目前的std::fmt宏包括:

  • format!:将格式化的字符串输出到一个String类型的变量中

  • write!,writeln!:向指定的流中输入格式化字符串,后者会在每次输出的最后加入换行

  • print!,println!:将格式化的字符串输出到标准输入输出流中,后者在每次输出时会换行

  • eprint!,eprintln!:将格式化的字符串输出到标准错误流中,后者在每次输出的时候会换行

  • format_args!:安全地传递格式化字符串相关的参数

write!我们还没有使用过,在介绍trait的概念之后我们就会看到它。而eprint!format_args!我们可能暂时不会用到。

使用 format! 进行格式化

使用format!进行格式化和之前使用println!的方法一致,不过其结果为一个String类型的变量

let string: String = format!("Hello World from {}", "Mike");
println!("{}", string);

指定打印的参数位置

现在的打印有一个很严重的问题,就是如果字符串中需要填写的空位很多,就很有可能让人感到非常的混乱。

println!("a long list: [{}, {}, {}, {}, {}, {}]", x1, x2, x3, x1, x2, x3);

如果其中还有一些元素要求是重复的就更加难受了。所以我们有两种方法来指定打印的参数在字符串中的位置。

第一种方法是使用数字进行标注,每个数字代表第几个参数,从 0 开始计数

println!("a long list: [{0}, {1}, {2}, {0}, {1}, {2}]", x1, x2, x3);

第二种方式是通过变量名进行绑定,在格式化字符串中写出绑定的变量名字,并且在后面进行赋值

println!("a long list: [{first}, {second}, {third}, {first}, {second}, {third}]",
    first=x1,
    second=x2,
    third=x3
);

格式化参数

我们现在把格式化的参数拿出来仔细观察一下(来自fmt官方文档: Syntax):

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 := identifier | '?' | ''
count := parameter | integer
parameter := argument '$'

好像一下子冒出来了一堆无法理解的符号。没关系,我们拆开来一点点说。

format_string说的就是我们那个充满了括号和字符串的"Hello World {}, {:?}"这样的格式化用的字符串。

format就是重点了,它指定了一个格式可以写成什么样子,我们来一点一点组建一个“完全体”的格式化字符串:

首先写一个使用变量名打印$\pi$的函数

println!("{argument}", argument = 3.1415926);
3.1415926

接下来我们为其增加一下精度,在format_spec中描述的['.' precision]便是我们要添加的参数的位置。(注:请注意不要手抖打上一个空格哦)

println!("{argument:.4}", argument=3.1415926);
3.1416

再接下来,我们为其增加添加一下符号,把[sign]区域填充上内容。这代表为每一个正数添加一个开头的'+'符号。

println!("{argument:+.4}", argument=3.1415926);
+3.1416

我们现在为其增加一个宽度,在[width]字段加入一个数字来描述字符串的最小宽度。这里也可以使用一个变量来表示,不过变量的结尾需要使用'$'来进行说明

println!("{argument:+width$.4}", argument=3.1415926, width=10);
   +3.1416

可以发现打印出来字符串前面多了若干空格。如果你觉得这样靠右的输出并不好看,你可以指定排版的方式。在[align]字段中进行指定。分别是:'<'靠左,'^'居中,'>'靠右。

println!("{argument:^+width$.4}", argument=3.1415926, width=10);
 +3.1416

如果在使用排版后希望换一个填充的方法,则可以在[fill]字段中填入想要用于填充的字符(否则默认使用空格)。在使用[align]字段之前不能指定填充的字符。

println!("{argument:*^+width$.4}", argument=3.1415926, width=10);
*+3.1416**

我们现在加入一个新的字段['0'],这代表你在显示数字的时候要求把最高的位都填充为0。对于我们只有数字的显示的情况下,这会覆盖掉[[fill]align]字段所描述的排版方法。

println!("{argument:*^+0width$.4}", argument=3.1415926, width=10);
+0003.1416

我们最后加入两个字段。['#']字段会让打印出来的数据更加好看易懂,比如说让 Debug 输出的东西排版更好,在各种进制的数字前面加上进制表示符号。[type]字段制定了打印的方式,使用'?'便是指定 Debug 打印,使用一些其他的符号可以使用不同进制或是大小写格式进行输出。具体符号请参考官方文档

println!("{argument:*^+#0width$.4?}", argument=3.1415926, width=10);
+0003.1416

恭喜你已经尝试过所有的格式化参数了,在未来的使用过程中可以参考文档自由地格式化字符串。

使用 Debug 格式化

在我们使用{}的形式进行格式化字符串时,我们实际上利用了元素的fmt::Display``trait获得其对应输出的字符串。trait的概念你现在可以认为是一种特征,比较像一些编程语言的“接口”的概念,不过其中的函数必须有实现。但是并不是所有的类型都实现了fmt::Display的属性,那么它们自然也就不能使用{}进行输出。

在上面的格式化参数部分我们也提到过{:?}实际的意思是指定字符串使用 Debug 的方式进行格式化。它使用的trait切换为fmt::Debug,这个trait已经被所有标准库函数继承,也就是所有的标准类型都可以进行 Debug 格式化,比如说元组和数组就是需要使用 Debug 进行格式化。

而自定义类型structenum是不会默认继承这个特性(trait)的,所以我们需要手动加上#[derive(Debug)]的描述,让其支持 Debug 格式化。(structenum已经有了默认的 Debug 格式化实现)

#[derive(Debug)]
struct Pair(i32, i32);
fn main(){
    println!("{:?}", Pair(2, 3)); // 使用 Debug 进行格式化
    println!("{:#?}", Pair(2, 3)); // 让 Debug 格式化更好看
}
Pair(2, 3)
Pair(
    2,
    3,
)

在我们介绍了trait之后我们就会讲解如何去让自定义类型也支持正常的{}格式化。

代码

fn main() {
    // 若干打印使用的宏
    let format_string: String = format!("Hello, {}", 233);
    print!("{}", format_string);
    println!("Hello World!");
    eprint!("Hello");
    eprintln!("World");

    // 使用数字指定打印的位置
    println!("{0}长,{1}宽。{1}没有{0}长,{0}没有{1}宽。", "扁担", "板凳");
    // 使用变量指定打印的位置
    println!("A hamburger has a structure of: {upper}|{center}|{lower}",
        upper="bread",
        center="meat",
        lower="bread"
    );

    // 格式化参数示意
    println!("{argument:*^+#0width$.4?}", argument=3.1415926, width=10); // 完全体参数示意
    println!("128 == 0b{:b}", 128); // 使用2进制进行打印,并且在前面加上0b前缀
    println!("27 == {:#x}", 27); // 使用16进制打印整数
    println!("{:*^20}", format!("{{Hello,}} {:+#08x}", 233)); // 各种参数的综合运用

    #[derive(Debug)]
    struct Pair(i32, i32);
    println!("{:?}", Pair(2, 3)); // 使用 Debug 进行格式化
    println!("{:#?}", Pair(2, 3)); // 让 Debug 格式化更好看
}

本节总结

在本节中,我们重点说明了格式化打印的相关宏以及打印参数,也说明了一般的打印以及 Debug 打印的区别。在之后的章节中我们会介绍如何实现 Display 特性来使用{}进行格式化打印。

你应该对以下内容有所掌握:

  1. 熟练地使用格式化字符串进行打印和 Debug 打印

  2. 可以使用数字或变量指定格式化字符串中变量的位置

你应该对以下内容有所了解:

  1. 若干没有使用过的格式化输出宏

  2. 为什么struct,enum需要继承 Debug 特性才能进行 Debug 打印

  3. 之前没有使用过的格式化参数

参考资料

最后更新于