Week 1 Exercises: Hello world

Rust编程练习:入门指南

目的

本周的练习旨在帮助你熟悉 Rust 代码的编译、运行以及基本语法。学习编程语言(无论是人类语言还是计算机语言)的最佳方式是沉浸式体验。我们希望通过这次练习为下周打好基础,当时我们将讨论你在其他语言中可能没接触过的新概念。

截止日期:4月14日,星期二,太平洋时间上午10:30
预计完成时间:1-3小时。如果遇到困难,可以联系助教或同学。

第 1 部分:环境设置与“Hello World”程序

Rust 开发环境设置

我们已经在 Myth 服务器上为你配置了 Rust 工具链。如果想在本地机器上(如因为网络或地理原因)运行代码,你需要安装 Rust 工具链。

  1. 在 Myth 上开发:Myth 已安装 Rust 工具链,可以直接使用。
  2. 在本地安装 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 提供了一系列数值类型:

  • 有符号整数i8i16i32i64,这些可以存储正数和负数。
  • 无符号整数u8u16u32u64,只能存储非负数。

位数(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
2
let mut n = 0;
n = n + 1; // 编译成功

字符串类型

Rust 有两种字符串类型:&strString

  • &str:不可变字符串切片,指向程序中的字符串常量。通常用于引用一个现有的字符串,类似于指针。

    1
    let s: &str = "Hello world"; // ": &str" 是可选的
  • String:堆分配的字符串,可以改变长度。

    1
    2
    let mut s: String = String::from("Hello ");
    s.push_str("world!"); // 将字符串连接在一起

向量(Vectors)和数组(Arrays)

  • 向量 (Vec<T>) 是动态大小的集合,适合存储同类型的多个值:

    1
    2
    3
    let mut v: Vec<i32> = Vec::new();
    v.push(2);
    v.push(3);
  • 数组 [T; N] 是固定大小的集合,在定义时就设定长度,元素类型相同,通常会在运行时检查数组访问的越界问题。

    1
    2
    3
    let mut arr: [i32; 4] = [0, 2, 4, 8];
    arr[0] = -2;
    println!("{}", arr[0] + arr[1]);

迭代

Rust 提供类似 Python 的迭代方式来遍历集合:

1
2
3
for i in v.iter() { // v 是一个向量
println!("{}", i);
}

循环

  • While 循环

    1
    2
    3
    while i < 20 {
    i += 1;
    }
  • 无限循环:使用 loop 可以创建一个循环,并用 break 退出。

    1
    2
    3
    4
    5
    let mut i = 0;
    loop {
    i += 1;
    if i == 10 { break; }
    }

函数

函数在 Rust 中通过 fn 声明。

  • 带返回类型的函数:

    1
    2
    3
    fn sum(a: i32, b: i32) -> i32 {
    a + b
    }
  • 无返回值的函数:

    1
    2
    3
    fn main() {
    // 执行代码...
    }

Rust 中一切都是表达式,因此函数的最后一个表达式没有分号,它的值就是返回值。如果加上分号,函数将不返回值,导致编译错误。

练习:实现函数

现在我们进入练习环节,完成以下几个函数:

  1. add_n:该函数接受一个整数向量 v 和一个整数 n,返回一个新向量,其中每个元素为 v 中的元素加上 n

    1
    2
    3
    fn 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>
    1. fn:声明一个函数。
    2. add_n:函数名称,表示该函数的作用是“添加 n”。
    3. v: Vec<i32>:第一个参数 v 是一个整数向量 (Vec<i32>),它存储了一组 i32 类型的有符号整数。
    4. n: i32:第二个参数 n 是一个 i32 整数,它将被加到 v 中的每个元素上。
    5. -> Vec<i32>:返回类型是 Vec<i32>,即一个新的整数向量。

    函数体

    1
    v.into_iter().map(|x| x + n).collect()

    这个表达式由三个步骤组成:

    1. v.into_iter()
      • into_iter 方法将向量 v 转换为一个迭代器(iterator)。此迭代器会按顺序“消费” v 中的元素,将每个元素逐个取出,不会保留原来的向量。
      • 这一步的结果是一个迭代器,它允许我们在 v 的元素上进行遍历和进一步处理。
    2. .map(|x| x + n)
      • map 是一个高阶方法,它接受一个闭包(closure)并将其应用于迭代器的每个元素。
      • |x| x + n 是一个闭包,它将当前元素 x 加上 n,然后返回结果。这里的 |x| 表示闭包参数,x + n 表示闭包的逻辑。
      • 结果是一个新的迭代器,其中的每个元素都是原始向量 v 中对应元素加上 n 的结果。
    3. .collect()
      • collect 方法将迭代器中的元素“收集”到一个集合中,这里将结果收集为一个新的 Vec<i32>
      • collect 会自动推断返回的集合类型为 Vec<i32>,因为函数签名中定义了返回类型为 Vec<i32>
  2. add_n_inplace:与 add_n 功能相同,但直接修改 v,不返回值。

    1
    2
    3
    4
    5
    fn add_n_inplace(v: &mut Vec<i32>, n: i32) {
    for x in v.iter_mut() {
    *x += n;
    }
    }
  3. dedup:去除向量中的重复元素,仅保留第一次出现的元素。可以使用 HashSet 辅助去重。

    1
    2
    3
    4
    5
    6
    use 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;:导入标准库中的 HashSetHashSet 是一种集合类型,它只存储唯一的值,适合用于去重操作。

函数签名

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。这个集合将用于跟踪已经遇到的元素。
  • HashSetinsert 方法会在添加元素时检查该元素是否已经存在,如果存在则返回 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

游戏功能

  1. 欢迎信息
    • 游戏开始时打印欢迎信息和初始状态。
  2. 显示当前状态
    • 显示当前猜测的单词状态,使用 - 表示未猜测的字母。
  3. 显示已猜测字母
    • 提示用户已经猜测过的字母。
  4. 剩余猜测次数
    • 显示用户剩余的猜测次数(初始为 5 次)。
  5. 用户输入
    • 提示用户输入一个字母进行猜测。
  6. 更新游戏状态
    • 根据用户输入的字母更新当前单词状态和已猜测字母。
    • 如果猜测的字母在单词中,更新显示;如果不在,减少剩余猜测次数并显示错误信息。
  7. 结束条件
    • 当用户猜出整个单词或用完所有猜测次数时,结束游戏并显示结果。

游戏实现的代码示例

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
use std::io::{self, Write}; // 导入必要的库
use std::collections::HashSet; // 导入 HashSet 用于去重

fn main() {
let secret_word = "lobster"; // 设置秘密单词
let mut guesses = HashSet::new(); // 已猜测字母集合
let mut remaining_guesses = 5; // 剩余猜测次数

println!("欢迎来到 CS110L 猜词游戏!");
let mut word_so_far = "-".repeat(secret_word.len()); // 初始化显示的单词状态

while remaining_guesses > 0 {
println!("当前单词状态是: {}", word_so_far);
println!("已猜测的字母: {:?}", guesses);
println!("剩余猜测次数: {}", remaining_guesses);

// 提示用户输入字母
print!("请猜一个字母: ");
io::stdout().flush().expect("刷新输出错误。");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取行错误。");

let guess_char = guess.trim(); // 去掉输入的空白字符
if guesses.contains(guess_char) {
println!("你已经猜过这个字母!");
continue; // 如果已经猜过,则重新提示
}

guesses.insert(guess_char.to_string()); // 记录猜测的字母

if secret_word.contains(guess_char) {
// 更新已猜对的字母状态
word_so_far = update_word_so_far(&word_so_far, secret_word, guess_char);
} else {
remaining_guesses -= 1; // 猜错则减少猜测次数
println!("抱歉,那个字母不在单词中");
}

// 检查是否猜出整个单词
if word_so_far == secret_word {
println!("恭喜你猜出了秘密单词: {}!", secret_word);
return;
}
}

println!("抱歉,你没有猜出单词。秘密单词是: {}!", secret_word);
}

// 更新当前单词状态的函数
fn update_word_so_far(word_so_far: &str, secret_word: &str, guess: &str) -> String {
let mut updated = String::new();
for (s, w) in word_so_far.chars().zip(secret_word.chars()) {
if s != '-' {
updated.push(s);
} else if w.to_string() == guess {
updated.push(w);
} else {
updated.push('-');
}
}
updated
}

代码说明

  • 导入库:使用 std::io 来处理输入输出,使用 std::collections::HashSet 来存储已猜测的字母。
  • 游戏主循环:使用 while 循环控制游戏流程,直到用户用完所有猜测次数。
  • 用户输入:提示用户输入一个字母,并通过 io::stdin() 读取。
  • 更新游戏状态:根据用户的输入更新显示的单词状态和已猜测的字母。
  • 结束条件:检查用户是否猜出了秘密单词或者用完了所有猜测次数。

Weekly survey

提交作业

  1. 使用 Git 进行版本控制

    • 学生在工作过程中,可以通过提交代码来保存工作快照。这有助于在遇到问题时,可以轻松恢复到之前的状态。
  2. Git 配置

    • 如果你从未使用过 Git,可能需要进行以下配置:

      1
      2
      git config --global user.name "Firstname Lastname"
      git config --global user.email "yourSunetid@stanford.edu"
    • 完成配置后,通常不需要再次执行。

  3. 提交代码

    • 使用以下命令提交你的工作:

      1
      git commit -am "Type some title here to identify this snapshot!"
    • -a 选项表示自动将已追踪的文件标记为已修改,-m 选项后跟的是提交消息,用于描述此次提交的内容。

  4. 推送到 GitHub

    • 为了提交作业,将你的更改推送到 GitHub,运行以下命令:

      1
      git push
    • 这将上传你的提交(快照)到 GitHub,方便教师访问。

  5. 验证提交

    • 你可以访问以下网址验证你的代码是否已成功提交:

      1
      https://github.com/cs110l/week1-yourSunetid
    • 在那里浏览你的代码,确保一切正常。