学习一门新语言首先要熟悉这门语言的语法,通过做一些简单的Leetcode题来熟悉新语法,也许能够达到事半功倍的效果吧。
可因为一些众所周知的原因,在一天之中的大部分时间都不太适合直接在Leetcode的网站上刷题。不过,好在可以先把题目和代码模版复制到VS Code等编辑器中,在那里编写代码解决题目,待调试测试过后,再找合适的时机把代码复制到Leetcode的网站上并提交。
例如,在学习Go语言时,我经常这样在VS Code中练习新语法:
// /path/to/leetcode/1.go
package main
import "fmt"
// 1. 两数之和
// 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
// ...
func twoSum(nums []int, target int) []int {
// ...
}
为了验证是否正确,只需要在main
函数中编写一些测试用例:
func main() {
// if expect != actual {
// ...
// fmt.Printf("❌expect != actual, expect: %v, actual: %v\n", tc.expect, actual)
// } else {
// fmt.Printf("✅expect == actual\n")
// }
}
最后,只要再go run 1.go
一下,就可以开始测试。
go run 1.go
✅expect == actual
✅expect == actual
main()
到处开花的笨办法
我用这个方法既学习了Go语言的语法,还顺带刷了不少Leetcode题。于是,在学习Rust时,就打算继续使用这个方法,但开头并不是那么顺利。
通过网上的一些教程,我知道先要通过cargo
命令创建一个项目,如cargo new rust-leetcode
。本打算每道题一个.rs
文件,并在main.rs
中调用相应的单元测试函数,最后调用cargo run
命令进行测试,例如:
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── 1.rs
│ └── main.rs
|...
// 1.rs
impl Solution {
pub fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {
// ...
}
}
fn test_two_sum() {
// ...
}
// main.rs
fn main() {
test_two_sum();
}
但执行cargo run
后,却报错了,
error[E0425]: cannot find function `test_two_sum` in this scope
--> src/main.rs:2:5
|
2 | test_two_sum();
| ^^^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `rust-lc` (bin "rust-lc") due to previous error
我当时并不知道如何解决这个错误,最终只得放弃使用cargo
,把fn main()
移动到每道题的源文件中(如1.rs
),然后通过rustc xxx.rs && ./xxx
进行测试,
$ fgrep 'fn main' -rn .
./1079.rs:53:fn main() {
./1925.rs:46:fn main() {
./main.rs:2:fn main() {
...
$ tree .
.
├── 1079
├── 1079.rs
├── 1925
├── 1925.rs
...
肥蟹书教会我如何组织项目内的代码
好在这种main()
到处开花的笨办法没持续多长时间,就迎来了称为“肥蟹书”的《Rust 程序设计(第2版)》。而且刚好第8章就讲到了如何组织项目内的代码以及Rust内置的文档测试框架。
肥蟹书的8.2节提到,“模块是关于项目内代码组织的“,那是不是把每道Leetcode题看作是一个模块就可以了?经过实验,答案是肯定的,而且Rust还支持3种组织模块的方法
模块位于自己的文件中
模块位于自己的带有
mod.rs
的目录中模块在自己的文件中,并带有包含子模块的补充目录
这3种方法说起来还挺绕嘴的,但好在书中给了一个具体示例,
fern_sim/
├── Cargo.toml
└── src/
├── main.rs
├── spores.rs
└── plant_structures/
├── mod.rs
├── leaves.rs
├── roots.rs
├── stems/
│ ├── phloem.rs
│ └── xylem.rs
└── stems.rs
我们可以对照着这个树形结构去正文中寻找每种方法的描述。下面是从书中摘录出来的内容:
第一种方法:
spores 模块保存在一个单独的名为 spores.rs 的文件中 …… 当 Rust 看到 mod spore; 时,会同时检查 spores.rs 和 spores/mod.rs……
第二种方法:
我们声明了 plant_structures 模块:
pub mod plant_structures;
这会导致 Rust 加载 plant_structures/mod.rs,该文件声明了 3 个子模块:
// 在plant_structures/mod.rs中
pub mod roots;
pub mod stems;
pub mod leaves;
这 3 个模块的内容存储在 leaves.rs、 roots.rs 和 stems.rs 这 3 个单独的文件中,与 mod.rs 一 样位于 plant_structures 目录下。
第三种方法:
也可以使用同名的文件和目录来组成模块。如果 stems(茎)需要包含称为 xylem(木质 部)和 phloem(韧皮部)的模块,那么可以选择将 stems 保留在 plant_structures/stems.rs 中 并添加一个 stems 目录…… 然后,在 stems.rs 中,我们声明了两个新的子模块:
// 在plant_structures/stems.rs中
pub mod xylem;
pub mod phloem;
虽然可能还是无法用一两句话说清楚这三种方法,但很容易“照猫画虎”,把刷Leetcode题的代码组织起来了,我还特意使用不同的方法来组织linked_list
模块和tree
模块。
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── lc1281.rs
│ ├── lc1572.rs
...
│ ├── lc833.rs
│ └── lc918.rs
│ ├── lib.rs
│ ├── linked_list
│ │ ├── lc21.rs
│ │ ├── lc83.rs
│ │ ├── mod.rs
│ │ └── offer06.rs
│ ├── tree
│ └── tree.rs
文档测试
为了避免频繁打开Leetcode的网页,提交代码以验证结果,最好是先在VS Code中进行单元测试。虽然肥蟹书的8.6节提到了:
Rust 中内置了一个简单的单元测试框架,测试是标有 #[test] 属性的普通函数:
#[test]
fn math_works() {
let x: i32 = 1;
assert!(x.is_positive());
assert_eq!(x + 1, 2);
}
cargo test 会运行项目中的所有测试:
$ cargo test
Compiling math_test v0.1.0 (file:///.../math_test)
Running target/release/math_test-e31ed91ae51ebf22
running 1 test
test math_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
这似乎已经满足验证代码是否正确的基本需求了,但只要再往下读两小节,就会发现书中介绍了一种更适合刷题时使用的测试方法——文档测试。
如上图所示,只要把代码写入到文档型注释中(以 3 个斜杠开头的注释),VS Code就会出现一个“Run Doctest”的按钮。只需要点击这个按钮,就可以开始运行单元测试。
于是在VS Code中用Leetcode练习Rust语法就变得非常方便了:
复制Leetcode的题目要求和函数模版
编写代码
在文档型注释中插入测试用例
点击“Run Doctest”按钮
复制写好的代码到Leetcode的页面并提交
现在,可以充分利用每天的宝贵时间安心学习Rust了。
哦对了,值得一提的是,肥蟹书的前两位作者是Mozilla创始团员,在系统编程领域已经有 20 多年的经验:
而译者是25年老码农雪狼(汪志成),他在这本书从翻译到出版的过程中,前前后后对稿子修订了7遍,每一遍都耗费了巨大的心血。
正在学习Rust的小伙伴,千万不要错过。