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
在那里浏览你的代码,确保一切正常。





