• print! 将格式化文本输出到标准输出,不带换行符
  • println! 用于向标准输出设备打印信息并在末尾自动添加换行符
  • format! 将格式化文本存入字符串。

在实际的项目中,最常用的还是 println!format! ,前者常用来调试输出,后者常用来生成格式化字符串。

1
2
3
4
5
6
fn main() {
let s = "hello";
println!("{}, world", s); // println!: 输出到终端,带换行符
let s1 = format!("{}, world", s); // format!: 将格式化文本存入字符串
print!("{}", s1); // print!: 输出到终端,不带换行符
}

输出结果:

1
2
hello, world
hello, world

eprint!eprintln!

  • eprint! 输出到标准错误输出,不带换行符
  • eprintln! 输出到标准错误输出,在末尾自动添加换行符
1
2
3
4
fn main() {
eprint!("Error: Could not complete task1"); // eprint!:输出到标准错误输出,不带换行符
eprintln!("Error: Could not complete task2"); // eprintln!:输出到标准错误输出,带换行符
}

输出结果:

1
2
Error: Could not complete task1Error: Could not complete task2

{} 格式化占位符

Rust 选择 {} 作为格式化占位符,无需再为特定的类型选择特定的占位符,统一用 {} 来替代,类型推导的细节则交给 Rust 编译器去做。

  • {} 是普通的占位符,它会将传入的值按照顺序进行替换。
  • {:?} 是调试占位符,除了将值进行替换以外,还会对值进行漂亮的打印,使得输出结果更易于阅读。
  • {:#?} 是更为详细的调试占位符,提供比 {:?} 更加深入的调试信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[derive(Debug)]
struct Apple {
x: i32,
y: char
}

fn main() {
let x = 42;
let y = "Hello, world!";
let z = Apple { x: 10, y: 'a' };

// 使用普通的占位符
println!("普通占位符: {}", x);
println!("普通占位符: {}", y);
// println!("普通占位符: {}", z); // `Apple` cannot be formatted with the default formatter

// 使用调试占位符
println!("调试占位符: {:?}", x);
println!("调试占位符: {:?}", y);
println!("调试占位符: {:?}", z);

// 使用详细的调试占位符
println!("详细调试占位符: {:#?}", x);
println!("详细调试占位符: {:#?}", y);
println!("详细调试占位符: {:#?}", z);
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
普通占位符: 42
普通占位符: Hello, world!
调试占位符: 42
调试占位符: "Hello, world!"
调试占位符: Apple { x: 10, y: 'a' }
详细调试占位符: 42
详细调试占位符: "Hello, world!"
详细调试占位符: Apple {
x: 10,
y: 'a',
}

Debug特性

Debug 是为开发者调试打印数据结构所设计的,在使用的时候,对于数值、字符串、数组,可以直接使用 {:?} 输出;但是对于结构体、元组和枚举,需要派生 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);
}

执行结果:

1
3.1415926, "hello", [1, 2, 3], Person { name: "sunface", age: 18 }

Display特性

