0%

引言

​ 作为一名具有 C 语言和 Java 语言基础的学生,通过参加操作系统训练营,在第一阶段通过Rustlings 实践。在这份总结报告中,我将分享我的体会。

与其他语言对比

Rust 与 C 语言的对比

内存安全

  • C 语言:虽然极其灵活,但 C 语言经常因其内存管理自由而导致缓冲区溢出、野指针等问题。
  • Rust:通过所有权(ownership)、借用(borrowing)和生命周期(lifetimes)的概念,保证了内存安全而无需垃圾回收机制。这些特性在编译时进行检查,几乎消除了运行时错误。

并发编程

  • C 语言:提供了原始的并发机制,依赖于操作系统的线程和锁。
  • Rust:提供了更现代、更安全的并发编程模型。通过所有权和类型系统,Rust 能够在编译时阻止数据竞争等并发问题。

Rust 与 Java 的对比

性能

  • Java:作为一种高级语言,拥有自动内存管理(垃圾回收)。虽然方便但通常牺牲了一些性能。
  • Rust:性能接近于 C/C++,因为它没有运行时和垃圾收集,是系统编程的理想选择。

内存管理

  • Java:自动内存管理减少了开发者的负担,但增加了运行时开销。
  • Rust:通过编译时的内存安全保证减少了运行时开销,但要求开发者理解更多的内存管理概念。

语言特色

末尾逗号写法

​ 在 Rust 中,末尾的逗号在很多语法结构中是可选的,包括类型约束列表、函数参数列表、枚举定义等。下面的两种写法都是正确的,编译器都会接受,本人更偏向于无逗号写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Cacher<T>
where
T: Fn(u32) -> u32
{
query: T,
value: Option<u32>,
}
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}

类型转换

From 特征允许一种类型定义如何从另一种类型显式地转换,提供了一种类型到另一种类型的单向转换。与 From 特征相对应,Into 特征通常用于相同的转换,实际上当类型实现了 From,Rust 自动为类型提供 Into 实现。两个特征让类型转换变得简单而且类型安全,无需手动处理转换逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Number {
value: i32,
}
// 为 `Number` 实现 `From<i32>`
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}
let num = Number::from(30);
println!("The number is: {}", num.value);
let int = 5;
let num: Number = int.into();

自动化测试

​ 在使用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
2
3
4
5
6
7
vector<int> arr(2,0);
int i=0;
for(auto it = arr.begin(); it != arr.end(); it++) {
if(i == 1) arr.push_back(2);
cout<<*it<<' ';
i++;
}

由于push_back导致了迭代器失效,这段代码行为是很难预测的。但是如果在rust中写出对应的代码如下

1
2
3
4
5
6
7
8
9
let mut arr = vec![1, 2, 3];
let mut i = 0;
for elem in &arr {
println!("{}",elem);
if i == 1 {
arr.push(6);
}
i += 1;
}

编译器会拒绝通过,报错如下。

1
2
3
4
5
6
7
8
9
10
11
error[E0502]: cannot borrow `arr` as mutable because it is also borrowed as immutable
--> src\main.rs:38:13
|
35 | for elem in &arr {
| ----
| |
| immutable borrow occurs here
| immutable borrow later used here
...
38 | arr.push(6);
| ^^^^^^^^^^^ mutable borrow occurs here

可以看到,rust确实从语言层面减少了很多写出错误代码的机会,但对于上面的例子,想要说清楚为什么,(我认为)并不容易。

首先for语句只是一个语法糖,将其展开,可以等价于以下形式(由于i并不重要,这里就省略它)。

1
2
3
4
5
6
7
8
9
10
11
let mut it = (&v).into_iter();
loop {
match it.next() {
Some(x) => {
v.push(6);
},
None => {
break;
}
}
}

如果从借用规则来检查上面代码,也许会觉得完全正确,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'a {
let mut it: IterMut<'a, i32> = into_iter(&'a mut arr);
'b {
loop {
match it.next() {
'c {
Some(x) =>
{Vec<i32>::push(&'c mut arr, &6);},
}
'd {
None => {break;}
}
}
}
}
}

由于into_iter的函数签名要求参数即对arr可变借用的生命周期与返回值一致,而返回值由于和it绑定,生命周期就是’a,那么相当于对arr的可变借用的生命期也是’a,而后续又有了一个生命期为’c的arr可变借用,且’c与’a有重叠,这样就违反了在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用的规则,所以rust编译器会报错(也有可能我的理解是错误的,请大家多指正)

有了这样的分析思路,接下来下面的两段代码也可以很方便的分析出报错原因。

1
2
let mut arr = vec![1, 2, 3];
swap(&mut arr[0], &mut arr[1]);

显然&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
2
3
4
5
6
7
8
let mut b = 3;
let c;
{
let x = &mut b;
c = &*x;
}
let d = &b;
println!("{}",c);

什么时候会发生移动

开始学习时这个问题困扰着我,因为有一次发现一条语句什么都不做:

1
x;

x的值也会被移动,从此以后甚至不敢在表达式中写变量名,生怕一下子就被移动走了。但其实这个困惑还是因为reference读的少了导致的。reference中对表达式进行了分类,分为值表达式和位置表达式,除了极少量位置表达式,其它都是值表达式,而若一个位置表达式在值表达式上下文中被求值时,才会发生复制或移动(根据是否实现copy trait)。

具体的请看reference: expression

另外,原来比较两个值大小(比较运算符表达式)会对两个操作数在位置表达式上下文求值,这样就不用比较两个值大小还要先clone了(我之前竟然是这么干的……)

印象比较深的主要就是以上两个,有可能理解还是有偏差,请大家多指正。

我查阅的一些资料如下:

[1] reference: expression

[2, 3] rustnomicon: lifetimes, rustnomicon: limits of lifetimes

[4] rust-std

[5] course.rs: 深入生命周期

第一次尝试做个Ruster日拱一卒

书籍

###建议结合照英文原版阅读
The Rust progamming language 中文版》 https://kaisery.github.io/trpl-zh-cn/title-page.html
Rust圣经》 https://course.rs/about-book.html
通过例子学Rust》 https://rustwiki.org/zh-CN/rust-by-example/
Rust 秘典(死灵书)》 https://nomicon.purewhite.io/
Rust标准库》 https://rustwiki.org/zh-CN/std/
Cargo 手册》 https://rustwiki.org/zh-CN/cargo/

The Rust progamming language 中文版》 https://doc.rust-lang.org/stable/book/ch03-02-data-types.html
通过例子学Rust》 https://doc.rust-lang.org/rust-by-example/hello.html
Rust 秘典(死灵书)》 https://doc.rust-lang.org/nomicon/safe-unsafe-meaning.html
Rust标准库》 https://doc.rust-lang.org/std/
Cargo 手册》 https://doc.rust-lang.org/cargo/
##第一阶段总结
###万事开头难,然后中间难,最后拨开迷雾。
Rust语法难度门槛高,入门难,我现在就是在迷雾中,万幸摸到了一点,只希望接下来能拨开迷雾见到山,我虽然担心迷了路,坚持不下去体会不到拨开迷雾的痛快,但我也相信日拱一卒。。。

前言

在这个开源操作系统训练营的第一阶段,我经历了一段充实而挑战性的学习过程。对于 Rust 这门新兴的编程语言,我抱着好奇和兴奋的心态开始了探索。尽管之前有一些编程经验,但 Rust 的独特特性和高度安全性的承诺,让我怀着期待和一些担忧开始了这段学习之旅。

过程

过程还是比较曲折的,其实在配环境的时候就出现了一些问题,比如用用ssh去clone的时候都出问题了。后来也是看群友的把ssh端口改成使用443以后才好。

rustlings更是酸爽,看了圣经和rust语言设计那本书还是有很多理解不到位的地方,导致做题的时候经常会出现需要调半天的bug。而且rust里面有很多概念对我来说比较新,比如说所有权和一些特性之类的,使用上又是重重限制。导致我用c/c++可能一下就能写完的题,换成rust得调bug半个小时。多亏生成式ai的帮助,否则可能一直要卡在这个阶段了。

总结

在第一阶段的学习中,我对 Rust 的理解和应用有了显著的提升。通过不断地实践和探索,我逐渐适应了 Rust 的编程风格。不过,这110道题做下来其实只能算有个概念,有些东西肯定已经忘了。打算再回去二刷rust圣经,看完以后就等遇到不会的再查吧。

第三阶段总结报告

Rustlings部分

由于之前学习过rust,对于rustlings很快就完成了。后面的几道算法题也是较为基础的数据结构,类似图,树,链表,还有排序算法等。

收获

通过算法实践加深了我对rust语言的理解。

期望

希望二阶段的我可以更加努力。顺利完成第二阶段的训练。

感谢

感谢老师和助教们为大家开发一套如此优秀的课程。

关于

为什么是rust

因为不想学C++,又想要学习一个语言表达力强的,没有GC的语言。Rust是我唯一的选择了。

为什么是操作系统

计算机的几大专业课中,操作系统的学习是最不够深入了。所以希望从实践出发,补全自己计算机专业知识。

感悟

写玩rustlings花费了大概一周的时间,每做两天都会休息一天。整体给人的感受是,难度不大。很多题目的设计不是希望难倒你,而想通过题目让你了解rust的语言特性。rustling的一个大前提就是你已经对其他编程语言有较好的掌握。所以做完全部题目只是rust学习的第一遍入门,还需要入门很多次,才能完全理解这些特性底层逻辑。

我参考的教材是rust圣经,跟题目的顺序有很大的出入,踩了不少坑,要一直翻阅官方文档才能做完题目。

目前遇到比较困难的地方是,裸指针的引用,至今没有完全理解。

还有一些很细节的地方,就是关于文件结构中lib和binary的区别,以下摘自我的笔记

binary 和 lib 只是rust中对源代码文件的类型标注,不是通常意义的二进制可执行文件,不过很接近.
例如 src/main.rs 表示,这个文件将会被编译为可执行文件,它是整个项目程序的入口。编译完成后,它将作为可执行文件被用户和其他程序直接调用。
src/lib.rs 表示, 这个文件是项目的库代码,由 main.rs 调用间接使用。这些文件不会被被编译为独立的可执行文件。
lib.rs 可以声明一些模块的存在,然后由其他模块文件对应实现。

