Rust官方文档学习记录

常用命令

  • rustc

    rustc xxx.rs // 编译Rust程序

  • cargo

    cargo build // 编译Rust项目,如block在更新lock文件,可执行 rm -rf ~/.cargo/.package-cache

    cargo run // 立刻运行Rust项目

    cargo doc –open // 生成并打开项目文档

常见概念

变量和可变性

  • 使用let xxx 声明一个变量,此时这个变量是不可变的,但是可以被隐藏
  • 使用let mut xxx 声明一个可变变量,此时变量可以被重新赋值
  • 使用const XXX声明一个常量,常量不可被重新赋值

隐藏

​ 重复使用let定义一个与之前变量同名的变量,我们称第一个变量被第二个变量隐藏了,此时使用该名称的变量会使用第二个变量,我们可以重复使用let来多次隐藏。

1
2
3
4
5
6
7
8
9
fn main() {
  let x = 5;
  let x = x + 1;
  {
    let x = x * 2;
    println!("block inner x:{}", x);
  }
  println!("block outer x:{}", x);
}

上面例子会输出:

1
2
block inner x:12
block outer x:6

​ 这个程序首先将 x 绑定到值 5 上。接着通过 let x = 隐藏 x,获取初始值并加 1,这样 x 的值就变成 6 了。然后,在内部作用域内,第三个 let 语句也隐藏了 x,将之前的值乘以 2x 得到的值是 12。当该作用域结束时,内部 shadowing 的作用域也结束了,x 又返回到 6

数据类型

标量类型

整型
长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize
浮点型

​ Rust 的浮点数类型是 f32f64,分别占 32 位和 64 位。默认类型是 f64

布尔型

​ Rust 中的布尔类型有两个可能的值:truefalse。Rust 中的布尔类型使用 bool 表示。

字符型

​ Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(意味着你可以使用emoji)。

复合类型

元组类型

相当于把一个或者多个类型的值组合成一个类型。元组的长度是固定的:一旦声明。其长度不会改变。

我们可以使用圆括号中逗号分割的值列表来创建一个元组。

1
2
3
4
fn main() {
  let one: (i32, f64, u8) = (500, 6.4 ,1);
  let one = (500, 6.4 ,1);
}

同时可以对元组进行解构,或者只用点号(.)跟着值的索引(从0开始)直接访问

1
2
3
4
5
6
fn main() {
  let one = (500, 6.4 ,1);
  let (x, y, z) = one;
  println("x:{} y:{} z:{}",x, y, z);
  println("x:{} y:{} z:{}",one.0, one.1, one.2);
}
数组类型

另一个包含多个值的方式是 数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。

可以使用以下两种方式来声明数组:

1
2
3
4
5
6
fn main() {
  let a = [1, 2, 3, 4, 5];
  // 当你想定义类型或者长度时可使用下面这种方式
  // 在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量
  let a: [i64; 5] = [1, 2, 3, 4, 5];
}

函数

Rust 中的函数定义以 fn 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn five() -> i32 {
    5
}

fn four() -> i32 {
  let a = 4;
  return a;
}

fn main() {
    let x = five();

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

语句和表达式

Rust是一门基于表达式(expression-based)的语言。

使用let关键字创建变量并绑定一个值是一个语句(let y = 6;)。

语句不返回值。不能把let语句赋值给另外一个变量(let x = (let y = 6))。

表达式可以计算出一个值,考虑一个数学运算,比如5+6,这是一个表达式并计算出值11。表达式可以是语句的一部分。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用于的大括号(代码块),{},也是一个表达式。

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

这个表达式:

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

是一个代码块,它的值是 4。这个值作为 let 语句的一部分被绑定到 y 上。注意结尾没有分号的那一行 x+1,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。

控制流

if表达式

if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”

1
2
3
4
5
6
7
8
fn main() {
  let number = 3;
  if number < 5 {
    // xxx
  } else {
    // yyy
  }
}

代码中的条件必须bool值。如果不是bool值,我们会得到一个错误。

因为if是一个表达式,所以我们可以在let语句的右侧使用它。注意,ifelse分支的结果都必须是相同类型

1
2
3
4
5
6
7
8
9
fn main() {
  let conditioin = true;
  let number = if condition {
    5
  } else {
    6
  };
  println!("number:{}", number);
}

循环

Rust 中有三种循环:loopwhilefor

loop表达式

loop就是一个无限循环,需要显式的调用break来退出这个循环。

如果存在嵌套循环,此时单独只用breakcontinue只应用于此时语句最内层的循环。可以在循环上指定一个循环标签,然后将标签与breakcontinue一起使用,此时这些关键字生效的则是已标记的循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
  let mut count = 0;
  'counting_up: loop {
    let mut remaining = 10;
    
    loop {
			println!("remaining = {}", remaining);
      if remaining == 9 {
				break;
      }
      if count == 2 {
        break 'counting_up;
      }
      remaining -= 1;
    }
    count += 1;
  }
  println!("End count = {}", count);
}

