Skip to content

基本语法

变量与可变性

1
2
3
4
5
6
fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}
1
2
3
4
5
6
➜  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(指针宽度)

isizeusize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的

浮点型

1
2
3
4
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;
}
1
2
3
4
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 并不符合

1
2
3
4
5
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

复合类型

元组

1
2
3
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}
1
2
3
4
5
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。

除了使用模式匹配解构外,也可以使用点号(.)后跟值的索引来直接访问它们

1
2
3
4
5
6
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 表示数组的大小)。

1
2
3
4
5
fn main() {
    let a = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May", "June", "July",
                "August", "September", "October", "November", "December"];
}

可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。

这样编写数组的类型类似于另一个初始化数组的语法:如果你希望创建一个每个元素都相同的数组,可以在中括号内指定其初始值,后跟分号,再后跟数组的长度,如下所示:

1
let a = [3; 5];
1
2
3
4
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

函数

1
2
3
4
5
6
7
fn main() {
    another_function(5);
}
fn another_function(x: i32) {
    println!("The value of x is : {}", x);
}
// The value of x is : 5

在函数签名时,必须声明每个参数的类型。
这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图

1
2
3
4
5
6
7
8
9
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关键字创建变量并绑定一个值是一个语句。

1
2
3
fn main() {
    let y = 6;
}

let y = 6;是一个语句
语句不返回值。因此,不能把let语句赋值给另一个变量

这与其他语言不同,例如 C 语言,它的赋值语句会返回所赋的值。可以这么写x = y = 6, 这样xy的值都是6; Rust 中不能这样写

表达式会计算出一些值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个简单的数学运算,比如5 + 6,这是一个表达式并计算出值11
表达式可以是语句的一部分:例如语句let y = 6;中的6是一个表达式,它计算出的值是6
函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的打括号(代码块),{},也是一个表达式

1
2
3
4
5
6
7
8
9
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

这个表达式

1
2
3
4
let y = {
    let x = 3;
    x + 1
};

是一个代码块,它的值是4。这个值作为let语句的一部分被绑定到y上。注意结尾没有分号的哪一行x + 1,与你见过的大部分代码行不同。
表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。

具有返回值的函数

函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)后声明它的类型。
在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用return关键字和指定值,可从函数中提前返回;
但大部分函数隐式的返回最后的表达式。

1
2
3
4
5
6
7
8
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,这一行与如下代码相同:

1
let x = 5;

其次,five函数没有参数并定义了返回值类型,不过函数体只有单单一个5也没有分号,因为这是一个表达式,我们想要返回它的值。

1
2
3
4
5
6
7
8
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的行尾加上一个分号,把它从表达式变成语句,会产生类型不匹配的错误

条件语句

1
2
3
4
5
6
7
8
fn main() {
    let number = 3;
    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

代码中的条件必须bool值。如果条件不是bool值,将会抛出错误
Rust 不像其它语言一样,它并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为if的条件。

1
2
3
4
5
6
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");
    }
}
1
2
3
4
5
6
7
8
9
fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };
    println!("The value of number is: {}", number);
}

循环

1
2
3
4
5
fn main() {
    loop {
        println!("again!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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,来终止一个陷入无限循环的程序

1
2
3
4
5
6
7
8
fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number = number - 1;
    }
    println!("LIFTOFF!!!");
}
1
2
3
4
5
6
7
8
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;
    }
}
1
2
3
4
5
6
fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }
}
1
2
3
4
5
6
fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}
1
2
3
4
5
6
7
8
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需要手动实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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! 上使用 ? 会像是这样:

1
write!(f, "{}", value)?;

有了 ?,对一个 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"

根据使用的参数类型是 Xo 还是未指定,同样的变量(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 }