Rust Study Notes - 基础

本文是Rust学习笔记基础篇,主要包括Rust基础概念、所有权、枚举与模式匹配等相关内容。

环境搭建

学习资料

1
2
3
4
5
6
https://doc.rust-lang.org/book/
https://doc.rust-lang.org/rust-by-example/index.html
https://rustwiki.org/
# Rust程序设计语言中文版
https://rustwiki.org/zh-CN/book/
https://www.rust-lang.org/zh-CN/learn

搭建环境

在wsl上搭建rust的开发环境

1
curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash

下载rustlings练习

1
2
3
4
# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.5.1)
git clone -b 5.5.1 --depth 1 https://github.com/rust-lang/rustlings
cd rustlings
cargo install --force --path .

如果不开代理,可能速度会很慢或者出现下载失败。

git开启代理的方法:

1
2
3
4
5
6
7
8
9
# ~/.ssh/config

Host github.com
Hostname ssh.github.com
Port 443

Host github.com
User git
ProxyCommand connect -H 127.0.0.1:4780 %h %p

其中4780是科学上网软件的代理端口。

基础编程概念

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

fn main() {
println!("Please input your guess.");

let mut guess = String::new();

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

println!("You guessed: {}", guess);
}
  • 变量默认不可变,如果要可变,需要添加 mut 关键字修饰。
  • 引用默认是不可变的,如果要修改,也需要写成&mut guess

变量与可变性

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let x = 5; // 不可变
let x = 6; // 遮蔽
let mut y = 1; // 变量
y = 2;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 30; // const 常量,约定用大写,需指定类型

{
let x = x + 1; // x = 7, 只存在这个内部作用域
}

// x 此时还是6
}

数据类型

Rust是静态类型的语言,需要在编译期就知道所有变量的类型。

标量类型

字符类型

char,4个字节,表示的是一个Unicode标量值。

复合类型

将多个值组合成一个类型。

元组类型

1
2
3
4
5
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1); // (i32, f64, u8) 这些类型标注是可选的
let (x, y, z) = tup; // 解构
let a = tup.0 // 也可以用.连上要访问的值的索引来访问该元素
}

元组类型的长度是固定的。

数组类型

数组的元素类型必须是相同的。

1
2
3
4
5
6
7
fn main() {
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; // 5个3
let first = a[0]; // 访问数组元素
let ele = a[5]; // 会产生panic
}

函数

1
let x = (let y = 6); // 报错

因为ley y = 6这个语句并不返回值。

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

ok。注意x+1后面不带分号,它是一个表达式。表达式后面加上分号,就会转换成为语句,而语句不会返回值。

带返回值的函数

1
2
3
fn five() -> i32 {
5
}

控制流

if语句

if 语句的条件必须是 bool 类型。

1
2
3
4
5
6
fn main() {
let number = 3;
if number { // 会报错
println!("number was three");
}
}

if 和 else 需要有相同的值类型。

1
let number = if condition {5} else {"six"}; // 会报错

loop循环

1
2
3
4
5
fn main() {
loop {
println!("again!")''
}
}

一般地,break和continue作用域内部的循环,如果希望作用在外部的循环,可以在后面添加loop label。

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 {
println!("count = {}", count);
let mut remaining = 10;
loop {
println!("remaining = {}", remaining);
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up';
}
remaining -= 1;
}
rount += 1;
}
println!("End count = {}", count);
}

带返回值的loop

1
2
3
4
5
6
7
8
let mut count = 0;
let result = loop {
count += 1;
if counter == 10 {
break counter * 2;
}
}
println!("The result is {}", result);

带条件的循环

1
2
3
4
let mut number = 3;
while number != 0 {
num -= 1;
}

也可以用for循环:

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

for ele in a {
println!("the value is {}", ele);
}

所有权

本节内容:

image-20230523232457565

所有权让Rust无需垃圾回收期即可保证内存安全。理解了Rust的所有权,将能更自然地编写出安全和高效的代码。

什么是所有权

所有权规则

  • Rust中的每一个值都有一个被成为其所有者的变量
  • 值在任意时刻有且只有一个所有者
  • 当所有者离开作用域时,这个值将被丢弃

变量作用域

从声明的那一刻开始直到当前作用域结束时都是有效的。

1
2
3
4
{					 // s在这里无效,尚未声明
let s = "hello"; // s从这里开始有效

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

String类型

1
2
3
4
{
let s = String::from("hello"); // 从此开始,s开始有效
// 使用s
} // 此作用域已结束,s不再有效

s离开作用域的时候,Rust为我们调用一个特殊的函数,这个函数叫做drop

变量与数据交互的方式一:移动

1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 会报错,因为s1不再有效
}

s1 moved to s2

结果是两个变量都绑定到同一块内存数据。且s1不再有效!

变量与数据交互的方式二:克隆

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

println!("s1 = {}, s2 = {}", s1, s2); // 不会报错

只在栈上的数据:拷贝

任何一组简单标量的组合都可以实现Copy。如果一个类型实现了Copt trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

1
2
3
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

如果已经实现了Drop trait,就不能再使用Copy trait。

所有权与函数

将值传递给函数在语义上与给变量赋值类似。

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

takes_ownership(s); // 后续s不可用

let x = 5;