loop的另外一个用法是重试可能会失败的操作,比如检查线程是否完成了任务。如果将返回值加入你用来停止循环的break表达式,它会被停止的循环返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

while条件循环

当条件为真,执行循环。当条件不再为真,则停止循环。这个循环类型可以通过组合 loopifelsebreak 来实现。或者直接使用while

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

for遍历集合

可以使用 for 循环来对一个集合的每个元素执行一些代码。

1
2
3
4
5
6
7
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

Rust的循环不像“C风格”的循环

1
2
3
for (x = 0; x < 10; x++) {
  printf("%d\n", x);
}

相反,Rust是这样的

1
2
3
4
5
6
7
for x in 0..10 {
  println!("{}", x);
}
// 当你需要 x <= 10 这种类型的时候
for x in 0..=10 {
  println!("{}", x);
}

当你需要知道当前已经循环多少次时,可以使用.enumerate()函数

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

输出:

1
2
3
4
5
i = 0 and j = 5
i = 1 and j = 6
i = 2 and j = 7
i = 3 and j = 8
i = 4 and j = 9

所有权

首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量作用域

变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。示例 4-1 的注释标明了变量 s 在何处是有效的。

1
2
3
4
5
{                      // s 在这里无效, 它尚未声明
    let s = "hello";   // 从此处起,s 是有效的

    // 使用 s
}                      // 此作用域已结束,s 不再有效

内存与分配

首先我们要知道变量的两种内存分配位置:。这个概念再此不做赘述。

当我们声明一个变量的类型,它的内存分配在上,我这习惯将其成为引用类型,如果它的内存分配在上,则称为值类型

值类型,如i32f64。引用类型,如string

1
2
3
4
fn main() {
  let x = 1;// i32 值类型
  let str = String::from("hello world!");// string 引用类型
}

值类型的传递方式都是copy

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

此时 xy都在栈上拥有属于自己的内存空间。

而引用类型的传递方式则不一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
  let x = String::from("hello world!");// x 获得 string 的所有权
  let y = x;// string 的所有权交给了 y
  // 此时 x 与 y 的地址共同指向堆的同一个地方
  // 我们只拷贝了其长度和容量信息,其在堆上的指针是相同的
  // 如果想拷贝其在堆上的数据
  let z = y.clone();// z clone 了一个 y,没有获得 string 的所有权
  // 此时则是 z 与 y 是地址完全不同的两个变量
  // 如果我们在这使用 let z = x.clone() 则对得到一个错误,因为Rust禁止你使用无效的引用
}

那如果是在函数里呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
  let str = String::from("hello world!");// str 进入作用域
  
  test_one(str);// str 所有权移动到函数内
  // str再此不在有效
  
  let x = 12;// x 进入作用域
  
  test_two(x);// x copy 进入函数内
  // x 是copy的,所以 x 在此依然有效
}// x移除作用域。 str 所有权已经移交,不做操作

fn test_one(some_string: String) {// some_string 进入作用域
  println!("{}", some_string);
}// some_string 离开作用域,调用 `drop` 方法释放内存

fn test_two(some_integer: i32) {// some_integer 进入作用域
  println!("{}", some_integer);
}// some_integer 离开作用域,不会有特殊操作

函数的返回值也可以转移所有权。

引用与借用

规则

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

如果我们只是想使用一个变量,而不是获取这个变量的所有权。我们则需要使用引用这个操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

我们在这里获得的是s1的引用,并没有获取s1的所有权。所以上述代码可以正常的运行。

使用的&符号就是引用,它允许你使用值但不获取其所有权。就像其他语言类似,获取一个变量的地址,也就是指针。

