Rust_Notes

Resources:

https://kaisery.github.io/trpl-zh-cn/ch01-02-hello-world.html

导学阶段:

  • rust的更新和卸载:
    • 更新:rustup update
    • 卸载:rustup self uninstall

Hello World!第一个rust程序:

1
2
3
fn main() {
println!("Hello, world!");
}

一些基础知识:

  • 函数的定义:

    • 如main函数
    1
    2
    3
    fn main(){

    }

    要有fn作为函数的基本标志,其余类似于c++

    ps:语句也需要有分号

  • 调用Rust宏:如果看到了 ! 说明调用了宏而不是普通函数。

包管理器Cargo

可以帮助我们构建代码,下载依赖库并编译这些库

  1. 使用cargo创建项目:cargo new xxx

会创建一个文件夹,里面包含了一个src文件夹和两个文件

  • 如果没有使用cargo开始项目,我们可以将其转换成cargo项目;
    • 将项目代码移入src目录
    • 创建一个合适的cargo.toml文件(可以使用 cargo init命令)
  1. 构建并运行cargo项目

    1. 构建项目:cargo build —可以看到生成的可执行文件落在了debug文件夹下。
    2. 运行:可以使用具体位置来运行,也可以直接输入 cargo run 来运行。推荐后者

    image.png

  2. 快速检查代码确保其可以编译:cargo check

  3. 发布构建:可以使用 cargo build --release 来优化编译项目。生成结果在target/release下

小例子:猜数游戏

  1. 作用域引入;e.g: use std::io;
  2. 变量存储用户输入:可变的变量和不可变的变量
    1. 使用let来创建变量:
      1. 可变:let mut var = String::new();
      2. 不可变:let var = 5;
  3. println!(”You guess:{}”,guess);这里的{}是一个占位符,用于打印变量的值,逗号后面是打印的表达式列表;
1
2
3
4
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

会打印出 x = 5 and y + 2 = 12

  1. 使用crate来增加更多功能
    1. crate 是一组 Rust 源代码文件。
    2. rand crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码,但是无法独立执行。
    3. 在我们使用 rand 编写代码之前,需要修改 Cargo.toml 文件,引入一个 rand 依赖。
  2. cargo.lock
    1. 当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 0.8.5 直到你显式升级,多亏有了 Cargo.lock 文件。由于 Cargo.lock 文件对于“可重复构建”非常重要,因此它通常会和项目中的其余代码一样纳入到版本控制系统中。
  3. 更新crate到新版本:使用 cargo update
    ,会忽略cargo.lock文件,并计算出所有符合cargo.toml声明的最新版本。接下来把这些版本写入cargo.lock中。
  4. Cargo 有一个很棒的功能是:运行 cargo doc --open 命令来构建所有本地依赖提供的文档,并在浏览器中打开。

常见编程概念

变量与可变性

  1. 变量默认不可改变的。当变量不可变时,一旦值被绑定一个名称上,就不能改变这个值。

image.png

如果我们对同一个不可变的变量赋值两次,会出现报错。

但如果我们修改一下,改成let mut x=5;这样子是可变的变量,就可以赋值多次。

image.png

image.png

常量

  • 常量也是绑定到一个名称的不允许改变的值。(q:常量和不可变量的区别?
    • 不允许对常量使用mut。常量总是不可变的。
    • 声明常量使用const,而不是let,并且 必须 注明值的类型
    • 常量可以在任何作用域中声明,包括全局作用域
    • 常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值

下面是一个声明常量的例子:

1
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

隐藏

定义一个与之前变量同名的新变量。就会把前面的变量隐藏。

数据类型

标量和复合

标量类型:

  • 整数类型:i8, i16, i32, i64, i128 和 u8, u16, u32, u64, u128(i表示有符号,u表示无符号)

    • 可以使用_来分隔数字,方便读数
    数字字面值 例子
    Decimal (十进制) 98_222
    Hex (十六进制) 0xff
    Octal (八进制) 0o77
    Binary (二进制) 0b1111_0000
    Byte (单字节字符)(仅限于 u8) b'A'
    • 溢出处理:

      • 所有模式下都可以使用 wrapping_* 方法进行 wrapping,如 wrapping_add
      • 如果 checked_* 方法出现溢出,则返回 None
      • 用 overflowing_* 方法返回值和一个布尔值,表示是否出现溢出
      • 用 saturating_* 方法在值的最小值或最大值处进行饱和处理
      1
      2
      3
      4
      let result = 255u8.wrapping_add(1); // 结果为 0
      let result = 255u8.checked_add(1); // 结果为 None
      let (result, overflowed) = 255u8.overflowing_add(1); // 结果为 (0, true)
      let result = 255u8.saturating_add(1); // 结果为 255
  • 浮点数类型:f32, f64。

    • 默认类型是 f64
  • 布尔类型:bool,值只能是true或false(可以显示或者隐式表示)

    1
    2
    let t = true;
    let f: bool = false; // with explicit type annotation
  • 字符类型:char,表示单个Unicode标量值

复合类型

将多个值组合成一个类型。元组和数组

元组:多个其他类型的值组合进一个符合类型的主要方式。长度固定:一旦声明,长度不会增大或缩小。

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

tup变量绑定到整个元组上,使用模式匹配来解构元组值

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

**let (x, y, z) = tup;**

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

将一个元组拆成x,y,z就叫做解构。

也可以使用 . 后跟值的索引来直接访问他们。

如:tup.0;tup.1这样子。(索引从0开始)

不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。

数组类型

每个元素类型必须相同。数组长度也是固定的。

1
2
3
4
5
fn main() {
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];//数据类型;数量
let a = [3; 5];//初始值;元素个数
}