还有很多其他小的理解的细节点,需要继续深入理解的。

Rust学习笔记

1、 Rust允许在同一个代码块中声明一个与之前已声明变量同名的新变量,新变量会遮蔽之前的变量,即无法再去访问前一个同名的变量,这样就实现了变量遮蔽。
常量不能遮蔽,不能重复定义。

1
2
3
4
5
6
7
8
fn main() {
let x = 3;
let x = x + 2;
let x = x * 2;
println!("x: {}", x);
let x = "Hello, Rust!";
println!("x: {}", x);
}

2、 复制

  • 对于元组类型,如果每个元素的类型都实现了Copy trait,那么该元组类型数据支持浅复制。
  • 结构体和枚举有些特殊,即使所有字段的类型都实现了Copy trait,也不支持浅复制。

3、 高效处理Result<T, E>

  • 如果Result的值是Ok,unwrap方法会返回Ok中的值。如果Result的值是Err,unwrap方法会自动做Panic处理并输出默认的错误消息。
  • expect方法不仅具备unwrap方法的功能,还允许自定义错误信息,这样更易于追踪导致程序错误的原因。
    1
    2
    3
    4
    5
    use std::fs::File;

    fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
    }
  • 如果Result的值是Ok,unwrap_or_else会返回Ok中的值。如果Result的值是Err,unwrap_or_else可以执行一个闭包。
  • “?”操作符可以用于返回值类型为Result的函数中,如果出现错误,“?”操作符会提前返回整个函数并将Err值传播给调用者。
    1
    2
    3
    4
    5
    fn read_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
    }

4、 迭代器类型
| 迭代器 | 所有权借用 | 创建方法 | 迭代器元素类型 |
| :—-: | :—-: | :—-: | —— |
| IntoIter | 转移所有权 | into_iter | T |
| Iter | 不可变借用 | iter | &T |
| IterMut | 可变借用 | iter_mut | &mut T |

总结

曾在去年看过一遍 Rust 的语法,当时 Rust 这种新颖的借用-生命周期设计给我了很深的印象。

后面一直没机会使用后面基本全忘了,这次又跟着学了一遍,又弥补了一些细节上的问题,感觉收获很多。

期待这次训练营的实操,Rust 会给我一个怎样的编程体验。

第一阶段学习感受

完成了rCore训练营的第一部分——rust基础编程,有以下收获

  • 感受了rust在开发程序时的安全性
  • 增进了对函数式编程的了解
  • 获得了一些与rust编译器斗争的经验:)
  • 对链表有了更加深刻的理解
  • 获得了一些关于引用和借用的知识

2024开源操作系统训练营第一阶段总结报告

寒假期间即了解了操作系统项目rCore,并跟着做了rCore的一些练习,相关项目可以参考KamijoToma/rCoreOS

在这期间通过rCore学习了Rust这门语言。Rust语言的威名我早已听闻,大家都说“学习Rust可以改变个人的一些编程习惯”,在我真正学习到Rust之后,对这句话有了更形象和深刻的理解。

下面我简单列出我的感想:

  1. 对编程语言的设计有了更深的理解

在学习其他语言和使用的过程中,我经常遇到如何在一个函数中返回异常的问题。高级语言,例如Java、C++,给出的解决方案是通过一个特殊的路径——异常,来返回这些错误。
然而,这些异于正常控制流的异常返回会令编译器的工作更加繁重,打乱执行流也意味着会造成额外的性能损失和更加复杂的汇编设计,也是基于此原因Google不建议在C++中使用异常。

另一门语言,Java,也使用异常。在学习Java异常的时候,我了解了Google的Optional库。它取代或部分取代了Java中异常的功能,通过将返回值包裹在一个Optional类中,来指示函数是否正确的处理了这个值。
到这里可能就有些熟悉了,这正是Rust中Option和Result枚举的设计思路。通过将异常和返回值包裹在一个枚举中作为函数的真正返回值,我们将异常的控制流非异常化,减少了编译器设计难度和性能损失。
同理,Rust中对各种trait进行组合也是我认为很优秀的设计思路。这些设计手法让我对编程语言设计的问题理解的更加深刻。

  1. 改进了编码习惯

提到Rust的特点,绕不开的关键词就是“所有权”。作为一门系统级语言,Rust当然不使用gc来自动管理内存,它也不完全需要程序员手动管理内存。事实上,它强迫程序员按照一定的规则来申请和释放内存。
在这里我不再提所有权的细节,而是想说,通过这种强迫的方式,我改掉了很多之前随意申请释放内存的情况。即使编写C程序,我也谨依Rust曾经教过我的管理方法去管理内存。

Rust是一门简洁有力但又有深度的编程语言,难怪有很多人为Rust所狂热。道路是曲折的,前途是光明的,我希望能同255DoesNotExist和TrisuyaN、reecho等同学一起,完整的学完这个夏令营~