因为没有获取到所有权,所以当引用离开作用域时,不会将内存释放。但是这里的引用只能使用被引用的变量,当我们想修改这个变量时,则会发现会抛出一个错误。正如变量默认不可变一样,引用默认也是不可改变的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
 --> error.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- use `&mut String` here to make mutable
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ cannot borrow as mutable

如果我们想修改一个引用的值,我们只需做一个小调整:

1
2
3
4
5
6
7
8
9
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

这里使用&mut获取到的是 s的可变引用,这就清除的表明,我需要修改这个引用的值。不过可变引用有一个很大的限制。

在同一时间只能有一个对某一特定数据的可变引用

这个限制的好处是 Rust 可以在编译时就避免数据竞争。

不可变引用可以存在多个,但不能同时与可变引用存在。原因也很明显,谁也不想自己引用的变量在某一时间被修改。

这种概念和读写锁类似。可以拥有多个读锁(不可变引用),只能存在一个写锁并与读锁互斥(可变引用)。当了解这个概念之后就很好理解了。

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如以下代码是可以编译的:

1
2
3
4
5
6
7
8
9
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用,作用域也再此结束

let r3 = &mut s; // 没问题 r1,r2的作用域已经结束
println!("{}", r3);

不可变引用 r1r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器在作用域结束之前判断不再使用的引用的能力被称为非词法作用域生命周期(Non-Lexical Lifetimes,简称NLL)。你可以在 The Edition Guide 中阅读更多关于它的信息。

结构体

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段field)。例如,示例 5-1 展示了一个存储用户账号信息的结构体:

1
2
3
4
5
6
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

当变量名与字段名相同时,可以简写

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

我们还可以使用结构体更新语法

1
2
3
4
let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

在这里..user1必须放在最后,其他字段实际是使用=的赋值,所以要注意所有权的移动。

我们还可以创建没有字段的元组结构体,使用.接上索引来访问单独的值。

1
2
3
4
5
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

或者创建一个没有任何字段的结构体(类单元结构体)

单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。

1
2
3
struct AlwaysEqual;

let subject = AlwaysEqual;

todo 字段的生命周期

打印结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[derive(Debug)]
struct Rectangle {
  width: u32,
  height: u32,
}

fn main() {
	let rect1 = Rectangle { width: 30, height: 50 };
  println!("{:#?}", rect1);
  println!("{:#?}", rect1);
  dbg!(&rect1);
}

分别输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Rectangle {
    width: 30,
    height: 30,
}
Rectangle {
    width: 30,
    height: 30,
}
[src/main.rs:15] &rect1 = Rectangle {
    width: 30,
    height: 30,
}

struct绑定方法

定义方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height        
    }
}
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 30,
    };
    println!("这个长方体的面积是:{}", rect1.area());
}

为了使函数定义在Rectangle的上下文中,我们定义了一个impl块。在这个块中的所有内容都与Rectangle相关联。

area的函数前面上,使用&self来代替rectangle: &Rectangle&self其实是self: &Self的缩写。在一个impl块中,方法的第一个参数必须有一个名为selfSelf类型的参数,所以Rust在这里提供了self这个名字来缩写,在这里我们使用&来表示只是借用这个示例,并没有获取所有权。同时,我们也一样可以获取所有权,或者可变借用。

那我们的&运算符去哪里了呢?

area()函数的参数是&self,但是我们调用的地方并没有借用rect1;那是Rust有一个叫做自动引用和解引用的功能。方法调用是Rust中少数几个拥有这种行为的地方。

这样我们的代码实际是这样工作的:

1
2
rect1.area();
(&rect1).area();

这样看起来第一种方式简洁的多。

关联函数

所有在impl块中定义的函数被称为关联函数。我们可以定义一个不以self为第一参数的关联函数(它不是方法),我们已经使用过这样的函数了,String::from()。通常这样的函数被我们用作为返回一个结构体新实例的构造函数。

1
2
3
4
5
6
7
8
9
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

fn main() {
  let sq = Rectangle::square(3);
}

枚举

在 Rust 中我们使用enum来定义一个枚举类型。

1
2
3
4
enum IpAddrKind {
  V4,
  V6,
}

在上述代码中定义了一个IpAddrKind枚举来列出可能的 IP 地址类型,V4V6。这两种类型被称为枚举的成员

我们可以像这样创建IpAddrKind两个不同成员的实例。

1
2
3
4
5
6
7
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