索引访问

函数

  1. 在fn后面输入函数名和一对圆括号来定义函数。大括号告诉编译器函数体的开始和结尾。

  2. rust不关心函数定义所在的位置;可以在main之前也可以在之后。

  3. 参数:

    1. 在函数签名中必须声明每个参数的类型
  4. 语句和表达式

    1. 语句:执行一些操作但不返回值的指令
    2. 表达式:计算并产生一个值。
    3. 表达式的结尾没有分号,如果加上分号就会变成语句。
    1
    2
    3
    4
    {
    let x = 3;
    x + 1 //这里没有分号。
    }
  5. 具有返回值的函数:不对返回值命名,但在箭头后声明他的类型。 可以使用return和指定值提前返回。否则函数会隐式地返回最后的表达式(保证最后是一个表达式,而不是语句)

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

控制流

if表达式:

  • if后面直接跟一个表达式,不需要加括号,表达式之后再加大括号
1
2
3
4
5
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}

rust不会在比较的时候将整数转换为布尔值

  • 在let语句中使用if:let number = if condition { 5 } else { 6 };

使用循环重复执行:

  • loop循环:
1
2
3
loop {
println!("again!");
}
  • 从循环返回值:
1
2
3
4
5
6
7
let result = loop {
counter += 1;

if counter == 10 {
**break counter * 2;//在break后面输入返回值**
}
};
  • 循环标签:在多个循环之间消除歧义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

count += 1;
}
println!("End count = {count}");
}
  • while条件循环
1
2
3
4
5
while number != 0 {
println!("{number}!");

number -= 1;
}
  • for遍历集合
1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a {
println!("the value is: {element}");
}
}
1
2
3
4
5
6
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}

翻转range

阶段一:

所有权

让 Rust 无需垃圾回收(garbage collector)即可保障内存安全

  1. 所有权是rust用于管理内存的一组规则。区别于其他语言的管理方式(垃圾回收机制or程序员亲自分配和释放内存),rust采用通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,如果违反了这些规则,程序就不能编译。
  2. 栈中的所有数据都必须占用已知且固定的大小,在编译时大小位置或大小可能变化的数据,要改为存储在堆上。向堆中放入数据时,你要请求一定大小的空间。内存分配器在堆上找到合适空位后,标记并返回表示该地址的指针,这个指针可以存储在栈上。
  3. 跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的
  4. 所有权规则:
    1. Rust 中的每一个值都有一个 所有者owner)。
    2. 值在任一时刻有且只有一个所有者
    3. 所有者(变量)离开作用域,这个值将被丢弃

ps: String类型

  1. 获取:let s=String::from(”hello”);
  2. 在字符串后追加字面值:s.push_str(”,world!”);

string类型可变,但是值不可变;string类型向堆申请内存。

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

  • 多个变量可以采取不同的方式与同一数据进行交互
  • 当s1赋值给s2后,s1就会失效。也就是s1被移动到了s2。(避免了二次释放)

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

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

println!("s1 = {s1}, s2 = {s2}");

**copy trait:**如果一个类型实现了copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用

任何一组简单标量值的组合都可以实现 Copy

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true 和 false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数

  • 将值传递给函数与变量赋值的原理相似,向函数传递值可能会移动或者复制。如果是可copy的,就是复制进去,原变量仍有效。但如果是移动进去,原变量无效了。

例子:

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() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效

let x = 5; // x 进入作用域

makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{some_string}");
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{some_integer}");
} // 这里,some_integer 移出作用域。没有特殊之处

返回值与作用域

  • 返回值也可以转移所有权

例子:

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
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 并移出给调用的函数
}

引用与借用

不获取所有权就可以使用值的功能。

例子:

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 '{s1}' is {len}.");
}

fn calculate_length(s: **&String**) -> usize {
s.len()
}
  • 在传参的时候也要显式地表明是引用传参

解引用:*

