速成课第1节

在本课中,我们只需要掌握一些基础知识: 工具、编译能力、基本语法等等。 让我们从工具开始,您可以在下载东西的同时继续阅读。

这篇文章是基于 FP 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在介绍文章的顶部找到这个系列中所有文章的链接。 也可订阅 RSS 频道。

工具

Rust 的引导需要是 rustup 工具,它将安装和管理 Rust 工具链。我之所以用复数形式,是因为它既可以管理 Rust 编译器的多个版本,也可以管理替代目标的交叉编译器。 现在,我们将做一些简单的事情。

这两页都会告诉你做同样的事情:

请阅读 rust-lang 页面上关于设置 PATH 环境变量的说明。 对于类 unix 系统,你需要在 PATH 中添加 ~/.cargo/bin。

除了 rustup 可执行文件之外,您还将获得:

  • cargo, Rust 构建工具

  • rustc, Rust 编译器

Hello, world!

好的,这部分很简单:cargo new hello && cd hello && cargo run

我们现在还没有学习所有关于 Cargo 的知识,但是需要掌握一些基本知识:

  • Cargo.toml包含项目的元数据,包括依赖项

  • Cargo.lockcargo自身生成

  • src源文件,目前只包含src/main.rs

  • target包含生成的文件

我们稍后将讨论源代码本身,首先是一些工具注释。

使用 rustc 构建

对于这么简单的东西,你不需要 Cargo 来完成。 相反,您可以只使用: rustc src/main.rs && ./main。 如果你喜欢用这种方式进行代码实验,那就去做吧。 但是通常情况下,最好使用 cargo new 创建一个临时项目并在其中进行实验。 完全由你决定。

运行测试

我们还不会在代码中添加任何测试,但是您可以使用 cargo test 在代码中运行测试。

额外的工具

两个有用的实用程序是 rustfmt 工具(用于自动格式化代码)和 clippy (用于获取代码建议)。 请注意 clippy 仍然是一个正在进行的工作,有时会给出错误的肯定。 为了让它们构建起来,需要运行:

$ rustup component add clippy-preview rustfmt-preview

然后你可以使用以下命令运行它们:

$ cargo fmt
$ cargo clippy

IDE

有一些 IDE 支持给那些想要它的人。我听说过有关 IntelliJ IDEA 的 Rust 插件很棒的事情。 就个人而言,我还没有使用过,但首先我也不是一个IDE用户。 此速成班将不使用任何IDE,仅提供基本的文本编辑器支持。

Macros

好了,我们终于可以查看 src/main.rs 中的源代码了:

fn main() {
    println!("Hello, world!");
}

很简单。 fn 表示正在编写一个函数。 名字是 main。 它不带参数,也没有返回值。 (或者,更准确地说,它返回单元类型, 类似于 C/C++ 中void,但实际上更接近 Haskell 中的单元类型)。字符串文字看起来非常普通,函数调用与其他 c 样式的语言几乎完全相同。

这是第一个“速成班”部分:为什么 println 后面有个叹号? 我说“速成班”是因为当我第一次学习 Rust 的时候,我没有看到这方面的解释,这让我困扰了一段时间。

Println 不是一个函数。 这是一个宏(macro)。 这是因为它采用一个格式字符串,需要在编译时检查它。 要证明这一点,请尝试将字符串更改为包含 {} 的字符串。 你会得到一个错误消息,大致如下:

error: 1 positional argument in format string, but no arguments were given

这可以通过提供一个填充占位符参数来解决:

println!("Hello , world! {} {} {}", 5, true, "foobar");

猜猜输出结果是什么,你可能是对的。 但是这给我们留下了一个问题: println 是怎么做到的! 宏知道如何显示这些不同类型?

Traits 和 Display

更多的速成课时间! 为了更好地了解 Display 的工作原理,让我们触发一个编译时错误。 为此,我们将定义一个新的数据类型 Person,创建该类型的值,并尝试打印它:

struct Person {
    name: String,
    age: u32,
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("Person: {}", alice);
}

