rust 基础

rust 教程:https://course.rs/advance/intro.html

安装 rust 环境:

1
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

使用 vscode,安装插件,语法补全等等:rust-analyzer;好看的 vscode 主题:One Dark Pro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rust 字符类型:使用单引号
rust 字符串类型:使用双引号

函数如下:
fn add(i: i32, j: i32) -> i32 {
i + j
}
当使用 ! 作为函数返回类型时,表示函数永不返回。这种语法往往用作会导致崩溃的函数,如下:
fn dead_end() -> ! {
panic!("panic");
}
如下创建了一个无限循环,该循环用不跳出,因此函数也永不返回:
fn forever() -> ! {
loop {
// ...
};
}

内存安全之所有权和借用

如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,是编程语言设计的难点。有三种方式:

  • 垃圾回收机制(GC):在程序运行时不断寻找不再使用的内存,比如:Java、Go
  • 手动管理内存的分配与释放,比如 C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查,比如 rust

关于所有权的规则:

  • rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(变量)离开作用域范围时,这个值将被丢弃

举一个例子,String 类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成。假设有 String 类型的对象 s1 和 s2。当 s1 赋给 s2 后,rust 认为 s1 不再有效,因此也无须在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。如下:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world", s1);
// 编译的时候就会报错。所以这种做法也称为 move(移动)

如果需要深拷贝,可以使用 clone 的方法:

1
2
let s1 = String::from("hello");
let s2 = s1.clone();

在 rust 中一般看到的浅拷贝,如下:

1
2
let x = 5
let y = x

这种浅拷贝一般是在栈上的拷贝,这种不会有所有权的问题。因为像整形这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。

不可变引用。使用引用传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn calculate_len(s: &String) -> usize {
s.len()
}

fn change(some_string: &String) {
some_string.push_str("world"); // 报错,不可修改
}

fn test_03() {
let s1 = String::from("hello");
let len = calculate_len(&s1);
change(s1);
println!("The length of '{}' is {}.", s1, len);
}

& 符号即是引用,允许我们使用值,但是没有获取到所有权。如上,使用 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。还有引用指向的值默认是不可变的。

可见引用

1
2
3
4
5
6
7
8
9
fn change_mut(some_string: &mut String) {
some_string.push_str(", world");
}

fn test_04() {
let mut s = String::from("hello");
change_mut(&mut s);
println!("{}", s);
}

如上,可以先声明 s 是可变类型,然后创建一个可变的引用 &mut s 和接受可变引用参数 some_thing: &mut String 的函数。

不过可变引用有一个很大的限制,同一作用域,特定数据只能有一个可变引用。

1
2
3
4
5
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;

数据竞争会导致未定义的行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码。

悬垂引用(Dangling References),意思是指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其他变量重新使用。在 rust 中,如果有悬垂引用,rust 会抛出一个编译时错误。

1
2
3
4
5
6
7
8
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");
&s
}

总的来说,借用规则如下:

  • 同一时刻只能拥有要么一个可变引用,要么任意多个不可变引用
  • 引用必须总是有效的

使用 #![allow(unused_variables)] 属性标记,告诉编译器忽略未使用的变量。

切片,如下,切片是一个左闭右开的区间。

1
2
3
let s = String::from("hello");
let slice = &s[0..2];
println!("{}", slice); // he

rust 中变量在离开作用域后,就会自动释放其占用的内存。

元组,是由多种类型组合到一起形成的,因此他是复合类型,元祖的长度是固定的,元祖中元素的顺序也是固定的。

1
2
3
4
5
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
println!("{}, {}, {}", five_hundred, six_point_four, one);

结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

let user_01 = User {
email: String::from("hello@world.com"),
username: String::from("user_name"),
active: true,
sign_in_count: 1,
};

初始化实例时,每个字段都需要进行初始化;初始化时的字段顺序不需要和结构体定义时的顺序一致。

打印结构体,使用 #[derive(Debug)] 作用于结构体,打印时使用 {:#?}{:?} 可以结构化打印调试结构体。

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,
}

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

# cargo run
rect1 is Rectangle { width: 30, height: 50 }

dbg! 输出到标准错误 stderr,而使用 println! 输出到标准输出 stdout。

1
2
3
4
5
6
// 枚举
enum PokerSuit {
Clubs,
Spades,
Diamonds,
}

在 rust 中,数组有两种。array 速度很快但是长度固定。vector 是可动态增长但是有性能损耗。

对于数组越界访问时,程序会直接崩溃。这种就是 rust 的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,会导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。

模式匹配,类型 switch 语句。在 rust 语言中,使用 match 来进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Directory {
East,
West,
North,
South,
}

fn test_11() {
let dire = Directory::South;
match dire {
Directory::East => println!("East"),
Directory::North | Directory::South => {
println!("South or North");
},
_ => println!("West"),
};
}
  • match 的匹配必须要穷举出所有可能,因此使用 _ 来代表未列出的所有可能性
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y :类似逻辑运算符或,代表 X 和 Y 满足一个即可

rust 的对象定义和方法定义是分离的,这种数据和使用分离的方式,会给予使用者极高的灵活度。

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

area 的签名中,我们使用 &self 替代 rectangle: &Rectangle&self 其实是 self: &Self 的简写(注意大小写)。在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例,换句话说,self 指代的是 Rectangle 结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解:我们为哪个结构体实现方法,那么 self 就是指代哪个结构体的实例。

self 依然有所有权的概念:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
  • &self 表示该方法对 Rectangle 的不可变借用
  • &mut self 表示可变借用

泛型编程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Point<T, U> {
x: T,
y: U,
}

impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}

fn test_14() {
let p1 = Point {x: 5, y: 10.4};
let p2 = Point {x: "hello", y: 'c'};
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

定义特征,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub trait Summary {
fn summarize(&self) -> String;
}

pub struct Post {
pub title: String,
pub author: String,
pub content: String,
}

impl Summary for Post {
fn summarize(&self) -> String {
format!("文章{}, 作者{}", self.title, self.author)
}
}

fn test_15() {
let post = Post{
title: "Rust 语言简介".to_string(),
author: "Sunface".to_string(),
content: "Very Good".to_string()
};
println!("{}", post.summarize());
}