Rust 语言之旅

基础知识

变量

变量使用 let 关键字来声明。

在赋值时,Rust 能够在 99% 的情况下自动推断其类型。如果不能,也可以手动将类型添加到变量声明中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
// rust 推断出x的类型
let x = 13;
println!("{}", x);

// rust也可以显式声明类型
let x: f64 = 3.14159;
println!("{}", x);

// rust 也支持先声明后初始化,但很少这样做
let x;
x = 0;
println!("{}", x);
}

Rust 非常关心哪些变量是可修改的。值分为两种类型:

  • 可变的 - 编译器允许对变量进行读取和写入。
  • 不可变的 - 编译器只允许对变量进行读取。

可变值用 mut 关键字表示。

基本类型

Rust 有多种常见的类型:

  • 布尔型 - bool 表示 true 或 false
  • 无符号整型- u8 u32 u64 u128 表示正整数
  • 有符号整型 - i8 i32 i64 i128 表示正负整数
  • 指针大小的整数 - usize isize 表示内存中内容的索引和大小
  • 浮点数 - f32 f64
  • 元组(tuple) - (value, value, ...) 用于在栈上传递固定序列的值
  • 数组 - 在编译时已知的具有固定长度的相同元素的集合
  • 切片(slice) - 在运行时已知长度的相同元素的集合
  • str(string slice) - 在运行时已知长度的文本

可以通过将类型附加到数字的末尾来明确指定数字类型(如 13u322u8

类型转换

使用 as 关键字

1
2
3
4
5
6
7
8
9
fn main() {
let a = 13u8;
let b = 7u32;
let c = a as u32 + b;
println!("{}", c);

let t = true;
println!("{}", t as u8);
}

常量

使用 const 关键字,常量必须始终具有显式的类型。

数组

数组是所有相同类型数据元素的固定长度集合。

一个数组的数据类型是 [T;N],其中 T 是元素的类型,N 是编译时已知的固定长度。

可以使用 [x] 运算符提取单个元素,其中 x 是所需元素的 usize 索引(从 0 开始)。

1
2
3
4
5
fn main() {
let nums: [i32; 3] = [1, 2, 3];
println!("{:?}", nums);
println!("{}", nums[1]);
}

函数

fn + 函数名 + 参数 + 返回类型

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

多个返回值

函数可以通过元组来返回多个值。

元组元素可以通过他们的索引来获取。

Rust 允许我们将后续会看到的各种形式的解构,也允许我们以符合逻辑的方式提取数据结构的子片段。

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

fn main() {
// 返回一个元组
let result = swap(123, 321);
println!("{} {}", result.0, result.1);

// 将元组解构为两个变量
let (a, b) = swap(result.0, result.1);
println!("{} {}", a, b);
}

没有返回值将会返回一个空元组,使用 () 进行表示

基本控制流

if/else if/else

Rust 的条件判断没有括号!所有常见的逻辑运算符仍然适用:==!=<><=>=!||&&

循环

break 会退出当前循环

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut x = 0;
loop {
x += 1;
if x == 42 {
break;
}
}
println!("{}", x);
}

loop 可以被中断以返回一个值。

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut x = 0;
let v = loop {
x += 1;
if x == 13 {
break "found the 13";
}
};
println!("from loop: {}", v);
}

while

1
2
3
4
5
6
fn main() {
let mut x = 0;
while x != 42 {
x += 1;
}
}

for

Rust 的 for 循环是一个强大的升级。它遍历来自计算结果为迭代器的任意表达式的值。

.. 运算符创建一个可以生成包含起始数字、但不包含末尾数字的数字序列的迭代器。

..= 运算符创建一个可以生成包含起始数字、且包含末尾数字的数字序列的迭代器。

1
2
3
4
5
6
7
8
9
fn main() {
for x in 0..5 {
println!("{}", x);
}

for x in 0..=5 {
println!("{}", x);
}
}

match

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
27
fn main() {
let x = 42;

match x {
0 => {
println!("found zero");
}
// 我们可以匹配多个值
1 | 2 => {
println!("found 1 or 2!");
}
// 我们可以匹配迭代器
3..=9 => {
println!("found a number 3 to 9 inclusively");
}
// 我们可以将匹配数值绑定到变量
matched_num @ 10..=100 => {
println!("found {} number between 10 to 100!", matched_num);
}
// 这是默认匹配,如果没有处理所有情况,则必须存在该匹配
_ => {
println!("found something else!");
}
}
}