创建一个引用的行为,称为借用

  • (默认)不允许修改引用的值。

可变引用

  • 类似的,我们通过添加 mut 来声明是可变的。
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");
}
  • 可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。
  • 不能在同一时间多次将 s 作为可变变量借用。
  • 我们  不能在拥有不可变引用的同时拥有可变引用。
  • 可以避免数据竞争。

总的来说,就是我们可以同时拥有多个不可变引用,但是不可以在拥有不可变引用时拥有可变引用(不能同时使用);也不可以同时拥有多个可变引用。

悬垂引用

在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

Slice类型

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

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

let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[0..len];
let slice = &s[..];

数据结构

vector:

类似于array,但长度可以改变,存储在heap中。存储的数据类型要相同

创建vector

  • 调用 Vec::new() 函数
1
let v : Vec<i32> = Vec::new();

C++:vector<int> v;

在没有显式的给定初始值的时候,我们需要显式地指出vector要存储的数据类型。可以使用 vec! 宏,会根据我们提供的值来创建一个新的vector

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

更新vector

向vector中添加新元素,可以使用 push() 方法;

比如:v.push(4);

**但是!**需要注意的是,同之前讨论的任何变量一样,如果想要改变他的值,必须要使用 mut 关键字使其可变!

1
2
let mut v=Vec::new();
v.push(2);//这样子才是可以通过编译的

读取vector的元素

  • 通过索引读取
  • 使用 get() 方法

例子:

1
2
3
4
5
6
7
8
9
let v= vec![1,2,3];
let third:&i32=&v[2];//这里相当于索引引用?
println!("The third element is {third}");

let third: Option<&i32>=v.get(2);
match third{
Some(third)=>println!("The third element is {third}"),
None => println!("There is no third element."),
}

如果是索引引用,程序员需要保证一定不会越界,否则程序会崩溃且报错;

但是如果是get方法,则可以产生一个无法获取的打印信息;

  • 不可以在拥有vector中项的引用的同时向其增加一个元素

在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

遍历vector中的元素

1
2
3
4
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
1
2
3
4
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

枚举:存储多种类型

1
2
3
4
5
6
7
8
9
10
11
12
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

丢弃vector时也会丢弃所有元素

一些函数:

  1. 可变引用iter_mut() 生成的引用是可变的,允许你修改集合中的元素。
  2. v.iter():
    • 返回一个不可变引用的迭代器(Iterator<Item = &T>)。
    • 这意味着你可以遍历集合中的每个元素,但不能修改它们。
  3. map(|element| { ... }):
    • map 是一个高阶函数,接受一个闭包|element| { ... })作为参数。
    • 闭包会对迭代器中的每个元素(element)进行处理,并返回一个新的值。
    • 这里的 element 是集合中元素的不可变引用(&T)。
  4. collect():
    • 将 map 处理后的结果收集到一个新的集合中(如 Vec<T>)。
    • collect() 的返回类型取决于上下文,通常需要显式指定类型或通过类型推断确定。

结构体

定义与实例化

  • 结构体的每一部分可以是不同类型;
  • 结构体比元组更加灵活,不需要依赖顺序来指定或访问实例中的值

定义方式与c++类似;

1
2
3
4
5
struct Name{
var1:type1,
var2:type2,
var3:type3,
}

在大括号中,定义每一部分数据结构的名字和类型,我们称为字段

  • 实例化:

创建一个实例,需要以结构体的名字开头,接着在大括号中使用 key:value 键-值对的形式提供字段。实例中字段顺序不需要和他们在结构体声明中的顺序一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
let mut user2 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user2.email = String::from("anotheremail@example.com");
}
  • 可以使用 . 来获取对应的值或者修改对应的值
  • 注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。

字段初始化简写语法

  • 如果在创建一个新实例的时候,我们哟啊将email字段的值设置为传入参数email的值,不需要显式地说明,他们可以直接对应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}

//简化后:
fn build_user(email: String, username: String) -> User {
User {
active: true,
**username,
email,**
sign_in_count: 1,
}
}

结构体更新语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//原版:
fn main() {
// --snip--

let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
//简化后:
fn main() {
// --snip--

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

这里是利用了移动。所以后续不能再使用user1了

但是如果我们丢username和email这种不是很轻松复制的变量都进行修改,那么就可以利用克隆,这样子user1仍然可用

使用没有命名字段的元组结构来创建不同的类型

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

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

没有任何字段的类单元结构体

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

方法语法

方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章第十八章讲解),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。

定义方法:

impl代表实例

1
2
3
4
5
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}

关联函数:

所有在 impl 块中定义的函数被称为 关联函数associated functions),因为它们与 impl 后面命名的类型相关。


Rust_Notes
https://pqcu77.github.io/2025/02/10/Rust-Notes/
作者
linqt
发布于
2025年2月10日
许可协议