Rust-base-learning-3
·
Rust基础学习–(三)
六、流程控制
1、分支控制:if-else
用if-else赋值:
1 |
|
用if-else进行逻辑判断:
1 |
|
2、循环控制
(1)for循环
示例:
1 |
|
【对于实现了 copy
特征的数组(例如 [i32; 10])而言, for item in arr
并不会把 arr
的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 arr
。】
如果不需要使用声明的i,也可以用如下方式控制循环:
1 |
|
总结如下:
使用方法 | 等价使用方式 | 所有权 |
---|---|---|
for item in collection |
for item in IntoIterator::into_iter(collection) |
转移所有权 |
for item in &collection |
for item in collection.iter() |
不可变借用 |
for item in &mut collection |
for item in collection.iter_mut() |
可变借用 |
如果想在循环中获取元素的索引:
1 |
|
continue:跳过本次循环继续,break:跳过当前整个循环(break 可以单独使用,也可以带一个返回值,有些类似 return
)。
(2)while循环
eg;
1 |
|
【for比while更安全,功能更强大,这是因为编译器增加了运行时代码来对每次while循环的每个元素进行条件检查,而,for
并不会使用索引去访问数组,因此更安全也更简洁,同时避免 运行时的边界检查
,性能更高】
(3)loop循环
又称无条件循环,使用if和break跳出循环。
eg:
1 |
|
七、模式匹配
1、match
eg;
1 |
|
match
的匹配必须要穷举出所有可能,因此这里用_
来代表未列出的所有可能性match
的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同- X | Y,类似逻辑运算符
或
,代表该分支可以匹配X
也可以匹配Y
,只要满足一个即可
(其实 match
跟其他语言中的 switch
非常像,_
类似于 switch
中的 default
。)
2、if let
当你只要匹配一个条件,且忽略其他条件时就用 if let
,否则都用 match
。
eg:
1 |
|
3、matches!宏
Rust 标准库中提供了一个非常实用的宏:matches!
,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true
or false
。
1 |
|
其他例子:
1 |
|
【注:无论是match
还是 if let
,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽】
4、模式及其适用场景
模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match
表达式联用,以实现强大的模式匹配能力。一般包括:
- 字面值
- 解构的数组、枚举、结构体或者元组
- 变量
- 通配符
- 占位符
适用场景:
match 分支
if let分支
while let条件循环
for循环
let语句
函数参数
let-else语句(新增)
使用
let-else
匹配,即可使let
变为可驳模式。它可以使用else
分支来处理模式不匹配的情况,但是else
分支中必须用发散的代码块处理(例如:break
、return
、panic
)。模式:
1
2
3
4let PATTERN = EXPRESSION else {
// 模式匹配失败时执行的代码
// 必须以发散表达式(如 panic!、return、break)结束
};PATTERN:要匹配的模式(如
Some(x)
、(a, b)
等)。EXPRESSION:要匹配的值。
else 块:模式匹配失败时执行的代码,必须以 发散表达式(diverging expression)结束(即程序控制流不会继续执行后续代码)。
eg:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18use std::str::FromStr;
fn get_count_item(s: &str) -> (u64, &str) {
let mut it = s.split(' ');
let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
panic!("Can't segment count item pair: '{s}'");
};
let Ok(count) = u64::from_str(count_str) else {
panic!("Can't parse integer: '{count_str}'");
};
// error: `else` clause of `let...else` does not diverge
// let Ok(count) = u64::from_str(count_str) else { 0 };
(count, item)
}
fn main() {
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
}(与
match
和if let
相比,let-else
的一个显著特点在于其解包成功时所创建的变量具有更广的作用域。在let-else
语句中,成功匹配后的变量不再仅限于特定分支内使用)
5、全模式列表
见链接
八、方法Method
定义:使用impl来定义方法
eg:
1 |
|
self、&self 和 &mut self:
(python里的self其实相当于这里的&mut self)
1 |
|
在 area
的签名中,我们使用 &self
替代 rectangle: &Rectangle
,&self
其实是 self: &Self
的简写(注意大小写)。在一个 impl
块内,Self
指代被实现方法的结构体类型,self
指代此类型的实例,换句话说,self
指代的是 Rectangle
结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解:我们为哪个结构体实现方法,那么 self
就是指代哪个结构体的实例。
self
依然有所有权的概念:
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用
在上面的例子中,选择 &self
的理由跟在函数中使用 &Rectangle
是相同的:我们并不想获取所有权,也无需去改变它,只是希望能够读取结构体中的数据。如果想要在方法中去改变当前的结构体,需要将第一个参数改为 &mut self
。仅仅通过使用 self
作为第一个参数来使方法获取实例的所有权是很少见的,这种使用方式往往用于把当前的对象转成另外一个对象时使用,转换完后,就不再关注之前的对象,且可以防止对之前对象的误调用。
总结,使用方法代替函数有以下好处:
- 不用在函数签名中重复书写
self
对应的类型 - 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大
【在 Rust 中,允许方法名跟结构体的字段名相同。eg,当我们使用 rect1.width()
时,Rust 知道我们调用的是它的方法,如果使用 rect1.width
,则是访问它的字段。一般来说,方法跟字段同名,往往适用于实现 getter
访问器,例如:
1 |
|
当从模块外部访问结构体时,结构体的字段默认是私有的,其目的是隐藏信息(封装)。我们如果想要从模块外部获取 Rectangle
的字段,只需把它的 new
, width
和 height
方法设置为公开可见,那么用户就可以创建一个矩形,同时通过访问器 rect1.width()
和 rect1.height()
方法来获取矩形的宽度和高度。
因为 width
字段是私有的,当用户访问 rect1.width
字段时,就会报错。注意在此例中,Self
指代的就是被实现方法的结构体 Rectangle
。
特别的是,这种默认的可见性(私有的)可以通过 pub
进行覆盖,这样对于模块外部来说,就可以直接访问使用 pub
修饰的字段而无需通过访问器。这种可见性仅当从定义结构的模块外部访问时才重要,并且具有隐藏信息(封装)的目的。
(暴论总结:感觉和c++的class的封装差不多)】
【->运算符在rust的替代方式:(rust并没有该运算符)
在 C/C++ 语言中,有两个不同的运算符来调用方法:.
直接在对象上调用方法,而 ->
在一个对象的指针上调用方法,这时需要先解引用指针。换句话说,如果 object
是一个指针,那么 object->something()
和 (*object).something()
是一样的。
Rust 并没有一个与 ->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
他是这样工作的:当使用 object.something()
调用方法时,Rust 会自动为 object
添加 &
(视可见性添加&mut
)、 *
以便使 object
与方法签名匹配。也就是说,这些代码是等价的:
1 |
|
第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self
的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。】
关联函数:
定义在 impl
中且没有 self
的函数被称之为关联函数: 因为它没有 self
,不能用 f.read()
的形式调用,因此它是一个函数而不是方法,它又在 impl
中,与结构体紧密关联,因此称为关联函数。
eg:
1 |
|
(Rust 中有一个约定俗成的规则,使用 new
来作为构造器的名称,出于设计上的考虑,Rust 特地没有用 new
作为关键字。)
因为是函数,所以不能用 .
的方式来调用,我们需要用 ::
来调用,例如 let sq = Rectangle::new(3, 3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
多个 impl 定义
Rust 允许我们为一个结构体定义多个 impl
块,目的是提供更多的灵活性和代码组织性,例如当方法多了后,可以把相关的方法组织在同一个 impl
块中,那么就可以形成多个 impl
块,各自完成一块儿目标。
为枚举实现方法
1 |
|
枚举类型之所以强大,不仅仅在于它好用、可以同一化类型,还在于,我们可以像结构体一样,为枚举实现方法。
(除了结构体和枚举,我们还能为特征(trait)实现方法)
九、泛型Generics
简介:
泛型是一种多态。泛型主要目的是为程序员提供编程的便利,减少代码的臃肿,同时可以极大地丰富语言本身的表达能力。换句话说,就是一个函数,可以代替几十个,甚至数百个函数。
1 |
|
上面代码的 T
就是泛型参数,实际上在 Rust 中,泛型参数的名称你可以任意起,但是出于惯例,我们都用 T
(T
是 type
的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。
使用泛型参数,有一个先决条件,必需在使用前对其进行声明:
1 |
|
该泛型函数的作用是从列表中找出最大的值,其中列表中的元素类型为 T。首先 largest<T>
对泛型参数 T
进行了声明,然后才在函数参数中进行使用该泛型参数 list: &[T]
(&[T]类型是数组切片) 。
结构体中使用泛型
结构体中的字段类型也可以用泛型来定义,下面代码定义了一个坐标点 Point
,它可以存放任何类型的坐标值:
1 |
|
这里有两点需要特别的注意:
- 提前声明,跟泛型函数定义类似,首先我们在使用泛型参数之前必需要进行声明
Point<T>
,接着就可以在结构体的字段类型中使用T
来替代具体的类型 - x 和 y 是相同的类型(如果想让
x
和y
既能类型相同,又能类型不同,就需要使用不同的泛型参数)
枚举中使用泛型
Option<T>
是一个拥有泛型 T
的枚举类型,它第一个成员是 Some(T)
,存放了一个类型为 T
的值。得益于泛型的引入,我们可以在任何一个需要返回值的函数中,去使用 Option<T>
枚举类型来做为返回值,用于返回一个任意类型的值 Some(T)
,或者没有值 None
。
此外,还有一个重要的枚举类型:result<T,E>
1 |
|
这个枚举和 Option
一样,主要用于函数返回值,与 Option
用于值的存在与否不同,Result
关注的主要是值的正确性。
如果函数正常运行,则最后返回一个 Ok(T)
,T
是函数具体的返回值类型,如果函数异常运行,则返回一个 Err(E)
,E
是错误类型。例如打开一个文件:如果成功打开文件,则返回 Ok(std::fs::File)
,因此 T
对应的是 std::fs::File
类型;而当打开文件时出现问题时,返回 Err(std::io::Error)
,E
对应的就是 std::io::Error
类型。
方法中使用泛型
1 |
|
使用泛型参数前,依然需要提前声明:impl<T>
,只有提前声明了,我们才能在Point<T>
中使用它,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。需要注意的是,这里的 Point<T>
不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T>
而不再是 Point
。
除了结构体中的泛型参数,我们还能在该结构体的方法中定义额外的泛型参数,就跟泛型函数一样:
1 |
|
这个例子中,T,U
是定义在结构体 Point
上的泛型参数,V,W
是单独定义在方法 mixup
上的泛型参数,它们并不冲突,说白了,你可以理解为,一个是结构体泛型,一个是函数泛型。
此外,对于 Point<T>
类型,不仅能定义基于 T
的方法,还能针对特定的具体类型,进行方法定义:
1 |
|
const 泛型:针对值的泛型
(rust1.51版本引入)
在数组中,[i32; 2]
和 [i32; 3]
是不同的数组类型。因此我们可以使用const泛型处理数组长度问题,使其可以打印出任意长度的数组。
1 |
|
const 泛型表达式
假设我们某段代码需要在内存很小的平台上工作,因此需要限制函数参数占用的内存大小,此时就可以使用 const 泛型表达式来实现:
1 |
|
const fn
在讨论完 const
泛型后,不得不提及另一个与之密切相关且强大的特性:const fn
,即常量函数。const fn
允许我们在编译期对函数进行求值,从而实现更高效、更灵活的代码设计。
要定义一个常量函数,只需要在函数声明前加上 const
关键字。例如:
1 |
|
虽然 const fn
提供了很多便利,但是由于其在编译期执行,以确保函数能在编译期被安全地求值,因此有一些限制,例如,不可将随机数生成器写成 const fn
。
无论在编译时还是运行时调用 const fn
,它们的结果总是相同,即使多次调用也是如此。唯一的例外是,如果你在极端情况下进行复杂的浮点操作,你可能会得到(非常轻微的)不同结果。因此,不建议使 数组长度 (arr.len())
和 Enum判别式
依赖于浮点计算。
结合 const fn 与 const 泛型
将 const fn
与 const 泛型
结合,可以实现更加灵活和高效的代码设计。例如,创建一个固定大小的缓冲区结构,其中缓冲区大小由编译期计算确定:
1 |
|
在这个例子中,compute_buffer_size
是一个常量函数,它根据传入的 factor
计算缓冲区的大小。在 main
函数中,我们使用 compute_buffer_size(4)
来计算缓冲区大小为 4096 字节,并将其作为泛型参数传递给 Buffer
结构体。这样,缓冲区的大小在编译期就被确定下来,避免了运行时的计算开销。
泛型的性能保证
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
编译器所做的工作正好与我们创建泛型函数的步骤相反,编译器寻找所有泛型代码被调用的位置并针对具体类型生成代码。
让我们看看一个使用标准库中 Option
枚举的例子:
1 |
|
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一种对应 i32
另一种对应 f64
。为此,它会将泛型定义 Option<T>
展开为 Option_i32
和 Option_f64
,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样:
1 |
|
我们可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
To Be Continue…