// Standard Output: found 42 number between 10 to 100!

基本数据结构类型

结构体

一个 struct 就是一些字段的集合。

字段是一个与数据结构相关联的数据值。它的值可以是基本类型或结构体类型。

1
2
3
4
5
6
7
8
struct SeaCreature {
// String 是个结构体
animal_type: String,
name: String,
arms: i32,
legs: i32,
weapon: String,
}

方法调用

与函数(function)不同,方法(method)是与特定数据类型关联的函数。

静态方法 — 属于某个类型,调用时使用 :: 运算符。

实例方法 — 属于某个类型的实例,调用时使用 . 运算符。

1
2
3
4
5
6
fn main() {
// 使用静态方法来创建一个String实例
let s = String::from("Hello world!");
// 使用实例来调用方法
println!("{} is {} characters long.", s, s.len());
}

类元组结构体

简洁起见,你可以创建像元组一样被使用的结构体。

1
2
3
4
5
6
7
struct Location(i32, i32);

fn main() {
// 这仍然是一个在栈上的结构体
let loc = Location(42, 32);
println!("{}, {}", loc.0, loc.1);
}

泛型

泛型允许我们不完全定义一个 structenum,使编译器能够根据我们的代码使用情况,在编译时创建一个完全定义的版本。

Rust 通常可以通过查看我们的实例化来推断出最终的类型,但是如果需要帮助,你可以使用 ::<T> 操作符来显式地进行操作, 该操作符也被称为 turbofish

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
// 一个部分定义的结构体类型
struct BagOfHolding<T> {
item: T,
}

fn main() {
// 注意:通过使用泛型,我们创建了编译时创建的类型,使代码更大
// Turbofish 使之显式化
let i32_bag = BagOfHolding::<i32> { item: 42 };
let bool_bag = BagOfHolding::<bool> { item: true };

// Rust 也可以推断出泛型的类型!
let float_bag = BagOfHolding { item: 3.14 };

// 注意:在现实生活中,不要把一袋东西放在另一袋东西里:)
let bag_in_bag = BagOfHolding {
item: BagOfHolding { item: "嘭!" },
};

println!(
"{} {} {} {}",
i32_bag.item, bool_bag.item, float_bag.item, bag_in_bag.item.item
);
}
// Standard Output: 42 true 3.14 嘭!

有点像是 C++ 中的模板类

表示空

Rust 没有 null,使用泛型来替代值,并采用 None 进行替代

1
2
3
4
5
6
7
8
9
10
enum Item {
Inventory(String),
// None represents the absence of an item
None,

}

struct BagOfHolding {
item: Item,
}

Option

Rust 有一个内置的泛型枚举叫做 Option,它可以让我们不使用 null 就可以表示可以为空的值。

