Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

2.2基本类型

基本类型

由于 Rust 是一门静态语言,所以Rust 每个值都有其确切的数据类型

总的来说,可以分为两类:基本类型和复合类型

基本类型意味着它们往往是一个最小化原子类型,无法解构为其他类型(一般意义上来说),由以下组成:

  • 数值类型:有符号整数(i8i16i32i64isize)、无符号整数(u8u16u32u64usize)、浮点数(f32f64)、以及有理数、复数
  • 字符串:字符串字面量和字符串切片&str
  • 布尔类型:truefalse
  • 字符类型:表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型:(),其唯一值也是()

类型推导与标注

与 Python、JavaScript 等动态语言不同,Rust 是一门静态类型语言。也就是说:编译器必须在编译期知道我们所有变量的类型,但这不意味着我们需要为每个变量指定类型。因为Rust 编译器很聪明,它可以根据变量的值和上下文中的使用方式来自动推导出变量的类型,但是在某些情况下,他无法推导出变量的类型,需要手动去给予一个类型标准。

代码示例:

1
let guess = "42".parse().expect("Not a number!");

先忽略,parse().expect..部分,这段代码的目的是将字符串"42"进行解析,而编译器在这里无法推导出我们想要的类型,因此编译器会报错:

1
2
3
4
5
6
7
8
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type

因此我们需要给编译器更多的信息,例如给guess一个显式的类型标注:let guess : i32 = ...或者"42".parse::<i32>()

—————————————

数值类型

Rust 允许在复杂类型上定义运算符,例如在自定义类型上定义+运算符,这种行为被称为运算符重载,Rust 具体支持的重载运算符见附录 B

整数类型

整数是没有小数部分的数字。之前使用过的i32类型,表示有符号的 32 位整数(i是英文单词integer的首字母,与之相反的是u,代表无符号unsigned类型)。下表显示了 Rust 中的内置的整数类型:

长度 有符号类型 无符号类型
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
视架构而定 isize usize

类型定义的形式统一为:有无符号 + 类型大小(位数)无符号数表示数字只能取正数和0,而有符号则表示数字可以取正数、负数、0。
当强调符号时,数字前面需要进行补码,而规定为无符号时,就不需要加上正好了。

每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。

此外,isizeusize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。

整形字面量可以用下表的形式书写:

数字字面量 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节 (仅限于 u8) b'A'

这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整型默认使用 i32,例如 let i = 1,那 i 就是 i32 类型,因此你可以首选它,同时该类型也往往是性能最好的。isizeusize 的主要应用场景是用作集合的索引。

整型溢出

假设有一个 u8,它可以存放从 0 到 255 的值,当将其修改为范围之外的值(如 256)则会发生整型溢出当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。

在使用--release参数进行 release (发布模式)模式构建时, Rust 不会检测溢出。当检测到整型溢出时,Rust 会按照补码循环溢出的规则处理,比如,在 u8 的情况下,256 会变成 0,257 会变成 1,以此类推。程序不会 panic,打不世故该变量的值可能不是我们期望的值。以来这种默认行为的代码都应该被认为是错误的代码。

如果要显式处理可能的溢出,可以使用标准库准对原始数字类型提供的这些方法:

  • 使用wrapping_*方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add
  • 如果使用checked_* 方法时发生溢出,则返回None
  • 使用overflowing_*方法返回该值和一个指示是否存在溢出的布尔值
  • 使用saturating_*方法使值达到最小值或最大值

下面是一个演示 wrapping_*方法的示例:

1
2
3
4
5
fn main() {
let a : u8 = 255;
let b : a.wrapping_add(20);
println!("{}",b); // 19
}

浮点类型

浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型:f32f64,分别为 32 位和 64 位大小。默认浮点类型是f64,在现代的 CPU 中它的速度与f32几乎相同,但精度更高。

下面是一个浮点数示例:

1
2
3
4
fn main() {
let x = 2.0; // f64
let y : f32 = 3.0; // f32
}

浮点数根据IEEE-754标准实现。f32类型是单精度浮点型,f64为双精度。

