基本语法
变量与可变性
| fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
|
| ➜ hello_cargo git:(master) ✗ cargo run
Compiling hello_cargo v0.1.0 (/Users/nocilantro/Desktop/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 1.65s
Running `target/debug/hello_cargo`
The value of x is: 5
The value of x is: 6
|
通过 mut
,允许把绑定到 x 的值从 5 改成 6。
数据类型
标量类型
标量(scalar)类型代表一个单独的值。
Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。
整型
长度 |
有符号 |
无符号 |
8-bit |
i8 |
u8 |
16-bit |
i16 |
u16 |
32-bit |
i32 |
u32 |
64-bit |
i64 |
u64 |
128-bit |
i128 |
u128 |
arch |
isize(指针宽度) |
usize(指针宽度) |
isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的
浮点型
| fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
|
1
2
3
4
5
6
7
8
9
10
11
12 | fn main() {
// 加法
let sum = 5 + 10;
// 减法
let difference = 95.5 - 4.3;
// 乘法
let product = 4 * 30;
// 除法
let quotient = 56.7 / 32.2;
// 取余
let remainder = 43 % 5;
}
|
| fn main() {
let t = true;
let f: bool = false; // 显式指定类型注解
}
|
char
(字符)
Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。
在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。
Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。
不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合
| fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}
|
单元类型(unit type): ()
。其唯一可能的值就是()
这个空元组
尽管单元类型的值是个元组,它却并不被认为是复合类型,因为并不包含多个值。
变量都能够显式地给出类型说明(type annotation)。
数字还可以通过后缀 (suffix)或默认方式来声明类型。
整型默认为 i32 类型,浮点型默认为 f64 类型。
注意 Rust 还可以根据上下文来推断(infer)类型(译注:比如一个未声明类型整 数和 i64 的整数相加,则该整数会自动推断为 i64 类型。仅当根据环境无法推断时 ,才按默认方式取整型数值为 i32,浮点数值为 f64)。
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 | fn main() {
// 变量可以给出类型说明。
let logical: bool = true;
let a_float: f64 = 1.0; // 常规说明
let an_integer = 5i32; // 后缀说明
// 否则会按默认方式决定类型。
let default_float = 3.0; // `f64`
let default_integer = 7; // `i32`
// 类型也可根据上下文自动推断。
let mut inferred_type = 12; // 根据下一行的赋值推断为 i64 类型
inferred_type = 4294967296i64;
// 可变的(mutable)变量,其值可以改变。
let mut mutable = 12; // Mutable `i32`
mutable = 21;
// 报错!变量的类型并不能改变。
mutable = true;
// 但可以用掩蔽(shadow)来覆盖前面的变量。
let mutable = true;
}
|
字面量和运算符
整数 1、浮点数 1.2、字符 'a'、字符串 "abc"、布尔值 true 和单元类型 () 可以用数字、文字或符号之类的 “字面量”(literal)来表示。
另外,通过加前缀 0x、0o、0b,数字可以用十六进制、八进制或二进制记法表示。
为了改善可读性,可以在数值字面量中插入下划线,比如:1_000 等同于 1000, 0.000_001 等同于 0.000001。
我们需要把字面量的类型告诉编译器。如前面学过的,我们使用 u32 后缀来表明字面量 是一个 32 位无符号整数,i32 后缀表明字面量是一个 32 位有符号整数。
Rust 提供了一系列的运算符(operator),它们的优先级 和类 C 语言的类似。(译注:类 C 语言包括 C/C++、Java、PHP 等语言。)
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 | fn main() {
// 整数相加
println!("1 + 2 = {}", 1u32 + 2);
// 整数相减
println!("1 - 2 = {}", 1i32 - 2);
// 试一试 ^ 尝试将 `1i32` 改为 `1u32`,体会为什么类型声明这么重要
// 短路求值的布尔逻辑
println!("true AND false is {}", true && false);
println!("true OR false is {}", true || false);
println!("NOT true is {}", !true);
// 位运算
println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
println!("1 << 5 is {}", 1u32 << 5);
println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
// 使用下划线改善数字的可读性!
println!("One million is written as {}", 1_000_000u32);
}
// 1 + 2 = 3
// 1 - 2 = -1
// true AND false is false
// true OR false is true
// NOT true is false
// 0011 AND 0101 is 0001
// 0011 OR 0101 is 0111
// 0011 XOR 0101 is 0110
// 1 << 5 is 32
// 0x80 >> 2 is 0x20
// One million is written as 1000000
|
复合类型
元组
| fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
|
| fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}
|
程序首先创建了一个元组并绑定到 tup 变量上。
接着使用了 let 和一个模式将 tup 分成了三个不同的变量,x、y 和 z。这叫做 解构(destructuring),因为它将一个元组拆成了三个部分。
最后,程序打印出了 y 的值,也就是 6.4。
除了使用模式匹配解构外,也可以使用点号(.)后跟值的索引来直接访问它们
| fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
|
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 | // 元组可以充当函数的参数和返回值
fn reverse(pair: (i32, bool)) -> (bool, i32) {
// 可以使用 `let` 把一个元组的成员绑定到一些变量
let (integer, boolean) = pair;
(boolean, integer)
}
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);
fn main() {
// 包含各种不同类型的元组
let long_tuple = (1u8, 2u16, 3u32, 4u64,
-1i8, -2i16, -3i32, -4i64,
0.1f32, 0.2f64,
'a', true);
// 通过元组的下标来访问具体的值
println!("long tuple first value: {}", long_tuple.0);
println!("long tuple second value: {}", long_tuple.1);
// 元组也可以充当元组的元素
let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);
// 元组可以打印
println!("tuple of tuples: {:?}", tuple_of_tuples);
// 但很长的元组无法打印
// let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
// println!("too long tuple: {:?}", too_long_tuple);
// 试一试 ^ 取消上面两行的注释,阅读编译器给出的错误信息。
let pair = (1, true);
println!("pair is {:?}", pair);
println!("the reversed pair is {:?}", reverse(pair));
// 创建单元素元组需要一个额外的逗号,这是为了和被括号包含的字面量作区分。
println!("one element tuple: {:?}", (5u32,));
println!("just an integer: {:?}", (5u32));
// 元组可以被解构(deconstruct),从而将值绑定给变量
let tuple = (1, "hello", 4.5, true);
let (a, b, c, d) = tuple;
println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);
let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
println!("{:?}", matrix);
}
// long tuple first value: 1
// long tuple second value: 2
// tuple of tuples: ((1, 2, 2), (4, -1), -2)
// pair is (1, true)
// the reversed pair is (true, 1)
// one element tuple: (5,)
// just an integer: 5
// 1, "hello", 4.5, true
// Matrix(1.1, 1.2, 2.1, 2.2)
|
数组和切片
数组(array)是一组拥有相同类型 T 的对象的集合,在内存中是连续存储的。
数组使用 中括号 [] 来创建,且它们的大小在编译时会被确定。
数组的类型标记为[T; size]
( 译注:T 为元素的类型,size 表示数组的大小)。
| fn main() {
let a = [1, 2, 3, 4, 5];
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}
|
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
| let a: [i32; 5] = [1, 2, 3, 4, 5];
|
这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。
这样编写数组的类型类似于另一个初始化数组的语法:如果你希望创建一个每个元素都相同的数组,可以在中括号内指定其初始值,后跟分号,再后跟数组的长度,如下所示:
| fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
|
切片(slice)类型和数组类似,但其大小在编译时是不确定的。相反,切片是一个双字 对象(two-word object),第一个字是一个指向数据的指针,第二个字是切片的长度。这 个 “字” 的宽度和 usize 相同,由处理器架构决定,比如在 x86-64 平台上就是 64 位。 slice 可以用来借用数组的一部分。
slice 的类型标记为&[T]
。
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 | use std::mem;
// 此函数借用一个 slice
fn analyze_slice(slice: &[i32]) {
println!("first element of the slice: {}", slice[0]);
println!("the slice has {} elements", slice.len());
}
fn main() {
// 定长数组(类型标记是多余的)
let xs: [i32; 5] = [1, 2, 3, 4, 5];
// 所有元素可以初始化成相同的值
let ys: [i32; 500] = [0; 500];
// 下标从 0 开始
println!("first element of the array: {}", xs[0]);
println!("second element of the array: {}", xs[1]);
// `len` 返回数组的大小
println!("array size: {}", xs.len());
// 数组是在栈中分配的
println!("array occupies {} bytes", mem::size_of_val(&xs));
// 数组可以自动被借用成为 slice
println!("borrow the whole array as a slice");
analyze_slice(&xs);
// slice 可以指向数组的一部分
println!("borrow a section of the array as a slice");
analyze_slice(&ys[1 .. 4]);
// 越界的下标会引发致命错误(panic)
// println!("{}", xs[5]);
}
// first element of the array: 1
// second element of the array: 2
// array size: 5
// array occupies 20 bytes
// borrow the whole array as a slice
// first element of the slice: 1
// the slice has 5 elements
// borrow a section of the array as a slice
// first element of the slice: 0
// the slice has 3 elements
|
函数
| fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is : {}", x);
}
// The value of x is : 5
|
在函数签名时,必须声明每个参数的类型。
这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图
| fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is : {}", x);
println!("The value of y is : {}", y);
}
// The value of x is : 5
// The value of y is : 6
|
包含语句和表达式的函数体
函数体由一系列的语句和一个可选的结尾表达式构成。
Rust 是一门基于表达式的语言,这是一个需要理解的(不同于其他语言)重要区别。
语句是执行一些操作但不返回值的指令。表达式计算并产生一个值。
使用let
关键字创建变量并绑定一个值是一个语句。
let y = 6;
是一个语句
语句不返回值。因此,不能把let
语句赋值给另一个变量
这与其他语言不同,例如 C 语言,它的赋值语句会返回所赋的值。可以这么写x = y = 6
, 这样x
和y
的值都是6
; Rust 中不能这样写
表达式会计算出一些值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个简单的数学运算,比如5 + 6
,这是一个表达式并计算出值11
。
表达式可以是语句的一部分:例如语句let y = 6;
中的6
是一个表达式,它计算出的值是6
。
函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的打括号(代码块),{}
,也是一个表达式
| fn main() {
let x = 5;
let y = {
let x = 3;
x + 1
};
println!("The value of y is : {}", y);
}
// The value of y is : 4
|
这个表达式
| let y = {
let x = 3;
x + 1
};
|
是一个代码块,它的值是4
。这个值作为let
语句的一部分被绑定到y
上。注意结尾没有分号的哪一行x + 1
,与你见过的大部分代码行不同。
表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。
具有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)
后声明它的类型。
在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用return
关键字和指定值,可从函数中提前返回;
但大部分函数隐式的返回最后的表达式。
| fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
// The value of x is: 5
|
在five
函数中没有函数调用、宏、甚至没有let
语句--只有数字5
。
这在 Rust 中是一个完全有效的函数。
注意,也指定了函数返回值的类型,就是-> i32
。
five
函数的返回值是5
,所以返回值类型是i32
。让我们仔细检查一下这段代码。
有两个重要的部分:首先,let x = five();
这一行表明我们使用函数的返回值初始化一个变量。因为five
函数返回5
,这一行与如下代码相同:
其次,five
函数没有参数并定义了返回值类型,不过函数体只有单单一个5
也没有分号,因为这是一个表达式,我们想要返回它的值。
| fn pulu_one(x: i32) -> i32 {
x + 1
}
fn main() {
let x = pulu_one(5);
println!("The value of x is: {}", x);
}
// The value of x is: 6
|
如果在包含x + 1
的行尾加上一个分号,把它从表达式变成语句,会产生类型不匹配的错误
条件语句
| fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
|
代码中的条件必须是bool
值。如果条件不是bool
值,将会抛出错误
Rust 不像其它语言一样,它并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为if
的条件。
| fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12 | fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
|
| fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
|
循环
| fn main() {
loop {
println!("again!");
}
}
|
| fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
// The result is 20
|
当运行这个程序时,我们会看到连续的反复打印 again!,直到我们手动停止程序。大部分终端都支持一个快捷键,ctrl-c,来终止一个陷入无限循环的程序
| fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
|
| fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index = index + 1;
}
}
|
| fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
|
| fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
|
| fn main() {
for i in [10, 0, 1].iter().cloned() {
println!("{}", i);
}
}
10
0
1
|
格式化输出
https://doc.rust-lang.org/std/fmt/
打印操作由std::fmt
里面所定义的一系列宏来处理,包括:
- format!:将格式化文本写到字符串(String)。(译注:字符串是返 回值不是参数。)
- print!:与 format! 类似,但将文本输出到控制台(io::stdout)。
- println!: 与 print! 类似,但输出结果追加一个换行符。
- eprint!:与 format! 类似,但将文本输出到标准错误(io::stderr)。
- eprintln!:与 eprint! 类似,但输出结果追加一个换行符。
这些宏都以相同的做法解析(parse)文本。另外有个优点是格式化的正确性会在编译时检查。
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 | fn main() {
// 通常情况下,`{}` 会被任意变量内容所替换。
// 变量内容会转化成字符串。
println!("{} days", 31);
// 不加后缀的话,31 就自动成为 i32 类型。
// 你可以添加后缀来改变 31 的类型。
// 用变量替换字符串有多种写法。
// 比如可以使用位置参数。
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");
// 可以使用命名参数。
println!("{subject} {verb} {object}",
object="the lazy dog",
subject="the quick brown fox",
verb="jumps over");
// 可以在 `:` 后面指定特殊的格式。
println!("{} of {:b} people know binary, the other half don't", 1, 2);
// 你可以按指定宽度来右对齐文本。
// 下面语句输出 " 1",5 个空格后面连着 1。
println!("{number:>width$}", number=1, width=6);
// 你可以在数字左边补 0。下面语句输出 "000001"。
println!("{number:>0width$}", number=1, width=6);
// println! 会检查使用到的参数数量是否正确。
// println!("My name is {0}, {1} {0}", "Bond");
// 改正 ^ 补上漏掉的参数:"James"
println!("My name is {0}, {1} {0}", "Bond", "James");
// 创建一个包含单个 `i32` 的结构体(structure)。命名为 `Structure`。
#[allow(dead_code)]
struct Structure(i32);
// 但是像结构体这样的自定义类型需要更复杂的方式来处理。
// 下面语句无法运行。
// println!("This struct `{}` won't print...", Structure(3));
// 改正 ^ 注释掉此行。
println!("Pi is roughly {:.3}", 3.141592);
}
// 31 days
// Alice, this is Bob. Bob, this is Alice
// the quick brown fox jumps over the lazy dog
// 1 of 10 people know binary, the other half don't
// 1
// 000001
// My name is Bond, James Bond
// Pi is roughly 3.142
|
std::fmt
包含多种 traits(trait 有 “特征,特性” 等意思) 来控制文字显示,其中重要的两种 trait 的基本形式如下:
fmt::Debug
:使用 {:?} 标记。格式化文本以供调试使用。
fmt::Display
:使用 {} 标记。以更优雅和友好的风格来格式化文本。
上例使用了fmt::Display
,因为标准库提供了那些类型的实现。若要打印自定义类型的 文本,需要更多的步骤。
调试(Debug)
所有的类型,若想用 std::fmt 的格式化 trait 打印出来,都要求实现这个 trait。
自动的实现只为一些类型提供,比如 std 库中的类型。所有其他类型 都必须手动实现。
fmt::Debug
这个 trait 使这项工作变得相当简单。
所有类型都能推导(derive,即自 动创建fmt::Debug
的实现。
但是fmt::Display
需要手动实现。
| fn main() {
// 这个结构体不能使用 `fmt::Display` 或 `fmt::Debug` 来进行打印。
struct UnPrintable(i32);
// `derive` 属性会自动创建所需的实现,使这个 `struct` 能使用 `fmt::Debug` 打印。
#[derive(Debug)]
struct DebugPrintable(i32);
let x = DebugPrintable(1);
println!("{:?}", x);
}
// DebugPrintable(1)
|
所有 std 库类型都天生可以使用{:?}
来打印:
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 | // 推导 `Structure` 的 `fmt::Debug` 实现。
// `Structure` 是一个包含单个 `i32` 的结构体。
#[derive(Debug)]
struct Structure(i32);
// 将 `Structure` 放到结构体 `Deep` 中。然后使 `Deep` 也能够打印。
#[derive(Debug)]
struct Deep(Structure);
fn main() {
// 使用 `{:?}` 打印和使用 `{}` 类似。
println!("{:?} months in a year.", 12);
println!("{1:?} {0:?} is the {actor:?} name.",
"Slater",
"Christian",
actor="actor's");
// `Structure` 也可以打印!
println!("Now {:?} will print!", Structure(3));
// 使用 `derive` 的一个问题是不能控制输出的形式。
// 假如我只想展示一个 `7` 怎么办?
println!("Now {:?} will print!", Deep(Structure(7)));
}
// 12 months in a year.
// "Christian" "Slater" is the "actor\'s" name.
// Now Structure(3) will print!
// Now Deep(Structure(7)) will print!
|
所以fmt::Debug
确实使这些内容可以打印,但是牺牲了一些美感。Rust 也通过{:#?}
提供了 “美化打印” 的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | #[derive(Debug)]
struct Person<'a> {
name: &'a str,
age: u8
}
fn main() {
let name = "Peter";
let age = 27;
let peter = Person { name, age };
// 美化打印
println!("{:#?}", peter);
}
// Person {
// name: "Peter",
// age: 27,
// }
|
显示(Display)
fmt::Debug
通常看起来不太简洁,因此自定义输出的外观经常是更可取的。
这需要通过 手动实现fmt::Display
来做到。fmt::Display
采用 {} 标记。实现方式看 起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | fn main() {
// (使用 `use`)导入 `fmt` 模块使 `fmt::Display` 可用
use std::fmt;
// 定义一个结构体,咱们会为它实现 `fmt::Display`。以下是个简单的元组结构体
// `Structure`,包含一个 `i32` 元素。
struct Structure(i32);
// 为了使用 `{}` 标记,必须手动为类型实现 `fmt::Display` trait。
impl fmt::Display for Structure {
// 这个 trait 要求 `fmt` 使用与下面的函数完全一致的函数签名
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// 仅将 self 的第一个元素写入到给定的输出流 `f`。返回 `fmt:Result`,此
// 结果表明操作成功或失败。注意 `write!` 的用法和 `println!` 很相似。
write!(f, "{}", self.0)
}
}
let s = Structure(1);
println!("{}", s);
}
// 1
|
fmt::Display
的效果可能比fmt::Debug
简洁,但对于 std 库来说,这就有一个问题。模棱两可的类型该如何显示呢?
举个例子,假设标准库对所有的Vec<T>
都实现了同 一种输出样式,那么它应该是哪种样式?下面两种中的一种吗?
Vec<path>
:/:/etc:/home/username:/bin
(使用 : 分割)
Vec<number>
:1,2,3(使用 , 分割)
我们没有这样做,因为没有一种合适的样式适用于所有类型,标准库也并不擅自规定一种样式。
对于Vec<T>
或其他任意泛型容器(generic container),fmt::Display
都没有 实现。因此在这些泛型的情况下要用fmt::Debug
。
这并不是一个问题,因为对于任何非泛型的容器类型,fmt::Display
都能够实现。
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 | use std::fmt; // 导入 `fmt`
// 带有两个数字的结构体。推导出 `Debug`,以便与 `Display` 的输出进行比较。
#[derive(Debug)]
struct MinMax(i64, i64);
// 实现 `MinMax` 的 `Display`。
impl fmt::Display for MinMax {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// 使用 `self.number` 来表示各个数据。
write!(f, "({}, {})", self.0, self.1)
}
}
// 为了比较,定义一个含有具名字段的结构体。
#[derive(Debug)]
struct Point2D {
x: f64,
y: f64,
}
// 类似地对 `Point2D` 实现 `Display`
impl fmt::Display for Point2D {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// 自定义格式,使得仅显示 `x` 和 `y` 的值。
write!(f, "x: {}, y: {}", self.x, self.y)
}
}
fn main() {
let minmax = MinMax(0, 14);
println!("Compare structures:");
println!("Display: {}", minmax);
println!("Debug: {:?}", minmax);
let big_range = MinMax(-300, 300);
let small_range = MinMax(-3, 3);
println!("The big range is {big} and the small is {small}",
small = small_range,
big = big_range);
let point = Point2D { x: 3.3, y: 7.2 };
println!("Compare points:");
println!("Display: {}", point);
println!("Debug: {:?}", point);
// 报错。`Debug` 和 `Display` 都被实现了,但 `{:b}` 需要 `fmt::Binary`
// 得到实现。这语句不能运行。
// println!("What does Point2D look like in binary: {:b}?", point);
}
// Compare structures:
// Display: (0, 14)
// Debug: MinMax(0, 14)
// The big range is (-300, 300) and the small is (-3, 3)
// Compare points:
// Display: x: 3.3, y: 7.2
// Debug: Point2D { x: 3.3, y: 7.2 }
|
fmt::Display 被实现了,而 fmt::Binary 没有,因此 fmt::Binary 不能使用。
std::fmt 有很多这样的 trait,它们都要求有各自的实现。
测试实例:List
对一个结构体实现 fmt::Display,其中的元素需要一个接一个地处理到,这可能会很麻烦。
问题在于每个 write! 都要生成一个 fmt::Result。
正确的实现需要 处理所有的 Result。Rust 专门为解决这个问题提供了?
操作符。
在 write! 上使用 ? 会像是这样:
有了 ?,对一个 Vec 实现 fmt::Display 就很简单了:
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 | use std::fmt; // 导入 `fmt` 模块。
// 定义一个包含单个 `Vec` 的结构体 `List`。
struct List(Vec<i32>);
impl fmt::Display for List {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// 使用元组的下标获取值,并创建一个 `vec` 的引用。
let vec = &self.0;
write!(f, "[")?;
// 使用 `v` 对 `vec` 进行迭代,并用 `count` 记录迭代次数。
for (count, v) in vec.iter().enumerate() {
// 对每个元素(第一个元素除外)加上逗号。
// 使用 `?` 或 `try!` 来返回错误。
if count != 0 { write!(f, ", ")?; }
write!(f, "{}", v)?;
}
// 加上配对中括号,并返回一个 fmt::Result 值。
write!(f, "]")
}
}
fn main() {
let v = List(vec![1, 2, 3]);
println!("{}", v);
}
// [1, 2, 3]
|
格式化
文档: https://doc.rust-lang.org/std/fmt/index.html
我们已经看到,格式化的方式是通过格式字符串来指定的:
format!("{}", foo)
-> "3735928559"
format!("0x{:X}", foo)
-> "0xDEADBEEF"
format!("0o{:o}", foo)
-> "0o33653337357"
根据使用的参数类型是 X
、o
还是未指定,同样的变量(foo)能够格式化 成不同的形式。
这个格式化的功能是通过 trait 实现的,每种参数类型都对应一种 trait。
最常见的格式化 trait 就是 Display,它可以处理参数类型为未指定的情况,比如 {}。
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 | use std::fmt::{self, Formatter, Display};
struct City {
name: &'static str,
// 纬度
lat: f32,
// 经度
lon: f32,
}
impl Display for City {
// `f` 是一个缓冲区(buffer),此方法必须将格式化后的字符串写入其中
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' };
let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' };
// `write!` 和 `format!` 类似,但它会将格式化后的字符串写入
// 一个缓冲区(即第一个参数f)中。
write!(f, "{}: {:.3}°{} {:.3}°{}",
self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c)
}
}
#[derive(Debug)]
struct Color {
red: u8,
green: u8,
blue: u8,
}
fn main() {
for city in [
City { name: "Dublin", lat: 53.347778, lon: -6.259722 },
City { name: "Oslo", lat: 59.95, lon: 10.75 },
City { name: "Vancouver", lat: 49.25, lon: -123.1 },
].iter() {
println!("{}", *city);
}
for color in [
Color { red: 128, green: 255, blue: 90 },
Color { red: 0, green: 3, blue: 254 },
Color { red: 0, green: 0, blue: 0 },
].iter() {
// 在添加了针对 fmt::Display 的实现后,请改用 {} 检验效果。
println!("{:?}", *color)
}
}
// Dublin: 53.348°N 6.260°W
// Oslo: 59.950°N 10.750°E
// Vancouver: 49.250°N 123.100°W
// Color { red: 128, green: 255, blue: 90 }
// Color { red: 0, green: 3, blue: 254 }
// Color { red: 0, green: 0, blue: 0 }
|