1
2
3
4
enum Option<T> {
None,
Some(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
// 一个部分定义的结构体
struct BagOfHolding<T> {
// 我们的参数类型T可以传递给其他
item: Option<T>,
}

fn main() {
// 注意:一个放 i32 的 bag,里面什么都没有!
// 我们必须注明类型,否则 Rust 不知道 bag 的类型
let i32_bag = BagOfHolding::<i32> { item: None };

if i32_bag.item.is_none() {
println!("there's nothing in the bag!")
} else {
println!("there's something in the bag!")
}

let i32_bag = BagOfHolding::<i32> { item: Some(42) };

if i32_bag.item.is_some() {
println!("there's something in the bag!")
} else {
println!("there's nothing in the bag!")
}

// match 可以让我们优雅地解构 Option,并且确保我们处理了所有的可能情况!
match i32_bag.item {
Some(v) => println!("found {} in bag!", v),
None => println!("found nothing"),
}
}

Result

Rust 有一个内置的泛型枚举叫做 Result,它可以让我们返回一个可能包含错误的值。 这是编程语言进行错误处理的惯用方法。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn do_something_that_might_fail(i:i32) -> Result<f32,String> {
if i == 42 {
Ok(13.0)
} else {
Err(String::from("this is not the right number"))
}
}

fn main() {
let result = do_something_that_might_fail(12);

// match 让我优雅地解构 Rust,并且确保我们处理了所有情况!
match result {
Ok(v) => println!("found {}", v),
Err(e) => println!("Error: {}",e),
}
}

main 函数有可以返回 Result 的能力!

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 do_something_that_might_fail(i: i32) -> Result<f32, String> {
if i == 42 {
Ok(13.0)
} else {
Err(String::from("this is not the right number"))
}
}

// 主函数不返回值,但可能返回一个错误!
fn main() -> Result<(), String> {
let result = do_something_that_might_fail(12);

match result {
Ok(v) => println!("found {}", v),
Err(_e) => {
// 优雅地处理错误

// 返回一个说明发生了什么的新错误!
return Err(String::from("something went wrong in main!"));
}
}

// Notice we use a unit value inside a Result Ok
// to represent everything is fine
Ok(())
}

错误处理

Result 如此常见以至于 Rust 有个强大的操作符 ? 来与之配合。 以下两个表达式是等价的:

1
2
3
4
5
do_something_that_might_fail()?
match do_something_that_might_fail() {
Ok(v) => v,
Err(e) => return Err(e),
}

Option/Result 处理

当只是试图快速地写一些代码时,Option/Result 对付起来可能比较无聊。 OptionResult 都有一个名为 unwrap 的函数:这个函数可以简单粗暴地获取其中的值。 unwrap 会:

  1. 获取 Option/Result 内部的值
  2. 如果枚举的类型是 None/Err, 则会 panic!

这两段代码是等价的:

1
2
3
4
5
my_option.unwrap()
match my_option {
Some(v) => v,
None => panic!("some error message generated by Rust!"),
}

类似的:

1
2
3
4
5
my_result.unwrap()
match my_result {
Ok(v) => v,
Err(e) => panic!("some error message generated by Rust!"),
}

Vectors

一些经常使用的泛型是集合类型。一个 vector 是可变长度的元素集合,以 Vec 结构表示。

比起手动构建,宏 vec! 让我们可以轻松地创建 vector。

Vec 有一个形如 iter() 的方法可以为一个 vector 创建迭代器,这允许我们可以轻松地将 vector 用到 for 循环中去。

内存细节:

  • Vec 是一个结构体,但是内部其实保存了在堆上固定长度数据的引用。
  • 一个 vector 开始有默认大小容量,当更多的元素被添加进来后,它会重新在堆上分配一个新的并具有更大容量的定长列表。(类似 C++ 的 vector)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 我们可以显式确定类型
let mut i32_vec = Vec::<i32>::new(); // turbofish <3
i32_vec.push(1);
i32_vec.push(2);
i32_vec.push(3);

// 但是看看 Rust 是多么聪明的自动检测类型啊
let mut float_vec = Vec::new();
float_vec.push(1.3);
float_vec.push(2.3);
float_vec.push(3.4);

// 这是个漂亮的宏!
let string_vec = vec![String::from("Hello"), String::from("World")];

for word in string_vec.iter() {
println!("{}", word);
}
}

所有权和数据借用

Rust 没有垃圾回收机制。

Rust 将使用资源最后被使用的位置或者一个函数域的结束来作为资源被析构和释放的地方。 此处析构和释放的概念被称之为 drop(丢弃)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Foo {
x: i32,
}

fn main() {
let foo_a = Foo { x: 42 };
let foo_b = Foo { x: 13 };

println!("{}", foo_a.x);
// foo_a 将在这里被 dropped 因为其在这之后再也没有被使用

println!("{}", foo_b.x);
// foo_b 将在这里被 dropped 因为这是函数域的结尾
}

移交所有权

将所有者作为参数传递给函数时,其所有权将移交至该函数的参数。 在一次移动后,原函数中的变量将无法再被使用。

内存细节:

  • 移动期间,所有者的堆栈值将会被复制到函数调用的参数堆栈中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Foo {
x: i32,
}

fn do_something(f: Foo) {
println!("{}", f.x);
// f 在这里被 dropped 释放
}

fn main() {
let foo = Foo { x: 42 };
// foo 被移交至 do_something
do_something(foo);
// 此后 foo 便无法再被使用
// println!("{}", foo); 产生相应报错
}

归还所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Foo {
x: i32,
}

fn do_something() -> Foo {
Foo { x: 42 }
// 所有权被移出
}

fn main() {
let foo = do_something();
// foo 成为了所有者
// foo 在函数域结尾被 dropped 释放
}

使用引用借用所有权

引用允许我们通过 & 操作符来借用对一个资源的访问权限。 引用也会如同其他资源一样被释放。

1
2
3
4
5
6
7
8
9
10
11
struct Foo {
x: i32,
}

fn main() {
let foo = Foo { x: 42 };
let f = &foo;
println!("{}", f.x);
// f 在这里被 dropped 释放
// foo 在这里被 dropped 释放
}

通过引用借用可变所有权

我们也可以使用 &mut 操作符来借用对一个资源的可变访问权限。 在发生了可变借用后,一个资源的所有者便不可以再次被借用或者修改。

内存细节:

