格式化输出
提到格式化输出,可能很多人立刻就想到 "{}"
,但是 Rust
能做到的远比这个多的多,本章节我们将深入讲解格式化输出的各个方面。
1. 满分初印象
先来一段代码,看看格式化输出的初印象:
1 2 3 4 5 6 7 println! ("Hello" ); println! ("Hello, {}!" , "world" ); println! ("The number is {}" , 1 ); println! ("{:?}" , (3 , 4 )); println! ("{value}" , value=4 ); println! ("{} {}" , 1 , 2 ); println! ("{:04}" , 42 );
可以看到 println!
宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式,包含其中形如
{}
的符号是占位符 ,会被
println!
后面的参数依次替换。
它们是 Rust 中用来格式化输出的三大金刚,用途如下:
print!
将格式化文本输出到标准输出,不带换行符
println!
同上,但是在行的末尾添加换行符
format!
将格式化文本输出到 String
字符串
在实际项目中,最常用的是 println!
及
format!
,前者常用来调试输出,后者常用来生成格式化的字符串:
1 2 3 4 5 6 7 fn main () { let s = "hello" ; println! ("{}, world" , s); let s1 = format! ("{}, world" , s); print! ("{}" , s1); print! ("{}\n" , "!" ); }
其中,s1
是通过 format!
生成的
String
字符串,最终输出如下:
1 2 hello, world hello, world!
2.1
eprint!
,eprintln!
除了三大金刚外,还有两大护法,使用方式跟
print!
,println!
很像,但是它们输出到标准错误输出:
1 eprintln! ("Error: Could not complete task" )
它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用
print!
系列。
3. {} 与
与其它语言常用的 %d
,%s
不同,Rust
特立独行地选择了 {}
作为格式化占位符(说到这个,有点想吐槽下,Rust
中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用
{}
来替代即可,剩下的类型推导等细节只要交给 Rust 去做。
与 {}
类似,{:?}
也是占位符:
{}
适用于实现了 std::fmt::Display
特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户
{:?}
适用于实现了 std::fmt::Debug
特征的类型,用于调试场景
其实两者的选择很简单,当你在写代码需要调试时,使用
{:?}
,剩下的场景,选择 {}
。
3.1 Debug
特征
事实上,为了方便我们调试,大多数 Rust 类型都实现了 Debug
特征或者支持派生该特征:
1 2 3 4 5 6 7 8 9 10 11 12 13 #[derive(Debug)] struct Person { name: String , age: u8 }fn main () { let i = 3.1415926 ; let s = String ::from ("hello" ); let v = vec! [1 , 2 , 3 ]; let p = Person{name: "sunface" .to_string (), age: 18 }; println! ("{:?}, {:?}, {:?}, {:?}" , i, s, v, p); }
对于数值、字符串、数组,可以直接使用 {:?}
进行输出,但是对于结构体,需要派生Debug
特征后,才能进行输出,总之很简单。
3.2 Display
特征
与大部分类型实现了 Debug
不同,实现了
Display
特征的 Rust
类型并没有那么多,往往需要我们自定义想要的格式化方式:
1 2 3 4 5 6 7 8 let i = 3.1415926 ;let s = String ::from ("hello" );let v = vec! [1 , 2 , 3 ];let p = Person { name: "sunface" .to_string (), age: 18 , };println! ("{}, {}, {}, {}" , i, s, v, p);
运行后可以看到 v
和 p
都无法通过编译,因为没有实现 Display
特征,但是你又不能像派生 Debug
一般派生
Display
,只能另寻他法:
使用 {:?}
或 {:#?}
为自定义类型实现 Display
特征
使用 newtype
为外部类型实现 Display
特征
下面来一一看看这三种方式。
{:#?}
与 {:?}
几乎一样,唯一的区别在于它能更优美地输出内容:
1 2 3 4 5 6 7 8 9 10 11 // {:?} [1, 2, 3], Person { name: "sunface", age: 18 } // {:#?} [ 1, 2, 3, ], Person { name: "sunface", }
因此对于 Display
不支持的类型,可以考虑使用
{:#?}
进行格式化,虽然理论上它更适合进行调试输出。
3.3 为自定义类型实现
Display
特征]
如果你的类型是定义在当前作用域中的,那么可以为其实现
Display
特征,即可用于格式化输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Person { name: String , age: u8 , }use std::fmt;impl fmt ::Display for Person { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { write! ( f, "大佬在上,请受我一拜,小弟姓名{},年芳{},家里无田又无车,生活苦哈哈" , self .name, self .age ) } }fn main () { let p = Person { name: "sunface" .to_string (), age: 18 , }; println! ("{}" , p); }
如上所示,只要实现 Display
特征中的 fmt
方法,即可为自定义结构体 Person
添加自定义输出:
1 大佬在上,请受我一拜,小弟姓名sunface,年芳18,家里无田又无车,生活苦哈哈
3.4 为外部类型实现
Display
特征
在 Rust 中,无法直接为外部类型实现外部特征,但是可以使用newtype
解决此问题:
1 2 3 4 5 6 7 8 9 10 11 12 struct Array (Vec <i32 >);use std::fmt;impl fmt ::Display for Array { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { write! (f, "数组是:{:?}" , self .0 ) } }fn main () { let arr = Array (vec! [1 , 2 , 3 ]); println! ("{}" , arr); }
Array
就是我们的
newtype
,它将想要格式化输出的 Vec
包裹在内,最后只要为 Array
实现 Display
特征,即可进行格式化输出:
至此,关于 {}
与 {:?}
的内容已介绍完毕,下面让我们正式开始格式化输出的旅程。
4. 位置参数
除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如
{1}
,表示用第二个参数替换该占位符(索引从 0 开始):
1 2 3 4 5 6 7 fn main () { println! ("{}{}" , 1 , 2 ); println! ("{1}{0}" , 1 , 2 ); println! ("{0}, this is {1}. {1}, this is {0}" , "Alice" , "Bob" ); println! ("{1}{}{0}{}" , 1 , 2 ); }
5. 具名参数
除了像上面那样指定位置外,我们还可以为参数指定名称:
1 2 3 4 5 fn main () { println! ("{argument}" , argument = "test" ); println! ("{name} {}" , 1 , name = 2 ); println! ("{a} {c} {b}" , a = "a" , b = 'b' , c = 3 ); }
需要注意的是:带名称的参数必须放在不带名称参数的后面 ,例如下面代码将报错:
1 2 3 4 5 6 7 8 println! ("{abc} {1}" , abc = "def" , 2 ); error: positional arguments cannot follow named arguments --> src/main.rs:4 :36 | 4 | println! ("{abc} {1}" , abc = "def" , 2 ); | ----- ^ positional arguments must be before named arguments | | | named argument
6. 格式化参数
格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位:
1 2 3 4 5 6 7 fn main () { let v = 3.1415926 ; println! ("{:.2}" , v); println! ("{:.2?}" , v); }
上面代码只输出小数点后两位。同时我们还展示了 {}
和
{:?}
的用法,后面如无特殊区别,就只针对 {}
提供格式化参数说明。
接下来,让我们一起来看看 Rust 中有哪些格式化参数。
6.1 宽度
宽度用来指示输出目标的长度,如果长度不够,则进行填充和对齐:
字符串填充
字符串格式化默认使用空格进行填充,并且进行左对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn main () { println! ("Hello {:5}!" , "x" ); println! ("Hello {:1$}!" , "x" , 5 ); println! ("Hello {1:0$}!" , 5 , "x" ); println! ("Hello {:width$}!" , "x" , width = 5 ); println! ("Hello {:1$}!{}" , "x" , 5 ); }
数字填充:符号和 0
数字格式化默认也是使用空格进行填充,但与字符串左对齐不同的是,数字是右对齐。
1 2 3 4 5 6 7 8 9 10 fn main () { println! ("Hello {:5}!" , 5 ); println! ("Hello {:+}!" , 5 ); println! ("Hello {:05}!" , 5 ); println! ("Hello {:05}!" , -5 ); }
6.2 对齐
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { println! ("Hello {:<5}!" , "x" ); println! ("Hello {:>5}!" , "x" ); println! ("Hello {:^5}!" , "x" ); println! ("Hello {:&<5}!" , "x" ); }
6.3 精度
精度可以用于控制浮点数的精度或者字符串的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let v = 3.1415926 ; println! ("{:.2}" , v); println! ("{:+.2}" , v); println! ("{:.0}" , v); println! ("{:.1$}" , v, 4 ); let s = "hi我是Sunface孙飞" ; println! ("{:.3}" , s); println! ("Hello {:.*}!" , 3 , "abcdefg" ); }
6.4 进制
可以使用 #
号来控制数字的进制输出:
#b
, 二进制
#o
, 八进制
#x
, 小写十六进制
#X
, 大写十六进制
x
, 不带前缀的小写十六进制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn main () { println! ("{:#b}!" , 27 ); println! ("{:#o}!" , 27 ); println! ("{}!" , 27 ); println! ("{:#x}!" , 27 ); println! ("{:#X}!" , 27 ); println! ("{:x}!" , 27 ); println! ("{:#010b}!" , 27 ); }
6.5 指数
1 2 3 4 fn main () { println! ("{:2e}" , 1000000000 ); println! ("{:2E}" , 1000000000 ); }
6.6 指针地址
1 2 let v = vec! [1 , 2 , 3 ];println! ("{:p}" , v.as_ptr ())
6.7 转义
有时需要输出
{
和}
,但这两个字符是特殊字符,需要进行转义:
1 2 3 4 5 6 7 8 9 10 fn main () { println! (" Hello \"{{World}}\" " ); }
7.
在格式化字符串时捕获环境中的值(Rust 1.58 新增)
在以前,想要输出一个函数的返回值,你需要这么做:
1 2 3 4 5 6 7 8 9 fn get_person () -> String { String ::from ("sunface" ) }fn main () { let p = get_person (); println! ("Hello, {}!" , p); println! ("Hello, {0}!" , p); println! ("Hello, {person}!" , person = p); }
问题倒也不大,但是一旦格式化字符串长了后,就会非常冗余,而在 1.58
后,我们可以这么写:
1 2 3 4 5 6 7 fn get_person () -> String { String ::from ("sunface" ) }fn main () { let person = get_person (); println! ("Hello, {person}!" ); }
是不是清晰、简洁了很多?甚至还可以将环境中的值用于格式化参数:
1 2 3 4 let (width, precision) = get_format ();for (name, score) in get_scores () { println! ("{name}: {score:width$.precision$}" ); }
但也有局限,它只能捕获普通的变量,对于更复杂的类型(例如表达式),可以先将它赋值给一个变量或使用以前的
name = expression
形式的格式化参数。 目前除了
panic!
外,其它接收格式化参数的宏,都可以使用新的特性。对于
panic!
而言,如果还在使用 2015版本
或
2018版本
,那 panic!("{ident}")
依然会被当成
正常的字符串来处理,同时编译器会给予 warn
提示。而对于
2021版本
,则可以正常使用:
1 2 3 4 5 6 7 fn get_person () -> String { String ::from ("sunface" ) }fn main () { let person = get_person (); panic! ("Hello, {person}!" ); }
输出:
1 2 thread 'main' panicked at 'Hello, sunface!', src/main.rs:6:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace