Week 1 Exercises Hello world
Week 1 Exercises: Hello world
Rust编程练习:入门指南
目的
本周的练习旨在帮助你熟悉 Rust 代码的编译、运行以及基本语法。学习编程语言(无论是人类语言还是计算机语言)的最佳方式是沉浸式体验。我们希望通过这次练习为下周打好基础,当时我们将讨论你在其他语言中可能没接触过的新概念。
截止日期:4月14日,星期二,太平洋时间上午10:30
预计完成时间:1-3小时。如果遇到困难,可以联系助教或同学。
第 1 部分:环境设置与“Hello World”程序
Rust 开发环境设置
我们已经在 Myth 服务器上为你配置了 Rust 工具链。如果想在本地机器上(如因为网络或地理原因)运行代码,你需要安装 Rust 工具链。
- 在 Myth 上开发:Myth 已安装 Rust 工具链,可以直接使用。
- 在本地安装 Rust:请访问 Rust 官网下载并安装 Rust 工具链。
编辑器插件:建议安装适合你使用的编辑器的 Rust 插件以提升编程体验。例如,如果使用 Vim,可以安装 Rust 插件。其他编辑器也可以搜索推荐的插件。
获取 Starter Code
我们将使用 GitHub 来管理作业提交。GitHub 是基于版本控制软件 Git 的协作平台,可以帮助你管理代码的不同版本。你可以使用以下命令将 Starter Code 克隆到本地计算机:
1 | git clone https://github.com/reberhardt7/cs110l-spr-2020-starter-code.git |
然后,进入 week1/part-1-hello-world
目录。这个目录包含一个 Rust 包。可以在 src/
目录中找到源代码,查看 src/main.rs
。
编译与运行代码
编译代码:在目录下运行以下命令来编译代码:
1 | cargo build |
Cargo 是 Rust 的构建工具,它不仅类似于 Make,还能处理项目依赖的下载和配置(类似于 JavaScript 中的 npm 或 Python 中的 setup.py)。后续学习中,Cargo 的测试、文档生成和基准测试功能会派上用场。
运行代码:编译后,执行以下命令运行生成的可执行文件:
1 | ./target/debug/hello-world |
输出结果应为:
1 | Hello, world! |
直接运行:为方便起见,Cargo 提供了
cargo run
命令,可以同时编译并运行程序。尝试修改
src/main.rs
文件中的打印内容,然后直接运行以下命令:
1 | cargo run |
Cargo 会检测到文件的更改,重新编译代码并运行:
1 | You rock! |
恭喜!
你已经成功运行了第一个 Rust 程序!
Rust 入门练习:语法和基础练习
这是对 Rust 语法和一些基础练习的详细中文笔记,基于 Will Crichton 的 CS 242 Rust 手册的内容。
数值类型
Rust 提供了一系列数值类型:
- 有符号整数:
i8
、i16
、i32
和i64
,这些可以存储正数和负数。 - 无符号整数:
u8
、u16
、u32
和u64
,只能存储非负数。
位数(8、16 等)表示值的存储位数。这种类型系统是为了避免 C
语言中因为类型宽度不统一而产生的问题。例如,i32
相当于 C
语言中的 int
,但在较旧的 C 代码中,int
可能只占用 2 个字节。
变量声明
Rust 中通过 let
关键字声明变量,并可以指定类型:
1 | let n: i32 = 1; // 声明一个 32 位有符号整数 |
Rust 具有 类型推导 功能。如果编译器可以推断类型,则可以省略显式类型声明:
1 | let n = 1; // 编译器自动推断为 i32 |
可变性
在 Rust
中,变量默认是不可变的,这有助于减少错误。如果需要修改变量,必须明确地用
mut
标记为可变:
1 | let mut n = 0; |
字符串类型
Rust 有两种字符串类型:&str
和
String
。
&str
:不可变字符串切片,指向程序中的字符串常量。通常用于引用一个现有的字符串,类似于指针。1
let s: &str = "Hello world"; // ": &str" 是可选的
String
:堆分配的字符串,可以改变长度。1
2let mut s: String = String::from("Hello ");
s.push_str("world!"); // 将字符串连接在一起
向量(Vectors)和数组(Arrays)
向量 (
Vec<T>
) 是动态大小的集合,适合存储同类型的多个值:1
2
3let mut v: Vec<i32> = Vec::new();
v.push(2);
v.push(3);数组
[T; N]
是固定大小的集合,在定义时就设定长度,元素类型相同,通常会在运行时检查数组访问的越界问题。1
2
3let mut arr: [i32; 4] = [0, 2, 4, 8];
arr[0] = -2;
println!("{}", arr[0] + arr[1]);
迭代
Rust 提供类似 Python 的迭代方式来遍历集合:
1 | for i in v.iter() { // v 是一个向量 |
循环
While 循环:
1
2
3while i < 20 {
i += 1;
}无限循环:使用
loop
可以创建一个循环,并用break
退出。1
2
3
4
5let mut i = 0;
loop {
i += 1;
if i == 10 { break; }
}
函数
函数在 Rust 中通过 fn
声明。
带返回类型的函数:
1
2
3fn sum(a: i32, b: i32) -> i32 {
a + b
}无返回值的函数:
1
2
3fn main() {
// 执行代码...
}
Rust 中一切都是表达式,因此函数的最后一个表达式没有分号,它的值就是返回值。如果加上分号,函数将不返回值,导致编译错误。
练习:实现函数
现在我们进入练习环节,完成以下几个函数:
add_n:该函数接受一个整数向量
v
和一个整数n
,返回一个新向量,其中每个元素为v
中的元素加上n
。1
2
3fn add_n(v: Vec<i32>, n: i32) -> Vec<i32> {
v.into_iter().map(|x| x + n).collect()
}函数签名
1
fn add_n(v: Vec<i32>, n: i32) -> Vec<i32>
fn
:声明一个函数。add_n
:函数名称,表示该函数的作用是“添加 n”。v: Vec<i32>
:第一个参数v
是一个整数向量 (Vec<i32>
),它存储了一组i32
类型的有符号整数。n: i32
:第二个参数n
是一个i32
整数,它将被加到v
中的每个元素上。-> Vec<i32>
:返回类型是Vec<i32>
,即一个新的整数向量。
函数体
1
v.into_iter().map(|x| x + n).collect()
这个表达式由三个步骤组成:
v.into_iter()
:into_iter
方法将向量v
转换为一个迭代器(iterator)。此迭代器会按顺序“消费”v
中的元素,将每个元素逐个取出,不会保留原来的向量。- 这一步的结果是一个迭代器,它允许我们在
v
的元素上进行遍历和进一步处理。
.map(|x| x + n)
:map
是一个高阶方法,它接受一个闭包(closure)并将其应用于迭代器的每个元素。|x| x + n
是一个闭包,它将当前元素x
加上n
,然后返回结果。这里的|x|
表示闭包参数,x + n
表示闭包的逻辑。- 结果是一个新的迭代器,其中的每个元素都是原始向量
v
中对应元素加上n
的结果。
.collect()
:collect
方法将迭代器中的元素“收集”到一个集合中,这里将结果收集为一个新的Vec<i32>
。collect
会自动推断返回的集合类型为Vec<i32>
,因为函数签名中定义了返回类型为Vec<i32>
。
add_n_inplace:与
add_n
功能相同,但直接修改v
,不返回值。1
2
3
4
5fn add_n_inplace(v: &mut Vec<i32>, n: i32) {
for x in v.iter_mut() {
*x += n;
}
}dedup:去除向量中的重复元素,仅保留第一次出现的元素。可以使用
HashSet
辅助去重。1
2
3
4
5
6use std::collections::HashSet;
fn dedup(v: &mut Vec<i32>) {
let mut seen = HashSet::new();
v.retain(|x| seen.insert(*x));
}
导入 HashSet
1 | use std::collections::HashSet; |
use std::collections::HashSet;
:导入标准库中的HashSet
。HashSet
是一种集合类型,它只存储唯一的值,适合用于去重操作。
函数签名
1 | fn dedup(v: &mut Vec<i32>) |
fn
:声明一个函数。dedup
:函数的名称,表明该函数用于去重。v: &mut Vec<i32>
:函数接受一个可变的引用&mut Vec<i32>
,这意味着该函数可以修改传入的向量v
。可变引用允许函数直接修改传入的向量,而不需要返回一个新向量。
创建 HashSet
1 | let mut seen = HashSet::new(); |
let mut seen = HashSet::new();
:创建一个新的可变的HashSet
,命名为seen
。这个集合将用于跟踪已经遇到的元素。HashSet
的insert
方法会在添加元素时检查该元素是否已经存在,如果存在则返回false
,否则返回true
。这使得我们能够轻松地判断一个元素是否已经出现过。
使用 retain
进行去重
1 | v.retain(|x| seen.insert(*x)); |
v.retain(...)
:retain
是一个用于过滤集合的方法。它会迭代向量v
中的每个元素,并保留返回值为true
的元素。即,只有当闭包返回true
时,元素才会保留在v
中。|x| seen.insert(*x)
:这是一个闭包,接收当前元素x
,并尝试将其插入到seen
中。*x
:因为x
是一个引用(&i32
),使用解引用操作符*
来获取其值。seen.insert(*x)
:如果*x
已经存在于seen
中,则insert
返回false
,此时该元素会被移除;如果*x
不存在,insert
返回true
,该元素会被保留并且添加到seen
中。
测试
在 src/main.rs
中提供了单元测试。可以通过运行
cargo test
验证你的实现是否正确。
Hangman
游戏功能
- 欢迎信息:
- 游戏开始时打印欢迎信息和初始状态。
- 显示当前状态:
- 显示当前猜测的单词状态,使用
-
表示未猜测的字母。
- 显示当前猜测的单词状态,使用
- 显示已猜测字母:
- 提示用户已经猜测过的字母。
- 剩余猜测次数:
- 显示用户剩余的猜测次数(初始为 5 次)。
- 用户输入:
- 提示用户输入一个字母进行猜测。
- 更新游戏状态:
- 根据用户输入的字母更新当前单词状态和已猜测字母。
- 如果猜测的字母在单词中,更新显示;如果不在,减少剩余猜测次数并显示错误信息。
- 结束条件:
- 当用户猜出整个单词或用完所有猜测次数时,结束游戏并显示结果。
游戏实现的代码示例
1 | use std::io::{self, Write}; // 导入必要的库 |
代码说明
- 导入库:使用
std::io
来处理输入输出,使用std::collections::HashSet
来存储已猜测的字母。 - 游戏主循环:使用
while
循环控制游戏流程,直到用户用完所有猜测次数。 - 用户输入:提示用户输入一个字母,并通过
io::stdin()
读取。 - 更新游戏状态:根据用户的输入更新显示的单词状态和已猜测的字母。
- 结束条件:检查用户是否猜出了秘密单词或者用完了所有猜测次数。
Weekly survey
提交作业
使用 Git 进行版本控制:
- 学生在工作过程中,可以通过提交代码来保存工作快照。这有助于在遇到问题时,可以轻松恢复到之前的状态。
Git 配置:
如果你从未使用过 Git,可能需要进行以下配置:
1
2git config --global user.name "Firstname Lastname"
git config --global user.email "yourSunetid@stanford.edu"完成配置后,通常不需要再次执行。
提交代码:
使用以下命令提交你的工作:
1
git commit -am "Type some title here to identify this snapshot!"
-a
选项表示自动将已追踪的文件标记为已修改,-m
选项后跟的是提交消息,用于描述此次提交的内容。
推送到 GitHub:
为了提交作业,将你的更改推送到 GitHub,运行以下命令:
1
git push
这将上传你的提交(快照)到 GitHub,方便教师访问。
验证提交:
你可以访问以下网址验证你的代码是否已成功提交:
1
https://github.com/cs110l/week1-yourSunetid
在那里浏览你的代码,确保一切正常。