Hello Rust
fn main() {
println!("Hello Rust!");
}
先画个饼,暂时来不及把写的总结部署到 Hexo 上了,后续补上,敬请期待。
刚好最近想要做一些OS相关的工作,又趁着OS训练营开营,于是赶着ddl把rustling干完了。在以前的学习中,常用C/C++,这次新学习了Rust语言,编程习惯还是有些不同的,要花大功夫去掌握难点。
我主要使用了《Rust 程序设计语言》官方文档作为学习指南,并结合rustling进行知识点的学习和巩固。开始的基础语法较容易,很快就做完了,但是到后面的tests和algorithm部分,难度增大了,花费了不少时间才完成题目呜呜呜。
总体来说在第一阶段这个学习过程还是挺顺利的,因为网络平台上存在许多大量的资源供学习参考。
在老师的悉心指导下和交流群互帮互助下,从最开始的 Hello World 起步,到深入系统地学习了操作系统的基本概念、原理,这使我对Rust和OS 有了更加全面和深入的理解。最后,衷心感谢两位教学讲师对我们的授课讲学。
希望我能把三阶段给坚持下来吧~
早在今年寒假时就打算学习Rust,奈何一直没有开工。四月份在群里偶然看到了2024春夏季开源操作系统训练营,这才算是开始了Rust学习之旅。
我的Rust入门学习主要是通过阅读和练习两点。
阅读其一是Rust 语言圣经
1. 寻找牛刀,以便小试 ✅ 2024-04-10
2. Rust 基础入门 ✅ 2024-04-20
阅读其二是The Rust Programming Language
Github: GitHub - rust-lang/book: The Rust Programming Language
原版: The Rust Programming Language - The Rust Programming Language
交互版: Experiment Introduction - The Rust Programming Language
1. 入门指南 ✅ 2024-04-10
2. 写个猜数字游戏 ✅ 2024-04-10
3. 常见编程概念 ✅ 2024-04-10
4. 认识所有权 ✅ 2024-04-11
5. 使用结构体组织相关联的数据 ✅ 2024-04-11
6. 枚举和模式匹配 ✅ 2024-04-12
7. 使用包、Crate 和模块管理不断增长的项目 ✅ 2024-04-12
8. 常见集合 ✅ 2024-04-15
9. 错误处理 ✅ 2024-04-16
10. 泛型、Trait 和生命周期 ✅ 2024-04-18
11. 编写自动化测试 ✅ 2024-04-20
12. 一个 I/O 项目:构建命令行程序 ✅ 2024-04-21
13. Rust 中的函数式语言功能:迭代器与闭包 ✅ 2024-04-21
14. 更多关于 Cargo 和 Crates.io 的内容 ✅ 2024-04-21
15. 智能指针 ✅ 2024-04-21
16. 无畏并发 ✅ 2024-04-21
17. Rust 的面向对象编程特性 ✅ 2024-04-21
18. 模式与模式匹配 ✅ 2024-04-21
19. 高级特征 ✅ 2024-04-21
练习其一是Rust By Practice
Solution: rust-by-practice/solutions at master · sunface/rust-by-practice · GitHub
1. 关于 practice.rs ✅ 2024-04-10
2. 值得学习的小型项目 ✅ 2024-04-10
3. 变量绑定与解构 ✅ 2024-04-10
4. 基本类型 ✅ 2024-04-10
5. 所有权和借用 ✅ 2024-04-11
6. 复合类型 ✅ 2024-04-11
7. 流程控制 ✅ 2024-04-12
8. 模式匹配 ✅ 2024-04-12
9. 方法和关联函数 ✅ 2024-04-17
10. 泛型和特征 ✅ 2024-04-18
11. 集合类型 ✅ 2024-04-18
13. 返回值和 panic! ✅ 2024-04-18
14. 包和模块 ✅ 2024-04-20
15. 注释和文档 ✅ 2024-04-20
16. 格式化输出 ✅ 2024-04-20
练习其二则是训练营第一阶段的rustlings
intro ✅ 2024-04-09
variables ✅ 2024-04-09
functions ✅ 2024-04-11
if ✅ 2024-04-11
primitive_types ✅ 2024-04-11
vecs ✅ 2024-04-16
primitive_types ✅ 2024-04-16
move_semantics ✅ 2024-04-16
structs ✅ 2024-04-16
enums ✅ 2024-04-16
strings ✅ 2024-04-16
modules ✅ 2024-04-16
options ✅ 2024-04-16
hashmaps ✅ 2024-04-16
error_handing ✅ 2024-04-16
generics ✅ 2024-04-20
lifetimes ✅ 2024-04-20
traits ✅ 2024-04-20
iterators ✅ 2024-04-20
tests ✅ 2024-04-21
macros ✅ 2024-04-21
threads ✅ 2024-04-21
clippy ✅ 2024-04-21
conversions ✅ 2024-04-21
smart_pointers ✅ 2024-04-21
quiz ✅ 2024-04-21
algorithms ✅ 2024-04-22
在此过程中,Rust的所有权(Ownership)、生命周期(Lifetimes)、特型(Traits),以及函数式编程中常见的模式匹配(Patterns and Matching)等概念给我留下了深刻的印象。当然,最为深刻的还是Rust的编译器。
我认为在编写极为熟练的代码时,Rust严苛的编译器确实拖慢了我的编码速度(例如写个链表那叫一个费劲),但是从另一方面看,它尽可能地保证了我写出没有歧义的、不会在运行时错误的更健壮的代码。
总的来说,Rust的入门体验还不错,希望在接下来的学习中,我能更进一步地掌握它的特性,以及它在操作系统中内核级代码的应用。
以前是做 c 和 java 开发的,也使用过 js、python 等语言,无意中接触到 rust,被宣称的安全和性能吸引,资料东拼西凑学习,写各种小工具,因为概念很多,而且写程序编译器各种报错,差点被劝退,想着既然学习了,就再坚持下,接触 rust 一段时间,慢慢喜欢上了这门语言,编译器提示的很详细,而且只要编译成功后,程序的运行机会不会有问题,rust 是一门上线很高,下线也很低的一门语言,之前都是用 rust 写写小工具,都是在应用层面的开发,这次看到开源操作系统训练营报名,果断报名了,想去深入到新的领域,接触新的东西。
参加了训练营一阶段的培训,跟着老师的教学内容,动手实践,又有 rustlings 的练手,自己对 rust 的理解又更加进了一步,学习到了很多内容,和之前自己自学不太一样,这次又老师的讲解,很多疑惑一扫而光。在做 rustlings 实践中,前面的题都很轻松解决,到了算法的内容时,被卡住了很久,一个是算法很久没有接触了,这个在网上搜搜内容能很快熟悉起来,难得是 rust 的所有权机制以及指针这块,自己之前没怎么接触指针模块,总是卡在编译上,不断的查资料,最终磕磕绊绊总算搞完了,确实学到了很多内容,短短的三周时间过完了,觉得很充实。
rust 现在是我喜欢的语言,到现在自己感觉还是没有入门,我一定会继续学习和使用这们语言工具,去拓展更多的领域空间。
RSICV架构是目前我接触最多的指令架构,学习过RISCV处理器的设计,但没有深入了解/写过RISCV操作系统,正好借训练营补足。
基础阶段主要是rust语法的学习。
之前了解到,Rust 以性能著称,可以在语言级别支持并发和并行编程,并且有严格的内存管理,适合编写对于内存安全和性能有很高的要求的程序。
对我而言,rust中的所有权,生命周期,智能指针,闭包,并发编程是比较新的概念。
基础阶段的学习还是挺精彩的,看着《Rust 程序设计语言 简体中文版》做完了rustlings,只有后面数据结构的题目比较有难度。
但是感觉做完全部题目还算不算真正入门rust,之后还会再输入研读一下《Rust语言圣经》。期待后面训练营的OS课。
耗时两周的工作日。
看课程结合rust圣经一起学,收获还行。虽然到目前为止,几乎没有一次是写完代码就直接能够通过编译器检测的。
不过写rust的一个好处就是,通过了编译器基本上代码也就不太愁了,涉及到裸指针这种也有unsafe 知道要着重检查哪里。还记得之前csapp的一个lab用c语言写malloc函数的底层的时候,用了半天的时间写完,结果找bug找了将近两天。指针飞舞,飞到这飞到那,都不知道自己咋错的。
还有一个很nice的地方就是不能隐式类型转换,并且溢出也会报错,参加程序设计竞赛用c++写题目的时候,已经不知道有多少罚时是因为整形溢出造成的了。
不过生命周期真的感觉很头大,现在也还不能够完全把握住。
然后这次写rustlings的时候一开始看到algorithm里面要用到链表的时候就去看了圣经部分的链表,当时看了两天,只感觉,emm这也太难了吧,那各种考虑因素捋了好久才勉强想通一点,到后面还要实现图论,感觉起飞。但真正去看题目的时候,才发现,nm,这直接用的裸指针实现的,后面建图也是用的邻接表建图。。。花了半天多一点的时间搞完就收工了。
接下来就是riscv的学习,之前看csapp学过x86 就把risc-v手册用了半天跳着通读了一下。感觉risc-v寄存器多这一点很舒服,基本上不太用放入栈。然后看了下特权机制,这个之前csapp的x86没说。
允许各个元素类型不相同
1 | fn main(){ |
1 | // quiz2.rs |
"rust is fun!".to_owned()
是一个字符串字面量(string literal),.to_owned()
是一个字符串切片(&str
)的方法,用于将字符串切片转换为一个拥有所有权的 String
类型的对象。这个方法的作用是创建一个新的 String
对象,其中包含了字符串切片的内容,同时拥有了自己的内存空间,与原始的字符串切片无关。
"nice weather".into()
是 Rust 中的一个特殊的语法,它实际上是调用了 From
trait 中的实现,将一个类型转换为另一个类型。在这种情况下,"nice weather".into()
将一个字符串字面量(&str
类型)转换为 String
类型。这种转换是通过实现了 From<&str> for String
的 trait 来完成的,它会将给定的字符串切片(&str
)复制到一个新的 String
对象中,并返回这个新对象。这种转换通常被称为”类型推导”,因为编译器会根据上下文自动推导出需要转换的目标类型。
Option
和match配合
通配符
相当于二元控制流
(1) trait作为参数
1 | fn main() { |
.fold()迭代器方法
1 | fn fold<B, F>(self, init: B, f: F) -> B |
这里的参数含义是:
init
是初始值,它是要合并的类型的默认值或起始状态。f
是一个闭包函数,它接受两个参数:累积值(accumulator)和当前迭代的元素,并返回一个新的累积值。.fold()
方法会迭代迭代器的每个元素,并在每次迭代中调用闭包函数 f
,将当前累积值和当前元素作为参数传递给闭包函数,然后将闭包函数的返回值作为新的累积值。最终,.fold()
方法返回的值是所有元素被合并后得到的单个值。
通常用结构体来实现,不同之处在于实现了Deref
和Drop trait
。Deref
trait 允许智能指针结构体示例表现的像引用一样,Drop
trait允许我们自定义当智能指针离开作用域时运行的代码
box允许将值直接放到到堆上
cons list
每一项都包含两个元素:当前的值和下一项。其最后一项
Rust知道为Message分配多少空间时,他会检查每一个成员,并发现quit并不需要任何空间,Move需要两个i32空间,依此类推
Deref
实现Deref允许我们重载解引用运算符*
我们无法直接使用指针所指向的数据,需要通过解引用运算符
Drop
制定在值离开作用域时应该执行Drop
trait。一般是自动进行。
当我们想手动执行,就可以使用公用的drop
引用计数
会打印出引用计数,可以调用strong_count
函数获得
但是,虽然可以共享数据,其还是没有违反借用和所有权的定义,只是使别的变量具有a变量的可读性
结合Rc < T >和 RefCell < T > 来拥有多个可变数据所有者
1 | #[derive(Debug)] |
可以拥有一个表面上不可变的List
,不过可以使用RefCell< T>中提供内部可变性的方法来再需要时修改
提供了信道(channel)来实现。信道分为发送者和接收者
可以允许多个发送者 但是只能有一个接收者
类比java
send
函数会获取其参数的所有权并移动这个值归接收者所有。
不想所有权转移:
同步和异步:async
和多线程
异步返回的都是future
.await
异步机制的实现 更像
宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules!
的 声明(Declarative)宏,和三种 过程(Procedural)宏:
#[derive]
宏在结构体和枚举上指定通过 derive
属性添加的代码为什么已经有了函数还需要宏呢?
宏是一种代码生成器,允许在编译时对代码进行操作和生成,可以在更大的范围内改变代码结构和行为。在某些情况下,使用宏可以提高代码的灵活性和效率,但同时也会增加代码的复杂性和难以理解性。
怀着java的心来学rust,在很多情况下,rust所有权问题总是是一个大坑,rust是对底层深入的语言,相比于java非常多的工具链,rust给我最大的感觉就是如同汽车修理工那样,一个零件一个零件的卸下和装配。说实话,时间太短了,很多概念是第一次接受,并没有特别深入的了解,之后会尽力去做的。
感想:rust还是得多上手实践才行
一些笔记如下:
Rust中变量有严格的初始化要求
在 Rust 中,变量的类型是静态类型的
可以使用隐藏,重新声明一个变量来更改其类型
const声明常量,且必须包括类型
数组切片,切片是引用,&a[1..5]
元组按索引寻址,x.0
初始化vec的宏为vec![]
vec2.rs,遍历vec
使用vec.clone()可以创造一个新的对象
在任何给定时间点,你只能拥有一个可变引用(不转移所有权的情况下修改数据)
String类型的所有权
三种结构体初始化方法
从另一个结构体更新{..user}
定义枚举时可以将数据附加到枚举成员中,类似于结构体
match匹配时要加入结构体成员数据类型
可以用+号连接字符串
区分&str和String
默认为private
借用与结构体中的声明
if let语句
传播错误使用?, 出现错误直接抛出,需要与返回值兼容
Box
unwrap和expect可以解开Ok中的值或者报错
泛型,在方法处也是impl
类似于Java中的接口和C++中的抽象类
Trait可以用作参数来标识实现了Trait的类型 impl Trait和&dyn Trait,前者接受拥有所有权的实现类型,后者接受对实现该类型的不可变引用,可以用+号指定多个Trait,impl Trait1 + Trait2
确保引用有效,不出现悬垂引用
需要在函数名处先定义,然后再引用,结构体中也是如此
使用#[should_panic]来检查panic
iterators3.rs 函数式
collect::Type 显式确定要收集的类型
(1..=num).fold(1, |acc, x| acc * x)
map, filter, sum
ref with some metadata and capabilities
普通引用是借用,在大部分情况下,智能指针 拥有 它们指向的数据
实现了deref和drop的trait
Box
Rc
Cow,灵活地确定借用还是拥有
thread::spawn(move || {})
使用macro_rules!声明宏
#[macro_export]导出宏
作为一名具有 C 语言和 Java 语言基础的学生,通过参加操作系统训练营,在第一阶段通过Rustlings 实践。在这份总结报告中,我将分享我的体会。
在 Rust 中,末尾的逗号在很多语法结构中是可选的,包括类型约束列表、函数参数列表、枚举定义等。下面的两种写法都是正确的,编译器都会接受,本人更偏向于无逗号写法。
1 | struct Cacher<T> |
From
特征允许一种类型定义如何从另一种类型显式地转换,提供了一种类型到另一种类型的单向转换。与 From
特征相对应,Into
特征通常用于相同的转换,实际上当类型实现了 From
,Rust 自动为类型提供 Into
实现。两个特征让类型转换变得简单而且类型安全,无需手动处理转换逻辑。
1 | struct Number { |
在使用Rustlings的过程中深刻体会到了Rust内置测试支持的强大之处,无需额外的库就可以编写和运行测试。使用 #[test]
属性标记测试函数,然后使用 cargo test
命令运行所有测试。在 Java 中,即使是未使用的测试代码也可能因为类加载等原因对应用性能有轻微影响,但是Rust 的测试构建只在需要时添加测试代码,不影响生产代码的性能。
Rust 的设计哲学是指通过零成本抽象,让使用者不会因为选择了更高级的编程方式而付出额外的运行时性能成本。在Java中,内存管理通过垃圾回收器自动进行,垃圾回收器周期性地运行带来了性能的不可预测性。但是代价却是牺牲了编译时间。
学习 Rust 对我来说既是挑战也是收获。作为一种注重安全性和性能的系统编程语言,Rust 的学习曲线比较陡峭,特别是对于我这样有 C 和 Java 背景的开发者。在 Rust 的世界里,内存管理、所有权、生命周期和并发处理等概念都是我需要新适应的。随着对 Rust 的深入,我发现虽然编写和调试 Rust 程序可能需要更多的时间,但这种时间投资最终转化为了更稳定和安全的软件。Rust 的包管理器和构建工具 Cargo 极大地简化了项目构建、依赖管理和测试,提高了我的开发效率。总的来说,学习 Rust 是一段值得的旅程。它不仅提升了我的编程技能,也改变了我对内存管理和系统编程的看法。
经过第1个阶段的学习,我对rust语法有了基本的了解。
rust这门语言以难著称,同时又以内存安全闻名,这导致一些初学者(比如我)在开始学习时有一种“神化”编译器的倾向,认为rust能纠出这么多的错误一定在于有什么“神秘”的静态分析法能够准确的分析内存的使用,借用,修改,释放等等。一些糖化了的语法也阻碍了我的理解。经过这一个月的学习,我慢慢明白rust并没有什么魔法,其能够在编译期检查出如此多的潜在错误,完全得益于rust最大的特色——所有权系统。编译器只是无情的对一切不符合所有权规则和借用规则的代码报错,在这其中只不过“恰好”消灭了大部分bug的存在罢了。
rust中,每一个值都有一个“所有者”,与C++需要程序员手动管理内存不同,rust以“谁拥有,谁负责”的原则,当变量离开作用域时,变量所拥有的内存就会被释放,这个机制阻断了由于粗心导致忘记释放或二次释放的bug的机会。
与所有权相关的一个重要概念就是“借用”,在其他语言里,就是引用的意思,但rust会对引用做语言层面的检查,一个是在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用,还有一个是引用必须总是有效的。rust对借用的严格检查从语言层面避免了数据竞争和访问悬垂引用,而这两个是在C++中极易犯下的错误。
比如,在C++中很容易犯下这样的错误。
1 | vector<int> arr(2,0); |
由于push_back导致了迭代器失效,这段代码行为是很难预测的。但是如果在rust中写出对应的代码如下
1 | let mut arr = vec![1, 2, 3]; |
编译器会拒绝通过,报错如下。
1 | error[E0502]: cannot borrow `arr` as mutable because it is also borrowed as immutable |
可以看到,rust确实从语言层面减少了很多写出错误代码的机会,但对于上面的例子,想要说清楚为什么,(我认为)并不容易。
首先for语句只是一个语法糖,将其展开,可以等价于以下形式(由于i并不重要,这里就省略它)。
1 | let mut it = (&v).into_iter(); |
如果从借用规则来检查上面代码,也许会觉得完全正确,v.iter虽然借用了v,但函数返回后应该这个借用就失效了,只是返回了一个新的迭代器对象,后面v.push重新可变借用v,应该是完全没问题的,那么为什么编译器会告诉我们违反了借用规则?
查阅std文档中Vec的into_iter函数,发现函数原型为fn into_iter(self) -> <&'a mut Vec<T, A> as IntoIterator>::IntoIter
,而IntoIter是IterMut<'a, T>
,所以我们调用(&v).into_iter()相当于
1 | into_iter(&'a mut Vec<i32>) -> IterMut<'a, i32> |
原来代码等价于如下形式。
1 | 'a { |
由于into_iter的函数签名要求参数即对arr可变借用的生命周期与返回值一致,而返回值由于和it绑定,生命周期就是’a,那么相当于对arr的可变借用的生命期也是’a,而后续又有了一个生命期为’c的arr可变借用,且’c与’a有重叠,这样就违反了在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用的规则,所以rust编译器会报错(也有可能我的理解是错误的,请大家多指正)
有了这样的分析思路,接下来下面的两段代码也可以很方便的分析出报错原因。
1 | let mut arr = vec![1, 2, 3]; |
显然&mut arr[0]和&mut arr[1]的生命期有重叠,而arr[…]只不过是fn index_mut(&'a mut self, index: I) -> &'a mut <Vec<T, A> as Index<I>>::Output
的语法糖,由刚才的分析规则,很容易能够明白为何编译器报错。
下面的示例留给读者练习。
1 | let mut b = 3; |
开始学习时这个问题困扰着我,因为有一次发现一条语句什么都不做:
1 | x; |
x的值也会被移动,从此以后甚至不敢在表达式中写变量名,生怕一下子就被移动走了。但其实这个困惑还是因为reference读的少了导致的。reference中对表达式进行了分类,分为值表达式和位置表达式,除了极少量位置表达式,其它都是值表达式,而若一个位置表达式在值表达式上下文中被求值时,才会发生复制或移动(根据是否实现copy trait)。
另外,原来比较两个值大小(比较运算符表达式)会对两个操作数在位置表达式上下文求值,这样就不用比较两个值大小还要先clone了(我之前竟然是这么干的……)
印象比较深的主要就是以上两个,有可能理解还是有偏差,请大家多指正。
我查阅的一些资料如下:
[2, 3] rustnomicon: lifetimes, rustnomicon: limits of lifetimes
[4] rust-std