  • Rust 之所以要避免同时存在两种可以改变所拥有变量值的方式,是因为此举可能会导致潜在的数据争用(data race)。
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
struct Foo {
x: i32,
}

fn do_something(f: Foo) {
println!("{}", f.x);
// f 在这里被 dropped 释放
}

fn main() {
let mut foo = Foo { x: 42 };
let f = &mut foo;

// 会报错: do_something(foo);
// 因为 foo 已经被可变借用而无法取得其所有权

// 会报错: foo.x = 13;
// 因为 foo 已经被可变借用而无法被修改

f.x = 13;
// f 会因为此后不再被使用而被 dropped 释放

println!("{}", foo.x);

// 现在修改可以正常进行因为其所有可变引用已经被 dropped 释放
foo.x = 7;

// 移动 foo 的所有权到一个函数中
do_something(foo);
}

所有权意味着我们需要注意我们的引用变量在原变量后是会产生数据争用,进而会产生报错。这意味着我们如果在 foo.x = 7 后面将 f.x 进行输出会引发报错

1
2
foo.x = 7;
println!("{}", f.x);

上述代码会产生报错信息

显示生命周期

尽管 Rust 不总是在代码中将它展示出来,但编译器会理解每一个变量的生命周期并进行验证以确保一个引用不会有长于其所有者的存在时间。 同时,函数可以通过使用一些符号来参数化函数签名,以帮助界定哪些参数和返回值共享同一生命周期。 生命周期注解总是以 ' 开头,例如 'a'b 以及 'c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Foo {
x: i32,
}

// 参数 foo 和返回值共享同一生命周期
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
return &foo.x;
}

fn main() {
let mut foo = Foo { x: 42 };
let x = &mut foo.x;
*x = 13;
// x 在这里被 dropped 释放从而允许我们再创建一个不可变引用
let y = do_something(&foo);
println!("{}", y);
// y 在这里被 dropped 释放
// foo 在这里被 dropped 释放
}

文本

字符串常量(String Literals)

字符串常量(String Literals)采用 Unicode 编码(注:下文提及的 utf-8 为 Unicode 的一部分)。

字符串常量的类型为 &'static str

  • & 意味着该变量为对内存中数据的引用,没有使用 &mut 代表编译器将不会允许对该变量的修改
  • 'static 意味着字符串数据将会一直保存到程序结束(它不会在程序运行期间被释放(drop)
  • str 意味着该变量总是指向一串合法的 utf-8 字节序列。

内存细节:

  • Rust 编译器可能会将字符串储存在程序内存的数据段中。
1
2
3
4
fn main() {
let a: &'static str = "你好 🦀";
println!("{} {}", a, a.len());
}

转义字符

Rust 支持类 C 语言中的常见转义字符;

  • \n - 换行符
  • \r - 回车符(回到本行起始位置)
  • \t - 水平制表符(即键盘 Tab 键)
  • \\ - 代表单个反斜杠 \
  • \0 - 空字符(null)
  • \' - 代表单引号 ’
1
2
3
4
fn main() {
let a: &'static str = "Ferris 说:\t\"你好\"";
println!("{}",a);
}

原始字符串常量

