在本章中,我们将学习为什么在 Rust 中进行设计、架构和编码,需要采用与你以往经验中可能熟悉的方法不同的技术、不同的设计模式,以及不同的思维方式。经验丰富的开发者通常都有一套工具箱,其中包含他们长期学习和积累出来的模式与策略,这些工具在过去非常有效。为什么这套工具箱在 Rust 中不能以同样方式工作?本章就会讨论这个问题。
首先,我们将讨论为什么熟悉的模式不再适用,以及为什么需要新的模式。然后,我们将讨论为什么即便是非常有经验的程序员,也可能遇到不只是“学习语言本身”那么简单的困难,并且有时会觉得 Rust 令人抓狂。我们会讨论变得熟练使用 Rust 的过程,以及这一路上常见的陷阱和里程碑。最后,我们会讨论在使用 Rust 时,从 Rust 视角思考的重要性。
我们还会介绍第一个项目:我们将使用熟悉、经过验证的传统模式来构建一个 calculator program,并发现当我们依赖其中一些实践时会遇到的问题。
最重要的是,在本章以及后续章节中,我们会聚焦于:什么有效、什么无效,以及如何有效使用这门语言。
本章将覆盖以下主要主题:
- Why do I need new patterns?
- Hitting the wall
- The Rust learning curve
- Learning to think in Rust
- Let's build a calculator, badly!
Technical requirements
要完成本书中的练习,你需要安装 Rust toolchain。本书中的示例使用 stable Rust 开发,版本为 1.78.0,这是完成练习所需的最低版本。推荐使用 rustup 安装较新的 toolchain version。可以在这里找到:
rustup.rs/
其中包含安装和使用说明。
练习的 source code 可以在 GitHub 上找到:
该 repository 按 chapter 组织。本章相关练习位于: github.com/PacktPublis…
Why do I need new patterns?
在本节中,我们将讨论为什么 Rust 需要新的 design patterns。我们会讨论 patterns 如何反映其所服务的语言,以及 Rust 虽然看起来与熟悉的语言相似,但实际上比表面上更不同,因此需要一套新的工具,才能有效地编写代码。
Working with the familiar
作为 developers,我们已经投入了大量时间和精力,通常是数年甚至数十年,来学习自己的技艺。我们每个人都会努力建立关于最佳实践的知识,用来创建 robust、clean 和 elegant code。这些知识可能来自我们自己的经验,也可能来自学习他人的经验。我们依靠这些知识避开危险,并走向稳固的解决方案。
Design patterns 之所以成为软件开发中的标准,是因为它们是一种清晰、富有表达力且简洁的知识共享方式,可以引导我们获得出色结果。Visitor pattern、Observer pattern 这样的名称已经很常见,builders 和 factories 也几乎人人都知道。通过学习和使用 design patterns,我们可以受益于那些曾面对类似挑战的深思熟虑者的经验。它们会预先为我们的工具箱装入有用工具,以应对我们可能从未见过的情境,并引导我们走向成功项目。
传统 design patterns 如此成功并被广泛使用的原因之一,是它们具有极其广泛的适用性。同一套常见 design patterns 可以跨多种 programming languages、domains、frameworks 和 applications 生效。它们在 Java web service、TypeScript frontend 或 C++ game engine 中都依然有效。在许多场景中,你熟悉的工具不会辜负你。
Experience can be misleading
当 developer 学习 Rust 时,首先会学习语言基础。一旦掌握这些基础,自然就会拿出那些一直以来对自己有效的 patterns 和 principles。这是合理的,因为一旦你适应了 Rust 的一些独特特性,就会发现很多东西看起来很熟悉:structs 看起来像 objects,traits 看起来像 interfaces,methods 甚至也叫同样的名字。Associated functions 并不完全是 constructors 或 static methods,但感觉非常相似。一旦你学完语言基础,就会觉得自己已经到家了,可以打开工具箱开始工作。
这种熟悉感可能具有欺骗性。熟悉的 design patterns 起初似乎会按预期工作,但 Rust 比表面看起来更不同。为了生成你想要达到的高质量结果,它有时需要不同的方法和技术。Rust 经常需要用新的 patterns 来解决熟悉的问题;即便在熟悉 patterns 可用的场景中,它们也可能需要进行某种扭转,才能适配 Rust 独特的模型。
熟悉的 design patterns 能在如此多场景中完美工作的原因,是流行 programming languages 共享许多共同特征和设计选择。你可以说,像 Python 和 C++ 这样差异很大的语言,在设计上也共享一些共同模式。正因为如此,我们才能在如此多的 languages、frameworks 和 applications 中使用 common design patterns。
与几乎所有现代 programming languages 一样,Rust 也借鉴并受到了早期语言的启发。它从过去的语言中吸收经验,有时甚至吸收核心元素,并以独特方式重新使用和重新混合它们。Rust 在许多方面看起来熟悉,是因为其中确实有很多熟悉的东西。
但 Rust 也确实不同。有些差异很明显,例如新的 syntax 或必须遵守的不同 rules。有些则更微妙,例如 Rust 的 move semantics,或者理解 generics 与 associated types 之间的区别。但有些差异位于这门语言应该如何工作的核心。这些差异可能看不见,却正是 developers 在开始编写越来越高级代码时遇到大量困难的根源。
要用 Rust 写代码,我们需要一个新的工具箱。其中一些工具会很熟悉。有些会有一点不同,但仍然可识别。还有一些则是全新的。
下一节中,我们将通过一个示例来说明这一点。我们会看到,如果不考虑 Rust 的核心原则,即便是看起来最简单的设计决策,也可能导致令人抓狂的错误。
Example – parsing messages in place
假设我们需要处理大量 incoming messages,并解析其中的内容。这些 messages 采用 text format,并包含大量 data。每个 message 内部有一个 header,然后是不同 blocks 或 sections,它们本身又是不同类型的 sub-messages。不同类型的 sub-messages 也各自有 header,然后拥有该 message type 独有的 field structure:
Message
├── Header
│ ├── MessageID: 12345
│ ├── Timestamp: 2024-09-18T14:30:00Z
│ └── SenderInfo: "Device_ABC"
│
├── SubMessage1 (User Data)
│ ├── Header
│ │ ├── Type: UserData
│ │ └── Length: 120 bytes
│ │
│ ├── UserID: "user123"
│ ├── Name: "John Doe"
│ └── Email: "john.doe@example.com"
│
├── SubMessage2 (Sensor Reading)
│ ├── Header
│ │ ├── Type: SensorData
│ │ └── Length: 64 bytes
│ │
│ ├── SensorID: "temp_sensor_01"
│ ├── Temperature: 22.5°C
│ ├── Humidity: 45%
│ └── Timestamp: 2024-09-18T14:29:55Z
│
└── SubMessage3 (System Status)
├── Header
│ ├── Type: SystemStatus
│ └── Length: 96 bytes
│
├── CPUUsage: 35%
├── MemoryUsage: 2.1GB
├── DiskSpace:
│ ├── Total: 500GB
│ └── Free: 350GB
└── UptimeHours: 720
假设保留 message 的原始未解析形式很重要,因为后续某些用途需要它,而且所有 messages 都需要保存在 memory 中。
幸运的是,我们的任务很简单:我们只需要一种方法,在某个 message 中找到特定 blocks,并返回一个 object,供使用者读取该 block 中的 bytes。这个 object 可能会用于进一步 parsing,但我们并不关心他们会如何处理这些 characters。我们只需要提供一个 object,让它能够读取某个特定 message 中某个特定 block 的 bytes。
由于 messages 数量如此多,data 量也如此大,而且全部都需要保存在 memory 中,copy data 并不实际。因此,我们希望直接 reference message 中原地存在的 bytes。
根据这些 requirements,我们需要编写一个 function,完成以下任务:
- 接收一个指向 memory 中 message 的 reference,以及一个 block identifier,可以把它叫做 integer
- 保持该 message intact
- 在 message 中找到该 identifier 对应的 specific block
- 返回一个 object,该 object 可用于读取该 block 的 bytes,但不会 copy 它
我有一个好消息!已经有人写了一个 Rust function,只要我们提供 block identifier,它就能为我们找到该 block 的 offset 和 length。
还有一个好消息!Rust 有大量非常有用的 crates,可以帮助我们,它们都在: crates.io
那里可能有某个 crate,可以添加到我们的 Cargo.toml 文件中。
但我们甚至不需要这么做!Rust standard library 中包含 std::io,它提供了有用的 traits 和 structs,可以帮我们完成大部分工作。我们可以使用 Read trait 来表示 byte reader object,并使用 Cursor struct 来完成实际读取 bytes 的工作。
这会很简单!
下面是一个满足前面所有 requirements 的 block reader 实现:
use std::io::{Cursor, Read};
fn find_block(_message: &[u8], _block_id: u64) -> (usize, usize) {
todo!()
}
struct BlockReader<'a> {
message: Vec<u8>,
block_reader: Cursor<&'a [u8]>
}
impl Read for BlockReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.block_reader.read(buf)
}
}
fn new_block_reader(message: Vec<u8>, block_id: u64) -> BlockReader<'static> {
let (pos, count) = find_block(&message,block_id);
let slice=&message[pos..(pos+count)];
BlockReader {
message,
block_reader: Cursor::new(slice)
}
}
这段代码做了以下事情:
- 它定义了一个 placeholder 版本的
find_blockfunction,该 function 接收一个 message,并返回 block 的 position 和 byte count 组成的 tuple,也就是 requirement 3 - 它定义了
BlockReaderstruct,用来保存 original message,也就是 requirement 2,以及一个Cursorstruct,可以作为 proxy object 来读取 bytes,用于实现 requirement 4 - 它为
BlockReader实现Readtrait,只是将 read 转发给Cursorstruct,也就是 requirement 4 - 最后,它实现了 requirement 1 中描述的 function,该 function 创建并返回我们的
BlockReaderstruct - 在这个 function 中,
find_block被用来确定 block 的 position 和 length,然后从 message 中取出一个 slice,不进行 copy,满足 requirement 4 - 基于这个 slice 创建一个
Cursorobject - 最后,构造并返回
BlockReader,所有 requirements 完成
完美!现在我们只需要 build:
cargo build
Compiling block_reader v0.1.0 error[E0515]: cannot return value referencing function parameter `message`
--> src/block_finder.rs:21:5
|
20 | let slice = &message[pos..(pos+count)];
| ------- `message` is borrowed here
21 | / BlockReader {
22 | | message,
23 | | block_reader: Cursor::new(slice)
24 | | }
| |_____^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `message` because it is borrowed
--> src/block_finder.rs:22:9
|
18 | fn new_block_reader(message: Vec<u8>, block_id: u64) -> BlockReader<'static> {
| ------- binding `message` declared here
19 | let (pos, count) = find_block(&message,block_id);
20|letslice=&message[pos..(pos+count)];
| ------- borrow of `message` occurs here
21 | / BlockReader {
22 | | message,
| | ^^^^^^^ move out of `message` occurs here
23 | | block_reader: Cursor::new(slice)
24 | | }
| |_____- returning this value requires that `message` is borrowed for `'static`
看起来 compiler 不喜欢这样。我们依赖的是:能够保留一个指向 message 内部 block 的 reference。但我们忘了,创建 BlockReader 意味着获取 message 的 ownership,这会使该 reference 失效。
我们可能会花几个小时试图找到一个可行的技巧,实际上确实有一个技巧。但这里真正发生了什么?
这里,我们试图使用 traditional object-oriented memory patterns。这些 patterns 在 Java 这样的语言中是合理的,但在 Rust 中并不合理。所谓 object-oriented memory patterns,指的是一种常见做法:创建可以自由互相 reference 的 objects,经常形成复杂的 relationships web,而无需思考 lifetimes 或 data races。Borrow checker 看到我们在做危险的事情,于是阻止了它。
那我们应该怎么做?
我们需要重新思考自己正在做的事情,以适配 Rust,而不是试图欺骗 borrow checker。我们会在下一章重新讨论这个例子,并找到一个对 Rust 来说更自然的解决方案。
到这里,我们已经理解了为什么 Rust 需要新的 patterns。带着这个理解,我们可以继续探索:为什么这么多 developers 一开始学习 Rust 时很开心,但当他们进入更高级项目时,却会遇到困难。
Hitting the wall
在本节中,我们将讨论一个常见问题:developers 在 Rust 初期取得成功后,一旦开始处理更高级项目,就会突然遇到困难。这个主题非常关键,因为它说明了为什么那些在 developer 过去经验中有效的 design patterns,可能会成为学习 Rust 时继续进步的障碍。我们会先解释这种现象,并描述其影响。然后讨论它为什么发生,以及应该怎么办。
A sudden stop
正在学习 Rust 的 developers 经常会经历某件事。这件事非常常见,似乎在 Rust sites 和 forums 上一直都是话题。尽管它如此常见,却似乎没有一个名字。因此,我们暂且称它为 “hitting the wall”。
许多人开始 Rust 之旅时充满热情。他们知道这门语言非常受欢迎,也广受赞誉。他们发现 Rust community 非常友好、支持性强,并且愿意帮助他们。这里有大量高质量 beginner documentation,也有很多 tutorials 和 guides。
学习 Rust 很有趣!Rust 的确有一些不同的方面,但在刚开始学习语言时,developers 往往会觉得自己几乎每天都在进步,而且能够构建复杂度不断提高的项目,并越来越多地使用 advanced features。
最终,他们会到达一个阶段,觉得自己已经完成学习过程,学会了这门语言。也许他们已经读完了 Rust book,完成了 tutorials,问过问题,也构建过一些东西。现在,他们已经是 Rust programmers,也就是 Rustaceans 了,并准备承担实质性项目。
这时,事情往往会突然变得困难。
从 high-level design 来看,项目似乎并不异常困难。他们开始编写一些 modules,这些 modules 能很好 compile,unit tests 似乎也能工作。他们继续构建更多内容,但随后撞上了墙。
某些代码无法 compile,这本身没问题,因为这种事总会发生。然而,他们尝试遵循 error message 中的 recommendations,但那样也不能 compile。他们沿着一条链回溯,一边修一边修复很多东西,但这又导致更多东西无法 compile。
退一步看,他们意识到 Rust 不允许他们做正在尝试做的事情,但整个 design 又要求他们必须这么做。他们一直在试图 appease compiler,但 compiler 不会让步,因为这违反了 Rust 的 rules。
他们可能会尝试用技巧绕过问题。他们可能到处添加 clone(),或者把东西包进 Rc<RefCell<>>。他们可能尝试写一些 unsafe code,或者寻找一个 crate 来修复他们正在斗争的问题。
有时,这确实能奏效!但它会让代码变得 messy、inefficient,并且很可能充满 bugs。更多时候,这并没有帮助:代码不能 compile,不能正确工作,或者看似能工作,却在他们构建更多项目之后出现另一个错误。
我听过有人说 Rust compiler 恨他们。我也听过有人说他们恨 Rust。我记得自己第一次撞墙时也说过这些话。
Learning why this happens
很多 developers 会有这种经历,原因可以简单概括为:对于更高级项目来说,沿用过去经验中的 architectural 和 design patterns,可能会导致代码无法在 Rust 中正确运行。在学习 Rust 时,学习如何为 Rust 设计,与学习语言本身一样必要。
这在实践中意味着什么?
Rust is not an object-oriented language:因为 object orientation 在现代 programming languages 中极其普遍,所以在设计 architecture 或构建 system 时,很自然会以 objects 和 class hierarchies 来思考。这在 Rust 中似乎可以工作,因为它确实有一些 object-oriented-style features。但由于没有 inheritance,并且 polymorphism 存在严格限制,试图实现 object-based design 通常会导致糟糕结果。我们会在下一章讨论,用可疑技巧绕过这一问题并没有帮助。
The Rust borrow checker prohibits designs that are natural in most languages:已经开始使用 Rust 的 experienced developers,通常习惯于几乎不受限制地 read、write 和 share data。在学习 Rust 时,他们会理解这门语言不同,也会理解它限制 data 如何被使用和共享。然而,如果他们继续用自己习惯的方式思考 system architecture 和 design patterns,他们最终会得到一些依赖于 share 和 mutate data 的 designs,而这些 designs 在 Rust 中无法工作。在 structs 和 functions 层面,代码可能遵循非常优秀的 practices,但 high-level design 中的缺陷会意味着整个 system 永远不可能工作。我们会在第 4 章《Don’t Fight the Borrow Checker》中进一步讨论这一点。
Rust uses functional programming paradigms to perform common tasks:Functional programming 被内置进 Rust。Rust standard library、core Rust types,甚至 language 的基本设计,都被设计为利用 functional programming 的力量。正因为如此,如果 system design 没有考虑这一点,就会产生 awkward、inefficient、confusing,甚至完全不可行的代码。在基础层面,Rust 中的 system design 必须考虑这一点。Rust 提供了极其有用的工具,帮助我们用 functional design 创建简洁自然的 code flow。但要使用它们,system design 就必须考虑这些内容。我们会在第 11 章《Patterns from Functional Programming》中讨论如何利用这一点。
Rust's advanced type system is core to effectively designing for the language:任何学过 Rust 的人都知道,它拥有独特、富有表达力且极其灵活的 type system。借助它,可以做许多在大多数语言中无法做到的事情。不过,这有两面性。正因为 type system 能做如此多的事,语言经常会让你在不利用 type system 的情况下很难或根本无法完成某些事情。在设计 system architecture 时,有必要以大多数 programming languages 中并不需要的方式思考 types。Rust 提供了强大的工具,但也期待 developer 使用这些工具。我们会在第 10 章《Patterns That Leverage the Type System》中进一步探索。
Rust has unique features that are purpose-built to allow you to create correct, idiomatic code:与 type system 一样,Rust 有许多独特 features,使代码中能够表达大多数 programming languages 无法表达的事情。“everything is an expression” model、built-in utility types、独特的 resource acquisition is initialization(RAII)方法,以及语言中许多 ergonomic features,都让编写 clean、elegant code 更容易。但正因为这些 features 被包含在语言中,Rust 也让你很难在不使用它们的情况下工作。与 type system 一样,它们通常不只是 preferred way,也往往是唯一有效的方法。我们会在第 12 章《Patterns Emerging from Rust's Core Features》中进一步讨论。
Rust has general architectural principles and patterns that are natural to the language:Rust 天然适合某些 design principles 和 architectural patterns。就像在 Java 或 C++ 中基于 class hierarchies 设计 systems 很自然一样,Rust 也有一些感觉像是语言本身固有的 unique architectural patterns。也正如用一个拥有数百个 static functions 的 class 来设计 Java program 会非常不自然一样,如果不考虑 architectural choices 如何与 idiomatic Rust programs 的自然结构协调,在 Rust 中设计 system 也会出问题。关注 data flow、限制 mutability 的 scope、以 modules 组织代码,以及 modularizing data 以避免 borrowing 问题,这些 basic principles 对创建可运行的 Rust architectures 至关重要。如果不考虑这些事情来设计 Rust application,就可能得到在 coding practices 上很优秀、但无法组合成可工作整体的代码。许多与 compiler 的无尽战斗,都源于没有考虑这些因素。我们会在第 9 章《Architectural Patterns》中进一步讨论。
Trying to work around architectural issues with local fixes never works:当 Rust code 无法 compile 时,有时只是一个简单错误,甚至是一个微妙错误,可以通过修改引发错误的代码来修复。通常 compiler error message 会准确告诉你如何解决问题!但当修复错误似乎不可能,或者它导致一个新错误,然后又一个,再又一个时,这说明存在更严重的 structural issue。在这种情况下,很容易把所有东西都扔向这个错误,包括 cloning、unsafe code,以及更多绕过它的方式。此时,重要的是停下来,思考为什么你在更基础的层面上会面对这个错误。通常你会发现,真正的原因是更早之前在 design level 做出的某个决策。我们会在第 3 章《Anti-Pattern: Using Clone and Rc Everywhere》中讨论为什么 “throwing everything at it” 是一种糟糕策略。
Hitting the wall 往往是 developer Rust 旅程中痛苦的一步。它可能非常令人沮丧,有时甚至令人愤怒。然而,它并不是必经之路。通过学习如何应用 Rust-specific architectural 和 design patterns,这堵墙可以消失,前方道路也可以变得清晰。
下一节中,我们将讨论这段旅程,以及学习 Rust 的过程,并讨论 design patterns 如何在每一步提供帮助。
The Rust learning curve
在本节中,我们将讨论 Rust 与大多数 programming languages 相比,不寻常的 learning curve。理解 developers 在变得熟练使用这门语言过程中所面对的 common challenges 很有价值,因为其中许多挑战都来自没有意识到哪里需要新的 patterns。我们会先讨论学习 Rust 与学习大多数语言有何不同。然后讨论学习这门语言时的常见体验。
Learning Rust can feel very different
Rust 有许多方面使它不同于大多数语言。它以新的方式组合了 earlier languages 中的多种 paradigms。Borrow checker 在推出时不像任何其他语言中的东西,尽管其大部分基础几十年前就已经被探索过。Async model 独特且激进。Rust 有太多东西不像其他语言。
Rust 还有一个特质,据我所知,在其他地方都找不到:Rust learning curve。
几乎所有 programming languages 的学习过程都有一个常量,那就是 progress。有些语言更难。有些语言不寻常,或需要新的思维方式。有些语言要学的东西非常多,可能需要多年才能吸收。但无论语言多么奇怪或困难,学习者日复一日都会随着时间看到进步。任何持续努力的人,都会觉得自己在每一步都更有知识、更有能力。
然而,对许多人来说,Rust experience 可能完全不同。
正如上一节所讨论的,学习 Rust 的 developers 经常会在刚开始真正感到舒服使用这门语言时,经历一种深刻 setback。那种 setback,那种在旅程中倒退的感觉,可能令人非常沮丧,以至于有些人会完全停止学习 Rust。
但 hitting the wall 虽然是一次重大 setback,却只是 Rust 之旅中曲折变化的一部分。
The uneven path
Rust 的学习过程可能相当不平坦。这门语言本身有许多可能令人困惑的 topics,包括 borrow checker、lifetime annotations、traits、generics、associated types、asynchronous programming、unsafe Rust 等等。与 compiler 的战斗可能时有时无,而当 developer 以为自己已经越过这一阶段时,这些战斗又会令人不安。
最麻烦的地方,并不是某个可靠进步感的缺失。那种今天还为自己的成就感到骄傲,第二天却又回到什么都不能 compile 的状态,会非常消耗人。
在我学习 Rust 的过程中,我把它描述为 Rust grief 的五个阶段:
Optimism:一开始,我对学习 Rust 感到兴奋和热情。有这么多东西可发现,探索起来也很有趣。我很快开始觉得自己真的开始了解这门语言了。Compiler error messages 甚至非常有帮助,会引导我修复错误,并在这个过程中学习。
Frustration:突然之间,我面对的 compiler errors 变得更难修复,甚至难以理解。我开始对 compiler 愤怒,也更生自己的气,因为我感觉自己似乎在倒退。有时我会取得一段进展,但随后又撞上下一个错误,然后重新陷入愤怒。
Doubt:愤怒已经很糟了,但当 doubt 开始出现时,我的 Rust 之旅跌入最低点。我开始怀疑自己,也开始怀疑继续使用 Rust 是否有意义。这就是我撞墙的地方。
Epiphany:在某个清晰时刻,我以不同方式看待事情。我开始以不同方式思考自己正在做什么。并非所有事情都立刻变得清楚,但我能看到路径,也能理解自己需要如何改变思维方式。我开始像最初一样欣赏 Rust。
Mastery:带着新的思维方式,我发现自己过去的敌人 compiler,现在成了朋友和盟友,而 Rust 也变成了我最喜欢的语言。
每个人的 Rust 之旅都不同,不是每个人都会像我一样经历 Rust grief,我希望这本书能帮助你避免它,但我的经历并不罕见。
对我来说,那个清晰时刻是这样的:我正在编写一个非常复杂的 concurrent system,其中有许多 workers 试图共享同一批 resources。我熟悉 concurrent programming,所以我知道所有危险,也知道如何避免它们。但我的代码无法 compile。我和 compiler 战斗了几个小时,但无论尝试什么,都无法让它 compile。
最终,我退一步思考自己正在试图做什么。我意识到,我正在试图写出一个微妙的 data race bug,而 compiler 不允许我这么做。惊讶过后,我开始思考自己在整个项目中一直在做什么,并看到:我应用在 Rust code 上的 tools、patterns、mental models 和 thinking ways 并不适合。我知道如何写 syntactically correct Rust,但我并没有用 Rust terms 思考自己正在构建什么。
那就是我的 lightbulb moment。
Learning to think in Rust
在本节中,我们将学习改变 Rust code 思维方式的价值。学习新的 design patterns 可以显著改变 developer 使用 Rust 的成功程度,但理解这些新 patterns 背后的原因,又能将这种成功放大。我们会先讨论,我们的思维如何被自己熟悉的 programming languages 塑造,以及当学习一门新语言时,这种思维又如何塑造我们的 solutions。然后,我们会讨论:通过理解这些 design patterns 来建立正确 mental model,如何让我们超越 patterns 本身能够做到的事情。
Thinking in code
本书的目的是教授有用的 design patterns 和有效 coding practices,使你能够写出 high-quality、robust、idiomatic Rust code。这些 practices 和 patterns 可以显著改善 Rust projects 中的 experience 和 results,但还有另一件事值得考虑。
如果你是一名 experienced developer,那么 Rust 很可能不是你的第一门 programming language,而且在开始学习 Rust 之前,你很可能已经在一门或多门语言中拥有大量经验。不管是 Python、C#、TypeScript,还是更小众的语言,你都可能已经非常熟悉这门语言,以至于它感觉与说或写自己的母语没有什么不同。
你在用 code 思考。
但就像 human languages 一样,你是在用某种特定的 code 思考,也就是你熟悉的那门语言。这种理解远远超出该语言的 syntax、features 及其 standard library。你知道事情如何工作,在什么情况下该做什么,也知道这门语言的 flow。你不必刻意思考,因为你已经在用它思考。
你还知道另一件事:你知道 design patterns。但你知道的不只是官方命名的那些 patterns。你知道数百个没有名字的 patterns,因为你并不把它们当作 design patterns,而只是知道该怎么做。
如果你还没有走到这个阶段,将来你也会到达。
从这个角度看,hitting the wall 和 Rust learning curve 就说得通了。它们是走向 thinking in Rust 的步骤。
Finding understanding
在本书中,我们将学习新的 design patterns,发现什么有效、什么无效,并思考如何组织代码,使其与 Rust 最优协作。我们将探索如何使用一些熟悉 patterns,以及一些由 Rust 独特特质带来的 unique patterns。
此外,我们会讨论为什么某些事情有效,为什么某些事情无效。我们不仅会讨论如何使用或修改 familiar patterns,使其在 Rust 中更有效,也会探索它们为什么与 Rust 协调,以及为什么有时必须改变。当我们探索为 solid、elegant design 提供基础的 architectural choices 时,也会探索 Rust 中是什么使这些 choices 如此有效。当我们讨论由 Rust features 使之成为可能的 unique patterns 时,也会讨论这些 features 如何创造新的可能性,并讨论如何利用这些 features 创造我们自己的可能性。
这一切的目标,是帮助你理解这些 patterns 从何而来,从而发现自己的 patterns。
目标是帮助你 think in Rust。
Rust 是一门相对年轻的语言。在 Rust design patterns 的讨论中,有时会出现一个有趣现象:一些广为人知并被广泛使用的 crates 使用了灵活、优雅但没有名字的 patterns。当人们阅读这些 crates 的 source code 来学习时,也开始在自己的代码中使用这些 unnamed patterns。我会在后续章节中提到并注明其中一些。
你可能不会发明会出名的 patterns,但我希望能帮助你 think in Rust。这样一来,你会知道数百个没有名字但有用的 patterns,并且能够发现更多。
下一节中,我们将讨论第一个 coding project,以及它如何帮助展示在使用 Rust 时不该做什么。
Let's build a calculator, badly!
在本节中,我们将构建一个 calculator application,类似于许多 tutorials 中的示例。我们会把它叫作 Bad Calculator,因为我们会故意把它构建得很糟糕。
为了说明使用以 Rust 为中心的 patterns 和 practices 的价值,我们将观察当我们不这样做时会发生什么。正如前文讨论的,使用那些源自无法在 Rust 中恰当表达的 paradigms 的 design patterns,例如 object orientation,会给我们编写的代码和构建的 applications 带来 systemic issues。我们的 Bad Calculator 将展示这些问题。
我们将做以下事情:
- 围绕在 Rust 中不适用的 paradigms 来设计它
- 使用不合适的 patterns 和 techniques
- 试图通过 workaround 克服 errors
- 试图欺骗 borrow checker,让它按我们的意愿工作
这样做的过程中,我们会讨论遇到的 roadblocks,以及为什么会面对它们。我们还会讨论我们本应该怎么做,以及应该如何思考正在构建的东西。
为了简单起见,这个项目中我们不会使用任何 crates。我们要自己犯错!
在本章中,我们只会创建 project,并创建一个非常简单的 main loop。我们会在后续章节中填入所有糟糕逻辑。
首先,我们创建 Cargo project 本身。切换到你想存放 project code 的目录,并初始化项目。我们将使用 cargo new 在名为 bad_calculator 的目录中创建一个新 application:
cargo new bad_calculator
Creating binary (application) `bad_calculator` package
你的目录结构现在应该是这样:
bad_calculator
├── Cargo.toml
└── src
└── main.rs
目前,我们只创建 main loop。在这个 loop 中,我们会读取 user input,在稍后要编写的 function 中 evaluate 它,并打印 result,直到 user 输入 "exit"。编辑 main.rs,使其如下:
use std::io::Write;
use std::process::exit;
fn evaluate_expression(expression: &str) -> Result<String, String> {
todo!()
}
fn main() {
let mut buf = String::new();
loop {
print!("> ");
std::io::stdout().flush().unwrap();
buf.clear();
std::io::stdin().read_line(&mut buf).unwrap();
if buf.trim() == "exit" {
exit(0)
}
match evaluate_expression(&buf) {
Ok(result) => println!("{result}"),
Err(error) => println!("Error: {error}"),
}
}
}
https://github.com/PacktPublishing/Design-Patterns-and-Best-Practices-in-Rust/blob/main/ch1/bad_calculator/src/main.rs
快速说明一下这里发生了什么:
- 首先,我们定义了一个
evaluate_expressionfunction,稍后会在其中填入实际执行计算的代码。它可以返回 result,也就是一个 string,也可以返回 error,也就是一个 string。 - 在
main中,我们创建一个 empty string,作为 input buffer。 - 我们创建一个 main loop,它会一直运行,直到我们退出。
- 我们打印 prompt,然后对 standard output 执行
flush(),以确保 prompt 在读取 input 之前被打印出来。 - 我们对 buffer 执行
clear(),确保其中没有 leftover characters,然后使用read_line()将 input 读取到 buffer 中。 - 我们检查 buffer 在 trim whitespace 后是否只包含
"exit"字符串,如果是,就退出。 - 如果不是,就对 buffer 调用
evaluate_expressionfunction。 - 我们 match function 的 return value,并打印 result 或 error。
目前就到这里。我们将在后续章节中以糟糕方式编写 expression evaluator。
Summary
本章中,我们讨论了为什么在 Rust 中开发代码需要新的 patterns、新的 practices 和新的 thinking。首先,我们讨论了为什么 traditional design patterns 虽然在许多语言中极其有用,但在使用 Rust 时需要被修订和扩展。然后,我们讨论了 Rust 学习旅程中的 hitting the wall,以及为什么随着项目复杂度上升,Rust 可能会突然变得更痛苦。之后,我们讨论了 Rust development 不寻常的 learning curve。随后,我们讨论了 think in Rust 意味着什么、为什么它对 Rust development 很关键,以及 Rust design patterns 如何提供帮助。最后,我们讨论了后续章节将构建的 project:一个故意忽略 Rust patterns 和 practices 的 calculator。
利用本章所学,你将能够识别某些情况:compiler errors 或 awkward workarounds 其实是在提示你没有使用正确 patterns。你也能识别为什么自己的 Rust 学习进展并不是平滑递进的。最重要的是,你现在有了一个基础,可以理解后续章节中 anti-patterns 的问题、整本书中新 patterns 的价值,以及它们所代表的新思维。
下一章中,我们将开始讨论 Rust anti-patterns,从试图以 object-oriented style 使用 Rust 开始。我们也会继续开发我们的 Bad Calculator。