稍后我们将介绍更多关于定义自己的结构和枚举的例子,但是如果您感到好奇,可以阅读《Rust book》相关章节

如果你尝试编译它,你会得到:

error[E0277]: `Person` doesn't implement `std::fmt::Display`
  --> src/main.rs:11:28
   |
11 |     println!("Person: {}", alice);
   |                            ^^^^^ `Person` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Person`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: required by `std::fmt::Display::fmt`

这有点罗嗦,但重要的是没有为 Person 实现 std::fmt::DisplayTraits。 在 Rust 中,Traits 类似于 Java 中的接口,或者更好的类似于 Haskell 的 typeclass。 (是否注意到某种类似于 Haskell 概念的模式? 是的,我也这样认为。)

定义自己的特征,并在以后学习实现这些特征,我们将获得很多乐趣。 但是我们现在正面临崩溃。 因此,让我们在此处添加该特征的实现:

impl Display for Person {
}

但这没有奏效:

error[E0405]: cannot find trait `Display` in this scope
 --> src/main.rs:6:6
  |
6 | impl Display for Person {
  |      ^^^^^^^ not found in this scope
help: possible candidates are found in other modules, you can import them into scope
  |
1 | use core::fmt::Display;
  |
1 | use std::fmt::Display;
  |

error: aborting due to previous error

For more information about this error, try `rustc --explain E0405`.
error: Could not compile `foo`.

我们没有将 Display 导入到本地名称空间中。 编译器很有帮助地推荐了两个我们可能需要的不同特性,并告诉我们可以使用 use 语句将它们导入到本地名称空间中。 我们在前面的错误消息中看到,我们希望 std::fmt::Display,因此将 use std::fmt::Display 添加到 src/main.rs 会修复这个错误消息。但是只是为了证明这一点,不需要使用 use 语句! 我们可以改为:

impl std::fmt::Display for Person {
}

好极了,我们之前的错误消息已经被其他东西取代了:

error[E0046]: not all trait items implemented, missing: `fmt`
 --> src/main.rs:6:1
  |
6 | impl std::fmt::Display for Person {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `fmt` in implementation
  |
  = note: `fmt` from trait: `fn(&Self, &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error>`

我们正在迅速达到“踢轮胎”课程要涵盖的内容的极限。但是希望这能帮助我们为下一次播下种子。

错误消息告诉我们需要在 Display trait 的实现中包含 fmt 方法, 也告诉我们这个的类型签名是什么。 让我们来看看这个签名,或者至少看看错误消息是怎么说的:

fn(&Self, &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error>

这里有很多东西要整理。 我将对每一部分都应用术语,您有可能不会完全理解这些。

  • Self 是得到实现的事物的类型。 在这种情况下,就是那个 Person

  • 在开头添加 使其成为对值的引用,而不是值本身。 C++ 开发人员已经习惯了该概念。 许多其他语言也谈论通过引用传递。 在 Rust 中,这与所有权有很大关系。 在 Rust 中,所有权是一个非常重要的话题,我们现在不再讨论。

  • &mut 是可变的引用。 默认情况下,Rust 中的所有内容都是不可变的,您必须明确地说出是可变的。 稍后我们将探讨为什么引用的可变性对于 Rust 的所有权至关重要。

  • 无论如何,第二个参数是对 Formatter 的可变引用。 Formatter 之后的 <'_> 是什么? 这是生命周期参数。 这也与所有权有关。 我们以后也会谈到。

  • -> 表示我们提供函数的返回类型。

  • Result 是一个枚举,它是一个类型或带标记的联合。 两个类型参数是泛型:成功时的值 和 错误时的值。

  • 在成功的情况下,我们的函数返回一个 () 或 单元值。 另一种说法: “如果事情进展顺利,我不会返回任何有用的值”。 在出现错误的情况下,我们返回 std::fmt::Error。

  • Rust 没有运行时异常。 相反,当出现问题时,您显式地返回它。 几乎所有的代码都使用 Result 类型来跟踪出错的情况。 这比基于异常的语言更加明确。 但是,像 C 语言这样的语言,很容易忘记检查返回类型以确定是否成功,或者进行错误处理的冗长乏味,Rust 使这个过程不那么痛苦。 我们待会再处理。

    • 注意: Rust确实有 panic 的概念,实际上它的行为类似于运行时异常。 但是,有两个重要区别。 首先,按照惯例,代码不应使用 panic 机制来发信号通知正常错误情况(如未找到文件),而应为完全意外的故障(如逻辑错误)保留panic。 其次,panic(大部分)是无法恢复的,这意味着它们会破坏当前线程。

    • 该文档的先前版本表示,紧急情况是无法恢复的,并且它们破坏了整个线程。然而,正如 j Haigh 指出的那样,这并不完全正确:catch unwind 函数通常允许您在不丢失当前线程的情况下捕获并从恐慌中恢复。 我不打算在这里谈论更多细节。

太棒了,这种类型签名本身就为我们提供了足够的素材,可供我们继续学习约5课! 不用担心,您将能够在不了解所有这些细节的情况下编写一些Rust代码,正如我们在本课程的其余部分中将演示的那样。 但是,如果您真的很喜欢冒险,请随时浏览Rust书以获取更多信息。

分号

回到我们的代码,实现我们的 fmt 方法:

struct Person {
    name: String,
    age: u32,
}

impl std::fmt::Display for Person {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(fmt, "{} ({} years old)", self.name, self.age)
    }
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("Person: {}", alice);
}

我们现在使用 write! 宏,将内容写入提供给我们的 Formatter 方法中。 这超出了我们的讨论范围,但是与生成一堆中间字符串相比,这允许更高效地构造值和生成I/O。

该方法的 &self 参数是一种特殊的说法,即“这是一个在这个对象上工作的方法”。 这与您在 Python 中编写代码的方式非常相似,尽管在 Rust 中,您必须处理按值传递与按引用传递。

第二个参数名为 fmt,&mut Formatter 是它的类型。

非常细心的人可能已经注意到,上面提到的错误消息是 Self。 但是,在我们的实现中,我们使用了 &self。 区别在于 &Self 是指值的类型,而小写的 &self 是值本身。 实际上,&self 参数语法是 self: &Self 的语法糖。

有人注意到缺少的东西吗? 您可能会认为我打错了字。 写完后分号在哪里? 好吧,首先,复制该代码并运行它,以向自己证明这不是拼写错误,并且该代码有效。 现在添加分号,然后尝试再次编译。 你会得到这样的东西:

error[E0308]: mismatched types
 --> src/main.rs:7:81
  |
7 |       fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
  |  _________________________________________________________________________________^
8 | |         write!(fmt, "{} ({} years old)", self.name, self.age);
  | |                                                              - help: consider removing this semicolon
9 | |     }
  | |_____^ expected enum `std::result::Result`, found ()
  |
  = note: expected type `std::result::Result<(), std::fmt::Error>`
             found type `()`

这可能会在 Rust 中造成巨大的混乱。 让我指出您可能已经注意到的事情,如果您是 C / C ++ / Java 背景:我们的方法有一个返回值,但我们从未使用过return!

第二个问题的答案很简单: Rust 中函数生成的最后一个值作为它的返回值。 这与 Ruby 和 Haskell 非常相似。 只有提前终止才需要返回。

但是我们仍然留下了第一个问题: 为什么这里我们不需要一个分号,为什么加上分号破坏了我们的代码? Rust 中的分号用于终止语句。 类似于我们之前看到的 use 语句,或者我们在这里简要演示的 let 语句, 它们的值总是 unit 或 ()。 这就是为什么当我们添加分号时,错误信息显示 found type‘()’ 。 去掉分号,表达式本身就是返回值,这就是我们想要的。

Rust 是一种面向表达式的语言,这种东西就是它所指的。 你可以在常见问题解答中看到这一点。就个人而言,我发现这样的分号用法可能很微妙,当我的 C/C++/Java 习惯逐渐出现时,我有时仍然会本能地尝试使用分号。但是幸运的是,编译器可以很快地识别出它们。

数字类型

在最后一个概念开始之前我们放入一些代码,我们将从使用数字开始。使用数字有一个很好的理由: 它们是复制值,编译器自动为我们克隆值。请记住,Rust 的很大一部分是所有权,并跟踪谁拥有不平凡的东西。但是,对于基本数值类型,复制这些值非常便宜,编译器会自动为您完成。 这就是我在介绍文章中提到的一些自动魔法。

为了演示,让我们来看看一些适用于数值类型的代码:

fn main() {
    let val: i32 = 42;
    printer(val);
    printer(val);
}

fn printer(val: i32) {
    println!("The value is: {}", val);
}

我们使用 let 语句创建了一个新变量 val。 我们已经明确指出它的类型是 i32,或者是一个32 位带符号整数。 通常,Rust 不需要这些类型注释,因为它通常能够推断类型。 尝试在这里省略类型注释。 不管怎样,我们在 val 上调用函数打印机两次。 一切正常。

现在,让我们用一个字符串来代替。 String 是一个堆分配的值,可以通过 String::from 从字符串文字创建。 (稍后将详细介绍许多字符串类型)。 复制 String 代价很高,所以编译器不会自动为我们做这件事。 因此,这段代码不能编译:

fn main() {
    let val: String = String::from("Hello, World!");
    printer(val);
    printer(val);
}

fn printer(val: String) {
    println!("The value is: {}", val);
}

你会得到这个吓人的错误消息:

error[E0382]: use of moved value: `val`
 --> src/main.rs:4:13
  |
3 |     printer(val);
  |             --- value moved here
4 |     printer(val);
  |             ^^^ value used here after move
  |
  = note: move occurs because `val` has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

练习1

有两种简单的方法可以修复这个错误消息:

  • 一种是使用 String 的 clone () 方法

  • 另一种是将 printer 更改为获取对 String 的引用

实施这两种解决方案(解决方案将在练习解答单独公布)。

打印数字

我们将用三种不同的循环方式来结束这一课,以打印数字1到10。 我让读者猜猜哪一种方法最合乎习惯。

loop 语句

loop 创建了一个无限循环。

fn main() {
    let i = 1;

    loop {
        println!("i == {}", i);
        if i >= 10 {
            break;
        } else {
            i += 1;
        }
    }
}

练习2:

这段代码不太管用。 不要问编译器就应该试着找出原因。 如果找不到问题,试着编译它。 然后修改代码。 如果您想知道:您可以等效地使用 return 或 return() 退出循环,因为循环的末尾也是函数的末尾。

while 语句

这类似于 c 样式的 while 循环: 它需要一个条件来检查。

fn main() {
    let i = 1;

    while i <= 10 {
        println!("i == {}", i);
        i += 1;
    }
}

这与前面的例子有相同的错误。

for 循环

For 循环允许您对集合中的每个值执行一些操作。 集合是使用迭代器惰性地生成的,这是 Rust 语言中内置的一个很好的概念。 迭代器与 Python 中的生成器有些类似。

fn main() {
    for i in 1..11 {
        println!("i == {}", i);
    }
}

练习3: 额外的分号

你能在上面的例子中省略分号吗? 与其把代码猛地扔进编译器,不如好好想想什么时候可以扔掉分号,什么时候不可以扔掉分号。

练习4:Fizzbuzz

在 Rust 中实现 Fizzbuzz。规则如下:

  1. 打印数字1到100

  2. 如果数字是3的倍数,输出 fizz 而不是数字

  3. 如果数字是5的倍数,输出 buzz 而不是数字

  4. 如果数字是3和5的倍数,输出fizzbuzz而不是数字

下一节

下一次,我们计划深入了解更多关于所有权的细节,本系列中的计划颇具延展性。 敬请期待。

最后更新于