原始字符串支持写入原始的文本而无需为特殊字符转义,因而不会导致可读性下降(如双引号与反斜杠无需写为 \"\\),只需以 r#" 开头,以 "# 结尾。

1
2
3
4
5
6
7
8
fn main() {
let a: &'static str = r#"
<div class="advice">
原始字符串在一些情景下非常有用。
</div>
"#;
println!("{}", a);
}

文件中的字符串常量

如果你需要使用大量文本,可以尝试用宏 include_str! 来从本地文件中导入文本到程序中:

1
let hello_html = include_str!("hello.html");

字符串片段(String Slice)

字符串片段是对内存中字节序列的引用,而且这段字节序列必须是合法的 utf-8 字节序列。

str 片段的字符串片段(子片段),也必须是合法的 utf-8 字节序列。

&str 的常用方法:

  • len 获取字符串常量的字节长度(不是字符长度)。
  • starts_with/ends_with 用于基础测试。
  • is_empty 长度为 0 时返回 true。
  • find 返回 Option<usize>,其中的 usize 为匹配到的第一个对应文本的索引值。
1
2
3
4
5
6
7
8
9
fn main() {
let a = "你好 🦀";
println!("{}", a.len());
let first_word = &a[0..6];
let second_word = &a[7..11];
// let half_crab = &a[7..9]; 报错
// Rust 不接受无效 unicode 字符构成的片段
println!("{} {}", first_word, second_word);
}

Char

为了解决使用 Unicode 带来的麻烦,Rust 提供了将 utf-8 字节序列转化为类型 char 的 vector 的方法。

每个 char 长度都为 4 字节(可提高字符查找的效率)。

1
2
3
4
5
6
7
fn main() {
// 收集字符并转换为类型为 char 的 vector
let chars = "你好 🦀".chars().collect::<Vec<char>>();
println!("{}", chars.len()); // 结果应为 4
// 由于 char 为 4 字节长,我们可以将其转化为 u32
println!("{}", chars[3] as u32);
}

字符串(String)

字符串String 是一个结构体,其持有以堆(heap)的形式在内存中存储的 utf-8 字节序列。

由于它以堆的形式来存储,字符串可以延长、修改等等。这些都是字符串常量(string literals)无法执行的操作。

常用方法:

  • push_str 用于在字符串的结尾添加字符串常量(&str)。
  • replace 用于将一段字符串替换为其它的。
  • to_lowercase/to_uppercase 用于大小写转换。
  • trim 用于去除字符串前后的空格。

如果字符串String 被释放(drop)了,其对应的堆内存片段也将被释放。

字符串String 可以使用 + 运算符来在其结尾处连接一个 &str 并将其自身返回。但这个方法可能并不像你想象中的那么人性化。

1
2
3
4
5
6
fn main() {
let mut helloworld = String::from("你好");
helloworld.push_str(" 世界");
helloworld = helloworld + "!";
println!("{}", helloworld);
}

字符串构建

concatjoin 可以以简洁而有效的方式构建字符串

1
2
3
4
5
6
7
8
9
10
fn main() {
let helloworld = ["你好", " ", "世界", "!"].concat();
let abc = ["a", "b", "c"].join(",");
println!("{}", helloworld);
println!("{}",abc);
}

// Standard Output:
// 你好 世界!
// a,b,c

字符串格式化

format! 可用于创建一个使用占位符的参数化字符串。(例:{}

format!println! 生成的参数化字符串相同,只是 format! 将其返回而 println! 将其打印出来

1
2
3
4
5
fn main() {
let a = 42;
let f = format!("生活诀窍: {}",a);
println!("{}",f);
}

字符串转换

许多类型都可以通过 to_string 转换为字符串。

而泛型函数 parse 则可将字符串或是字符串常量转换为其它类型,该函数会返回 Result 因为转换有可能失败。

1
2
3
4
5
6
7
fn main() -> Result<(), std::num::ParseIntError> {
let a = 42;
let a_string = a.to_string();
let b = a_string.parse::<i32>()?;
println!("{} {}", a, b);
Ok(())
}

面向对象

使用方法进行封装

Rust 支持对象的概念。“对象”是一个与一些函数(也称为方法)相关联的结构体。

任何方法的第一个参数必须是与方法调用相关联的实例的引用。(例如 instanceOfObj.foo())。Rust 使用:

  • &self —— 对实例的不可变引用。
  • &mut self —— 对实例的可变引用。

方法是在一个有 impl 关键字的实现块中定义的:

1
2
3
4
5
6
impl MyStruct { 
...
fn foo(&self) {
...
}
}

有如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct SeaCreature {
noise: String,
}

impl SeaCreature {
fn get_sound(&self) -> &str {
&self.noise
}
}

fn main() {
let creature = SeaCreature {
noise: String::from("blub"),
};
println!("{}", creature.get_sound());
}

抽象与选择性暴露

Rust 可以隐藏对象的内部实现细节。

默认情况下,字段和方法只有它们所属的模块才可访问。

pub 关键字可以将字段和方法暴露给模块外的访问者。

priv 关键字可以将字段和方法隐藏。

使用 Trait 实现多态

Rust 支持多态的特性。Trait 允许我们将一组方法与结构类型关联起来。

我们首先在 Trait 里面定义函数签名:

1
2
3
4
trait MyTrait {
fn foo(&self);
...
}

当一个结构体实现一个 trait 时,它便建立了一个契约,允许我们通过 trait 类型与结构体进行间接交互(例如 &dyn MyTrait),而不必知道其真实的类型。

结构体实现 Trait 方法是在实现块中定义要实现的方法:

1
2
3
4
5
6
impl MyTrait for MyStruct { 
fn foo(&self) {
...
}
...
}

有如下例子:

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
struct SeaCreature {
pub name: String,
noise: String,
}

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}

}

