0%

耗时两周的工作日。

看课程结合rust圣经一起学,收获还行。虽然到目前为止,几乎没有一次是写完代码就直接能够通过编译器检测的。

不过写rust的一个好处就是,通过了编译器基本上代码也就不太愁了,涉及到裸指针这种也有unsafe 知道要着重检查哪里。还记得之前csapp的一个lab用c语言写malloc函数的底层的时候,用了半天的时间写完,结果找bug找了将近两天。指针飞舞,飞到这飞到那,都不知道自己咋错的。

还有一个很nice的地方就是不能隐式类型转换,并且溢出也会报错,参加程序设计竞赛用c++写题目的时候,已经不知道有多少罚时是因为整形溢出造成的了。

不过生命周期真的感觉很头大,现在也还不能够完全把握住。

然后这次写rustlings的时候一开始看到algorithm里面要用到链表的时候就去看了圣经部分的链表,当时看了两天,只感觉,emm这也太难了吧,那各种考虑因素捋了好久才勉强想通一点,到后面还要实现图论,感觉起飞。但真正去看题目的时候,才发现,nm,这直接用的裸指针实现的,后面建图也是用的邻接表建图。。。花了半天多一点的时间搞完就收工了。

接下来就是riscv的学习,之前看csapp学过x86 就把risc-v手册用了半天跳着通读了一下。感觉risc-v寄存器多这一点很舒服,基本上不太用放入栈。然后看了下特权机制,这个之前csapp的x86没说。

基本数据类型-原生类型

image-20240410195209385

  • 元组(tuple)

​ 允许各个元素类型不相同

1
2
3
fn main(){
let tup: (i32,f64,u8) = (500,6.4,1);
}
  • 字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// quiz2.rs
//
// This is a quiz for the following sections:
// - Strings
// - Vecs
// - Move semantics
// - Modules
// - Enums
//
// Let's build a little machine in the form of a function. As input, we're going
// to give a list of strings and commands. These commands determine what action
// is going to be applied to the string. It can either be:
// - Uppercase the string
// - Trim the string
// - Append "bar" to the string a specified amount of times
// The exact form of this will be:
// - The input is going to be a Vector of a 2-length tuple,
// the first element is the string, the second one is the command.
// - The output element is going to be a Vector of strings.
//
// No hints this time!

// I AM NOT DONE

pub enum Command {
Uppercase,
Trim,
Append(usize),
}

mod my_module {
use super::Command;

// TODO: Complete the function signature!
pub fn transformer(input: Vec<(String,Command)>) -> Vec<String> {
// TODO: Complete the output declaration!
let mut output: Vec<String> = vec![];
for (string, command) in input.iter() {
// TODO: Complete the function body. You can do it!
match command{
Command::Uppercase => output.push(string.to_uppercase()),
Command::Trim => output.push(string.trim().to_string()),
Command::Append(n) => {
let mut append_stirng = string.clone();
for _ in 0..*n {
append_stirng += "bar";
}
output.push(append_stirng);
}
}

}
output
}
}

#[cfg(test)]
mod tests {
// TODO: What do we need to import to have `transformer` in scope?
use super::my_module::transformer;
use super::Command;

#[test]
fn it_works() {
let output = transformer(vec![
("hello".into(), Command::Uppercase),
(" all roads lead to rome! ".into(), Command::Trim),
("foo".into(), Command::Append(1)),
("bar".into(), Command::Append(5)),
]);
assert_eq!(output[0], "HELLO");
assert_eq!(output[1], "all roads lead to rome!");
assert_eq!(output[2], "foobar");
assert_eq!(output[3], "barbarbarbarbarbar");
}
}
  • String

image-20240414142656028

  • "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配合

    image-20240417114555485

  • 通配符

image-20240417114753525

  • if let 简介控制流

相当于二元控制流

  • 泛型

image-20240417120202658

  • Trait(interface)

image-20240417120533444

(1) trait作为参数

image-20240417230445201

小计:

.fold()

1
2
3
4
5
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("The sum is: {}", sum); // 输出:The sum is: 15
}

.fold()迭代器方法

1
2
3
fn fold<B, F>(self, init: B, f: F) -> B
where
F: FnMut(B, Self::Item) -> B,

这里的参数含义是:

  • init 是初始值,它是要合并的类型的默认值或起始状态。
  • f 是一个闭包函数,它接受两个参数:累积值(accumulator)和当前迭代的元素,并返回一个新的累积值。

.fold() 方法会迭代迭代器的每个元素,并在每次迭代中调用闭包函数 f,将当前累积值和当前元素作为参数传递给闭包函数,然后将闭包函数的返回值作为新的累积值。最终,.fold() 方法返回的值是所有元素被合并后得到的单个值。

智能指针

通常用结构体来实现,不同之处在于实现了DerefDrop traitDereftrait 允许智能指针结构体示例表现的像引用一样,Drop trait允许我们自定义当智能指针离开作用域时运行的代码

  • Box

box允许将值直接放到到堆上

递归类型

cons list

每一项都包含两个元素:当前的值和下一项。其最后一项

  • 计算非递归类型的大小

image-20240421090726614

Rust知道为Message分配多少空间时,他会检查每一个成员,并发现quit并不需要任何空间,Move需要两个i32空间,依此类推

Deref

实现Deref允许我们重载解引用运算符* 我们无法直接使用指针所指向的数据,需要通过解引用运算符

Drop

制定在值离开作用域时应该执行Droptrait。一般是自动进行。

当我们想手动执行,就可以使用公用的drop

image-20240421092345178

Rc< T >智能指针

引用计数

  • Rc< T >来共享数据

image-20240421092803615

  • 可以用来计数

image-20240421092829590

会打印出引用计数,可以调用strong_count函数获得

但是,虽然可以共享数据,其还是没有违反借用和所有权的定义,只是使别的变量具有a变量的可读性

RefCell < T >和内部可变性

  • RefCell 大量使用unsafe代码来模糊rust规则,要手动检查是否符合规则

结合Rc < T >和 RefCell < T > 来拥有多个可变数据所有者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

可以拥有一个表面上不可变的List,不过可以使用RefCell< T>中提供内部可变性的方法来再需要时修改

多线程

spawn创建多线程

image-20240421094452825

线程间传送数据

提供了信道(channel)来实现。信道分为发送者和接收者

image-20240421103518959

可以允许多个发送者 但是只能有一个接收者
类比java

信道与所有权转移

send函数会获取其参数的所有权并移动这个值归接收者所有。

image-20240421104012845

不想所有权转移:

互斥器Mutex< T >

image-20240421104351246

同步和异步async和多线程

异步返回的都是future

.await异步机制的实现 更像

Macro)指的是 Rust 中一系列的功能:使用 macro_rules!声明Declarative)宏,和三种 过程Procedural)宏:

  • 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码
  • 类属性(Attribute-like)宏定义可用于任意项的自定义属性
  • 类函数宏看起来像函数不过作用于作为参数传递的 token

为什么已经有了函数还需要宏呢?

宏是一种代码生成器,允许在编译时对代码进行操作和生成,可以在更大的范围内改变代码结构和行为。在某些情况下,使用宏可以提高代码的灵活性和效率,但同时也会增加代码的复杂性和难以理解性。

总结

怀着java的心来学rust,在很多情况下,rust所有权问题总是是一个大坑,rust是对底层深入的语言,相比于java非常多的工具链,rust给我最大的感觉就是如同汽车修理工那样,一个零件一个零件的卸下和装配。说实话,时间太短了,很多概念是第一次接受,并没有特别深入的了解,之后会尽力去做的。

感想:rust还是得多上手实践才行
一些笔记如下:

Variables

  • Rust中变量有严格的初始化要求

  • 在 Rust 中,变量的类型是静态类型的

  • 可以使用隐藏,重新声明一个变量来更改其类型

  • const声明常量,且必须包括类型

    Primitive Types

  • 数组切片,切片是引用,&a[1..5]

  • 元组按索引寻址,x.0

    Vectors

  • 初始化vec的宏为vec![]

  • vec2.rs,遍历vec

    Move Semantics

  • 使用vec.clone()可以创造一个新的对象

  • 在任何给定时间点,你只能拥有一个可变引用(不转移所有权的情况下修改数据)

  • String类型的所有权

    Structs

  • 三种结构体初始化方法

  • 从另一个结构体更新{..user}

    Enums

  • 定义枚举时可以将数据附加到枚举成员中,类似于结构体

  • match匹配时要加入结构体成员数据类型

    Strings

  • 可以用+号连接字符串

  • 区分&str和String

    Modules

  • 默认为private

  • 借用与结构体中的声明

    Options

  • if let语句

    Errors

  • 传播错误使用?, 出现错误直接抛出,需要与返回值兼容

  • Box智能指针与Trait

  • unwrap和expect可以解开Ok中的值或者报错

    Generics

  • 泛型,在方法处也是impl

    Traits

  • 类似于Java中的接口和C++中的抽象类

  • Trait可以用作参数来标识实现了Trait的类型 impl Trait和&dyn Trait,前者接受拥有所有权的实现类型,后者接受对实现该类型的不可变引用,可以用+号指定多个Trait,impl Trait1 + Trait2

    Lifetime

  • 确保引用有效,不出现悬垂引用

  • 需要在函数名处先定义,然后再引用,结构体中也是如此

    Tests

  • 使用#[should_panic]来检查panic

    Iterators

  • iterators3.rs 函数式

  • collect::Type 显式确定要收集的类型

  • (1..=num).fold(1, |acc, x| acc * x)

  • map, filter, sum

    Smart Pointers

  • ref with some metadata and capabilities

  • 普通引用是借用,在大部分情况下,智能指针 拥有 它们指向的数据

  • 实现了deref和drop的trait

  • Box,解决循环类型定义,因为编译时需要确定数据空间占用

  • Rc,Solar System: Planet and the Sun

  • Cow,灵活地确定借用还是拥有

    Threads

  • thread::spawn(move || {})

    Macros

  • 使用macro_rules!声明宏

  • #[macro_export]导出宏

引言

​ 作为一名具有 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 |