Display 是给用户显示数据结构所设计的,在使用的时候,Display 用 {} 打印。与大部分类型实现了 Debug 特性不同,实现了 Display 特性的 Rust 类型没有那么多,自定义类型的 Display 必须自己实现。

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);

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
error[E0277]: `Vec<{integer}>` doesn't implement `std::fmt::Display`
--> demo.rs:14:38
|
14 | println!("{}, {}, {}, {}", i, s, v, p);
| ^ `Vec<{integer}>` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Vec<{integer}>`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: `Person` doesn't implement `std::fmt::Display`
--> demo.rs:14:41
|
14 | println!("{}, {}, {}, {}", i, s, v, p);
| ^ `Person` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Person`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0277`.

从运行结果来看:数组和结构体都没法通过编译,因为这两种类型没有实现 Display 特性。解决的方法有以下三种:

  • 使用 {:?}{:#?}
  • 为自定义类型实现 Display 特性。
  • 使用 newtype 为外部类型实现 Display 特性。
  • {:#?}{:?} 几乎一样,区别是 {:#?} 能够更优美地输出内容。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[derive(Debug)]
struct Person {
name: String,
age: u8
}

fn main() {
let v = vec![1, 2, 3];
let p = Person {
name: "sunface".to_string(),
age: 18,
};
// {:?}
println!("{:?}, {:?}", v, p);

// {:#?}
println!("{:#?}, {:#?}", v, p);
}

输出结果:

1
2
3
4
5
6
7
8
9
[1, 2, 3], Person { name: "sunface", age: 18 }
[
1,
2,
3,
], Person {
name: "sunface",
age: 18,
}

位置参数

每个格式化参数既可以按照顺序进行格式化,也可以按照指定的顺序进行格式化。

  • {}:没有指定索引位置,会按照参数迭代逐个使用。
  • {n}:指定了索引位置,则只用指定索引的参数,n 从 0 开始计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
}

具名参数

可以通过为参数指定名称来指定参数输出的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}");
}

注意:带名称的参数必须放在不带名称参数的后面。

1
println!("{abc} {1}", abc = "def", 2);

报错:

1
2
3
4
| println!("{abc} {1}", abc = "def", 2);
| ----- ^ positional arguments must be before named arguments
| |
| named argument

格式化参数

宽度

宽度用来指示输出目标的最小长度,如果长度不够则进行填充和对齐;如果长度超出并不会进行截断。

  • {:n}:通过数字直接指定宽度,如 {:5} 。
  • {:n$}:通过参数索引指定宽度,如 {:1$} 。
  • {:name$}:通过具名参数指定宽度,如 {:width$} 。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
// 1. 通过数字直接指定宽度
// - 通过输出目标长度不足时
// 不足长度5,填空空格
// 输出: Hello a !
println!("Hello {:5}!", "a");

// - 输出目标长度超出时
// 超出长度2,仍然完整输出
// 输出: Hello World!
println!("Hello {:2}!", "World");

// 2. 通过参数索引指定宽度
// 因为未指定位置索引,按照顺序首先引用 "abc"
// 通过 $ 符指定宽度的索引是1,即宽度为5
// 输出: Hello abc !
println!("Hello {:1$}!", "abc", 5);

// 指定了位置索引为 1,使用 $ 符指定宽度的索引为 0 ,即宽度为5
// 输出: Hello abc !
println!("Hello {1:0$}!", 5, "abc");

// 3. 通过具名参数指定宽度为5
// 输出: Hello abc !
println!("Hello {:width$}!", "abc", width = 5);
}

填充和对齐

在格式化字符串中,一般按照 : + 填充字符 + 对齐方式 来排列。

  • [fillchar]<:左对齐
  • [fillchar]^:居中对齐
  • [fillchar]>:右对齐

[] 表示可选。

关于填充:

  • 非数字时,默认填充使用 空格 + 左对齐
  • 数字时,默认填充使用 空格 + 右对齐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 trait 。确保应用填充的一种比较好的方法是格式化输入,再填充此结果字符串进行输出。

进制

Rust 中使用 # 来控制数字的进制输出。

  • #b :二进制
  • #o :八进制
  • #x :小写十六进制
  • #X :大写十六进制
  • x :不带前缀的小写十六进制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 以二进制形式输出
assert_eq!(format!("Hello {:#b}!", 10251), "Hello 0b10100000001011!");
// 以八进制形式输出
assert_eq!(format!("Hello {:#o}!", 10251), "Hello 0o24013!");
// 以十进制形式进行输出
assert_eq!(format!("Hello {}!", 10251), "Hello 10251!");
// 以小写十六进制形式进行输出
assert_eq!(format!("Hello {:#x}!", 10251), "Hello 0x280b!");
// 以大写十六进制形式进行输出
assert_eq!(format!("Hello {:#X}!", 10251), "Hello 0x280B!");
// 以不带前缀的十六进制形式进行输出
assert_eq!(format!("Hello {:x}!", 10251), "Hello 280b!");
// 使用0填充二进制,宽度为27 => 0000011011!
assert_eq!(format!("{:#010b}", 27), "0b00011011");
}

{:#010b}:

  • # 是一个选项,表示在输出的二进制数前添加 0b 前缀。
  • 0 是填充字符,表示用 0 填充空白部分。
  • 10 是宽度,表示最少要输出 10 位。
  • b 是类型,表示以二进制形式输出。

精度控制

精度控制是指最大输出宽度,当超出精度控制时就会进行截止,一般针对浮点类型和字符串类型。

  • .n:整数 n 就是精度。
  • .n$:使用参数 n 作为精度,可以使用参数索引或者具名参数。
  • .*:与两个输入格式关联,第一个输入是要保存的精度,第二个是要打印的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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"
);
}

指数

  • :e:LowerExp,小写 e 的科学计数法。
  • :E:UpperExp,大写 E 的科学计数法。
1
2
3
4
fn main() {
println!("{:e}", 1000000000); // => 1e9
println!("{:E}", 1000000000); // => 1E9
}

指针地址

  • :p:将变量以十六进制的形式输出。
1
2
3
4
fn main() {
let v= vec![1, 2, 3];
println!("{:p}", v.as_ptr()) // => 0x5632538a7ba0
}

转义

字符 { 使用 {{` 进行转义,字符 `}` 使用 `}} 进行转义。

1
2
assert_eq!(format!("Hello {{}}"), "Hello {}");
assert_eq!(format!("{{ Hello"), "{ Hello");

参考引用