trait NoiseMaker {
fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound());
}
}

fn main() {
let creature = SeaCreature {
name: String::from("Ferris"),
noise: String::from("blub"),
};
creature.make_noise();
}

动态调度和静态调度

方法的执行有两种方式:

  • 静态调度——当实例类型已知时,我们直接知道要调用什么函数。
  • 动态调度——当实例类型未知时,我们必须想方法来调用正确的函数。

Trait 类型 &dyn MyTrait 给我们提供了使用动态调度间接处理对象实例的能力。

当使用动态调度时,Rust 会鼓励你在你的 trait 类型前加上dyn,以便其他人知道你在做什么。

内存细节:

  • 动态调度的速度稍慢,因为要追寻指针以找到真正的函数调用。
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
struct SeaCreature {
pub name: String,
noise: String,
}

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}
}

trait NoiseMaker {
fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound());
}
}

fn static_make_noise(creature: &SeaCreature) {
// 我们知道真实类型
creature.make_noise();
}

fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
// 我们不知道真实类型
noise_maker.make_noise();
}

fn main() {
let creature = SeaCreature {
name: String::from("Ferris"),
noise: String::from("咕噜"),
};
static_make_noise(&creature);
dynamic_make_noise(&creature);
}

泛型函数

Rust中的泛型与 Trait 是相辅相成的。 当我们描述一个参数化类型 T 时,我们可以通过列出参数必须实现的 Trait 来限制哪些类型可以作为参数使用。

在以下例子中,类型 T 必须实现 Foo 这个 Trait:

1
2
3
4
5
6
fn my_function<T>(foo: T)
where
T:Foo
{
...
}

通过使用泛型,我们在编译时创建静态类型的函数,这些函数有已知的类型和大小,允许我们对其执行静态调度,并存储为有已知大小的值。

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
struct SeaCreature {
pub name: String,
noise: String,
}

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}
}

trait NoiseMaker {
fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound());
}
}

fn generic_make_noise<T>(creature: &T)
where
T: NoiseMaker,
{
// 我们在编译期就已经知道其真实类型
creature.make_noise();
}

fn main() {
let creature = SeaCreature {
name: String::from("Ferris"),
noise: String::from("咕噜"),
};
generic_make_noise(&creature);
}

泛型函数简写

Rust 为由 Trait 限制的泛型函数提供了简写形式:

1
2
3
fn my_function(foo: impl Foo) {
...
}

这段代码等价于:

1
2
3
4
5
6
fn my_function<T>(foo: T)
where
T:Foo
{
...
}

有以下例子:

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
struct SeaCreature {
pub name: String,
noise: String,
}

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}
}

trait NoiseMaker {
fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound());
}
}

fn generic_make_noise(creature: &impl NoiseMaker)
{
// 我们在编译期就已经知道其真实类型
creature.make_noise();
}

fn main() {
let creature = SeaCreature {
name: String::from("Ferris"),
noise: String::from("咕噜"),
};
generic_make_noise(&creature);
}

Box

Box 是一个允许我们将数据从栈上移到堆上的数据结构。

Box 是一个被称为智能指针的结构,它持有指向我们在堆上的数据的指针。

由于 Box 是一个已知大小的结构体(因为它只是持有一个指针), 因此它经常被用在一个必须知道其字段大小的结构体中存储对某个目标的引用。

Box 非常常见,它几乎可以被用在任何地方:

1
Box::new(Foo { ... })

有以下例子

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
struct SeaCreature {
pub name: String,
noise: String,
}

impl SeaCreature {
pub fn get_sound(&self) -> &str {
&self.noise
}
}

trait NoiseMaker {
fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
fn make_noise(&self) {
println!("{}", &self.get_sound());
}
}

