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 |
|
一些基础知识:
函数的定义:
- 如main函数
1
2
3fn main(){
}要有fn作为函数的基本标志,其余类似于c++
ps:语句也需要有分号
调用Rust宏:如果看到了
!
说明调用了宏而不是普通函数。
包管理器Cargo
可以帮助我们构建代码,下载依赖库并编译这些库
- 使用cargo创建项目:
cargo new xxx
会创建一个文件夹,里面包含了一个src文件夹和两个文件
- 如果没有使用cargo开始项目,我们可以将其转换成cargo项目;
- 将项目代码移入src目录
- 创建一个合适的cargo.toml文件(可以使用 cargo init命令)
构建并运行cargo项目
- 构建项目:
cargo build
—可以看到生成的可执行文件落在了debug文件夹下。 - 运行:可以使用具体位置来运行,也可以直接输入
cargo run
来运行。推荐后者
- 构建项目:
快速检查代码确保其可以编译:
cargo check
发布构建:可以使用
cargo build --release
来优化编译项目。生成结果在target/release下
小例子:猜数游戏
- 作用域引入;e.g:
use std::io;
- 变量存储用户输入:可变的变量和不可变的变量
- 使用let来创建变量:
- 可变:
let mut var = String::new();
- 不可变:
let var = 5;
- 可变:
- 使用let来创建变量:
- println!(”You guess:{}”,guess);这里的{}是一个占位符,用于打印变量的值,逗号后面是打印的表达式列表;
1 |
|
会打印出 x = 5 and y + 2 = 12
- 使用crate来增加更多功能
- crate 是一组 Rust 源代码文件。
rand
crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码,但是无法独立执行。- 在我们使用
rand
编写代码之前,需要修改 Cargo.toml 文件,引入一个rand
依赖。
- cargo.lock
- 当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用
0.8.5
直到你显式升级,多亏有了 Cargo.lock 文件。由于 Cargo.lock 文件对于“可重复构建”非常重要,因此它通常会和项目中的其余代码一样纳入到版本控制系统中。
- 当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用
- 更新crate到新版本:使用
cargo update
,会忽略cargo.lock文件,并计算出所有符合cargo.toml声明的最新版本。接下来把这些版本写入cargo.lock中。 - Cargo 有一个很棒的功能是:运行
cargo doc --open
命令来构建所有本地依赖提供的文档,并在浏览器中打开。
常见编程概念
变量与可变性
- 变量默认是不可改变的。当变量不可变时,一旦值被绑定一个名称上,就不能改变这个值。
如果我们对同一个不可变的变量赋值两次,会出现报错。
但如果我们修改一下,改成let mut x=5;这样子是可变的变量,就可以赋值多次。
常量
- 常量也是绑定到一个名称的不允许改变的值。(q:常量和不可变量的区别?
- 不允许对常量使用mut。常量总是不可变的。
- 声明常量使用const,而不是let,并且 必须 注明值的类型。
- 常量可以在任何作用域中声明,包括全局作用域
- 常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值
下面是一个声明常量的例子:
1 |
|
隐藏
定义一个与之前变量同名的新变量。就会把前面的变量隐藏。
数据类型
标量和复合
标量类型:
整数类型: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
4let 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
2let t = true;
let f: bool = false; // with explicit type annotation字符类型:char,表示单个Unicode标量值
复合类型
将多个值组合成一个类型。元组和数组
元组:多个其他类型的值组合进一个符合类型的主要方式。长度固定:一旦声明,长度不会增大或缩小。
1 |
|
tup变量绑定到整个元组上,使用模式匹配来解构元组值
1 |
|
将一个元组拆成x,y,z就叫做解构。
也可以使用 .
后跟值的索引来直接访问他们。
如:tup.0;tup.1这样子。(索引从0开始)
不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。
数组类型
每个元素类型必须相同。数组长度也是固定的。
1 |
|
索引访问
函数
在fn后面输入函数名和一对圆括号来定义函数。大括号告诉编译器函数体的开始和结尾。
rust不关心函数定义所在的位置;可以在main之前也可以在之后。
参数:
- 在函数签名中必须声明每个参数的类型。
语句和表达式
- 语句:执行一些操作但不返回值的指令
- 表达式:计算并产生一个值。
- 表达式的结尾没有分号,如果加上分号就会变成语句。
1
2
3
4{
let x = 3;
x + 1 //这里没有分号。
}具有返回值的函数:不对返回值命名,但在箭头后声明他的类型。 可以使用return和指定值提前返回。否则函数会隐式地返回最后的表达式(保证最后是一个表达式,而不是语句)
1 |
|
控制流
if表达式:
- if后面直接跟一个表达式,不需要加括号,表达式之后再加大括号
1 |
|
rust不会在比较的时候将整数转换为布尔值
- 在let语句中使用if:
let number = if condition { 5 } else { 6 };
使用循环重复执行:
- loop循环:
1 |
|
- 从循环返回值:
1 |
|
- 循环标签:在多个循环之间消除歧义
1 |
|
- while条件循环
1 |
|
- for遍历集合
1 |
|
1 |
|
翻转range
阶段一:
所有权
让 Rust 无需垃圾回收(garbage collector)即可保障内存安全
- 所有权是rust用于管理内存的一组规则。区别于其他语言的管理方式(垃圾回收机制or程序员亲自分配和释放内存),rust采用通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,如果违反了这些规则,程序就不能编译。
- 栈中的所有数据都必须占用已知且固定的大小,在编译时大小位置或大小可能变化的数据,要改为存储在堆上。向堆中放入数据时,你要请求一定大小的空间。内存分配器在堆上找到合适空位后,标记并返回表示该地址的指针,这个指针可以存储在栈上。
- 跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的
- 所有权规则:
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
ps: String类型
- 获取:let s=String::from(”hello”);
- 在字符串后追加字面值:s.push_str(”,world!”);
string类型可变,但是值不可变;string类型向堆申请内存。
变量与数据交互的方式(一):移动
- 多个变量可以采取不同的方式与同一数据进行交互
- 当s1赋值给s2后,s1就会失效。也就是s1被移动到了s2。(避免了二次释放)
变量与数据交互的方式(二):克隆
1 |
|
**copy trait:**如果一个类型实现了copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用
任何一组简单标量值的组合都可以实现 Copy
:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
所有权与函数
- 将值传递给函数与变量赋值的原理相似,向函数传递值可能会移动或者复制。如果是可copy的,就是复制进去,原变量仍有效。但如果是移动进去,原变量无效了。
例子:
1 |
|
返回值与作用域
- 返回值也可以转移所有权
例子:
1 |
|
引用与借用
不获取所有权就可以使用值的功能。
例子:
1 |
|
- 在传参的时候也要显式地表明是引用传参
解引用:*
创建一个引用的行为,称为借用
- (默认)不允许修改引用的值。
可变引用
- 类似的,我们通过添加
mut
来声明是可变的。
1 |
|
- 可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。
- 不能在同一时间多次将
s
作为可变变量借用。 - 我们 也 不能在拥有不可变引用的同时拥有可变引用。
- 可以避免数据竞争。
总的来说,就是我们可以同时拥有多个不可变引用,但是不可以在拥有不可变引用时拥有可变引用(不能同时使用);也不可以同时拥有多个可变引用。
悬垂引用
在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
Slice类型
slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
1 |
|
数据结构
vector:
类似于array,但长度可以改变,存储在heap中。存储的数据类型要相同
创建vector
- 调用
Vec::new()
函数
1 |
|
C++:vector<int> v;
在没有显式的给定初始值的时候,我们需要显式地指出vector要存储的数据类型。可以使用 vec!
宏,会根据我们提供的值来创建一个新的vector
1 |
|
更新vector
向vector中添加新元素,可以使用 push()
方法;
比如:v.push(4);
**但是!**需要注意的是,同之前讨论的任何变量一样,如果想要改变他的值,必须要使用 mut
关键字使其可变!
1 |
|
读取vector的元素
- 通过索引读取
- 使用
get()
方法
例子:
1 |
|
如果是索引引用,程序员需要保证一定不会越界,否则程序会崩溃且报错;
但是如果是get方法,则可以产生一个无法获取的打印信息;
- 不可以在拥有vector中项的引用的同时向其增加一个元素
在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
遍历vector中的元素
1 |
|
1 |
|
枚举:存储多种类型
1 |
|
丢弃vector时也会丢弃所有元素
一些函数:
- 可变引用:
iter_mut()
生成的引用是可变的,允许你修改集合中的元素。 v.iter()
:- 返回一个不可变引用的迭代器(
Iterator<Item = &T>
)。 - 这意味着你可以遍历集合中的每个元素,但不能修改它们。
- 返回一个不可变引用的迭代器(
map(|element| { ... })
:map
是一个高阶函数,接受一个闭包(|element| { ... }
)作为参数。- 闭包会对迭代器中的每个元素(
element
)进行处理,并返回一个新的值。 - 这里的
element
是集合中元素的不可变引用(&T
)。
collect()
:- 将
map
处理后的结果收集到一个新的集合中(如Vec<T>
)。 collect()
的返回类型取决于上下文,通常需要显式指定类型或通过类型推断确定。
- 将
结构体
定义与实例化
- 结构体的每一部分可以是不同类型;
- 结构体比元组更加灵活,不需要依赖顺序来指定或访问实例中的值
定义方式与c++类似;
1 |
|
在大括号中,定义每一部分数据结构的名字和类型,我们称为字段
- 实例化:
创建一个实例,需要以结构体的名字开头,接着在大括号中使用 key:value
键-值对的形式提供字段。实例中字段顺序不需要和他们在结构体声明中的顺序一致。
1 |
|
- 可以使用
.
来获取对应的值或者修改对应的值 - 注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。
字段初始化简写语法
- 如果在创建一个新实例的时候,我们哟啊将email字段的值设置为传入参数email的值,不需要显式地说明,他们可以直接对应
1 |
|
结构体更新语法
1 |
|
这里是利用了移动。所以后续不能再使用user1了
但是如果我们丢username和email这种不是很轻松复制的变量都进行修改,那么就可以利用克隆,这样子user1仍然可用
使用没有命名字段的元组结构来创建不同的类型
1 |
|
没有任何字段的类单元结构体
1 |
|
方法语法
方法(method)与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十八章讲解),并且它们第一个参数总是 self
,它代表调用该方法的结构体实例。
定义方法:
impl代表实例
1 |
|
关联函数:
所有在 impl
块中定义的函数被称为 关联函数(associated functions),因为它们与 impl
后面命名的类型相关。