fn route(ip_type: IpAddrKind) {}

route(four);
route(six);

可以看到我们可以使用任意成员来调用route函数。

枚举的成员也可以关联一个值。

1
2
3
4
5
6
7
enum IpAddr {
  V4(String),
  V6(String),
}

let home =  IpAddr::V4(String::from("127.0.0.1"));
let loopback =  IpAddr::V4(String::from("::1"));

或者关联上一个元组。

1
2
3
4
5
6
7
enum IpAddr {
  V4(u8, u8, u8, u8),
  V6(String),
}

let home =  IpAddr::V4(127, 0, 0, 1);
let loopback =  IpAddr::V4(String::from("::1"));

我们还可以对枚举类型关联上方法。

1
2
3
4
5
impl IpAddr {
 fn call(&self) {
   dbg!(self)
 }
}

我们就可以直接使用home.call()来调用这个方法。

todo option

相关控制流运算符

match运算符

如果有其他语言基础的话,看到match这个关键字,应该能联想到switch。它的用法和switch是差不多的,只是有一些细节不同。这里我们用上面的IpAddr绑定的方法call举例:

1
2
3
4
5
6
fn call(&self) {
  match self {
    IpAddr::V4(a, b, c, d) => println!("{}:{}:{}:{}", a, b, c, d),
    IpAddr::V6(str) => println!("{}", str),
  }
}

可以看到,我们使用了match匹配了self这个变量,在 case 里我们用上了IpAddr::V4(a, b, c, d),使用了a b c d四个变量来分别匹配绑定上的V4(u8, u8, u8, u8)4个值。而在IpAddr::V6(str)使用了str来接收绑定的String这样我们就可以根据不同的类型在做出不同的操作了。

但是注意 Rust 默认需要我们处理match后变量的所有可能情况,也就是说,下述代码是不正确的:

1
2
3
4
5
fn call(&self) {
  match self {
    IpAddr::V4(a, b, c, d) => println!("{}:{}:{}:{}", a, b, c, d),
  }
}

Rust 会提示我们少了IpAddr::V6case

上述例子只有2种情况,那如果是10种?20种呢?如果真的全部都要列出来的话,那未免太蠢了。所以 Rust 和其他语言一样,提供了类似default的功能,使用的关键字是_,我们来看看例子。

1
2
3
4
5
6
fn call(&self) {
  match self {
    IpAddr::V4(a, b, c, d) => println!("{}:{}:{}:{}", a, b, c, d),
    _ => (),
  }
}

注意必须要显式调用_ => () Rust不会默认给你加上。这样的话就可以忽略掉V6的匹配了。

if let简单控制流

想上面的例子,只有2种情况,我们还需要显式的用_去忽略匹配,实在是过于冗长,所以 Rust给了我们另外一种方式来处理这种情况。

1
2
3
4
5
fn call(&self) {
	if let IpAddr::V4(a, b, c, d) = self {
    println!("{}:{}:{}:{}", a, b, c, d);
  }
}

这样我们就不用再用那么冗长的方式来编写了。

package / crate / module

cargo new会生成项目的雏形,提供了 src/main.rshe src/lib.rs文件,随着项目复杂度的增长,代码量也会随之增长,如果靠一个文件来维护一大堆代码,肯定是不合适的。一般都会使用模块来拆分文件。

在这里学习一下rust中代码的组织方式,主要涉及到一下几个名词:

  • package:Cargo中的概念,用于管理crate
  • crate:模块的集合,编译单位,有binlib两种,分别是可执行文件,和供他人调用
  • module:用户在crate内组织代码
  • workspace:项目复杂时,管理多个package

package

使用cargo new命令会创建一个新项目,也就是一个package,里面带有一个Cargo.toml文件,用于定义package、所需外部依赖,以及如何编译crate

crate

Rust里有两种cratelib类型和bin类型,并且默认以文件名为标准处理crate:

  • src/main.rs:表示该crate时一个bin类型大crate
  • src/lib.rs:表示该crate时一个lib类型的crate

并且,一个package中的crate还有如下与约束:

  • 可以有多个bin类型的crate
  • 只能有0个或者1个lib类型的crate

以上两条约束并不互斥,也就是说一个项目下可以有一个lib和多个bin类型的crate,也就是可以编译出多个可执行文件

只是如果有多个bin类型的crate,一个src/main.rs就不够了,需要放到src/bin下,每个crate一个文件

