2025OS训练营一阶段记录
说明一下,我是提前为训练营做准备,所以记录时间比开营要早,毕竟以我这种基础不笨鸟先飞怕跟不上哈哈。
3.12
今天开始学Rust,之前对这个语言也没咋接触过,但是之前学过点go,都是函数式编程,应该有相通处吧。
我找了经验贴都说看course.rs的Rust圣经,于是按照提示搭建了下环境,我用的是ubuntu20.04,装起环境没有碰到什么问题。还跟着作者用vscode,装了几个推荐的插件。
了解了cargo 跑了下helloworld,之前写c++的,每次都被环境折腾的不行,找不到的包还要自己手动编译,而且是win环境下…不会写makefile还要用cmake,这东西更新还快,每种库外部文件导入方式还不一样,可能被折磨习惯了觉得这很正常。原来现在的编程语言能做到这么厉害,不仅不用自己编译,手动导入,连库的可用性也会检测。而且rust还有媲美c++的性能,太厉害了!编译的方式也非常简单,toml的依赖方式也比任何语言的依赖方式都要简单。Rust 原生支持 UTF-8 编码的字符串,可以很容易的使用世界各国文字作为字符串内容,再也不用被msvc的字符集问题折腾了。{}作为占位符,数字、字符串、结构体都能打印。rust有很好的链式编程特性,标准库的函数熟练运用起来应该非常方便优雅。
第一章变量绑定与解构。变量默认不可变 :Rust变量默认不能修改,必须加mut
才能变成可变变量。这个设计挺特别的,一开始不习惯但确实能避免很多意外修改的bug。变量遮蔽:可以用同名变量覆盖之前的变量,实际上是创建了新变量。和mut
的区别在于会创建新内存空间,还能用来改变变量类型。解构赋值 :可以从元组、结构体等复杂类型中提取值赋给变量,写法很简洁。常量 :用const
声明,必须指定类型,命名习惯是全大写加下划线。命名规范 :下划线开头的变量名可以避免未使用变量的警告。
整体感觉Rust的变量系统设计得很严谨,虽然开始有点不习惯,但这些特性确实能写出更安全的代码。特别是默认不可变这个设计,强迫开发者想清楚哪些变量真的需要修改。
第二章基本类型。基本类型与c++没什么差别,特别的就是单元类型 ()
,其唯一的值也是 ()
。rust编译器必须在编译期知道我们所有变量的类型,但这不意味着需要为每个变量指定类型,它可以根据变量的值和上下文中的使用方式来自动推导出变量的类型,在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注。在整数上,在release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理,也有一些函数用来检查溢出。在处理浮点数计算时要相当注意。NaN为数学上为定义,与之交互都会成为NaN。
我注意到range for i in 1..=5这样的方式非常有意思和方便,还可以用字母。Rust 拥有相当多的数值类型. 需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及选择的类型是否能表达负数。类型转换必须是显式的. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数。
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,需要区分语句和表达式,表达式总要返回值,分号结尾的是语句。
当用 !
作函数返回类型的时候,表示该函数永不返回,这种语法往往用做会导致程序崩溃的函数。
3.13
今天学习基础第三章所有权和借用。为了解决内存安全问题,rust提出所有权概念,所有权三条规则:Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者;一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者;当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)。
1 | let x = 5; |
这段代码并没有发生所有权的转移,原因很简单: 代码首先将 5
绑定到变量 x
,接着拷贝 x
的值赋给 y
,最终 x
和 y
都等于 5
,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
1 | let s1 = String::from("hello"); |
当 s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。其实类似于c++里的move。如果不想夺取原有的所有权可以使用clone(),rust基本类型自带clone属性。
Rust通过借用(Borrowing) 获取变量的引用,称之为借用。引用分为可变引用和不可变引用,区别就是可不可变,这种限制的好处就是使 Rust 在编译期就避免数据竞争,两个或更多的指针同时访问同一数据,至少有一个指针被用来写入数据,没有同步数据访问的机制。可变引用和不可变引用不可以同时存在,可变引用可以同时存在一个,不可变引用可以同时存在多个。
基础第四章复合类型。&s[0..len]切片操作很方便得用到string,字节序等,0..len为range类,左闭右开,在处理字节流时要注意,汉字在utf8占三个字节。字符串字面量也是个切片。
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。可以通过以下语法创建一个元组:
1 | let tup: (i32, f64, u8) = (500, 6.4, 1); |
变量 tup
被绑定了一个元组值 (500, 6.4, 1)
,该元组的类型是 (i32, f64, u8)
,元组是用括号将多个类型组合到一起,可以使用模式匹配或者 .
操作符来获取元组中的值。
用模式匹配解构元组:
1 | let (x, y, z) = tup; |
struct结构体和c语言类似,访问字段用 . ,初始化实例时,每个字段都需要进行初始化,初始化时的字段顺序不需要和结构体定义时的顺序一致。结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段,
结构体必须要有名称,但是元组结构体的字段可以没有名称,例如:
1 | struct Color(i32, i32, i32); |
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。
如果想在结构体里包含一个引用,必须声明生命周期。可以为结构体添加debug属性,实现fmt函数用来自定义打印结构体信息。
枚举类比大多语言要强大,每个元素可以是结构体包含不同信息,
1 | enum Message { |
Option 枚举用于处理空值,在其它编程语言中,往往都有一个 null 关键字,当null 异常导致程序的崩溃时我们需要处理这些 null 空值。Rust 吸取了众多教训,决定抛弃 null,而改为使用 Option 枚举变量来表述这种结果。Option
枚举包含两个成员,一个成员表示含有值:Some(T)
, 另一个表示没有值:None
,定义如下:
1 | enum Option<T> { |
其中 T
是泛型参数,Some(T)
表示该枚举成员的数据类型是 T
,换句话说,Some
可以包含任何类型的数据。
在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
,在本书中,我们称 array
为数组,Vector
为动态数组。数组的三要素:长度固定、元素必须有相同的类型、依次线性排列。声明方式:
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
3.14
今天学习基础第五章流程控制和第六章模式匹配。流程控制就是for … in while loop很容易。模式匹配还是对c++er比较新颖。match的用法和switch相似,强大的点在于可以同时解构内容如:
1 | match action { |
if let 相当于单个匹配的match,同样用于获取并解构。
matches!
,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true
or false
。
match 和 if let 都可以触发变量遮蔽,就是可以同名变量优先使用当前作用于下的,在处理时很方便。
Option解构在rust经常用到,返回结果要么是Some(T),要么是None,在处理时解构处理不同情况非常强大。
解构方式还有个while let ,可以循环处理解构,数组和元组也可以用对应的结构来解构,_可以用来忽略这个位置的变量,匹配的范围同样可以使用range。
基础第七章方法。可以使用impl 对struct enum实现方法,方法公开需前置pub,方法的函数如若调用结构体的内容需要第一个参数位self,一般情况时&self,在需要对结构体内容更改时为&mut self,当然也可以不是引用,self转让所有权到方法里。在函数里没有结构体的引用时(关联函数)需要使用结构体::来访问,否则一概使用 . 来访问。impl 内部也可以声明变量、常量等。impl 可以多次进行实现。
第八章泛型与特征。在 Rust 中,泛型参数的名称可以任意起,但是出于惯例都用 T
,使用泛型参数,有一个先决条件,必需在使用前对其进行声明:
1 | fn largest<T>(list: &[T]) -> T { |
该泛型函数的作用是从列表中找出最大的值,其中列表中的元素类型为 T。首先 largest<T>
对泛型参数 T
进行了声明,然后才在函数参数中进行使用该泛型参数 list: &[T]
,函数 largest
有泛型类型 T
,它有个参数 list
,其类型是元素为 T
的数组切片,最后,该函数返回值的类型也是 T
。
T
可以是任何类型,但不是所有的类型都能进行比较,编译器建议我们给 T
添加一个类型限制:使用 std::cmp::PartialOrd
特征(Trait)对 T
进行限制。
有时候,编译器无法推断你想要的泛型参数,这时候需要显式地来声明。
1 | fn create_and_print<T>() where T: From<i32> + Display { |
结构体和枚举的泛型
1 | enum Option<T> { |
方法中使用泛型:使用泛型参数前要提前声明:impl<T>
,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。注意,这里的 Point<T>
不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T>
而不再是 Point
。除了结构体中的泛型参数,还能在该结构体的方法中定义额外的泛型参数,就跟泛型函数一样:
1 | impl<T, U> Point<T, U> { |
const泛型:[i32; 3]
和 [i32; 2]
确实是两个完全不同的类型,因此无法用同一个函数调用,const 泛型,也就是针对值的泛型可以用于处理数组长度的问题:
1 | fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { |
const fn
,即常量函数。通常情况下,函数是在运行时被调用和执行的。然而,在某些场景下,我们希望在编译期就计算出一些值,以提高运行时的性能或满足某些编译期的约束条件。例如,定义数组的长度、计算常量值等。有了 const fn
,我们可以在编译期执行这些函数,从而将计算结果直接嵌入到生成的代码中。
特征trait:如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。
1 | pub trait Summary { |
关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A
实现特征 T
,那么 A
或者 T
至少有一个是在当前作用域中定义的! 例如可以为上面的 Post
类型实现标准库中的 Display
特征,这是因为 Post
类型定义在当前的作用域中。同时也可以在当前包中为 String
类型实现 Summary
特征,因为 Summary
定义在当前作用域中.
特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法.
使用特征作为函数参数:
1 | pub fn notify(item: &impl Summary) { |
impl Summary
顾名思义,它的意思是 实现了Summary
特征 的 item
参数。
如果想要强制函数的两个参数是同一类型只能使特征约束来实现:
1 | pub fn notify<T: Summary>(item1: &T, item2: &T) {} |
泛型类型 T
说明了 item1
和 item2
必须拥有同样的类型,同时 T: Summary
说明了 T
必须实现 Summary
特征。
多重约束:除了单个约束条件,可以指定多个约束条件,
1 | pub fn notify(item: &(impl Summary + Display)) {} |
除了上述的语法糖形式,还能使用特征约束的形式:
1 | pub fn notify<T: Summary + Display>(item: &T) {} |
特征约束,可以让我们在指定类型 + 指定特征的条件下去实现方法,也可以有条件地实现特征,例如,标准库为任何实现了 Display
特征的类型实现了 ToString
特征。
可以通过 impl Trait
来说明一个函数返回了一个类型,该类型实现了某个特征:
1 | fn returns_summarizable() -> impl Summary { |
这种 impl Trait
形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时。
#[derive(Debug)]
:是一种特征派生语法,被 derive
标记的对象会自动实现对应的默认特征代码,继承相应的功能。例如 Debug
特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用 println!("{:?}", s)
的形式打印该结构体的对象。
再如 Copy
特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现 Copy
特征,进而可以调用 copy
方法,进行自我复制。
总之,derive
派生出来的是 Rust 默认给我们提供的特征,在开发过程中极大的简化了自己手动实现相应特征的需求,当然,如果你有特殊的需求,还可以自己手动重载该实现
特征对象
1 | pub trait Draw { |
只要组件实现了 Draw
特征,就可以调用 draw
方法来进行渲染。假设有一个 Button
和 SelectBox
组件实现了 Draw
特征:
1 | pub struct Button { |
此时,还需要一个动态数组来存储这些 UI 对象:
1 | pub struct Screen { |
特征对象**指向实现了 Draw
特征的类型的实例,也就是指向了 Button
或者 SelectBox
的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。
draw1
函数的参数是Box<dyn Draw>
形式的特征对象,该特征对象是通过Box::new(x)
的方式创建的draw2
函数的参数是&dyn Draw
形式的特征对象,该特征对象是通过&x
的方式创建的dyn
关键字只用在特征对象的类型声明上,在创建时无需使用dyn
1 | pub struct Screen { |
其中存储了一个动态数组,里面元素的类型是 Draw
特征对象:Box<dyn Draw>
,任何实现了 Draw
特征的类型,都可以存放其中。
再来为 Screen
定义 run
方法,用于将列表中的 UI 组件渲染在屏幕上:
1 | impl Screen { |
泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发,因为是在编译期完成的,对于运行期性能完全没有任何影响。与静态分发相对应的是动态分发(dynamic dispatch),直到运行时,才能确定需要调用什么方法。
3.15
第九章集合类型
使用 Vec::new
创建动态数组
1 | let v: Vec<i32> = Vec::new(); |
这里,v
被显式地声明了类型 Vec<i32>
,这是因为 Rust 编译器无法从 Vec::new()
中得到任何关于类型的暗示信息,因此也无法推导出 v
的具体类型:
1 | let mut v = Vec::new(); |
此时,v
就无需手动声明类型,因为编译器通过 v.push(1)
,推测出 v
中的元素类型是 i32
,因此推导出 v
的类型是 Vec<i32>
。
如果预先知道要存储的元素个数,可以使用
Vec::with_capacity(capacity)
创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能
还可以使用宏 vec!
来创建数组,与 Vec::new
有所不同,前者能在创建同时给予初始化值:
1 | let v = vec![1, 2, 3]; |
同样,此处的 v
也无需标注类型,编译器只需检查它内部的元素即可自动推导出 v
的类型是 `Vec
跟结构体一样,Vector
类型在超出作用域范围后,会被自动删除:
1 | { |
当 Vector
被删除后,它内部存储的所有内容也会随之被删除。
同时借用多个数组元素 遇到同时借用多个数组元素的情况
1 | let mut v = vec![1, 2, 3, 4, 5]; |
此时编译器报错。数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存。
1 | use std::collections::HashMap; |
3.16
第十章生命周期。
借用检查:为了保证 Rust 的所有权和借用的正确性,Rust 使用了一个借用检查器(Borrow checker)来检查程序的借用正确性。
函数中的生命周期:
1 | fn longest(x: &str, y: &str) -> &str { |
编译器无法知道该函数的返回值到底引用 x
还是 y
,因为编译器需要知道这些,来确保函数调用后的引用生命周期分析。在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。
生命周期标注:生命周期的语法以 '
开头,名称往往是一个单独的小写字母,大多数人都用 'a
来作为生命周期的名称。 如果是引用类型的参数,那么生命周期会位于引用符号 &
之后,并用一个空格来将生命周期和引用参数分隔开
1 | &i32 // 一个引用 |
一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系。例如,有一个函数,它的第一个参数 first
是一个指向 i32
类型的引用,具有生命周期 'a
,该函数还有另一个参数 second
,它也是指向 i32
类型的引用,并且同样具有生命周期 'a
。此处生命周期标注仅仅说明,这两个参数 first
和 second
至少活得和’a 一样久,至于到底活多久或者哪个活得更久,我们都无法得知:
1 | fn useless<'a>(first: &'a i32, second: &'a i32) {} |
函数签名中的生命周期标注
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
- 和泛型一样,使用生命周期参数,需要先声明
<'a>
x
、y
和返回值至少活得和'a
一样久(因为返回值要么是x
,要么是y
)
该函数签名表明对于某些生命周期 'a
,函数的两个参数都至少跟 'a
活得一样久,同时函数的返回引用也至少跟 'a
活得一样久。实际上,这意味着返回值的生命周期与参数生命周期中的较小值一致:虽然两个参数的生命周期都是标注了 'a
,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a
不代表生命周期等于 'a
,而是大于等于 'a
)。在通过函数签名指定生命周期参数时,并没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过。
因此 longest
函数并不知道 x
和 y
具体会活多久,只要知道它们的作用域至少能持续 'a
这么长就行。
该例子证明了 result
的生命周期必须等于两个参数中生命周期较小的那个:
1 | fn main() { |
在上述代码中,result
必须要活到 println!
处,因为 result
的生命周期是 'a
,因此 'a
必须持续到 println!
。
结构体中的生命周期:
1 | struct ImportantExcerpt<'a> { |
ImportantExcerpt
结构体中有一个引用类型的字段 part
,因此需要为它标注上生命周期。结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>
。该生命周期标注说明,结构体 ImportantExcerpt
所引用的字符串 str
生命周期需要大于等于该结构体的生命周期
生命周期消除:尽管我们没有显式的为其标注生命周期,编译依然可以通过。其实原因不复杂,编译器为了简化用户的使用,运用了生命周期消除大法。
三条消除规则:
编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。
每一个引用参数都会获得独自的生命周期
例如一个引用参数的函数就有一个生命周期标注:
fn foo<'a>(x: &'a i32)
,两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
, 依此类推。若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
例如函数
fn foo(x: &i32) -> &i32
,x
参数的生命周期会被自动赋给返回值&i32
,因此该函数等同于fn foo<'a>(x: &'a i32) -> &'a i32
若存在多个输入生命周期,且其中一个是
&self
或&mut self
,则&self
的生命周期被赋给所有的输出生命周期拥有
&self
形式的参数,说明该函数是一个方法
,该规则让方法的使用便利度大幅提升。
3.17
方法中的生命周期:
1 | struct ImportantExcerpt<'a> { |
impl
中必须使用结构体的完整名称,包括<'a>
,因为生命周期标注也是结构体类型的一部分!- 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则
静态生命周期:生命周期 'static
,拥有该生命周期的引用可以和整个程序活得一样久。
字符串字面量是被硬编码进 Rust 的二进制文件中,因此这些字符串变量全部具有 'static
的生命周期:
1 | let s: &'static str = "就是活得久,嘿嘿"; |
Rust 中的错误主要分为两类:
- 可恢复错误,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响
- 不可恢复错误,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的
Rust 提供了 panic!
宏,当调用执行该宏时,程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序。
当出现 panic!
时,程序提供了两种方式来处理终止流程:栈展开和直接终止。
其中,默认的方式就是 栈展开
,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止
,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 Cargo.toml
文件,实现在 release
模式下遇到 panic
直接终止:
1 | [profile.release] |
panic后如果是 main
线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 main
线程。
*Result<T, E>
*是一个枚举类型,定义如下:
1 | enum Result<T, E> { |
泛型参数 T
代表成功时存入的正确值的类型,存放方式是 Ok(T)
,E
代表错误时存入的错误值,存放方式是 Err(E)
,
不想使用 match
去匹配 Result<T, E>
以获取其中的 T
值,因为 match
的穷尽匹配特性,你总要去处理下 Err
分支。有个办法简化这个过程就是 unwrap
和 expect
。它们的作用就是,如果返回成功,就将 Ok(T)
中的值取出来,如果失败,就直接 panic。expect
跟 unwrap
很像,也是遇到错误直接 panic
, 但是会带上自定义的错误提示信息,相当于重载了错误打印的函数.
1 | let f = File::open("hello.txt").unwrap(); |
错误传播:
1 | fn read_username_from_file() -> Result<String, io::Error> |
- 该函数返回一个
Result<String, io::Error>
类型,当读取用户名成功时,返回Ok(String)
,失败时,返回Err(io:Error)
File::open
和f.read_to_string
返回的Result<T, E>
中的E
就是io::Error
3.18
- 项目(Package):可以用来构建、测试和分享包
- 工作空间(WorkSpace):对于大型项目,可以进一步将多个包联合在一起,组织成工作空间
- 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行
- 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
包 Crate
对于 Rust 而言,包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。
一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。例如标准库中没有提供但是在三方库中提供的 rand
包,它提供了随机数生成的功能,只需要将该包通过 use rand;
引入到当前项目的作用域中,就可以在项目中使用 rand
的功能:rand::XXX
。
同一个包中不能有同名的类型,但是在不同包中就可以。例如,虽然 rand
包中,有一个 Rng
特征,可是依然可以在自己的项目中定义一个 Rng
,前者通过 rand::Rng
访问,后者通过 Rng
访问,不会存在引用歧义。
项目 Package
由于 Package
就是一个项目,因此它包含有独立的 Cargo.toml
文件,以及因为功能性被组织在一起的一个或多个包。一个 Package
只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。
库 Package
1 | cargo new my-lib --lib |
如果你试图运行 my-lib
,会报错:
1 | cargo run |
原因是库类型的 Package
只能作为三方库被其它项目引用,而不能独立运行,只有之前的二进制 Package
才可以运行。
与 src/main.rs
一样,Cargo 知道,如果一个 Package
包含有 src/lib.rs
,意味它包含有一个库类型的同名包 my-lib
,该包的根文件是 src/lib.rs
。
模块
- 使用
mod
关键字来创建新模块,后面紧跟着模块名称 - 模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
- 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
- 所有模块均定义在同一个文件中
模块树为模块的嵌套结构,他们都有一个根模块 crate
。如果模块 A
包含模块 B
,那么 A
是 B
的父模块,B
是 A
的子模块。
想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式:
- 绝对路径,从包根开始,路径名以包名或者
crate
作为开头 - 相对路径,从当前模块开始,以
self
,super
或当前模块的标识符作为开头
Rust 出于安全的考虑,默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量,是的,就连模块本身也是私有化的。父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项。
模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为 pub
。
当外部的模块项 A
被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 A
,那么可以对它进行再导出。使用 pub use
即可实现。
引入第三方包中的模块,关于如何引入外部依赖:
修改 Cargo.toml
文件,在 [dependencies]
区域添加一行:rand = "0.8.3"
3.21-3.24 rustlings
关于rustlings,系统学过rust后几乎没有难题,所以我在6号一天就完成了70多道,前两天在熟悉github classroom和rustlings的自检方式。