struct Ocean {
animals: Vec<Box<dyn NoiseMaker>>,
}

fn main() {
let ferris = SeaCreature {
name: String::from("Ferris"),
noise: String::from("咕噜"),
};
let sarah = SeaCreature {
name: String::from("Sarah"),
noise: String::from("哧溜"),
};
let ocean = Ocean {
animals: vec![Box::new(ferris), Box::new(sarah)],
};
for a in ocean.animals.iter() {
a.make_noise();
}
}

智能指针

指针

引用可以转换成一个更原始的类型,指针(raw pointer)。 像数字一样,它可以不受限制地复制和传递,但是Rust 不保证它指向的内存位置的有效性。 有两种指针类型:

  • *const T - 指向永远不会改变的 T 类型数据的指针。
  • *mut T - 指向可以更改的 T 类型数据的指针。

指针可以与数字相互转换(例如usize)。
指针可以使用 unsafe 代码访问数据(稍后会详细介绍)。

内存细节:

  • Rust中的引用在用法上与 C 中的指针非常相似,但在如何存储和传递给其他函数上有更多的编译时间限制。
  • Rust中的指针类似于 C 中的指针,它表示一个可以复制或传递的数字,甚至可以转换为数字类型,可以将其修改为数字以进行指针数学运算。
1
2
3
4
5
fn main() {
let a = 42;
let memory_location = &a as *const i32 as usize;
println!("Data is here {:x}", memory_location);
}

运算符 *

* 运算符是一种很明确的解引用的方法。

1
2
3
4
let a: i32 = 42;
let ref_ref_ref_a: &&&i32 = &&&a;
let ref_a: &i32 = **ref_ref_ref_a;
let b: i32 = *ref_a;

内存细节:

  • 因为 i32 是实现了 Copy 特性的原始类型,堆栈上变量 a 的字节被复制到变量 b 的字节中。

运算符 .

.运算符用于访问引用的字段和方法,它的工作原理更加巧妙。

1
2
3
let f = Foo { value: 42 };
let ref_ref_ref_f = &&&f;
println!("{}", ref_ref_ref_f.value);

. 运算符会做一些列自动解引用操作。 最后一行由编译器自动转换为以下内容。

1
println!("{}", (***ref_ref_ref_f).value);

智能指针

除了能够使用&运算符创建对现有类型数据的引用之外, Rust 给我们提供了能够创建称为智能指针的类引用结构。
我们可以在高层次上将引用视为一种类型,它使我们能够访问另一种类型. 智能指针的行为与普通引用不同,因为它们基于程序员编写的内部逻辑进行操作. 作为程序员的你就是智能的一部分。
通常,智能指针实现了 DerefDerefMutDrop 特征,以指定当使用 *. 运算符时解引用应该触发的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Deref;
struct TattleTell<T> {
value: T,
}
impl<T> Deref for TattleTell<T> {
type Target = T;
fn deref(&self) -> &T {
println!("{} was used!", std::any::type_name::<T>());
&self.value
}
}
fn main() {
let foo = TattleTell {
value: "secret message",
};
// dereference occurs here immediately
// after foo is auto-referenced for the
// function `len`
println!("{}", foo.len());
}

测试

测试编写

我们可以通过添加#[cfg(test)]标签来表明我们来进行测试,对我们需要进行测试的函数打上对应标签#[test],有以下例子:

1
2
3
4
5
6
7
8
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}

我们使用assert_eq!()来根据运算结果来判断是否产生一个panic信息,当我们发生错误时会在测试时用FAILED进行显示出来,方便我们定位到发生错误的对应函数中去

当我们要进行测试时,我们使用以下代码来进行运行

1
cargo test

当我们测试一个应该发生panic的错误处理时我们可以添加 #[should_panic]标签来表明对应代码应该产生panic错误信息,当其未按预期产生错误信息时便会产生FAILED

并行或连续的运行测试

当运行多个测试时,Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。

举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 test-output.txt 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。

如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 --test-threads 参数和希望使用线程的数量给测试二进制文件。例如:

1
$ cargo test -- --test-threads=1

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。

显示函数输出

默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。

如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 --show-output 告诉 Rust 显示成功测试的输出。

1
$ cargo test -- --show-output

Rust 语言之旅
https://equinox-shame.github.io/2023/09/06/Rust 语言之旅/
作者
梓曰
发布于
2023年9月6日
许可协议