mod

当项目逐渐膨胀后,可以对代码以mod「文件/文件夹」为单位进行拆分,而不是把所有代码都写在src/main.rs或者src/lib.rs

lib类型的crate为例,该类型的crate入口在src/lib.rs,也就是crate的根。定义一个模块也很简单

1
2
3
4
5
6
// src/lib.rs
mod testMod {
  fn test() {
    println!("test")
  }
}

而在实际项目中,我们不会只有一个lib.rs文件,而是会将代码按功能进行拆分成多个模块

模块拆分

一般来说,一个文件都会被看作为一个mod,并且mod可以嵌套定义。嵌套定义的mod可以卸载一个文件里,也可以通过文件夹的形式来实现。具体的我们来看几个例子。

假设当前项目文件结构如下:

1
2
3
4
5
6
src
├── lib.rs
├── mod_a
│   ├── mod.rs
│   └── mod_b.rs
└── mod_c.rs

在这里定义了3个mod:mod_a、mod_b 和 mod_c,其中mod_a为文件夹形式,而mod_bmod_c都有对应的文件。其中mod_bmod_a的子模块。

我们来看下各个模块之间如何声明和引用。

首先我们先来看看crate的根,也就是lib.rs

1
2
pub mod mod_a;
mod mod_c;

在这里声明了两个mod,如果需要在crate外部访问,需要在mod前面加上pub关键字。注意这里不需要声明mod_a的子模块mod_b,这个需要mod_a来声明。

再来看一下这两个mod。先看mod_a,这是一个文件夹形式存在的mod,按cargo规定,这时候需要在该文件夹下有一个名为mod.rs的文件定义该mod下的内容。该文件内容如下:

1
2
// src/mod_a/mod.rs
pub mod mod_b;

可以看到,这个文件和lib.rs类似,都可以声明mod。但该文件声明的mod可以保存到mod.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/mod_a/mod_b.rs
use super::super::mod_c;

pub fn test() {
  println!("i'm mod_b")
}

fn call_mod_c() {
  mod_c::test();
}

我们再来看看mod_c.rs的代码:

1
2
3
4
5
6
7
// src/mod_c.rs
use crate::mod_a::mod_b;

pub fn test() {
  mod_b::test();
  println!("i'm mod_c");
}

除了如何定义mod,我们还需要的是如何引用其他mod的定义。在mod_c中,要想使用mod_b,可以使用:

  • 绝对路径use crate::mod_a::mod_b

而在mod_b中使用mod_c的时候,使用了use super::super::mod_c这种相对路径的形式。

添加main.rs

最后在上面代码的基础上添加main.rs,看看作为外部crate如何使用上面的mod_a

1
2
3
4
5
6
7
// src/main.rs
use testlib::mod_a::mod_b;

fn main() {
  println!("main")
  mod_b::test();
}

在这里需要注意的是,引用自己lib的方法不能使用上面说的绝对路径或者相对路径这两种引用方式,必须使用该crate的名称「也就是Cargo.toml里的名称」来应用。因为mainlib属于不同的crate

pub 修饰符

要想访问其他mod中的结构体、方法、枚举等,需要对方声明为pub。如果是想操作结构体中的字段,可以有一下两种方法

  • 提供对应的pub方法
  • 直接修改字段为pub

use 语句

讲了这么做如何定义mod,我们来看下如何使用

crate和模块中我们可能定义了函数、结构体等,要想在其他模块或者crate中使用,需要将其引入到当前文件中,类似phpuse,或者java中的import,在Rust中我们需要使用use

如何表示要被引用的对象,Rust里称之为path,我们可以理解为操作系统中的文件目录

path有两种形式,也和文件系统一样,有绝对路径和相对路径:

  • 绝对路径始于crate的根(src/main.rs or src/lib.rs),可以使用crate名或者crate这个字面值表示
  • 相对路径可以使用当前模块名,当前模块中可以使用的对象,superself

path中的层级使用两个冒号::,类似文件系统中的斜线.

假设有一下代码(来自官方文档):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mod front_of_house {
  pub mod hosting {
    pub fn add_to_waitlist() {}
  }
}

pub fn eat_at_restautant() {
  // 绝对路径
  crate::front_of_house::hosting::add_to_waitlist();
  // 相对路径
  front_of_house::hosting::add_to_waitlist();
}

