速成课第1节
最后更新于
这有帮助吗?
最后更新于
这有帮助吗?
在本课中,我们只需要掌握一些基础知识: 工具、编译能力、基本语法等等。 让我们从工具开始,您可以在下载东西的同时继续阅读。
这篇文章是基于 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在找到这个系列中所有文章的链接。 也可 频道。
Rust 的引导需要是 rustup 工具,它将安装和管理 Rust 工具链。我之所以用复数形式,是因为它既可以管理 Rust 编译器的多个版本,也可以管理替代目标的交叉编译器。 现在,我们将做一些简单的事情。
这两页都会告诉你做同样的事情:
在类 Unix 系统运行 curl https://sh.rustup.rs -sSf | sh
或者运行一个 Windows Installer,可能是
请阅读 rust-lang 页面上关于设置 PATH 环境变量的说明。 对于类 unix 系统,你需要在 PATH 中添加 ~/.cargo/bin。
除了 rustup 可执行文件之外,您还将获得:
cargo
, Rust 构建工具
rustc
, Rust 编译器
好的,这部分很简单:cargo new hello && cd hello && cargo run
我们现在还没有学习所有关于 Cargo 的知识,但是需要掌握一些基本知识:
Cargo.toml
包含项目的元数据,包括依赖项
Cargo.lock
cargo自身生成
src
源文件,目前只包含src/main.rs
target
包含生成的文件
我们稍后将讨论源代码本身,首先是一些工具注释。
对于这么简单的东西,你不需要 Cargo 来完成。 相反,您可以只使用: rustc src/main.rs && ./main
。 如果你喜欢用这种方式进行代码实验,那就去做吧。 但是通常情况下,最好使用 cargo new
创建一个临时项目并在其中进行实验。 完全由你决定。
我们还不会在代码中添加任何测试,但是您可以使用 cargo test
在代码中运行测试。
两个有用的实用程序是 rustfmt 工具(用于自动格式化代码)和 clippy (用于获取代码建议)。 请注意 clippy 仍然是一个正在进行的工作,有时会给出错误的肯定。 为了让它们构建起来,需要运行:
然后你可以使用以下命令运行它们:
有一些 IDE 支持给那些想要它的人。我听说过有关 IntelliJ IDEA 的 Rust 插件很棒的事情。 就个人而言,我还没有使用过,但首先我也不是一个IDE用户。 此速成班将不使用任何IDE,仅提供基本的文本编辑器支持。
好了,我们终于可以查看 src/main.rs 中的源代码了:
很简单。 fn 表示正在编写一个函数。 名字是 main。 它不带参数,也没有返回值。 (或者,更准确地说,它返回单元类型, 类似于 C/C++ 中void,但实际上更接近 Haskell 中的单元类型)。字符串文字看起来非常普通,函数调用与其他 c 样式的语言几乎完全相同。
这是第一个“速成班”部分:为什么 println 后面有个叹号? 我说“速成班”是因为当我第一次学习 Rust 的时候,我没有看到这方面的解释,这让我困扰了一段时间。
Println 不是一个函数。 这是一个宏(macro)。 这是因为它采用一个格式字符串,需要在编译时检查它。 要证明这一点,请尝试将字符串更改为包含 {} 的字符串。 你会得到一个错误消息,大致如下:
这可以通过提供一个填充占位符参数来解决:
猜猜输出结果是什么,你可能是对的。 但是这给我们留下了一个问题: println 是怎么做到的! 宏知道如何显示这些不同类型?
更多的速成课时间! 为了更好地了解 Display 的工作原理,让我们触发一个编译时错误。 为此,我们将定义一个新的数据类型 Person,创建该类型的值,并尝试打印它:
如果你尝试编译它,你会得到:
这有点罗嗦,但重要的是没有为 Person 实现 std::fmt::Display
Traits。 在 Rust 中,Traits 类似于 Java 中的接口,或者更好的类似于 Haskell 的 typeclass。 (是否注意到某种类似于 Haskell 概念的模式? 是的,我也这样认为。)
定义自己的特征,并在以后学习实现这些特征,我们将获得很多乐趣。 但是我们现在正面临崩溃。 因此,让我们在此处添加该特征的实现:
但这没有奏效:
我们没有将 Display 导入到本地名称空间中。 编译器很有帮助地推荐了两个我们可能需要的不同特性,并告诉我们可以使用 use 语句将它们导入到本地名称空间中。 我们在前面的错误消息中看到,我们希望 std::fmt::Display,因此将 use std::fmt::Display 添加到 src/main.rs 会修复这个错误消息。但是只是为了证明这一点,不需要使用 use 语句! 我们可以改为:
好极了,我们之前的错误消息已经被其他东西取代了:
我们正在迅速达到“踢轮胎”课程要涵盖的内容的极限。但是希望这能帮助我们为下一次播下种子。
错误消息告诉我们需要在 Display trait 的实现中包含 fmt 方法, 也告诉我们这个的类型签名是什么。 让我们来看看这个签名,或者至少看看错误消息是怎么说的:
这里有很多东西要整理。 我将对每一部分都应用术语,您有可能不会完全理解这些。
Self
是得到实现的事物的类型。 在这种情况下,就是那个 Person
。
在开头添加 &
使其成为对值的引用,而不是值本身。 C++ 开发人员已经习惯了该概念。 许多其他语言也谈论通过引用传递。 在 Rust 中,这与所有权有很大关系。 在 Rust 中,所有权是一个非常重要的话题,我们现在不再讨论。
&mut
是可变的引用。 默认情况下,Rust 中的所有内容都是不可变的,您必须明确地说出是可变的。 稍后我们将探讨为什么引用的可变性对于 Rust 的所有权至关重要。
无论如何,第二个参数是对 Formatter 的可变引用。 Formatter 之后的 <'_>
是什么? 这是生命周期参数。 这也与所有权有关。 我们以后也会谈到。
->
表示我们提供函数的返回类型。
Result 是一个枚举,它是一个类型或带标记的联合。 两个类型参数是泛型:成功时的值 和 错误时的值。
在成功的情况下,我们的函数返回一个 () 或 单元值。 另一种说法: “如果事情进展顺利,我不会返回任何有用的值”。 在出现错误的情况下,我们返回 std::fmt::Error。
Rust 没有运行时异常。 相反,当出现问题时,您显式地返回它。 几乎所有的代码都使用 Result 类型来跟踪出错的情况。 这比基于异常的语言更加明确。 但是,像 C 语言这样的语言,很容易忘记检查返回类型以确定是否成功,或者进行错误处理的冗长乏味,Rust 使这个过程不那么痛苦。 我们待会再处理。
注意: Rust确实有 panic 的概念,实际上它的行为类似于运行时异常。 但是,有两个重要区别。 首先,按照惯例,代码不应使用 panic 机制来发信号通知正常错误情况(如未找到文件),而应为完全意外的故障(如逻辑错误)保留panic。 其次,panic(大部分)是无法恢复的,这意味着它们会破坏当前线程。
太棒了,这种类型签名本身就为我们提供了足够的素材,可供我们继续学习约5课! 不用担心,您将能够在不了解所有这些细节的情况下编写一些Rust代码,正如我们在本课程的其余部分中将演示的那样。 但是,如果您真的很喜欢冒险,请随时浏览Rust书以获取更多信息。
回到我们的代码,实现我们的 fmt 方法:
我们现在使用 write! 宏,将内容写入提供给我们的 Formatter 方法中。 这超出了我们的讨论范围,但是与生成一堆中间字符串相比,这允许更高效地构造值和生成I/O。
该方法的 &self 参数是一种特殊的说法,即“这是一个在这个对象上工作的方法”。 这与您在 Python 中编写代码的方式非常相似,尽管在 Rust 中,您必须处理按值传递与按引用传递。
第二个参数名为 fmt,&mut Formatter 是它的类型。
非常细心的人可能已经注意到,上面提到的错误消息是 Self。 但是,在我们的实现中,我们使用了 &self。 区别在于 &Self 是指值的类型,而小写的 &self 是值本身。 实际上,&self 参数语法是 self: &Self 的语法糖。
有人注意到缺少的东西吗? 您可能会认为我打错了字。 写完后分号在哪里? 好吧,首先,复制该代码并运行它,以向自己证明这不是拼写错误,并且该代码有效。 现在添加分号,然后尝试再次编译。 你会得到这样的东西:
这可能会在 Rust 中造成巨大的混乱。 让我指出您可能已经注意到的事情,如果您是 C / C ++ / Java 背景:我们的方法有一个返回值,但我们从未使用过return!
第二个问题的答案很简单: Rust 中函数生成的最后一个值作为它的返回值。 这与 Ruby 和 Haskell 非常相似。 只有提前终止才需要返回。
但是我们仍然留下了第一个问题: 为什么这里我们不需要一个分号,为什么加上分号破坏了我们的代码? Rust 中的分号用于终止语句。 类似于我们之前看到的 use 语句,或者我们在这里简要演示的 let 语句, 它们的值总是 unit 或 ()。 这就是为什么当我们添加分号时,错误信息显示 found type‘()’ 。 去掉分号,表达式本身就是返回值,这就是我们想要的。
Rust 是一种面向表达式的语言,这种东西就是它所指的。 你可以在常见问题解答中看到这一点。就个人而言,我发现这样的分号用法可能很微妙,当我的 C/C++/Java 习惯逐渐出现时,我有时仍然会本能地尝试使用分号。但是幸运的是,编译器可以很快地识别出它们。
在最后一个概念开始之前我们放入一些代码,我们将从使用数字开始。使用数字有一个很好的理由: 它们是复制值,编译器自动为我们克隆值。请记住,Rust 的很大一部分是所有权,并跟踪谁拥有不平凡的东西。但是,对于基本数值类型,复制这些值非常便宜,编译器会自动为您完成。 这就是我在介绍文章中提到的一些自动魔法。
为了演示,让我们来看看一些适用于数值类型的代码:
我们使用 let 语句创建了一个新变量 val。 我们已经明确指出它的类型是 i32,或者是一个32 位带符号整数。 通常,Rust 不需要这些类型注释,因为它通常能够推断类型。 尝试在这里省略类型注释。 不管怎样,我们在 val 上调用函数打印机两次。 一切正常。
现在,让我们用一个字符串来代替。 String 是一个堆分配的值,可以通过 String::from 从字符串文字创建。 (稍后将详细介绍许多字符串类型)。 复制 String 代价很高,所以编译器不会自动为我们做这件事。 因此,这段代码不能编译:
你会得到这个吓人的错误消息:
练习1
有两种简单的方法可以修复这个错误消息:
一种是使用 String 的 clone () 方法
另一种是将 printer 更改为获取对 String 的引用
实施这两种解决方案(解决方案将在练习解答单独公布)。
我们将用三种不同的循环方式来结束这一课,以打印数字1到10。 我让读者猜猜哪一种方法最合乎习惯。
loop
创建了一个无限循环。
练习2:
这段代码不太管用。 不要问编译器就应该试着找出原因。 如果找不到问题,试着编译它。 然后修改代码。 如果您想知道:您可以等效地使用 return 或 return() 退出循环,因为循环的末尾也是函数的末尾。
这类似于 c 样式的 while 循环: 它需要一个条件来检查。
这与前面的例子有相同的错误。
For 循环允许您对集合中的每个值执行一些操作。 集合是使用迭代器惰性地生成的,这是 Rust 语言中内置的一个很好的概念。 迭代器与 Python 中的生成器有些类似。
练习3: 额外的分号
你能在上面的例子中省略分号吗? 与其把代码猛地扔进编译器,不如好好想想什么时候可以扔掉分号,什么时候不可以扔掉分号。
练习4:Fizzbuzz
在 Rust 中实现 Fizzbuzz。规则如下:
打印数字1到100
如果数字是3的倍数,输出 fizz 而不是数字
如果数字是5的倍数,输出 buzz 而不是数字
如果数字是3和5的倍数,输出fizzbuzz而不是数字
下一次,我们计划深入了解更多关于所有权的细节,本系列中的计划颇具延展性。 敬请期待。
稍后我们将介绍更多关于定义自己的结构和枚举的例子,但是如果您感到好奇,可以阅读。
该文档的先前版本表示,紧急情况是无法恢复的,并且它们破坏了整个线程。然而,正如 j Haigh 指出的那样,这并不完全正确: 函数通常允许您在不丢失当前线程的情况下捕获并从恐慌中恢复。 我不打算在这里谈论更多细节。