浮点数陷阱

浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:

  1. 浮点数往往是你想要数字的近似表达 浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如 0.1 在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
  2. 浮点数在某些特性上是反直觉的 例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用 >>= 等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为 f32f64 上的比较运算实现的是 std::cmp::PartialEq 特征(类似其他语言的接口),但是并没有实现 std::cmp::Eq 特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:

==下面来自 Rust 圣经==

Rust 的 HashMap 数据结构,是一个 KV 类型的 Hash Map 实现,它对于 K 没有特定类型的限制,但是要求能用作 K 的类型必须实现了 std::cmp::Eq 特征,因此这意味着你无法使用浮点数作为 HashMapKey,来存储键值对,但是作为对比,Rust 的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为 HashMapKey

为了避免上面说的两个陷阱,你需要遵守以下准则:

  • 避免在浮点数上测试相等性
  • 当结果在数学上可能存在未定义时,需要格外的小心

来看个小例子:

1
2
3
4
fn main() {
// 断言0.1 + 0.2与0.3相等
assert!(0.1 + 0.2 == 0.3);
}

你可能以为,这段代码没啥问题吧,实际上它会 panic(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。

那如果非要进行比较呢?可以考虑用这种方式 (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 ,具体小于多少,取决于你对精度的需求。

下面是一段更神奇的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let abc : (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz : (f64, f64, f64) = (0.1, 0.2, 0.3);

println!("abc (f32)");
println!(" 0.1 + 0.2 : {:x}", (abc.0 + abc.1).to_bits());
println!(" 0.3 : {:x}", (abc.2).to_bits());
println!();

println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
println!(" 0.3: {:x}", (xyz.2).to_bits());
println!();

assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}

允许该程序,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
abc (f32)
0.1 + 0.2: 3e99999a
0.3: 3e99999a

xyz (f64)
0.1 + 0.2: 3fd3333333333334
0.3: 3fd3333333333333

thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2',
➥ch2-add-floats.rs.rs:14:5
note: run with `RUST_BACKTRACE=1` environment variable to display
➥a backtrace

仔细看,对 f32 类型做加法时,0.1 + 0.2 的结果是 3e99999a0.3 也是 3e99999a,因此 f32 下的 0.1 + 0.2 == 0.3 通过测试,但是到了 f64 类型时,结果就不一样了,因为 f64 精度高很多,因此在小数点非常后面发生了一点微小的变化,0.1 + 0.24 结尾,但是 0.33结尾,这个细微区别导致 f64 下的测试失败了,并且抛出了异常。

NaN

对于数学上未定义的结果,例如对负数取平方根-42.1.sqrt(),会产生一个特殊的结果: Rust 的浮点数类型使用NaN(not a number)来处理这些情况。

**所有跟NaN交互的操作,都会返回一个NaN**,而且NaN不能用来比较,下面的代码会崩溃:

1
2
3
4
fn main() {
let x = (-42.0_f32).sqrt();
assert_eq!(x, x);
}

出于防御性编程的考虑,可以使用is_nan()等方法,来判断一个数值是否是NaN

1
2
3
4
5
6
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}

数字运算

Rust 支持所有数字类型的基本数学运算:

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
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
// 编译器自动推导,给予 twenty i32 类型
let twenty = 20;
// 类型标准:
let twenty_one : i32 = 21;
// 通过 类型后缀 的方式进行类型标准:22是i32类型
let twenty_two = 22i32;

// 相同类型才能运算
let addition = twenty + twenty_one + twenty_two;
println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition)

// 对于较长的数字,使用 _ 进行分割,提升可读性
let one_million : i64 = 1_000_000;
println!("{}",one_million.pow(2));

// 定义一个 f32 数组,其中 42.0 会自动被推导为 f32 类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];

// 打印数组中第一个值,并控制小数位为 2 位
println!("{:2}", forty_twos[0]);
}

位运算

Rust的位运算基本上和其他语言一样