有一些限制也需要知道:

  • 在夫模块中不能使用子模块中的私有项目
  • 子模块可以使用父模块中的所有项目
  • 同一模块内可以直接互相使用

下面是一个使用了super的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn server_order() {}

mod back_of_house() {
  fn fix_incorrect_order() {
    cook_order();
    super::server_order();
  }
  
  fn cook_order() {}
}

fix_incorrect_order方法和cook_order同属于一个模块,可以直接调用。server_order方法和back_of_house同级,因此需要使用super访问到同级的server_order方法

如果use后面的路径具有共同的父路径,可以使用简化的模式。比如:

1
2
use std::io;
use std::cmp::Ordering;

可以简化为:

1
use std::{cmp::Ordering, io};

如果usemod直接有父子关系,也可以像上面那样简化,使用self代表父mod。比如:

1
2
use std::io;
use std::io::Write;

可以简化为:

1
use std::io::{self, Write};

如果想将某一路径下的所有pubitem都引入到当前文件中,可以使用*

1
use std::collections::*;

一般会在单元测试中常用,不推荐在业务代码中使用

1
2
3
4
5
6
#[cfg(test)]
mod tests{
  use super::*;
  #[test]
  fn it_works() {}
}

引用层级

对比一下两种引用方式:

1
2
3
4
5
6
7
// case 1
use crate::front_of_house::hosting;
hosting::add_to_waitlist();

// case 2
use crate::front_of_house::hosting::add_to_waitlist;
add_to_waitlist();

这两种方法的结果都是一样的,但是阅读起来给人的感觉不一样。一般来说推荐第一个 case ,这样能明确的知道使用的方法是外部 hosting 模块的方法,后者的话不知道是 use 进来的,还是本模块定义的

重命名

有时候从不同的 crate 或者 mod 引入了同名的 item,这个时候最简单的方式是使用as关键字进行重命名。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::fmt;
use std::io;

fn function1() -> fmt::Result {}
fn function2() -> io::Result<()> {}

###########################

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {}
fn function2() -> IoResult<()> {}

re-exporting 再导出

当使用use关键字将外部 item 导入到当前文件之后,这个 item 在当前代码是 private 的,如果使用pub use的话,还能让使用当前 mod 的第三者,使用在该 mod 中引入的 item。

该机制称为 re-exporting 。

workspace

workspace 用于管理多个相关的 package,不同的 package 有各自的 Cargo.toml,但是整个 workspace 共享一个Cargo.lock,也只有一个 target 目录(编译输出)。

虽然 workspace 内的项目共享一个 Cargo.lock,但是他们之间默认不互相依赖,需要显示添加它们之间的依赖关系。而且在一个项目中添加的依赖,在其他项目中如果想使用,还需要再次声明依赖才行。

不过据我观察 workspace 功能没有什么特别强大之处,不使用该功能也可以同时管理几个 Cargo 项目,因此这里就不再深入介绍了。

常见集合

一般来说在 Rust 常用的集合

  • vector允许我们一个挨着一个地存储一系列数量可变的值
  • 字符串是字符的集合,String
  • 哈希 map 允许我们将值与一个特定的键相关联

vector

新建vector

1
2
3
  let v: Vec<i32> = Vec::new();
  
  let v = vec![1, 2, 3];

更新vector

1
2
3
4
5
6
let mut v = Vec::new();

v.push(1);
v.push(2);

let a = v.pop();

类似于其他任何的struct,vector 在离开其作用域时会被释放。

访问vector

1
2
3
4
let v = vec![1, 2, 3];

let v1 = &v[1]; // 索引语法
let v2 = v.get(2); // 方法

使用索引语法访问vector会导致程序panic,而使用 get 方法访问时 会返回一个 None

在拿到 vector 中任意一个有效的引用,借用检查器将会窒息所有权和借用规则,来确保 vector 内容的这个引用和任何其他引用保持有效。当我们获取到了 vector 的第一个元素的不可变引用并在 vector 末尾增加一个元素的时候,编译无法通过。

1
2
3
4
5
6
7
let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6); // 不通过编译

println!("The first element is: {}", first);

为什么第一元素的引用会关心到末尾的变化?那是因为 vector 和 golang 的 slice 一样,在内部的空间不足时,会进行拷贝扩容,这样第一个元素的引用就指向了被释放的内存。

遍历 vector

