一些学习资源 rust学习资源,rust magazine。
推荐这本国人教程Rust语言圣经,github上已经上万star啦
连续六年成为全世界最受欢迎的语言,特点是没有GC(垃圾回收),无需手动内存管理,性能比肩C艹和C,也能直接调用它们,安全性极高。。。门槛也极高。。。
环境搭建
参考环境搭建,后续VScode上的配置也可同时搭建,一些问题解决方案上面都有
cargo基础
常用命令
一个包管理工具。从项目的建立、构建到测试、运行直至部署,为 Rust 项目的管理提供尽可能完整的手段,与 Rust 语言及其编译器 rustc 紧密结合。一些常用命令,
- 创建项目:
cargo new ProjectName
默认创建bin类型项目,即可运行项目,还有一种是lib,代表依赖库项目。
- 编译项目:
cargo build
执行后将产生一个target
文件夹,然后对应程序将以同名的形式存在./target/debug
目录里
- 运行项目:cd到项目目录,然后运行
cargo run
run有两步,即先编译再运行,默认运行的是debug模式,编译器不会做任何优化,所以编译速度很快,但是运行速度会很慢。如果想更高性能的代码,添加一个--release
- 检查:
cargo check
:快速检测编译能否通过 - 重建包:
cargo clean
除了基本必须文件其他都将被删除
目录结构如下:
BTW,code里看到文件后面多了个字符,遂查了一下意思,U代表本地新建了文件,但没提交到github上。
两个特殊文件
- Cargo.toml
是cargo特有项目描述文件,存储项目所有原配置信息,默认如下
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
[dependencies]
以上被称为manifest。
添加依赖项[dependencies]
,添加后会从Crates.io
下载编译
更新依赖catgo update
可以加-p
参数指定包名,这个会改变Cargo.lock文件。
- Cargo.lock
是cargo根据同项目toml生成的项目依赖清单,一般不用修改。主要是使得每次拉取的依赖都是同一个版本防止因为更新而导致版本不兼容。另外当项目是一个可以运行的程序的时候,应该把这个文件传到git仓库上,如果是一个依赖库项目,那么添加到 .gitignore 中。
一个标准的包目录结构
下载依赖包很慢解决方法
基础语法
变量
小概念
- 变量在命名时同样需要注意命名规范,和大多数语言一样
- 变量绑定: 方式为
let a = "hello, pretty girl.";
无需声明变量类型。也可叫做变量赋值,这里涉及到一个Rust核心原则,即所有权 - Rust变量可以手动设置可变变量和不可变变量,默认情况下时不可变,如果想设置为可变的,需要通过
mut
关键字,另外,如果变量设置为不可变,那么它绑定值之后就不能再改,重新赋值是不行的,但可以通过let x = x + 1
这种操作来遮蔽掉前面的值。这和mut
变量的使用是不同的,第二个let
生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。 - 当创建了一个变量却不使用,Rust将会给出一个warning,可以在变量名称之前加一个下划线来解决
let _a = 1;
所有权
对于和计算机内存空间的控制,不同于Java、Go的GC机制,也不同于C艹的手动内存管理,Rust采用了一种叫做所有权的机制,在编译时期会进行检查,不会有性能上的损失。重要基本原则如下:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有。
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
所有权的转移
这里提一下堆栈,栈的数据都必须占用已知且固定大小的内存空间,而堆用于存储大小未知或者可能变化的数据,在往堆上放数据的时候,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。堆上的数据缺乏组织,所以很需要跟踪这些数据何时分配和释放,否则易产生内存泄漏。
一般存在栈中的数据(基本类型)会通过自动拷贝的方式来赋值,而对于需要在堆上分配的数据(复合类型)赋值,会将所有权转移给后者,前者将立刻失效。
//可以,对基本类型赋值,会通过自动拷贝把x的值给y,最终二者都有值
let x = 5;
let y = x;
/*
不行!!!这里是复合类型存储在堆上,当把s1赋给s2时,相当于把s1的值绑定给s2。
s1立刻失效,这个操作被称为移动
*/
let s1 = String::from("hello");
let s2 = s1;
//可以,这是字符串,其值是不可变的
let x: &str = "hello, world";
let y = x;
Rust中的字符串&str
和String类型,前者是被硬编码到代码中,其值是不可变的,而后者是动态的。
这里提一下String
复合类型,它由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,容量是堆内存分配空间的大小,长度是目前已经使用的大小。拷贝的时候是“浅拷贝”,不会将数据一同拷贝,只复制String
类型。
如果确实要拷贝数据,可以使用clone
方法。
注意:将值传递给函数和函数返回值一样会发生 移动 或者 复制,就跟 let 语句一样。
引用
类似指针,指向内存地址,允许使用,但不是所有权的转移。
let x = 5;
let y = &x;//引用
assert_eq!(5, x);
assert_eq!(5, *y);//解引用
//string型
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()
}
默认情况下,引用变量的值自然是不可以改变的,想要可变需要调整,加个mut
。
可变引用
let mut s = String::from("hello");
change(&mut s);
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
注意:
- 同一作用域,特定数据只能有一个可变引用
- 可变借用不能用于不可变借用上
- 有了可变借用就不能再有不可变借用
- 引用作用域和变量作用域不一样,它的结束位置再最后一次使用的位置
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题,无法将可变用于不可变
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);
}// 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
悬垂指针
也就是指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用,rust不允许这样。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
/*这里s的作用域在dangle里,执行完毕后将被释放,
此时返回引用就是悬垂指针,所以会报错
*/
//可以选择返回类型为string,即通过函数返回值的方式来传递
基本变量类型
存储在栈上
- 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数。
指定类型:let y: f32 = 3.0; // f32
或者let y = 3.0f32
注意:
- release模式下不检测溢出,会按照补码循环溢出
- 避免在浮点数测试相等性(精度问题)
- 同类型下才能运算
Rust编译器可以通过变量值和上下文中的使用方式来自动推导出变量的类型,但是某些情况下需要手动给一个显式的类型标注,如let guess: i32 = ...
或者 "42".parse::<i32>()。
- 序列:用于生成连续的数值
1..=5
代表生成从1到4的连续数字。也可生成字符'a'..='z'
- 有理数和复数用库
num
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节,所有的Unicode值都可以作为字符,包括单个中文、日文、emoji表情等等。注意:字符只能用
''
来表示。 - 布尔类型:
true
和false
- 单元类型: 即
()
,其唯一的值也是()
,可以作为一个值用来占位。不占用内存。
复合变量类型
存储在堆上
- 字符串
切片:是对集合的部分引用,其他集合类型也有。
- 语法为
[开始索引..终止索引]
,左闭右开区间 [..2]
(从0到2)、[2..]
(从2到结束)、[..]
(全部)- 字符串切片的标识是
&str
- 字符串字面量
let s: &str = "Hello, world!";
唯一个不可变引用
注意:字符串索引以字节为单位,它必须落在字符之间的边界,例如一个中文字符在UTF-8中占用了三个字节,不能使用[..2]
,要用[..3]
。Rust中字符是4个字节内存空间的Unicode类型,而字符串是UTF-8。
str和String
- 概念
str
时语言级别上唯一的字符串类型,通常以引用类型&str
出现,是硬编码进可执行文件,无法被修改。String
则是标准库中的一种字符串类型,可改变拥有所有权的字符串。
- 互相转换
- 转
String
:String::from("hello,world")
和"hello,world".to_string()
- 转
&str
:取引用&s
、切片&s[..]
或者s.as_str()
- 转
- **索引:**Rust 不允许去索引字符串,也就是没有
s[0]
,UTF-8编码
字符串操作:
- 追加:字符
push()
和字符串字面量push_str()
- 插入:
insert(index,'')
、insert_str(index,'')
- 替换:
replace(要被替换的字符串,新字符串)
,匹配替换全部,返回新的字符串replacen(要被替换的字符串,新字符串,替换的个数)
,返回新的字符串replace_range(要替换的字符串范围,新的字符串)
,仅用于String
,直接操作原字符串,需要用mut
修饰
- 删除
pop()
:删除并返回字符串的最后一个字符remove(index)
:删除并返回字符串中指定位置的字符,按照字节处理的,注意边界,容易报错。truncate(index)
:删除字符串中从指定位置开始到结尾的全部字符,按照字节处理的,注意边界,容易报错。clear()
:清空字符串
- 连接
+
或者+=
:要求右边的参数必须为字符串的切片引用,返回一个新的字符串,无需mut
修饰变量。format!
:format!("{} {}!", s1, s2);
- 转义
\x
输出ASCII字符,\u
输出Unicode字符- 保持字符串原样
r"Hello"
,保留双引号r#""Hello""#
- **元组:**由多种类型组合,长度固定,顺序固定
创建:let tup: (i32, f64, u8) = (500, 6.4, 1);
解构:let (x, y, z) = tup;
访问:通过tup.0``tup.1``tup.2
访问
- 结构体
- 结构如下,结构体字段可以没有名称
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
- 实例:每个字段要初始化,顺序无所谓,如果字段和函数参数同名可以缩略。
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
- 访问和修改:
.
访问,修改需要将结构体整个设定为可变 - 更新
let user2 = User {
email: String::from("another@example.com"),
..user1
};
- 单元结构体
struct AlwaysEqual;
let subject = AlwaysEqual;
// 不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
- 打印结构体信息:需要使用
#[derive(Debug)]
和{:?}
,原因是结构体没有像基本类型那样直接继承了debug
和display
的特征
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
- 输出dbg信息:
dbg!
- 枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
}
- 枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。
- 访问成员用
::
- 任何类型数据都可以放入枚举成员
- 空值
**Option**
,使用时无需加前缀,被提前引入了
enum Option<T> {//T表示任何任何类型的泛型,后面有
Some(T),
None,
}
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;//使用None的时候要告知类型
//要想继续操作需要将Option<T>转变为T
- 结果
**Result**
,和Option
类似,主要关注正确性
enum Result<T, E> {
Ok(T),
Err(E),
}
- 数组
- 长度固定但速度快的
array
和动态的Vector
,关系类似&str
和String
array
存储在栈上,Vector
堆上- 创建,元素类型必须一致
- 长度固定但速度快的
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];//可以声明类型和数量
let a = [3; 5];//表示[类型;长度],5个3,底层逻辑是不断copy,不适用于复杂类型
//复杂类型写法
let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!"));
println!("{:#?}", array);
- 切片多用固定大小的
&[T]
语句
小概念
语句与表达式:语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,注意:
let
是语句,不能将let
语句赋值给其它值。- 表达式不能加分号,加了就变成语句。
- 能返回值的算作表达式,如调用一个函数,调用宏,甚至于花括号包括的能最终返回一个值的语句块儿也是表达式。
- 不返回任何值的表达式会隐式返回一个
()
//这是错的
let b = (let a = 8);
//语句块二{},下面表示返回一个x+1给y,不能加分号哦!
let y = {
let x = 3;
x + 1
};
//if 语句块作为表达式,可以用于赋值
let z = if x % 2 == 1 { "odd" } else { "even" };
- 在 Rust 中,一定要严格区分表达式和语句的区别
流程控制
条件
- 常规
if,else,else if
,值得注意的是if
语句块是一个表达式,可以用于赋值。
循环
for
一些例子,性能比while
高
for 元素 in 集合 {}
for i in 1..=5//从1到5,序列,仅适用于数字或者字符
for item in &container//使用引用形式较多,所有权问题。
for item in &mut collection//可变借用
/*这里第一种比第二种性能高,安全性也更高,第二种会检查边界,且非连续访问*/
for item in collection
for i in 0..collection.len()
//迭代器,使用 enumerate 方法产生一个迭代器,该迭代器每次迭代会返回一个 (索引,值) 形式的元组,然后用 (index,value) 来匹配。
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
while
:常规loop
:无条件循环,是一个表达式可以返回值break
:可以单独用也可以带个值,类似return
模式匹配
match
和if let
match
可以返回值,必须穷举所有可能,可以用_
代表未列出的所有可能,每一个分支必须是表达式且最终返回值类型同,也可以使用逻辑符号如|
,通用形式如下
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
模式3 if => 表达式3,//匹配守卫,就是额外多个if
_ => 表达式4
}
if let
:仅匹配一个while let
:只要匹配就一直循环matches!
:宏,可以将一个表达式跟模式进行匹配,然后返回匹配的结果true
orfalse
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));
@
运算符允许为一个字段绑定另外一个变量
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable)
},//代码捕获id为5,并绑定给id_variable
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}
函数
小概念
如图
注意:
- 命名使用蛇形命名法,如
fn add9) -> {}
- 函数位置随便放,有定义就行
- 每个函数参数都需要标注类型
返回值
一般情况
可以用return
关键字,也可以把函数体最后一条表达式的返回值作为函数的返回值,例子如下
fn plus_or_minus(x:i32) -> i32 {
if x > 5 {
return x - 5
}
x + 5
}
特殊返回情况
- 无返回值
- 函数没有返回值的时候,会返回一个单元类型
()
- 通过
;
结尾的表达式会返回一个()
- 发散函数
- 使用
!
作函数返回类型,表示函数永不返回,通常用作会导致程序崩溃的函数
方法
方法往往跟结构体、枚举、特征(Trait)一起使用(这三个都定义方法),使用impl
来定义,可以定义多个impl
块,也可以集中到一起。
借用一张图来看看和一般语言中class中的方法区别,rust是把对象和方法定义分开的。
**关联函数:**构造器方法,返回该结构体的实例,参数中不包含self
,有个约定成俗的规则,使用new
来作为构造器的名称。
看看代码
pub struct Rectangle {
width: u32,
height: u32,//可以不加pub来设置私有属性
}
impl Rectangle {
pub fn new(width: u32, height: u32) -> Self {//关联函数
Rectangle { width, height }
}
pub fn width(&self) -> u32 {
return self.width;
}
}
fn main() {
let rect1 = Rectangle::new(30, 50);//关联函数调用要用::
println!("{}", rect1.width());//方法调用用.
}
要注意self
依然有所有权概念
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用
泛型
即泛型参数,一般用T
代替,要使用必须先声明之后才能在后面字段中用T
来替代,如fn largest<T>(list: &[T]) -> T
它在编译器完成处理,编译器会为每一个泛型参数对应的具体类型生成一份代码。
要注意的是,
- 使用前必须声明
- 泛型可以是任何类型,但不是所有类型都能执行统一操作,比如上面的函数代表找最大值,就有类型不支持比较大小,所以会报错。(可以通过特征来解决)
- 使用同一个
T
意味着参数类型都是相同的,如若想不同,就使用多个泛型如T
和U
- 方法中对泛型的声明为
impl<T,U> Point<T,U>
,如果使用的具体类型那么代表着对具体类型进行方法定义,其他类型不会有这个方法的定义,如impl Point<f32>
针对值的泛型:const
,定义语法为const N: usize
。如处理传递的数组长度的问题
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}//这里定义了一个[T;N]的数组,T为类型,N为值
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr);
}
特征
跟接口很类似,代码如下
定义:每一个实现特征的类型都需要具体实现该特征对应方法,调用使用.
就行
pub trait Summary {//定义一个特征
fn summarize(&self) -> String;
}
pub struct Post {
pub title: String, // 标题
pub author: String, // 作者
pub content: String, // 内容
}
impl Summary for Post {//具体实现,可描述为Post实现了Summary
fn summarize(&self) -> String {
format!("文章{}, 作者是{}", self.title, self.author)
}
}
默认实现
pub trait Summary {//默认实现方法,可以像上面那样进行方法重载
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
函数参数
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}//item就是一个实现了summary特征的参数,可以直接调用特征方法。
函数返回,只能有一个类型
fn returns_summarizable() -> impl Summary {
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}
特征约束
//使用泛型来进行特征约束
pub fn notify<T: Summary>(item1: &T, item2: &T) {
}
//多重约束 +号
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}
//where约束,简化一下
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
//代码
}
//有条件的实现方法或特征,代表只有具有这些特征的实例才能调用这个方法
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
特征对象
是指向实现了特征的类型的示例。这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法,达到类似多态的效果。
鸭子类型:只关心值是什么样子,而不在意它实际是什么。
通过&
引用或者Box<T>
智能指针(被它包裹的值会强制分配到堆上)来创建特征对象
声明&dyn T
或者Box<dyn T>
,创建&T
或者Box::new(T)
trait Draw {
fn draw(&self) -> String;
}
impl Draw for u8 {
fn draw(&self) -> String {
format!("u8: {}", *self)
}
}
impl Draw for f64 {
fn draw(&self) -> String {
format!("f64: {}", *self)
}
}
// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
fn draw1(x: Box<dyn Draw>) {//dyn关键字只用于特征对象类型声明,创建时无需用。
// 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
x.draw();
}
fn draw2(x: &dyn Draw) {
x.draw();
}
fn main() {
let x = 1.1f64;
// do_something(&x);
let y = 8u8;
// x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw>
// 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
draw1(Box::new(x));
// 基于 y 的值创建一个 Box<u8> 类型的智能指针
draw1(Box::new(y));//这里两个不同类型的对象都能实现Draw特征,由智能指针选择执行哪个
draw2(&x);
draw2(&y);
}
升级一下
pub struct Screen<T: Draw> {//特征约束,这里是存储实现了Draw的类型为T的元素
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {//这里则是通过where约束,定义了方法
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {//这里使用Box::new(T)来创建Box<dyn Draw>对象
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();//在run的时候,无需知道组件的类型是什么,也不检查,只要实现了Draw特征,就可以生成特征对象,达到多态的效果。
}
注意
- 孤儿规则:你想要为类型
A
实现特征T
,那么A
或者T
至少有一个是在当前作用域中定义的。 - 可以在特征中定义默认实现方法,允许调用相同特征中的其他方法(未默认实现的也可以)
- 可以使用泛型来进行约束。
- 调用方法需要引入特征。
- 特征派生如
#[derive(Debug)]
被标记的对象会自动实现对应的默认特征代码并继承相应功能。 - self和Self:前者指代当期实例对象,后者代表当前调用者的具体类型
- 当一个特征只有满足所有方法返回类型不能是
Self
,所有方法没有任何泛参才是安全的,才可以拥有特征对象。
动态分发
静态分发:编译期间,编译器为泛型参数对应的具体类型生成一份代码
动态分发:直到运行时,才能确定需要调用什么方法,前面的dyn
就是强调动态
特征对象必须使用动态分发,因为特征对象大小不固定,它的引用类型大小是固定的(由两个指针组成)
这里ptr
指向实现了特征的具体类型实例,vptr
则是指向一张保存了该实例可以调用的方法的虚表
特征进阶(脑瓜子嗡嗡的)
集合类型
Vector
动态数组Vec<T>
,只能存储相同类型的元素,它在超出作用域后会被自动删除。
- 创建
let v: Vec<i32> = Vec::new();//这是调用了Vec的关联函数
//也可以不指定类型,编译器会根据加入进去的元素自动推导
let mut v = Vec::new();
v.push(1);
Vec::with_capacity(capacity)//如果预先知道要存储的个数,可以这样创建
//使用宏来创建,这个可以初始化值,也无需标注类型
let v = vec![1, 2, 3];
- 更新:就是
push()
,需要创建的时候声明mut
- 读取:下标索引和
get
方法
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("第三个元素是 {}", third);
match v.get(2) {
Some(third) => println!("第三个元素是 {third}"),//新的格式化输出方式
None => println!("去你的第三个元素,根本没有!"),
}
这里get
方法返回的是Option<&T>
,也就是前面的的枚举类型,需要match
解构一下。在越界的情况下,使用get
更加安全,它会在有值的时候返回Some<T>
,无值的时候返回None
。而下标索引会直接报错。
- 遍历
for i in &v
迭代遍历即可,也可以修改for i in &mut v
- **存储不同类型元素:**采用枚举或者特征对象的实现
//枚举
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String)
}
fn main() {
let v = vec![
IpAddr::V4("127.0.0.1".to_string()),
IpAddr::V6("::1".to_string())
];
for ip in v {
show_addr(ip)
}
}
fn show_addr(ip: IpAddr) {
println!("{:?}",ip);
}
//特征对象数组
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self.0)
}
}
fn main() {
let v: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in v {
ip.display();
}
}
HashMap
KV映射,也是动态的,K和V都必须分别是相同类型的
它的所有权规则和其他类型类似,要注意实现了Copy
特征的无所谓所有权,否则将转移。
- 创建
//使用new创建,再用insert插入
use std::collections::HashMap;//需要手动从标准库中引入
let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
HashMap::with_capacity(capacity)//指定大小创建
//使用迭代器和collect方法创建
//先将Vec转为迭代器,接着通过collect转成HashMap
fn main() {
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
//通过标注告诉编译器推导
println!("{:?}",teams_map)
}
- 查询:
get
方法查询k
- 遍历:
for (Key,value) in &map
- 更新:
use std::collections::HashMap;
let text = "hello world wonderful world";
//新建一个map来存储词语出现的次数
let mut map = HashMap::new();
// 根据空格来切分字符串(英文单词都是通过空格切分)
/*
若之前没有插入过,则使用该词语作 Key,插入次数 0 作为 Value,
若之前插入过则取出之前统计的该词语出现的次数,对其+1。
*/
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);//这里or_insert 返回了 &mut v 引用
*count += 1;//返回的是引用,+=不能用于引用,所以要解引用
}
println!("{:?}", map);
哈希函数可以考虑引入第三方的库。
生命周期
就是引用的有效作用域,之所以需要标注生命周期,是因为
在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。
标注的语法:一般以'
开头,名称往往是一个单独的小写字母,大多数以'a
命名。如果是引用类型的参数,那么这个名称将位于&
之后,用空格隔开,如下
&i32 // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
- 用作函数参数
需要提前声明<'a>
,如
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
`'a`的生命周期会等于作用域最小的那个(参数,新建的引用等)
- 用到结构体上
同样需要提前声明
struct ImportantExcerpt<'a> {
part: &'a str,
}
- 用到方法上
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
//约束语法
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,//使得a必须比b活得久
{
println!("Attention please: {}", announcement);
self.part
}
}
- 三条生命周期消除规则
- 每一个引用参数都会获得独自的生命周期
- 若函数参数只有一个引用类型,那么该生命周期会被赋予给所有的返回值,也就是返回值的生命周期等于参数的生命周期
- 若存在多个输入生命周期(函数参数),其中有
&self
或&mut self
,也就是方法。则它的生命周期将被赋给所有的输出生命周期(返回值)
- 静态生命周期:
'static
代表活得和程序一样久
错误处理
分为可恢复和不可恢复错误
panic
不可恢复的错误,可以被动触发(如文件读取错误,数组越界等),也可以主动调用
**主动调用:**使用panic!()
宏,执行该宏时,程序会打印出一个错误信息,展开报错点前往的函数调用堆栈,最后退出程序。加上一个环境变量就可以获取更加详细的栈展开信息
- Linux/macOS 等 UNIX 系统:
RUST_BACKTRACE=1 cargo run
- Windows 系统(PowerShell):
$env:RUST_BACKTRACE=1 ; cargo run
unwrap()
错误处理:用于示例,测试
use std::net::IpAddr;
//这里parse试图解析前面字符串为IP,返回一个Result<IpAddr,E>类型
let home: IpAddr = "127.0.0.1".parse().unwrap();
//如果成功则Ok(IpAddr) 中的值赋给 home,如果失败,则不处理 Err(E),而是直接 panic。
总结就是,非预期错误,会对后续代码产生显著影响,内存安全时使用不可恢复的panic错误。
Result
可恢复的错误,也就是枚举里面提到的Result类型
,可以用match
自定义如何处理错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,//打开成功就赋予文件句柄
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {//文件不存在就创建
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
let f = File::open("hello.txt").unwrap();//也可以用unwrap,文件不存在会直接panic
let f = File::open("hello.txt").expect("Failed to open hello.txt");//expect效果一样,但是可以自带报错信息
?
功能类似match
,但是可以自动转换类型,可以用于Result
和Option
的传播,需要一个变量来承载正确的值。
//一般用于如下形式
let v = xxx()?;
xxx()?.yyy()?;
//用于Result
/*
File::open 报错时返回的错误是 std::io::Error 类型,
但是 open_file 函数返回的错误类型是 std::error::Error 的特征对象,
可以看到一个错误类型通过 ? 返回后,变成了另一个错误类型
*/
fn open_file() -> Result<File, Box<dyn std::error::Error>> {
let mut f = File::open("hello.txt")?;
Ok(f)
}
//链式调用,遇到错误就返回,没有错误就将 Ok 中的值取出来用于下一个方法调用
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
//用于Option
fn first(arr: &[i32]) -> Option<&i32> {
let v = arr.get(0)?;
Some(v)
}
项目管理
主要为workspace>package>crate>Moudle
包和项目
**包Crate:**一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库,一般会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。一般分为两种
- 二进制binary:就是应用程序,创建
cargo new my-project
- 库library:只能作为三方库被引用,创建
cargo new my-lib --lib
项目package:就是一个项目,因此它包含有独立的 Cargo.toml
文件,以及因为功能性被组织在一起的一个或多个包。一个 Package
至少要包含一个crate
,只能包含一个库(library)类型的包,但是可以包含多个二进制(binary)可执行类型的包(放在src/bin
目录,每个文件都会被编译成一个独立的binary crate)。
对于库类型的包,如果有多个rs文件,可以通过在lib.rs
中声明pub mod rs文件名
可以直接将该文件化为模块,形成模块树。
模块
Moudle 定义
采用mod+名称
来定义,可以进行嵌套形成父子模块,里面可以定义各种类型。所有模块定义在同一个文件中。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
引用:**::**
引用方法
-
绝对路径:从根开始,以包名或者
crate
开头,如crate::front_of_house::hosting::add_to_waitlist();
-
相对路径:从当前模块开始,以
self
,super
或当前模块标识符开头,如front_of_house::hosting::add_to_waitlist();
。super
代表以父模块开始的引用,如super::serve_order();
-
使用
use
引入模块use crate::front_of_house::hosting;
后续就可以直接使用hosting::seat_at_table()
。 -
简化
use
引用多个包用{}
,如use std::{cmp::Ordering, io};
-
使用
*
引用所有项 -
为避免冲突,可以使用
as
来赋予别名use XXXX as xxx
Rust默认所有类型都是是私有的,子模块可以访问父模块私有,但不能反过来,要变为公有可以用关键字pub
。
- pub 意味着可见性无任何限制
- pub(crate) 表示在当前包可见
- pub(self) 在当前模块可见
- pub(super) 在父模块可见
- pub(in
) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块
第三方包:在crates.io
和lib.rs
中检索
工作空间
workspace,可以用来使用多个lib包。
先创建一个文件夹为工作空间,然后创建Cargo.toml
作为配置文件
[workspace]
members = ["包名", "包名"]
运行时可以指定包名cargo run -p 包名
如果一个包的Cargo.toml既有package又有workspace,那么称它为root package,也就是最外层的包空间。如果没有package则称为Virtual manifest,主要适用于没有主package,又想把其他package集中到一起。
在bin包中调用同工作空间的lib
要在Cargo.toml
的denpendencies
表明lib包:包名={path="路径"}
注释
一般注释,文档注释,包注释等
格式化输出
常用的println!
和format!