运算符 说明
& 位与 相同位置均为1时则为1,否则为0
| 位或 相同位置只要有1时则为1,否则为0
^ 异或 相同位置不相同则为1,相同则为0
! 位非 把位中的0和1相互取反,即0置为1,1置为0
<< 左移 所有位向左移动指定位数,右位补0
>> 右移 所有位向右移动指定位数,带符号移动(正数补0,负数补1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
// 二进制为 00000010
let a:i32 = 2;
// 二进制为 00000011
let b:i32 = 3;

println!("(a & b) value is {}", a & b);

println!("(a | b) value is {}", a | b);

println!("(a ^ b) value is {}", a ^ b);

println!("(!b) value is {}", !b);

println!("(a << b) value is {}", a << b);

println!("(a >> b) value is {}", a >> b);

let mut a = a;
// 注意这些计算符除了!之外都可以加上=进行赋值 (因为!=要用来判断不等于)
a <<= b;
println!("(a << b) value is {}", a);
}

序列(Range)

Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如**1..5,生成从 1 到 4 的连续数字,不包含 5;1..=5,生成从 1 到 5 的连续数字,包含 5 ,**它的用途很简单,常常用于循环(感觉像 Python 的 **range()**)中:

1
2
3
for i in 1..=5 {
println!("{}",i);
}

程序输出:

1
2
3
4
5
1
2
3
4
5

序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。如下是一个使用字符类型序列的例子:

1
2
3
for i in 'a'..='z' {
println!("{}",i);
}

使用 As 完成类型转换

Rust 中可以使用 As 来完成一个类型到另一个类型的转换,其最常用于将原始类型转换为其他原始类型,但是它也可以完成诸如将指针转换为地址、地址转换为指针以及将指针转换为其他指针等功能。你可以在这里了解更多相关的知识。

有理数和复数

Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中:

  • 有理数和复数
  • 任意大小的整数和任意精度的浮点数
  • 固定精度的十进制小数,常用于货币相关的场景

好在社区已经开发出高质量的 Rust 数值库:num

按照以下步骤来引入 num 库:

  1. 创建新工程 cargo new complex-num && cd complex-num
  2. Cargo.toml 中的 [dependencies] 下添加一行 num = "0.4.0"
  3. src/main.rs 文件中的 main 函数替换为下面的代码
  4. 运行 cargo run
1
2
3
4
5
6
7
8
9
use num::complex::Complex;

fn main() {
let a = Complex { re: 2.1, im: -1.2 };
let b = Complex::new(11.1, 22.2);
let result = a + b;

println!("{} + {}i", result.re, result.im)
}

总结

之前提到了过 Rust 的数值类型和运算跟其他语言较为相似,但是实际上,除了语法上的不同之外,还是存在一些差异点:

  • Rust 拥有相当多的数值类型. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
  • 类型转换必须是显式的. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
  • Rust 的数值上可以使用方法. 例如你可以用以下方法来将 13.14 取整:13.14_f32.round(),在这里我们使用了类型后缀,因为编译器需要知道 13.14 的具体类型

数值类型的讲解已经基本结束,接下来,来看看字符和布尔类型。

—————————————

字符、布尔、单元类型

字符类型(char)

注意:在Rust 中,使用''表示字符,使用""表示字符串

字符可以把它理解为英文中的字母,中文中的汉字。

下面的代码展示了几个颇具异域风情的字符:

1
2
3
4
5
6
fn main() {
let c = 'z';
let z = 'ℤ';
let g = '国';
let heart_eyed_cat = '😻';
}

在 Rust 语言中这些都是字符,Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。**Unicode 值的范围从 U+0000 ~ U+D7FFU+E000 ~ U+10FFFF。**不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。

由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:

1
2
3
4
fn main() {
let x = '中';
println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x));
}

输出如下:

1
2
3
4
$ cargo run
Compiling ...

字符'中'占用了4字节的内存大小

注意,我们还没开始讲字符串,但是这里提前说一下,和一些语言不同,Rust 的字符只能用 '' 来表示, "" 是留给字符串的。

布尔(bool)

Rust 中的布尔类型有两个可能的值:truefalse,布尔值占用内存的大小为 1 个字节:

1
2
3
4
5
6
7
8
9
fn main() {
let t = true;

let f: bool = false; // 使用类型标注,显式指定f的类型

if f {
println!("这是段毫无意义的代码");
}
}

使用布尔类型的场景主要在于流程控制,例如上述代码的中的 if 就是其中之一。

单元类型

单元类型就是 (),唯一值也是()

在我们看到了主函数fn main()函数,该函数返回的就是单元类型()。我们不能说main函数没有返回值,因为没有返回值的函数在 Rust 中有单独的定义:发散函数( diverge function),顾名思义,无法收敛的函数。

例如,常见的 println!() 的返回值也是一个单元类型()

再比如,可以使用()作为map的值,表示我们不关注具体的值,只关注key可以作为一个值来占位,但 完全不占用 任何内存

—————————————

语句和表达式

Rust 的函数体是由一系列语句组成,最后一个表达式来返回值,例如:

1
2
3
4
5
fn add_with_extra(x : i32, y : i32) -> i32 {
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式
}

语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。

对于 Rust 而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念,但是对于很多其他语言而言,这两个往往无需区分。基于表达式时函数式语言的重要特征,表达式总要返回值

语句:

1
2
3
let a = 8;
let b : Vec<f64> = Vec::new();
let (a, c) = ("hi", false);

以上都是语句,他们完成了一个具体的操作,但没有返回值,因此时语句。

由于let是语句,因此不能将let语句赋值给其他值,以下形式是错误的:

1
let b = (let a = 8);

错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error: expected expression, found statement (`let`) // 期望表达式,却发现`let`语句
--> src/main.rs:2:13
|
2 | let b = let a = 8;
| ^^^^^^^^^
|
= note: variable declaration using `let` is a statement `let`是一条语句

error[E0658]: `let` expressions in this position are experimental
// 下面的 `let` 用法目前是试验性的,在稳定版中尚不能使用
--> src/main.rs:2:13
|
2 | let b = let a = 8;
| ^^^^^^^^^
|
= note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
= help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

以上的错误告诉我们 let 是语句,不是表达式,因此它不返回值,也就不能给其它变量赋值。但是该错误还透漏了一个重要的信息, let 作为表达式已经是试验功能了,也许不久的将来,我们在 stable rust 下可以这样使用。

表达式

表达式会进行求值,然后返回一个值。例如5 + 6,在求值后,返回值11,因此它就是一条表达式。

表达式可以成为语句的一部分,例如let y = 66就是一个表达式,它在求值后返回一个值6(有些反直觉,但是确实是表达式)。

调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之能返回值就是一个表达式:

1
2
3
4
5
6
7
8
fn main() {
let y = {
let x = 3;
x + 1
};

println!("The value of y is: {}", y);
}

上面使用一个语句块表达式将值赋给y变量,语句块长这样:

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

该语句块是表达式的原因是:它的最后一行是表达式,返回了 x + 1 的值,注意 x + 1 不能以分号结尾,否则就会从表达式变成语句, 表达式不能包含分号。这一点非常重要,一旦你在表达式后加上分号,它就会变成一条语句,再也不会返回一个值,请牢记!

最后,表达式如果不返回任何值,会隐式地返回一个 ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
assert_eq!(ret_unit_type(), ())
}

fn ret_unit_type() {
let x = 1;
// if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
// 类似三元运算符,在Rust里我们可以这样写
let y = if x % 2 == 1 {
"odd"
} else {
"even"
};
// 或者写成一行
let z = if x % 2 == 1 { "odd" } else { "even" };
}

—————————————

函数

上图:

img

函数要点

  • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
  • 每个函数参数都需要标注类型

函数参数

Rust 是强类型语言,因此需要为每一个函数参数都标识出它的具体类型,如:

1
2
3
4
5
6
7
8
fn main() {
another_function(5, 6.1);
}

fn another_function(x: i32, y : f32) {
println!("The value of x is : {}", x);
println!("The value of y is : {}", y);
}

another_function 函数有两个参数,其中 xi32 类型,yf32 类型,然后在该函数内部,打印出这两个值。这里去掉 x 或者 y 的任何一个的类型,都会报错:

1
2
3
4
5
6
7
8
fn main() {
another_function(5, 6.1);
}

fn another_function(x: i32, y) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}

错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error: expected one of `:`, `@`, or `|`, found `)`
--> src/main.rs:5:30
|
5 | fn another_function(x: i32, y) {
| ^ expected one of `:`, `@`, or `|` // 期待以下符号之一 `:`, `@`, or `|`
|
= note: anonymous parameters are removed in the 2018 edition (see RFC 1685)
// 匿名参数在 Rust 2018 edition 中就已经移除
help: if this is a parameter name, give it a type // 如果y是一个参数名,请给予它一个类型
|
5 | fn another_function(x: i32, y: TypeName) {
| ~~~~~~~~~~~
help: if this is a type, explicitly ignore the parameter name // 如果y是一个类型,请使用_忽略参数名
|
5 | fn another_function(x: i32, _: y) {
| ~~~~

函数返回

在上一章节语句和表达式中,我们有提到,在 Rust 中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者

函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 return 提前返回,下面的函数使用最后一条表达式来返回一个值:

1
2
3
4
5
6
7
8
9
fn plus_five(x:i32) -> i32 {
x + 5
}

fn main() {
let x = plus_five(5);

println!("The value of x is: {}", x);
}

x + 5 是一条表达式,求值后,返回一个值,因为它是函数的最后一行,因此该表达式的值也是函数的返回值。

再来看两个重点:

  1. let x = plus_five(5),说明我们用一个函数的返回值来初始化 x 变量,因此侧面说明了在 Rust 中函数也是表达式,这种写法等同于 let x = 5 + 5;
  2. x + 5 没有分号,因为它是一条表达式,这个在上一节中我们也有详细介绍

再来看一段代码,同时使用 return 和表达式作为返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn plus_or_minus(x:i32) -> i32 {
if x > 5 {
return x - 5
}

x + 5
}

fn main() {
let x = plus_or_minus(5);

println!("The value of x is: {}", x);
}

plus_or_minus 函数根据传入 x 的大小来决定是做加法还是减法,若 x > 5 则通过 return 提前返回 x - 5 的值,否则返回 x + 5 的值。

Rust 中的特殊返回类型

无返回值()

例如单元类型 (),是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:

  • 函数没有返回值,那么返回一个 ()
  • 通过 ; 结尾的表达式返回一个 ()

例如下面的 report 函数会隐式返回一个 ()

1
2
3
4
5
6
use std::fmt::Debug;

fn report<T: Debug>(item: T) {
println!("{:?}", item);

}

与上面的函数返回值相同,但是下面的函数显式的返回了 ()

1
2
3
fn clear(text: &mut String) -> () {
*text = String::from("");
}

在实际编程中,你会经常在错误提示中看到该 () 的身影出没,假如你的函数需要返回一个 u32 值,但是如果你不幸的以 表达式; 的方式作为函数的最后一行代码,就会报错:

1
2
3
fn add(x:u32,y:u32) -> u32 {
x + y;
}

错误如下:

1
2
3
4
5
6
7
8
9
error[E0308]: mismatched types // 类型不匹配
--> src/main.rs:6:24
|
6 | fn add(x:u32,y:u32) -> u32 {
| --- ^^^ expected `u32`, found `()` // 期望返回u32,却返回()
| |
| implicitly returns `()` as its body has no tail or `return` expression
7 | x + y;
| - help: consider removing this semicolon

还记得我们在语句与表达式中讲过的吗?只有表达式能返回值,而 ; 结尾的是语句,在 Rust 中,一定要严格区分表达式语句的区别,这个在其它语言中往往是被忽视的点。

永不返回的发散函数`!

当用 ! 作函数返回类型的时候,表示该函数永不返回(diverge function),这种语法往往用于会导致程序崩溃的函数

1
2
3
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}

下面的函数创建了一个无限循环,该循环永不跳出,因此函数也永不返回:

1
2
3
4
5
fn forever() -> ! {
loop {
//...
};
}