1
2
3
4
5
6
7
8
let mut v = vec![1, 2, 3];
for i in &v {
  println!("{}", i)
}

for i in &mut v {
  *i += 50;
}

同时 vector 也可以使用枚举来存储多种类型的值

1
2
3
4
5
6
7
8
9
enum Test {
  Int(i32),
  Text(String),
}

let row = vec![
  Test::Int(3),
  Test::Text(String::from("test"))
];

字符串

创建一个字符串

1
2
3
4
5
let s1 = String::new(); // 创建一个空字符串

// 使用初始数据创建字符串
let s2 = "test".to_string();
let s3 = String::from("test");

更新字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut s = String::from("foo");
let mut x = String::from(" bar");

s.push_str(x);
s.push('~');

println!("x is {}", x);

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 这里 s1 被移动了,不能继续使用

执行上述代码之后,s将会包含xx还可以继续使用,因为push_str方法使用了字符串 slice,因此我们不需要获取参数的所有权。

然而直接使用+运算符将两个String值合并到一个新String值中,此时s1在相加后失去了所有权。因为+调用了add函数,这个函数看起来像这样

1
fn add(self, s:  &str) -> String{}

这并不是标准库当中实际的签名,标准库中的add使用泛型定义。

那为什么add方法的第二个参数是&str,我们在调用时却是&String,并且可以通过编译。

那是因为 Rust 使用了一个被称为 Deref 强制转换 的技术,可以理解为把&s2变成了&s2[..]

并且在签名中add方法获取self的所有权,这意味着s1的所有权将被移动到add调用中,之后不在生效,这样的好处是不会生成很多拷贝,这个实现比拷贝更加高效。

如果想获取多个字符相加:

1
2
3
4
5
let s1 = String::from("hello");
let s2 = String::from(" ");
let s3 = String::from("world");

let s = format!("{}{}{}", s1, s2, s3);

上述代码会将s设置为hello worldformat!println!的工作原理相同,并且它使用索引不会获取任何参数的所有权。

索引字符串

在很多语言中,通过索引来一用字符串中的单独字符是很常见的操作,比如说 golang。然而在 Rust 中,不允许使用索引语法访问String的一部分,会出现错误。

1
2
let s = String::from("hello");
let h = s[0]; // error

原因则是在 Rust 中,String是一个Vec<u8>的封装,相当于在底层存储的是字节,和大多数编程语言一样。直接使用索引语法获取String的一部分,相当于获取UTF-8字符的一个字节,这样可能会返回意外的值,Rust 根本不会编译这些代码。

Golong中可以直接索引访问,那是因为Golang会默认把字符串按照rune来 遍历。

在 Rust 中也有这样的操作,只不过需要显示使用:

1
2
3
for c in "hello world".chars() {
  println!("{}", c);
}

这样可以获取到每个UTF-8编码的字符,也就相当于 Golang 中的rune

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let s = String::from("你好世界👋");
    let c = s.chars().nth(10000);
    match c {
        Some(tmp) => println!("{}",tmp),
        None => (
            println!("this index is none")
        ),
    }
}

上述代码可以安全的使用索引来获取单个字符。

Hash Map

新建

可以使用new来创建一个空的HashMap,并使用insert增加元素。

1
2
3
4
use std::collections::HashMap;

let mut map = HashMap::new();
map.insert(String::from("hello"), String::form("world"));

注意,使用HashMap需要引入

访问

可以 使用get方法传入对应的键,从HashMap中获取值

1
2
let name = String::from("hello");
let s = map.get(&name); // s : Option<V>

或者使用与 vector 类似的方式来遍历

1
2
3
for (key, value) in &map {
  println!("{}: {}", key, value);
}

更新

直接使用相同的键重新调用insert方法,这样会直接替换成新值。

当我们需要检查对应键是否存在值时,可以使用entry方法

1
2
3
map.insert(String::from("Ronin"));
let s = map.entry(String::from("hello")).or_insert("世界");
*s = farmat!("{}", s, String::from("~"));

并且or_insert方法返回这个值的可变引用&mut V,我们可以直接改变它。

所有权

对于类似i32这样实现了Copy trait 的类型,值可以直接拷贝进HashMap。对于拥有所有权的值,其值将被移动而HashMap会成为这些值的所有者。或者将值的引用传入HashMap,但是需要保证生命周期,后续会了解。