makes_copy(x); // 后续x可用
}

fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // 退出作用域之后会调用`drop`,内存释放,后续不可用

fn makes_copy(some_interger: i32) {
println!("{}", some_interger);
} // 退出作用域不会释放

也可以返回所有权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域

let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数

let some_string = String::from("yours"); // some_string 进入作用域

some_string // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

a_string // 返回 a_string 并移出给调用的函数
}

但是这样就很麻烦了。传递一个变量,后续还想继续用,怎么办呢?Rust对此提供了一个功能,叫做引用

引用和借用

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()
}

但是,借用别人的东西,不可以改变!

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

fn change(some_string: &String) {
some_string.push_str(", world"); // 编译报错
}

如果要改变,要怎么办呢?

可变引用

1
2
3
fn change(some_string: &mut String) { // 答案是使用可变引用
some_string.push_str(", world");
}

不过可变引用有一个很大的限制:同一时间,只能由一个对某一个特定数据的可变引用。

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

let r1 = &mut s;
let r2 = &mut s; // 有问题,不能同时两个可变引用
println!("{}, {}", r1, r2);

这样会导致出现数据竞争。

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

{
let r1 = &mut s;
}
let r2 = &mut s; // 这样是可以的

也不能同时有可变与不可变引用:

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);
}

但是如果确定不可变引用不再使用,那也是没有问题的。

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

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

let r3 = &mut s; // 没问题
println!("{}", r3);
}

悬垂引用

1
2
3
4
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回字符串s的引用
} // 这里s离开作用域就会被丢弃,内存被释放,危险!

为什么这里是危险的?因为引用是不会把所有权移出去的,所以s会在函数返回时被丢弃。

1
2
3
4
5
6
7
8
9
fn main() {
let string = no_dangle();
}

fn no_dangle() -> String {
let s = String::from("hello");

s
}

这样确是可以的。因为s的所有权被移交出去了。

总结

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

切片

slice允许引用集合中一段连续的元素序列。slice 也没有所有权。

字符串slice

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

let hello = &s[0..5]; // 不包含最后的索引5
let world = &s[6..11]; // 注意,这是一个不可变引用,因此,后面也不能对s进行修改
let slice = &s[..2]; // 从0开始可以省略
let slice = &s[3..]; // 也可以这样表示到最后

s.push_str(" and more"); // 不允许,因为push_str是一个可变引用

返回一个slice:

1
2
3
4
5
6
7
8
fn first_world(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
}

字符串字面量就是slice

1
let s = "Hello World!";

这里的s的类型就是&str:指向二进制程序特定位置的slice。

其他类型的slice

1
2
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

枚举与模式匹配

通用概念

模式匹配(Pattern Match)是指判断一个给定的标记序列(Token Sequence)是否存在某种模式(Pattern)的结构的匹配(Match)行为。

  • Token Sequence:对应Rust中的表达式:在rust中几乎一切都可以视为表达式
  • Pattern:用来匹配表达式的模式语法,用于匹配和绑定变量
  • Match:用来触发模式匹配的语法结构

最终效果可以是:1. 变量绑定,2. 条件选择,3. 结构解构

枚举类型

最简单的枚举定义:

1
2
3
4
5
6
enum IpAddr {
V4,
V6,
}

let four = IpAddr::V4;

枚举也可以关联数据:

1
2
3
4
5
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));

还可以关联不同的类型:

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

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

枚举也可以定义方法:

1
2
3
4
5
6
7
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}
let m = Message::Write(String::from("hello"))‘
m.call();

Option枚举

Option是标准库定义的另一个枚举。

Rust中没有空值功能,但是它有一个可以编码存在或不存在概念的枚举,就是Option<T>

1
2
3
4
enum Option<T> {
Some(T),
None,
}

例如

1
2
let some_number = Some(5);
let absent_num: Option<i32> = None;

为什么 absent_num 不定义为 i32 类型? 这表示它可以为 None!这种i情况下,我们需要处理为 None 的情况。

换个角度:如果一个变量可能为空,那么我们需要把它装在Option中,这样我们就必须显式处理它为 None 的情况。

模式匹配

match表达式

1
2
3
4
5
6
7
8
9
10
enum Coin {
Penny,
Nickel,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
}
}

也可以绑定值,在匹配时我们可以提取值出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("state {:?}", state);
25
}
}
}

可以看到,在上面的模式匹配中,除了条件判断外,还实现了变量的绑定

匹配是穷尽的

这里有一个重要的概念,在Rust中,匹配的时候,我们不能只匹配部分。

1
2
3
4
5
6
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i+1),
None => None, // 如果去掉则会编译报错,rust不允许你对可能的匹配置之不理!
}
}

通配模式和占位模式

1
2
3
4
5
6
7
8
9
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other), // other是通配其他所有。
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
1
2
3
4
5
6
7
8
9
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // 占位则是匹配任何值但不绑定

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

if let

为什么要引入if let?回看match,所有分支都要处理,是不是很麻烦?如果只关心特定分支,怎么办?

match 表达式有时候就可以用 if let 来简化,看例子:

1
2
3
4
5
6
7
8
9
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}

if let Some(3) = some_u8_value {
println!("three");
}

与match不同,if let 后面跟的是pattern,语法:if let <pattern> = <Token Sequenc>