【译】Rust 常见的问题

原文:https://github.com/dtolnay/rust-faq

本文档的存在是为了回答有关 Rust 编程语言的常见问题。它不是一个完整的语言指南,也不是一个教授该语言的工具。它只是一个参考,用来回答 Rust 社区中人们经常遇到的问题,并澄清 Rust 的一些设计决定背后的原因。

如果你觉得有一些常见的或重要的问题在这里没有得到解答,请在 GitHub 上针对这个 repo提一个 issue!

这些内容大部分以前都在 rust-lang/rust 库中,并且在网站上有一个专门的 FAQ 页面。但是在 2018 年的网站重新设计中,它被删除了。我在这里把它恢复了,因为这些问题中的许多问题仍然被频繁询问。

目录

The Rust Project

这个项目的目标是什么?

设计并实现一种安全的、并发的、实用的系统级语言。

Rust 之所以存在,是因为在这个抽象和效率水平上的其他语言并不令人满意。特别是:

  1. 对安全性的关注太少。
  2. 他们对并发性的支持很差。
  3. 缺乏实际的承受力。
  4. 它们对资源的控制有限。

Rust 作为一种替代方案存在,它既能提供高效的代码,又能提供舒适的抽象水平,同时在上述四点上都有改进。

这个项目是由 Mozilla 控制的吗?

Rust 在 2006 年作为 Graydon Hoare 的兼职项目开始,并保持了 3 年多。2009 年,当该语言成熟到可以运行基本测试并展示其核心概念时,Mozilla 参与其中。虽然它仍然由 Mozilla 赞助,但 Rust 是由来自世界各地不同地方的爱好者组成的一个多样化社区开发的。Rust 团队由 Mozilla 和非 Mozilla 成员组成,GitHub 上的rust到目前为止已经有超过2300 个独特的贡献者

项目管理而言,Rust 由一个核心团队管理,为项目设定愿景和优先级。从全球角度来指导它。还有一些子团队来指导和促进特定兴趣领域的发展,包括核心语言、编译器、Rust 库、Rust 工具和 Rust 官方社区的管理。这些领域的设计都是通过[RFC](https://github.com/rust-lang/rfcs)来推进的。对于不需要 RFC 的变化,通过rustc仓库的 PR 来决定。

Rust的一些非目标是什么?

  1. 我们不采用任何特别前沿的技术。旧的、成熟的技术会更好。
  2. 我们并不把表现力、极简主义或优雅性置于其他目标之上。这些都是可取的,但是从属的目标。
  3. 我们不打算涵盖 C++ 或任何其他语言的完整功能集。Rust 应该提供大多数情况下的功能。
  4. 我们不打算做到 100% 的静态,100% 的安全,100% 的反射,或在任何其他意义上过于教条化。存在权衡。
  5. 我们不要求 Rust 在“所有可能的平台”上运行。它最终必须在广泛使用的硬件和软件平台上没有不必要的妥协地运行。

Mozilla 在哪些项目中使用 Rust?

主要的项目是Servo,这是 Mozilla 正在进行的实验性浏览器引擎。他们也在努力将Rust 组件整合到 Firefox 中。

有哪些大型 Rust 项目的例子?

现在最大的两个 Rust 开源项目是ServoRust 编译器本身。

还有谁在使用 Rust?

越来越多的组织!

我怎样才能轻松地尝试 Rust?

尝试Rust的最简单方法是通过playpen,这是一个用于编写和运行 Rust 代码的在线应用程序。如果你想在你的系统上尝试 Rust,安装它并通过书中的猜谜游戏教程。

我怎样才能得到 Rust 问题的帮助?

有几种方法。你可以。

为什么 Rust 随着时间的推移发生了如此大的变化?

Rust 最初的目标是创造一种安全但可用的系统编程语言。在追求这一目标的过程中,它探索了很多想法,其中一些被保留了下来(生命周期,Trait),而另一些则被抛弃了(类型状态系统,绿色线程)。另外,在 1.0 之前,很多标准库都被重写了,因为早期的设计被更新以最好地使用 Rust 的特性,并提供高质量的、一致的跨平台 API。现在 Rust 已经达到了 1.0,该语言被保证是“稳定的”;虽然它可能继续发展,但在当前 Rust 上运行的代码应该继续在未来的版本上运行。

Rust语言的版本管理是如何进行的?

Rust 的语言版本管理遵循SemVer,只有当需要进行编译器错误的修复、安全漏洞的修补或者需要更多注释以改变类型推断和分发的时候,才允许在小版本中对稳定的 API 进行向后不兼容的修改。更详细的小版本修改指南可参考语言标准库的 RFC。

Rust 有三个“发布 channel”:稳定版、测试版和 nightly 版。稳定版和测试版每六周更新一次,当前的 nightly 版成为新的测试版,而当前的 nightly 版成为新的稳定版。标记为不稳定的语言和标准库功能或隐藏在特性开关后面的功能只能在 nightly 中使用。新功能以不稳定的形式出现,一旦被核心团队和相关的子团队批准,就会被“解禁”。这种方法允许实验,同时为稳定频道提供强大的向后兼容性保证。

更多的细节,请阅读 Rust 的博文“Stability as a Deliverable”

我可以在测试版或稳定版频道上使用不稳定的功能吗?

不,你不能。Rust 努力为测试版和稳定版频道上提供的功能的稳定性提供强有力的保证。当某项功能不稳定时,这意味着我们还不能为它提供这些保证,并且不希望人们依赖它保持不变。这使我们有机会在 nightly 上尝试改变,同时仍然为寻求稳定的人保持强有力的保证。

事情一直在变稳定,测试版和稳定版频道每六周更新一次,其他时候偶尔也会接受测试版的修复。如果你在等待一个功能,而不使用 nightly,你可以通过检查问题追踪器上的B-unstable标签来定位其追踪问题。

什么是“特性开关”?

“特性开关”是 Rust 用来稳定编译器、语言和标准库的特性的机制。一个被“开关控制”的特性只有在 nightly 上才能访问,而且只有在通过#[feature]属性或-Z unstable-options命令行参数明确启用后才能访问。当一个特性被稳定化后,它就可以在稳定发布通道上使用,并且不需要明确启用,这时候这个特性就被认为是稳定的。特性开关允许开发者在开发中的实验性功能在它们在稳定语言中可用之前进行测试。

为什么要采用 MIT/ASL2 双许可证?

Apache 许可证包括对专利侵犯的重要保护,但它与 GPL 第 2 版不兼容。为了避免 Rust 与 GPL2 的使用出现问题,Rust 采用了 MIT 许可。

为什么是 BSD 风格的许可,而不是 MPL 或三合一许可?

这一方面是由于原始开发者(Graydon)的偏好,另一方面是由于语言往往比网络浏览器等产品有更广泛的受众和更多样化的可能嵌入和最终用途。我们希望尽可能多地吸引这些潜在的贡献者。

性能

Rust有多快?

非常快! 在许多基准测试中,Rust 已经可以与 C 和 C++ 竞争(比如基准游戏其他)。

像 C++ 一样,Rust 把零成本抽象作为它的核心原则之一:Rust 的抽象没有一个施加全局性能惩罚,也没有传统意义上的任何运行时系统的开销。

鉴于 Rust 是建立在 LLVM 之上的,并努力从 LLVM 的角度类似于 Clang,任何 LLVM 的性能改进也有助于 Rust。从长远来看,Rust 的类型系统中更丰富的信息也应该能够实现 C/C++ 代码难以实现或无法实现的优化。

Rust 有垃圾收集吗?

不,Rust 的关键创新之一是在不需要垃圾收集的同时保证内存安全(无 segfault)。

通过避免 GC,Rust 可以提供许多好处:可预测的资源清理,较低的内存管理开销,以及基本上没有运行时系统。所有这些特征都使 Rust 变得精干,并且容易嵌入到任意的上下文中,并使其更容易将 Rust 代码与有 GC 的语言集成

Rust 通过其所有权和借用系统避免了对 GC 的需求,但同样的系统也有助于解决一系列其他问题,包括
一般的资源管理并发性

当单一所有权不够用时,Rust 程序依靠标准的引用计数智能指针类型Rc,以及它的线程安全对应类型Arc,而不是 GC。

然而,我们正在研究可选的垃圾收集作为未来的扩展。我们的目标是使其能够顺利地与垃圾收集的运行时,例如那些由SpidermonkeyV8的 JavaScript 引擎提供的。最后,一些人已经在没有编译器的支持情况下研究了实现纯 Rust 垃圾收集器

为什么我的程序很慢?

Rust 编译器不会用优化来编译,除非被要求这样做,因为优化会降低编译速度,而且在开发过程中通常是不可取的

如果你用cargo编译,请使用--release标志。如果你直接用rustc编译,使用-O标志。这两个标志中的任何一个都会打开优化功能。

Rust的编译似乎很慢。这是为什么呢?

代码翻译和优化。Rust 提供了高水平的抽象,可以编译成高效的机器代码,这些翻译需要时间来运行,特别是在优化时。

但是 Rust 的编译时间并不像看起来那么糟糕,而且有理由相信它会有所改善。当比较 C++ 和 Rust 之间类似规模的项目时,一般认为整个项目的编译时间是相当的。人们普遍认为 Rust 的编译速度很慢,这在很大程度上是由于 C++ 和 Rust 的编译模型的不同。C++ 的编译单元是文件,而 Rust 的编译单元是由许多文件组成的 crate。因此,在开发过程中,修改一个 C++ 文件可能会导致比 Rust 少得多的重新编译。目前正在努力重构编译器以引入增量编译,这将为 Rust 提供 C++ 模型的编译时间优势。

除了编译模型之外,Rust 的语言设计和编译器实现还有其他几个方面会影响编译时的性能。

首先,Rust 有一个适度复杂的类型系统,必须花费不可忽视的编译时间来执行约束,使 Rust 在运行时安全。

其次,Rust 编译器有长期的技术债务,特别是产生了质量很差的 LLVM IR,LLVM 必须花时间“修复”。在 Rust 编译器中加入一个新的内部表示法,称为MIR,有可能进行更多的优化,提高生成的 LLVM IR 的质量,但这项工作还没有发生过。

第三,Rust 使用 LLVM 来生成代码是一把双刃剑:虽然它使 Rust 拥有世界一流的运行时性能,但 LLVM 是一个大型框架,不注重编译时的性能,特别是在处理质量差的输入时。

最后,虽然 Rust 的首选策略是单态泛型(类似于 C++),但它要求生成的代码比其他翻译策略多得多。Rust 的程序员可以使用特征对象,通过使用动态调度来换取这种代码的膨胀。

为什么 Rust 的HashMap很慢?

默认情况下,Rust 的HashMap使用 SipHash 散列算法,该算法旨在防止散列表碰撞攻击,同时提供各种工作负载下的合理性能

虽然 SipHash 在许多情况下表现出有竞争力的性能,但它比其他散列算法明显慢的一种情况是在短键,如整数。这就是为什么 Rust 程序员经常观察到HashMap的性能缓慢。在这种情况下,经常推荐使用 FNV hasher,但要注意它不具备与 SipHash 一样的抗碰撞特性。

为什么没有集成的基准测试基础设施?

有,但它只在 nightly 上可用。我们最终计划建立一个可插拔的系统来进行综合基准测试,但与此同时,目前的系统被认为是不稳定的

Rust 是否做了尾调用优化?

一般来说,不会。在有限的情况下可能会进行尾部调用优化,但不保证。由于这个功能一直是人们所希望的,Rust 保留了一个关键字(become),尽管目前还不清楚它在技术上是否可行,也不清楚它是否会被实现。曾经有一个拟议的扩展,允许在某些情况下消除尾随调用,但目前被推迟了。

Rust 有 runtime 吗?

不是 Java 等语言所使用的典型意义上的运行时,但是 Rust 标准库的一部分可以被认为是“运行时”,它提供了一个堆、回溯、解开和堆栈守护。有一个少量的初始化代码,在用户的main函数之前运行。Rust 标准库还链接了 C 标准库,它也做了类似的运行时初始化。Rust 代码可以在没有标准库的情况下进行编译,在这种情况下,运行时与 C 语言大致相当。

语法

为什么要用大括号? 为什么 Rust 的语法不能像 Haskell 或 Python 那样?

使用大括号来表示块是各种编程语言中常见的设计选择,而 Rust 的一致性对于已经熟悉这种风格的人来说是很有用的。

大括号还可以为程序员提供更灵活的语法,并在编译器中提供更简单的解析器。

我可以在if条件上不加小括号,那么为什么我必须在单行块周围加上大括号?为什么不允许使用 C 语言的风格?

C 语言要求“if”语句的条件必须有小括号,但大括号是可选的,而 Rust 对其“if”表达式做出了相反的选择。这使得条件语句与语句主体明确分开,并避免了可选大括号的危害,这可能导致在重构过程中出现容易被忽略的错误,比如苹果的 goto fail 错误。

为什么没有字典的字面语法?

Rust 的整体设计倾向于限制语言的大小,同时启用强大的。虽然 Rust 确实为数组和字符串字面提供了初始化语法,但这是语言中唯一的集合类型。其他库定义的类型,包括无处不在的Vec集合类型,都使用宏进行初始化,如vec!宏。

这种使用 Rust 的宏设施来初始化集合的设计选择在未来可能会被通用地扩展到其他集合,不仅可以简单地初始化HashMapVec,还可以初始化其他集合类型,如BTreeMap。同时, 如果你想要一个更方便的初始化集合的语法, 你可以创建你自己的宏来提供它.

我应该在什么时候使用隐式返回?

Rust 是一种非常面向表达式的语言,而“隐式返回”是这种设计的一部分。像ifs, matches, 和普通块这样的结构在 Rust 中都是表达式。例如,下面的代码检查一个i64是否为奇数,通过简单地将其作为一个值来返回结果。

1
2
3
fn is_odd(x: i64) -> bool {
if x % 2 != 0 { true } else { false }
}

虽然它可以进一步简化,比如说。

1
2
3
fn is_odd(x: i64) -> bool {
x % 2 != 0
}

在每个例子中,函数的最后一行是该函数的返回值。需要注意的是,如果一个函数以分号结束,其返回类型将是(),表示没有返回值。隐式返回必须省略分号,才能发挥作用。

显式返回只有在隐式返回不可能时才会使用,因为你要在函数主体结束前返回。虽然上面的每个函数都可以用return关键字和分号来写,但这样做是不必要的冗长,而且与 Rust 代码的惯例不一致。

为什么不推断出函数的签名?

在 Rust 中,声明往往带有明确的类型,而实际代码的类型是推断出来的。这种设计有几个原因:

  • 强制性的声明签名有助于在模块和板块层面上执行接口的稳定性。
  • 签名提高了程序员对代码的理解,消除了 IDE 在整个板块中运行推理算法来猜测一个函数的参数类型的需要;它总是显式的,就在附近。
  • 在机制上,它简化了推理算法,因为推理只需要一次看一个函数。

为什么match必须是详尽的?

为了帮助重构和清晰化。

首先,如果每一种可能性都被match所覆盖,那么将来在enum中增加变体将导致编译失败,而不是在运行时出错。这种类型的编译器帮助使得 Rust 中的无畏重构成为可能。

其次,穷举式检查使默认情况的语义变得明确:一般来说,非穷举式match的唯一安全方式是在没有匹配到任何东西时让线程恐慌。Rust 的早期版本并不要求match情况是详尽的,而且发现它是一个很大的 bug 来源。

通过使用_通配符,可以很容易地忽略所有未指定的情况。

1
2
3
4
match val.do_something() {
Cat(a) => { /* ... */ }
_ => { /* ... */ }
}

Numerics

对于浮点运算,我应该选择f32f64中的哪一个?

选择哪种方式取决于程序的目的。

如果你对浮点数的最大精度感兴趣, 那么就选择f64. 如果你对保持数值的大小或最大的效率更感兴趣,并且不关心每个数值的位数较少所带来的误差,那么f32更好。对f32的操作通常更快,即使是在 64 位硬件上。作为一个常见的例子,图形编程通常使用f32,因为它需要高性能,而 32 位浮点数足以代表屏幕上的像素。

如果有疑问,可以选择f64以获得更大的精度。

为什么我不能比较浮点数或用它们作为HashMapBTreeMap的键?

浮点数可以用==, !=, <, <=, >, 和>=运算符,以及partial_cmp()函数进行比较。==!=PartialEq特性的一部分,而<<=>>=partial_cmp()PartialOrd 特性的一部分。

浮点数不能用cmp()函数进行比较,它是Ord特性的一部分,因为浮点数没有总排序。此外,浮点数没有全等关系,所以它们也没有实现Eq特性。

由于浮点数NaN不小于、大于或等于任何其他浮点数或其本身,所以浮点数没有总排序或平等关系。

因为浮点数没有实现EqOrd,所以它们不能被用于特质边界需要这些特质的类型,例如BTreeMap或[HashMap]。这一点很重要,因为这些类型假设它们的键提供了一个总排序或总等价关系,否则会出现故障。

有一个crate包装了f32f64以提供OrdEq的实现,这在某些情况下可能很有用。

我如何在数字类型之间进行转换?

有两种方法:as关键字,它为原始类型做简单的转换,以及IntoFrom特性,它们是为一些类型转换而实现的(你也可以为你自己的类型实现)。IntoFrom特性只在转换无损的情况下实现,所以例如,f64::from(0f32)会被编译,而f32::from(0f64)不会。另一方面,as将在任何两个原始类型之间进行转换,必要时截断数值。

为什么Rust没有增量和减量运算符?

Preincrement 和 Postincrement(以及与之对应的 Decrement)虽然方便,但也相当复杂。它们需要对计算顺序的了解,并经常导致 C 和 C++ 中的微妙错误和未定义行为。和x = x + 1相比x += 1只是稍微长一点,但不明确。

字符串

如何将一个StringVec<T>转换为一个片断(&str&[T])?

通常情况下,你可以在期望有片断的地方传递一个对StringVec<T>的引用。使用Deref coercionsStringsVecs在用&&mut传递引用时,将自动联合到各自的片上。

&str&[T]上实现的方法可以直接访问StringVec<T>。例如,some_string.trim()可以工作,尽管trim&str上的方法,而some_string是一个String

在某些情况下,例如通用代码,有必要进行手动转换。手动转换可以使用切片操作符来实现,像这样。&my_vec[...]

我如何从&str转换到String或反过来?

to_string()方法可以将&str转换为String,当你借用一个引用时,String自动转换为&str。这两种情况在下面的例子中都有演示。

1
2
3
4
5
6
7
8
fn main() {
let s = "Jane Doe".to_string();
say_hello(&s);
}

fn say_hello(name: &str) {
println! ("Hello {}!", name);
}

两种不同的字符串类型之间有什么区别?

String是一个在堆上分配的 UTF-8 字节的自有缓冲区。可变的String可以被修改,根据需要增加其容量。&str是在其他地方分配的String的一个固定容量的“视图”,如果是从String中引用的片断,通常在堆上,如果是字符串字面,在静态内存中。

&str是由 Rust 语言实现的原始类型,而String是由标准库实现的。

我如何在一个String中进行 O(1) 的字符访问?

你不能。至少在你不清楚“字符”是什么意思的情况下,以及在对字符串进行预处理以找到所需字符的索引的情况下是不行的。

Rust 字符串是 UTF-8 编码的。UTF-8 中的单个视觉字符不一定是一个字节,因为它在 ASCII 编码的字符串中是一个字节。每个字节被称为“代码单元”(在 UTF-16 中,代码单元是 2 个字节;在 UTF-32 中是4个字节)。“代码点”由一个或多个代码单元组成,并组合成最接近于字符的“字素群”。

因此,即使你可以对 UTF-8 字符串中的字节进行索引,你也无法在恒定时间内访问第 i 个码位或字母群。然而,如果你知道所需的码位或字形群从哪个字节开始,那么你就可以在恒定时间内访问它。包括str::find()和 regex 匹配在内的函数都会返回字节索引,以方便这种访问。

为什么字符串默认为 UTF-8?

str类型是 UTF-8,因为我们在野外观察到更多的文本是用这种编码的–特别是在网络传输中,它是 endian-agnostic 的–而且我们认为最好不要让 I/O 的默认处理涉及到在每个方向重新编码代码点。

这确实意味着在一个字符串中定位一个特定的 Unicode 编码点是一个 O(n) 操作,尽管如果开始的字节索引已经知道,那么它们可以在 O(1) 中被访问。一方面,这显然是不可取的;另一方面,这个问题充满了权衡,我们想指出几个重要的限定条件。

扫描一个str的 ASCII 范围的代码点仍然可以安全地逐个字节地进行。如果你使用.as_bytes(),取出一个u8只需花费O(1),并产生一个可以被转换并与 ASCII 范围的char比较的值。因此,如果你(比如)在\n上断行,基于字节的处理方法仍然有效。UTF-8 就是这样被精心设计的。

大多数“面向字符”的文本操作只有在非常有限的语言假设下才能工作,如“仅 ASCII 范围的代码点”。在 ASCII 范围之外,你往往不得不使用复杂的(非恒定时间)算法来确定语言单位(字形、单词、段落)的边界。我们建议使用一种“诚实的”具有语言意识的、经 Unicode 批准的算法。

char类型是 UTF-32。如果你确定你需要做一个代码点的算法,写一个type wstr = [char],并将一个str一次性解压到其中,然后用wstr工作,这是非常容易的。换句话说:如果你需要使用这种编码,语言没有默认解码为 UTF32的事实不应该阻止你解码(或以任何其他方式重新编码)。

关于为什么 UTF-8 通常比 UTF-16 或 UTF-32 更受欢迎,请阅读 UTF-8 Everywhere 宣言

我应该使用什么字符串类型?

Rust 有四对字符串类型,每一对都有不同的用途。在每一对中,都有一个“自有”的字符串类型,和一个“分片”的字符串类型。这个组织看起来像这样。

“Slice” type“Owned” type
UTF-8strString
OS-compatibleOsStrOsString
C-compatibleCStrCString
System pathPathPathBuf

Rust 的不同字符串类型有不同的用途。Stringstr是 UTF-8 编码的通用字符串。OsStringOsStr是根据当前平台编码的,在与操作系统交互时使用。CStringCStr相当于C 语言中的字符串,在 FFI 代码中使用。PathBufPath是对OsStringOsStr的方便包装,提供特定于路径操作的方法。

我怎样才能写一个既接受&str又接受String的函数?

有几种选择,取决于函数的需要。

  • 如果函数需要一个自有的字符串,但又想接受任何类型的字符串,可以使用一个Into<String>绑定。
  • 如果函数需要一个字符串分片,但希望接受任何类型的字符串,使用AsRef<str>绑定。
  • 如果函数不关心字符串的类型,而想统一处理这两种可能性,使用Cow<str>作为输入类型。

使用Into<String>

在这个例子中,该函数将同时接受自有字符串和字符串片,要么不做任何事情,要么在函数主体内将输入的字符串转换为自有字符串。注意,转换需要明确进行,否则不会发生。

1
2
3
4
fn accepts_both<S: Into<String>>(s: S) {
let s = s.into(); // 这将把 s 转换成一个`String`。
// ... 其余的函数
}

使用AsRef<str>

在这个例子中,该函数将接受拥有的字符串和字符串片断,要么不做任何事情,要么将输入的字符串片断转换为字符串。这可以通过引用输入来自动完成,像这样。

1
2
3
fn accepts_both<S: AsRef<str>>(s: &S) {
// ... 该函数的主体
}

使用Cow<str>

在这个例子中,函数接收了一个Cow<str>,它不是一个通用类型,而是一个容器,根据需要包含一个自有的字符串或字符串片断。

1
2
3
fn accepts_cow(s: Cow<str>) {
// ... 该函数的主体
}

集合

我可以在 Rust 中有效地实现向量和链表等数据结构吗?

如果你实现这些数据结构的原因是为了在其他程序中使用它们,那就没有必要了,因为这些数据结构的有效实现已经由标准库提供了。

然而,如果你的理由只是为了学习,那么你很可能需要涉足不安全代码。虽然这些数据结构可以完全用安全的 Rust 来实现,但其性能可能会比使用不安全的代码要差。原因很简单,向量和链接列表等数据结构依赖于指针和内存操作,而这些操作在安全 Rust 中是不允许的。

例如,一个双链接列表需要对每个节点有两个可变引用,但这违反了 Rust 的可变引用别名规则。你可以用Weak<T>来解决这个问题,但是性能会比你想要的差。使用不安全的代码,你可以绕过可变引用别名规则的限制,但必须手动验证你的代码是否引入了内存安全违规。

我怎样才能在不移动/消耗集合的情况下对其进行迭代?

最简单的方法是通过使用集合的IntoIterator实现。下面是一个关于&Vec的例子。

1
2
3
4
5
let v = vec! [1,2,3,4,5];
for item in &v {
print! ("{} ", item);
}
println! ("\nLength: {}", v.len());

Rust 的for循环对它们要迭代的东西调用into_iter()(定义在IntoIteratortrait 上)。任何实现了IntoIteratortrait 的东西都可以用for循环进行循环。IntoIterator是为&Vec&mut Vec实现的,导致来自into_iter()的迭代器借用集合的内容,而不是移动/消费它们。这对其他标准集合也是如此。

如果需要一个移动/消耗的迭代器,编写for循环时不要在迭代中使用&&mut

如果你需要直接访问一个借用的迭代器,你通常可以通过调用iter()方法得到它。

为什么我需要在数组声明中输入数组大小?

你不一定要这样做。如果你直接声明一个数组,大小是根据元素的数量推断出来的。但是如果你声明的是一个接收固定大小的数组的函数,编译器就必须知道这个数组有多大。

有一点需要注意的是,目前 Rust 并没有对不同大小的数组提供泛型。如果你想接受一个连续的可变数量的值的容器,使用Vec或 slice(取决于你是否需要所有权)。

所有权

我怎样才能实现一个包含环的图或其他数据结构?

至少有四种选择(在Too Many Linked Lists中详细讨论过)。

  • 你可以使用RcWeak实现它,以允许节点的共享所有权。尽管这种方法需要付出内存管理的代价。
  • 你可以使用“不安全”的代码实现它,使用原始指针。这将更加高效,但却绕过了 Rust 的安全保证。
  • 使用向量和这些向量的索引。有几个可用这种方法的例子和解释。
  • UnsafeCell使用借用的引用。对于这种方法有解释和代码

我怎样才能定义一个包含对其自身字段之一的引用的结构?

这是有可能的,但是这样做没有用。该结构会被自己永久借用,因此不能被移动。下面是一些说明这个问题的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::cell::Cell;

#[derive(Debug)]
struct Unmovable<'a> {
x: u32,
y: Cell<Option<&'a u32>>。
}

fn main() {
let test = Unmovable { x: 42, y: Cell::new(None) }。
test.y.set(Some(&test.x))。

println! ("{:?}", test);
}

按值传递、消耗、移动和转移所有权之间有什么区别?

这些是同一事物的不同术语。在所有的情况下,这意味着值已经被转移到另一个所有者那里,并且脱离了原所有者的占有,原所有者不能再使用它。如果一个类型实现了Copy特性,那么原所有者的值就不会被废止,仍然可以使用。

为什么某些类型的值在传递给一个函数后可以使用,而重复使用其他类型的值会导致错误?

如果一个类型实现了Copy特性,那么它在传递给函数时就会被复制。Rust 中的所有数字类型都实现了Copy,但结构类型默认不实现Copy,所以它们被移动。这意味着该结构不能再被用于其他地方,除非通过返回将其移回函数之外。

如何处理“use of moved value”的错误?

这个错误意味着你要使用的值已经被转移到一个新的所有者那里。首先要检查的是有关的移动是否是必要的:如果它移动到一个函数中,也许可以重写该函数以使用一个引用,而不是移动。否则,如果被移动的类型实现了Clone,那么在移动前对其调用clone()将移动它的一个副本,留下原始的仍然可以继续使用。但是请注意,克隆一个值通常应该是最后的手段,因为克隆可能很昂贵,会导致进一步的分配。

如果移动的值是你自己的自定义类型,考虑实现Copy(用于隐式复制,而不是移动)或Clone(显式复制)。Copy最常用的实现方式是#[derive(Copy, Clone)]Copy需要Clone),而Clone则是`#[derive(Clone)]。

如果这些都不可能,你可能想修改获得所有权的函数,以便在函数退出时返回数值的所有权。

在方法声明中使用self&self&mut self的规则是什么?

  • 当一个函数需要消耗值的时候,使用self
  • 当一个函数只需要一个对值的只读引用时,使用&self
  • 当一个函数需要在不消耗该值的情况下改变该值时,使用&mut self

我怎样才能理解借用检查器?

借用检查器在评估 Rust 代码时只应用一些规则,这些规则可以在 Rust 书的借用部分中找到。这些规则是:

首先,任何借用必须持续的范围不大于所有者的范围。第二,你可以有这两种借用中的一种或另一种,但不能同时存在:

  • 对一个资源的一个或多个引用(&T)。
  • 一个可变的引用(&mut T)。

虽然这些规则本身很简单,但持续地遵守这些规则并不容易,特别是对于那些不习惯推理寿命和所有权的人来说。

了解借用检查器的第一步是阅读它产生的错误。为了确保借用检查器在解决它所发现的问题方面提供高质量的帮助,我们做了大量的工作。当你遇到借用检查器的问题时,第一步是慢慢地、仔细地阅读所报告的错误,只有在理解了所描述的错误之后,才能接近代码。

第二步是熟悉 Rust 标准库提供的所有权和可变性相关的容器类型,包括CellRefCellCow。这些都是表达某些所有权和可变性情况的有用和必要的工具,并且被写成性能代价最小。

理解借用检查器最重要的一个部分是实践。Rust 的强静态分析保证是严格的,与许多程序员之前的工作有很大不同。需要一些时间才能完全适应一切。

如果你发现自己在借用检查器上挣扎,或者没有耐心了,请随时联系 Rust 社区寻求帮助。

什么时候Rc有用?

这在Rc的官方文档中有所涉及,Rust 的非原子引用计算的指针类型。简而言之,Rc和它的线程安全表亲Arc对于表达共享所有权是很有用的,当没有人访问相关内存时,系统会自动将其取消。

我如何从一个函数中返回一个闭包?

要从一个函数中返回一个闭包,它必须是一个“移动闭包”,也就是说,闭包是用move关键字声明的。正如 Rust 书中所解释的,这使得闭包拥有自己的捕获变量的副本,独立于其父级堆栈框架。否则,返回一个闭包将是不安全的,因为它将允许访问不再有效的变量;换句话说:它将允许读取可能无效的内存。闭包还必须被包裹在一个Box中,这样它就被分配在堆上。阅读更多关于这个的内容在书中

什么是 deref coercion,它是如何工作的?

deref coercion 是一个很方便的 coercion。自动将对指针的引用(例如, &Rc<T>&Box<T>)转换为对其内容的引用(例如,&T)。Deref coercion 的存在是为了使 Rust 的使用更符合人体工程学,并通过Deref特性实现。

Deref 的实现表明实现类型可以通过调用deref方法转换为目标类型,该方法接收对调用类型的不可变的引用,并返回对目标类型的引用(具有相同的生命周期)。*前缀操作符是deref方法的简写。

它们被称“coercions”,因为下面的规则,这里引用了 Rust 书

如果你有一个类型U,并且它实现了Deref<Target=T>,那么&U的值将自动被强制为T

例如,如果你有一个&Rc<String>,它将通过这个规则联合成一个&String,然后以同样的方式联合成一个&str。因此,如果一个函数需要一个&str参数,你可以直接传入一个&Rc<String>,所有的强制都通过Deref特性自动处理。

最常见的 Derefcoercions 种类是:

  • &Rc<T>&T
  • &Box<T>&T
  • &Arc<T>&T
  • &Vec<T>改为&[T]
  • &String改为&str

生命周期

为什么是生命周期?

生命周期是 Rust 对内存安全问题的回答。它允许 Rust 确保内存安全而不需要付出垃圾回收的性能代价。它们是基于各种学术工作的。

为什么生命周期的语法是这样的?

'a语法来自于 ML 系列编程语言,其中'a用于表示一个通用类型参数。对于 Rust 来说,这种语法必须是明确的、明显的,并且适合在类型声明中与 traits 和 reference 一起使用。其他的语法已经被讨论过了,但是还没有其他的语法被证明是更好的。

我如何将一个借来的东西返回到我从函数中创建的东西?

你需要确保借来的东西会超过函数的寿命。这可以通过将输出寿命与一些输入寿命绑定来实现,比如说。

1
2
3
4
5
6
7
8
type Pool = TypedArena<Thing>;

// 下面的生命周期只是为了说明问题而明确写的;它可以通过后面描述的删除规则省略。
fn create_borrowed<'a>(pool: &'a Pool,
x: i32,
y: i32) -> &'a Thing {
pool.alloc(Thing { x: x, y: y })
}

另一种方法是通过返回一个自有类型如String来完全消除引用。

1
2
3
fn happy_birthday(name: &str, age: i64) -> String {
format! ("Hello {}! You're {} years old!", name, age)
}

这种方法比较简单,但往往会导致不必要的分配。

为什么有些引用有寿命,如&'a T,而有些则没有,如&T

事实上, 所有引用类型都有一个寿命, 但大多数时候你不必明确写出
它是明确的。规则如下。

  1. 在一个函数体中,你永远不需要明确地写出生命周期;正确的值应该总是被推断出来的。

  2. 在一个函数的签名中(例如,在其参数的类型或其返回类型中),你可能会需要写一个生命周期。这里的生命周期使用一个简单的默认方案,称为“lifetime elision”。它由以下三条规则组成:

    • 在一个函数的参数中,每一个被省略的生命周期都成为一个独立的生命周期参数。
    • 如果正好只有一个输入生命周期,无论是否被省略,该生命周期都被分配给所有返回值中被省略的生命周期。
    • 如果有多个输入生命周期,但其中一个是 &self 或 &mut self,那么 self 的生命周期将被分配给所有被忽略的返回生命周期。
  3. 最后,在“结构”或“枚举”的定义中,所有的生命周期必须被明确地声明。

如果这些规则导致了编译错误,Rust 编译器将提供一个错误信息,指出所造成的错误,并根据推理过程的哪一步造成的错误,提出一个潜在的解决方案。

Rust如何保证“没有空指针”和“没有悬空指针”?

构造一个&Foo&mut Foo类型的值的唯一方法是指定一个引用所指向的Foo类型的现有值。引用在给定的代码区域内(引用的生命周期)“借用”原始值,在借用期间,被借用的值不能被移动或销毁。

我如何用“null”来表达一个值的缺失?

你可以用Option类型来做,它可以是Some(T)NoneSome(T)表示其中包含一个T类型的值,而None表示没有值。

泛型

什么是“单态化”?

单态化是将泛型函数(或结构)的每一次使用都基于调用该函数(或使用该结构)的参数类型用特定的实例进行单态化。

在单态化过程中,泛型函数的一个新副本被翻译为该函数实例化的每一组独特类型。这与 C++ 使用的策略相同。它的结果是为每个调用点专门设计的快速代码,并且是静态调度的,其代价是用许多不同类型实例化的函数会导致“代码膨胀”,即多个函数实例会导致比用其他翻译策略创建的二进制文件更大。

接受 Trait Object 而不是类型参数的函数不进行单态化。相反,特质对象上的方法在运行时被动态地分配。

一个函数和一个没有捕获任何变量的闭包之间有什么区别?

函数和闭包在操作上是等价的,但由于它们的实现方式不同,所以有不同的运行时表示。

函数是语言的内置基元,而闭包本质上是三种特征之一的语法糖。Fn, FnMut, 和 FnOnce。当你创建一个闭包时,Rust 编译器会自动创建一个结构,实现这三个结构的相应特性,并将捕获的环境变量作为成员,并使该结构可以作为一个函数被调用。裸露的函数不能捕获环境。

这些特征之间的最大区别是它们如何接受“self”参数。Fn使用&selfFnMut使用&mut self,而FnOnce使用self

即使一个闭包没有捕获任何环境变量,它在运行时也被表示为两个指针,与其他闭包相同。

什么是高阶类型,为什么我需要它们,以及为什么 Rust 没有它们?

高等类型是指具有未填充参数的类型。类型构造器,如VecResult,和HashMap都是高类型类型的例子:每个类型都需要一些额外的类型参数,以便实际表示一个特定的类型,如Vec<u32>。对高类型的支持意味着这些“不完整”的类型可以在任何可以使用“完整”类型的地方使用,包括作为函数的泛型。

任何完整的类型,像i32boolchar都属于*类型(这个符号来自类型理论领域)。一个有一个参数的类型,像Vec<T>是属于* -> *,意思是Vec<T>接收一个完整的类型,像i32,并返回一个完整类型Vec<i32>。一个有三个参数的类型,如HashMap<K, V, S>是一种* -> * -> * -> *,并接收三个完整的类型(如i32String,和RandomState),产生一个新的完整类型HashMap<i32, String, RandomState>

除了这些例子之外,类型构造函数还可以接受生命周期参数,我们将其表示为Lt。例如,slice::Iter的种类是Lt -> * -> *,因为它必须像Iter<'a, u32>一样被实例化。

由于缺乏对高阶类型的支持,因此很难编写某些类型的通用代码。对于像迭代器这样的概念的抽象来说,这尤其成问题,因为迭代器通常至少要在一个生命周期内进行参数化。这反过来又阻碍了对 Rust 的集合进行抽象的 traits 的创建。

另一个常见的例子是像 functors 或 monads 这样的概念,它们都是类型构造函数,而不是单一类型。

Rust 目前并不支持高类型的类型,因为与我们想做的其他改进相比,这并不是一个优先事项。由于该设计是一个重大的、跨领域的变化,我们也想谨慎地对待它。但是目前缺乏支持并没有什么内在的原因。

通用类型中像<T=Foo>这样的命名类型参数是什么意思?

这些被称为关联类型,它们允许表达不能用where子句表达的特征边界。例如,一个泛型约束X: Bar<T=Foo>意味着”X必须实现 trait Bar,在Bar的实现中,X必须选择Foo作为Bar的关联类型T“。这种约束不能通过where子句来表达的例子包括像Box<Bar<T=Foo>>这样的 trait object。

关联类型的存在是因为泛型经常涉及类型家族,其中一个类型决定了一个家族中的所有其他类型。例如,一个图的 trait 可能将图本身作为其Self类型,并有节点和边的关联类型。每个图的类型唯一地决定了相关的类型。使用关联类型使这些类型族的工作更加简洁,并且在许多情况下提供更好的类型推理。

我可以重载运算符吗? 哪些操作符,如何操作?

你可以使用它们的关联特性为各种运算符提供自定义的实现。Add代表+Mul代表*,等等。它看起来像这样。

1
2
3
4
5
6
7
8
9
10
11
use std::ops::Add。

struct Foo;

impl Add for Foo {
type Output = Foo;
fn add(self, rhs: Foo) -> Self::Output {
println!("Adding!");
self
}
}

以下操作符可以被重载。

OperationTrait
+Add
+=AddAssign
binary -Sub
-=SubAssign
*Mul
*=MulAssign
/Div
/=DivAssign
unary -Neg
%Rem
%=RemAssign
&BitAnd
&=BitAndAssign
|BitOr
|=BitOrAssign
^BitXor
^=BitXorAssign
!Not
<<Shl
<<=ShlAssign
>>Shr
>>=ShrAssign
*Deref
mut *DerefMut
[]Index
mut []IndexMut

为什么要在Eq/PartialEqOrd/PartialOrd之间划分?

在 Rust 中,有一些类型的值只有部分排序,或者只有部分相等。部分排序的意思是,在给定的类型中可能存在既不小于也不大于对方的值。部分平等意味着可能有给定类型的值不等于自己。

浮点类型(f32f64)是每种类型的很好的例子。任何浮点类型都可以有NaN(意思是“不是一个数字”)的值。NaN不等于自己(NaN == NaN是 false),也不小于或大于任何其他浮点值。因此,f32和[f64]都实现了PartialOrdPartialEq,但没有实现Ord和``Eq`]Eq

正如在先前关于 floats 的问题中解释的那样,这些区别很重要,因为有些集合依赖于总排序/equality,以便给出正确的结果。

输入/输出

如何将一个文件读成一个“字符串”?

使用read_to_string()方法, 这个方法是在std::io中的Read特性上定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::io::Read;
use std::fs::File;

fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut s = String::new();
let _ = File::open(path)?.read_to_string(&mut s); // `s` contains the contents of "foo.txt"
Ok(s)
}

fn main() {
match read_file("foo.txt") {
Ok(_) => println!("Got file contents!"),
Err(err) => println!("Getting file contents failed with error: {}", err)
};
}

如何有效地读取文件输入?

File类型实现了Read特性,它有多种函数用于读写数据,包括read(), read_to_end(), bytes(), chars(), 和take() 。这些函数中的每一个都从一个给定的文件中读取一定量的输入。read() 在一次调用中读取底层系统所能提供的输入量。read_to_end() 将整个缓冲区读入一个向量,需要多少空间就分配多少。bytes()chars()分别允许你对文件的字节和字符进行迭代。最后,take()允许你从文件中读取任意数量的字节。总的来说,这些应该允许你有效地读入任何你需要的数据。

对于缓冲读取,使用BufReader结构,这有助于减少读取时的系统调用数量。

我如何在 Rust 中进行异步输入/输出?

使用 tokio

我如何在 Rust 中获得命令行参数?

最简单的方法是使用Args,它提供了一个输入参数的迭代器。

如果你正在寻找更强大的库,在 crates.io 上有一些选项

错误处理

为什么 Rust 没有异常?

异常使控制流的理解复杂化,它们在类型系统之外表达有效性/无效性,而且它们与多线程代码(Rust 的主要焦点)的互操作性很差。

Rust 更倾向于采用基于类型的错误处理方法,这在书中有详细介绍。这与 Rust 的控制流、并发性和其他一切都更加吻合。

到处都有`unwrap()’是怎么回事?

unwrap()是一个提取OptionResult里面的值的函数,如果没有值就会 panic。

unwrap()不应该是你处理预期出现的错误的默认方式,例如用户输入不正确。在生产代码中,它应该被视为一个断言,即该值是非空的,如果违反,将使程序崩溃。

它对快速原型也很有用,在那里你还不想处理错误,或者在博客文章中,错误处理会分散对重点的注意力。

当我试图运行使用try!宏的示例代码时,为什么我得到一个错误?

这可能是函数的返回类型的问题。try!宏要么从Result中提取数值,要么提前返回,错误是Result携带的。这意味着try只对返回Result本身的函数有效,其中Err构造的类型实现了From::from(err)。特别是,这意味着try!宏不能在main函数中工作。

有没有比到处都是“Result”更简单的方法来做错误处理?

如果你正在寻找一种方法来避免在其他人的代码中处理Result,总是有unwrap(),但这可能不是你想要的。Result是一个指标,表明某些计算可能会或可能不会成功完成。要求你明确地处理这些失败是 Rust 鼓励健壮性的方式之一。Rust 提供了像try!这样的工具,使处理失败的过程符合人体工程学。

如果你真的不想处理错误,可以使用unwrap(),但要知道,这样做意味着代码在失败时 panic,这通常会导致关闭进程。

并发

我可以在没有“不安全”块的情况下跨线程使用静态值吗?

如果是同步的,修改是安全的。修改一个静态的Mutex(通过lazy-static crate 懒惰地初始化)不需要一个unsafe块,修改一个静态的AtomicUsize(可以不用 lazy_static 初始化)也是如此。

更一般地说,如果一个类型实现了Sync,并且没有实现Drop,它可以在static中使用

我可以写一个宏来生成标识符吗?

目前不能。Rust 的宏是“卫生宏”,它有意避免捕捉或创建可能与其他标识符发生意外碰撞的标识符。它们的功能与通常与 C 预处理器相关的宏的风格明显不同。宏调用只能出现在被明确支持的地方:项目、方法声明、语句、表达式和模式。这里,“方法声明”指的是可以放置方法的空白处。它们不能被用来完成部分方法声明。按照同样的逻辑,它们也不能用来完成一个部分变量声明。

Debugging and Tooling

我如何调试 Rust 程序?

Rust 程序可以使用 gdblldb 进行调试,与 C 和 C++ 相同。事实上,每一个 Rust 的安装都带有 rust-gdb 和 rust-lldb 中的一个或两个(取决于平台支持)。这些是对 gdb 和 lldb 的封装,并启用了 Rust pretty-printing。

rustc说标准库代码中发生了 panic。我如何定位我的代码中的错误?

这个错误通常是由客户端代码中unwrap()ing一个NoneErr引起的。通过设置环境变量RUST_BACKTRACE=1来启用回溯,有助于获得更多信息。在调试模式下编译(默认为“cargo build”)也有帮助。使用调试器,如提供的rust-gdbrust-lldb也很有帮助。

我应该使用什么 IDE?

Rust 的开发环境有很多选择,所有这些都在非官方的 IDE 支持页面上有详细说明。

Low-Level

我怎样才能memcpy字节?

如果你想安全地克隆一个现有的分片,你可以使用clone_from_slice

要复制可能重叠的字节,使用copy。要复制不重叠的字节,使用copy_nonoverlapping。这两个函数都是“不安全”的,因为它们都可以被用来破坏语言的安全保证。在使用它们时要注意。

没有标准库,Rust 能合理地运行吗?

当然可以。Rust 程序可以使用#![no_std]属性设置为不加载标准库。设置了这个属性后,你可以继续使用 Rust 核心库,它只是平台无关的原语。因此,它不包括 IO、并发性、堆分配等。

我可以用 Rust 写一个操作系统吗?

是的!事实上,有几个正在进行的项目就是这样

我如何在文件或其他字节流中以大数或小数格式读写数字类型如i32f64?

你应该看看 byteorder crate,它提供了相应的实用程序。

Rust 是否保证一个特定的数据布局?

默认情况下不是。在一般情况下,enumstruct的布局是未定义的。这允许编译器进行潜在的优化,比如为判别式重新使用填充物,压缩嵌套的enum的变体,重新排序字段以移除填充物,等等。不携带数据的enum(“C-like”)有资格拥有一个定义的表示。这种枚举很容易区分,因为它们只是一个没有数据的名字列表。

1
2
3
4
5
6
snum CLike {
A,
B = 32,
C = 34,
D
}

#[repr(C)]属性可以应用于这些“enum”,使它们在同等的 C 代码中具有相同的表示。这允许在 FFI 代码中使用 Rust 的“enum”,而在大多数情况下也使用 C 的“enum”。该属性也可以应用于struct,以获得与C struct相同的布局。

跨平台

在 Rust 中表达特定平台行为的习惯性方法是什么?

平台特定行为可以用条件编译属性来表达,如target_os, target_family, target_endian,等等。

Rust 可以用于 Android/iOS 编程吗?

是的,它可以! 在 AndroidiOS 中都已经有使用 Rust 的例子。它确实需要一些工作来设置,但 Rust 在这两个平台上的功能都很好。

我可以在网络浏览器中运行我的 Rust 程序吗?

有可能。Rust 对asm.jsWebAssembly都有实验性支持

我如何在 Rust 中进行交叉编译?

在 Rust 中可以进行交叉编译,但需要一点工作来设置。每个 Rust 编译器都是一个交叉编译器,但是库需要针对目标平台进行交叉编译。

Rust 确实为每个支持的平台分发了标准库的副本,这些副本包含在分发页面上找到的每个构建目录的rust-std-*文件中,但目前还没有自动安装的方法。

mod 和 crate

mod 和 crate 之间的关系是什么?

  • crate 是一个编译单元,它是 Rust 编译器可以操作的最小的代码量。
  • mod 是 crate 内的一个(可能是嵌套的)代码组织单元。
  • 一个 crate 包含一个隐含的、未命名的顶层 mod。
  • 递归定义可以跨越 mod,但不能跨越 crate。

为什么 Rust 编译器找不到我正在使用的这个库?

有很多可能的答案,但一个常见的错误是没有意识到use声明是相对于 crate root 的。试着改写你的声明,使用它们在你的项目根文件中定义的路径,看看是否能解决这个问题。

还有“self”和“super”,它们分别将“use”路径区分为相对于当前 mod 或父 mod。

关于use库的完整信息,请阅读 Rust 书中的“Packages, Crates, and Modules”一章。

为什么我必须在 crate 的顶层用mod声明 mod 文件,而不是直接use它们?

在 Rust 中,有两种方法来声明模块,内联或在另一个文件中。下面是各自的一个例子。

1
2
3
4
5
6
7
8
9
10
// In main.rs
mod hello {
pub fn f() {
println!("hello!");
}
}

fn main() {
hello::f();
}
1
2
3
4
5
6
7
8
9
10
11
// In main.rs
mod hello;

fn main() {
hello::f();
}

// In hello.rs
pub fn f() {
println!("hello!");
}

在第一个例子中,模块被定义在它所使用的同一文件中。在第二个例子中,主文件中的模块声明告诉编译器寻找hello.rshello/mod.rs,并加载该文件。

注意moduse之间的区别:mod声明一个模块的存在,而use引用一个在其他地方声明的模块,将其内容纳入当前模块的范围。

我如何配置 Cargo 使用代理?

参考 https://rsproxy.cn/

为什么我已经“use”了 crate,但编译器还是找不到方法的实现?

对于定义在 trait 上的方法,你必须明确导入 trait 声明。这意味着仅仅导入一个结构实现 trait 的模块是不够的,你还必须导入 trait 本身。

为什么编译器不能为我推断出use声明?

它可能可以,但你也不希望它这样做。虽然在很多情况下,编译器有可能通过简单地寻找给定标识符的定义位置来确定导入的正确模块,但在一般情况下可能不是这样的。rustc中任何用于选择竞争性选项的决策规则,在某些情况下可能会引起惊讶和混乱,Rust 更倾向于明确说明名称的来源。

例如,编译器可以说,在标识符定义相互竞争的情况下,会选择最早导入的模块的定义。所以如果模块foo和模块bar都定义了标识符baz,但是foo是第一个注册的模块,编译器会插入use foo::baz;

1
2
3
4
5
6
7
8
mod foo;
mod bar;

// use foo::baz // to be inserted by the compiler.

fn main() {
baz();
}

如果你知道这种情况会发生,也许它可以节省少量的按键,但它也大大增加了当你真正想把baz()变成bar::baz()时出现令人惊讶的错误信息的可能性,而且它通过使函数调用的意义依赖于模块声明而降低了代码的可读性。这些都是我们不愿意做的折衷。

然而,IDE 可以帮助管理声明,这将给你带来两方面的好处:机器协助拉入名字,但明确声明这些名字的来源。

我如何进行动态 Rust 库加载?

libloading 导入 Rust 中的动态库,它提供了一个跨平台的动态链接系统。

为什么 crates.io 没有命名空间?

引用 crates.io 设计的官方解释

在使用 crates.io 的第一个月里,很多人问我们是否有可能引入命名空间

虽然 namespace 允许多个作者使用单一的、通用的名称,但它们增加了包在 Rust 代码中的引用和人类对包的交流的复杂性。乍一看,它们允许多个作者使用“http”这样的名字,但这仅仅意味着人们需要将这些包称为“wycats’http”或“reem’http”,与“wycats-http”或“reem-http”这样的包名相比没有什么好处。

当我们研究没有命名空间的软件包生态系统时,我们发现人们倾向于使用更有创意的名字(如nokogiri而不是tenderlove's libxml2)。这些有创意的名字往往简短易记,部分原因是缺乏任何层次结构。它们使人们更容易简洁明了地交流软件包。他们创造了令人兴奋的品牌。我们已经看到了一些 10,000+ 软件包生态系统的成功,如 NPM 和 RubyGems,它们的社区在一个单一的命名空间内蓬勃发展。

简而言之,我们认为如果 Piston 选择bvssvni/game-engine这样的名字(允许其他用户选择wycats/game-engine)而不是简单的piston,那么 Cargo 的生态系统就不会好转。

因为命名空间在很多方面严格来说都比较复杂,而且如果将来有必要的话,还可以兼容添加,所以我们要坚持使用单一的共享命名空间。

我怎样才能发出 HTTP 请求?

标准库不包括 HTTP 的实现,所以你要使用一个外部的 crate。
reqwest 是最简单的。它建立在hyper上,用 Rust 编写,但也有一些其他的curl crate 被广泛使用,它提供了与 curl 库的绑定。

我如何用 Rust 编写 GUI 应用程序?

有多种方法可以在 Rust 中编写 GUI 应用程序。只要看看这个 GUI 框架的列表

我怎样才能解析 JSON/XML?

Serde是推荐的 Rust 数据序列化和反序列化的库,可以从许多不同的格式中获取。

是否有一个标准的 2D+ 矢量和形状 crate?

还没有! 想写一个吗?

我如何在 Rust 中编写一个 OpenGL 应用程序?

Glium 是 Rust 中 OpenGL 编程的主要库。GLFW 也是一个可靠的选择。

我可以用 Rust 写一个视频游戏吗?

是的,你可以。Rust 的主要游戏编程库是Piston,而且还有一个 Rust 游戏编程的 subreddit 和一个 IRC 频道(#rust-gamedev on Mozilla IRC)。

设计模式

Rust是面向对象的吗?

它是多范式的。很多在 OO 语言中可以做的事情在 Rust 中也可以做,但不是所有的事情,也不总是使用你所习惯的那种抽象方式。

我如何将面向对象的概念映射到 Rust 中?

这取决于。有一些方法可以将面向对象的概念,如多重继承翻译成 Rust,但由于 Rust 不是面向对象的,所以翻译的结果可能与它在 OO 语言中的外观有很大不同。

我如何处理带有可选参数的结构的配置?

最简单的方法是在你用来构建结构实例的任何函数中使用Option类型(通常是new())。另一种方法是使用构建器模式,在构建所构建的类型之前,只必须调用某些实例化成员变量的函数。

我如何在 Rust 中做全局变量?

Rust 中的全局变量可以使用const声明来实现编译时计算的全局常量,而static可以用来实现可变的全局变量。请注意,修改static mut变量需要使用unsafe,因为它允许数据竞争,而在安全的 Rust 中保证不会发生这种情况。conststatic值之间的一个重要区别是,你可以对static值进行引用,但不能对const值进行引用,后者没有指定的内存位置。关于conststatic的更多信息,请阅读 Rust 书

我如何设置程序化定义的编译时常量?

Rust 目前对编译时常量的支持有限。你可以使用“const”声明来定义基元(类似于“static”,但是是不可变的,在内存中没有指定的位置),也可以定义“const”函数和固有方法。

要定义不能通过这些机制定义的程序性常量,可以使用lazy-static crate,它通过在第一次使用时自动计算常量来模拟编译时计算。

我可以运行发生在 main 之前的初始化代码吗?

Rust 没有“在main之前的生命”的概念。最接近的是通过lazy-static crate 来完成,它通过在静态变量第一次使用时懒散地初始化静态变量来模拟“main之前”。

Rust 允许 globals 使用非结构表达式的值吗?

不允许。全局变量不能有一个非结构表达式的构造函数,也不能有一个析构函数。静态构造函数是不可取的,因为确保静态初始化顺序的可移植性是很困难的。main 之前的生命通常被认为是一个错误的功能,所以 Rust 不允许它。

参见 C++ FQA 中关于“静态初始化顺序惨败”的内容,以及 Eric Lippert 的博客中关于 C# 的挑战,它也有这种特性。

你可以用 lazy-static 工具箱来近似非内容表达式的 globals。

其他语言

我怎样才能在 Rust 中实现类似 C 语言的struct X { static int X; };的东西呢?

Rust 没有上面代码片断中所示的静态字段。相反,你可以在一个给定的模块中声明一个静态变量,这个变量对该模块是私有的。

我如何将 C 风格的枚举转换为整数,反之亦然?

将 C 风格的枚举转换为整数可以用as表达式来完成,比如e as i64(其中e是某个枚举)。

另一个方向的转换可以用match语句来完成, 它将不同的数字值映射到枚举的不同潜在值上.

为什么 Rust 程序的二进制大小比 C 程序大?

有几个因素导致 Rust 程序默认比功能相当的 C 程序有较大的二进制大小。一般来说,Rust 更倾向于对现实世界的程序性能进行优化,而不是对小程序的大小进行优化。

单态化

Rust 对泛型进行了单态化处理,这意味着在程序中每使用一个具体类型,就会生成一个新的泛型函数或类型。这类似于 C++ 中模板的工作方式。例如,在下面的程序中:

1
2
3
4
5
6
7
8
fn foo<T>(t: T) {
// ... do something
}

fn main() {
foo(10); // i32
foo("hello"); // &str
}

两个不同版本的foo将出现在最终的二进制文件中,一个专门用于i32输入,一个专门用于&str输入。这使得通用函数的静态调度更加有效,但代价是一个更大的二进制文件。

调试符号

Rust 程序在编译时保留了一些调试符号,即使是在 release 模式下编译。这些符号用于提供 panic 时的 backtrace,可以用strip或其他调试符号移除工具移除。值得注意的是,用 Cargo 在 release 模式下编译,相当于用 rustc 设置优化级别 3。另一个优化级别(称为sz已被添加,它告诉编译器为大小而不是性能进行优化。

链接时优化

Rust 默认不做链接时优化,但可以被指示这样做。这增加了 Rust 编译器可能做的优化量,并对二进制的大小有小的影响。与之前提到的尺寸优化模式相结合,这种影响可能更大。

标准库

Rust 标准库包括 libbacktrace 和 libunwind,这在某些程序中可能是不可取的。因此,使用#![no_std]可以带来更小的二进制文件,但通常也会对你正在编写的那种 Rust 代码造成实质性的改变。请注意,在没有标准库的情况下使用 Rust,通常在功能上更接近于同等的 C 代码。

举个例子,下面的 C 程序读入一个名字,并对有这个名字的人说“你好”。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
printf("What's your name?\n");
char input[100] = {0};
scanf("%s", input);
printf("Hello %s!\n", input);
return 0;
}

用Rust重写这个,你可能会得到如下的东西。

1
2
3
4
5
6
7
8
use std::io;

fn main() {
println!("What's your name?");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
println!("Hello {}!", input);
}

这个程序在编译后与 C 程序相比,会有更大的二进制,使用更多的内存。但是这个程序并不完全等同于上面的 C 代码。等价的 Rust 代码反而会是这样的。

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
#![feature(lang_items)]
#![feature(libc)]
#![feature(no_std)]
#![feature(start)]
#![no_std]

extern crate libc;

extern "C" {
fn printf(fmt: *const u8, ...) -> i32;
fn scanf(fmt: *const u8, ...) -> i32;
}

#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
unsafe {
printf(b"What's your name?\n\0".as_ptr());
let mut input = [0u8; 100];
scanf(b"%s\0".as_ptr(), &mut input);
printf(b"Hello %s!\n\0".as_ptr(), &input);
0
}
}

#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"] fn panic_fmt() -> ! { loop {} }
#[lang="stack_exhausted"] extern fn stack_exhausted() {}

这确实应该在内存使用方面与 C 语言大致相同,但代价是更多的程序员复杂性,以及缺乏通常由 Rust 提供的静态保证(在这里通过使用unsafe来避免)。

为什么 Rust 不像 C 那样有一个稳定的 ABI,为什么我必须用 extern 来注解东西?

对 ABI 的承诺是一个重大的决定,会限制未来潜在的有利的语言变化。鉴于 Rust 在 2015 年 5 月才达到 1.0,现在做出像稳定 ABI 这样大的承诺还为时过早。但这并不意味着未来不会发生。(尽管 C++ 已经成功地运行了很多年而没有指定一个稳定的 ABI)。

extern关键字允许 Rust 使用特定的 ABI,例如定义明确的 C ABI,以便与其他语言互操作。

Rust 代码可以调用 C 代码吗?

可以。从 Rust 中调用 C 代码的设计与从 C++ 中调用 C 代码一样高效。

C 代码可以调用 Rust 代码吗?

是的,Rust 代码必须通过“extern”声明公开,这使得它与 C-ABI 兼容。这样的函数可以作为一个函数指针传递给 C 代码,或者,如果赋予#[no_mangle]属性以禁用符号纠缠,可以直接从 C 代码中调用。

我已经写了完美的 C++ 代码。Rust 能给我什么?

现代 C++ 包含了许多使编写安全和正确的代码不容易出错的特性,但它并不完美,而且仍然很容易引入不安全因素。这是 C++ 的核心开发人员正在努力克服的问题,但是 C++ 受限于悠久的历史,它比他们现在试图实现的很多想法都要早。

Rust 从第一天起就被设计成一种安全的系统编程语言,这意味着它不会受到历史上的设计决定的限制,而这些决定使 C++ 的安全问题变得如此复杂。在 C++ 中,安全是通过谨慎的个人纪律实现的,而且很容易出错。在 Rust 中,安全是默认的。它让你有能力在一个包括不如你完美的人在内的团队中工作,而不必花时间反复检查他们的代码是否存在安全漏洞。

我如何在 Rust 中实现相当于 C++ 模板的专业化?

Rust 目前还没有与模板专业化完全对等的东西,但它正在研究中,希望能很快加入。然而,类似的效果可以通过关联类型实现。

Rust 的所有权系统与 C++ 的移动语义有什么关系?

底层的概念是相似的,但这两个系统在实践中的工作方式是非常不同的。在这两个系统中,“move”一个值都是一种为了转移其底层资源的所有权的方式。例如,移动一个字符串会转移字符串的缓冲区,而不是复制它。

在 Rust 中,所有权转移是默认行为。例如,如果我编写了一个以“String”为参数的函数,这个函数将对其调用者提供的String值拥有所有权。

1
2
3
4
5
6
7
fn process(s: String) { }

fn caller() {
let s = String::from("Hello, world!");
process(s); // Transfers ownership of `s` to `process`
process(s); // Error! ownership already transferred.
}

正如你在上面的片段中看到的,在函数caller中,对process的第一次调用转移了变量s的所有权。编译器会跟踪所有权,所以第二次调用process会导致一个错误,因为将同一个值的所有权转让两次是非法的。如果一个值有一个未完成的引用,Rust 也会阻止你移动这个值。

C++ 采取了一种不同的方法。在 C++ 中,默认的做法是复制一个值(更确切地说,是调用复制构造函数)。然而,被调用者可以使用一个“rvalue reference”来声明他们的参数,例如string&&,以表明他们将获得该参数所拥有的一些资源的所有权(在这个例子中,字符串的内部缓冲区)。然后调用者必须传递一个临时表达式或使用std::move进行明确的移动。大致相当于上面的函数process的粗略等价物是:

1
2
3
4
5
6
7
void process(string&& s) { }

void caller() {
string s("Hello, world!");
process(std::move(s));
process(std::move(s));
}

C++ 编译器没有义务去跟踪移动。例如,上面的代码在编译时没有任何警告或错误,至少在使用默认的设置的情况下,上述代码在编译时没有任何警告或错误。此外,在C++中,字符串s本身的所有权(如果不是它的内部缓冲区的话)仍然属于caller,所以s的析构函数会在caller返回时运行,即使它已经被移动了(相反,在 Rust 中,被移动的值只被其新主人丢弃)。

我怎样才能从 Rust 与 C++ 互操作,或者从 C++ 与 Rust 互操作?

Rust 和 C++ 可以通过 C 语言进行互操作。Rust 和 C++ 都为 C 语言提供了一个外来函数接口,并可以用它来进行相互之间的通信。如果编写 C 语言绑定太过繁琐,你可以使用rust-bindgen来帮助自动生成可行的 C 语言绑定。

Rust 有 C++ 风格的构造函数吗?

不,函数的作用与构造函数相同,不会增加语言的复杂性。在 Rust 中,相当于构造函数的通常名称是new(),尽管这只是一个惯例而不是语言规则。new()函数实际上就像其他函数一样。它的一个例子是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Foo {
a: i32,
b: f64,
c: bool,
}

impl Foo {
fn new() -> Foo {
Foo {
a: 0,
b: 0.0,
c: false,
}
}
}

Rust 有复制构造函数吗?

不完全是。实现了Copy的类型会做一个标准的类似于 C 语言的“浅拷贝”,不需要额外的工作(类似于 C++ 中的 trivially copyable 类型)。不可能实现需要自定义复制行为的Copy类型。相反,在 Rust 中,“复制构造器”是通过实现Clone特性,并明确调用clone方法来创建的。将用户定义的复制操作符显性化,使开发者更容易识别潜在的昂贵操作。

Rust 有移动构造函数吗?

没有。所有类型的值都是通过memcpy移动的。这使得编写通用的不安全代码变得更加简单,因为赋值、传递和返回都是已知的,不会产生像解绑(unwinding)那样的副作用。

Go 和 Rust 有什么相似之处,又有什么不同?

Rust 和 Go 的设计目标有很大不同。以下的差异并不是唯一的差异(这些差异太多,无法一一列举),但却是其中几个比较重要的差异:

  • Rust 比 Go 层级更低。例如,Rust 不需要垃圾收集器,而 Go 需要。一般来说,Rust 提供的控制水平与 C 或 C++ 相当。
  • Rust 的重点是确保安全和效率,同时提供高层次的能力,而 Go 的重点是成为一种小而简单的语言,可以快速编译并与各种工具很好地配合。
  • Rust 对泛型有很强的支持,而 Go (目前)却没有。
  • Rust 受到函数式编程世界的强烈影响,包括从 Haskell 的 typeclasses 中提取的类型系统。Go 有一个更简单的类型系统,使用接口进行基本的泛型编程。

Rust traits 与 Haskell typeclasses 相比如何?

Rust traits 类似于 Haskell 的 typeclasses,但目前还没有那么强大,因为 Rust 不能表达更高类型的类型。Rust 的关联类型等同于 Haskell 类型族。

Haskell typeclasses 和 Rust traits 之间的一些具体区别包括:

  • Rust traits 有一个隐含的第一个参数,叫做Self。Rust 中的trait Bar对应于 Haskell 中的class Bar self,而 Rust 中的trait Bar<Foo>对应于 Haskell 中的class Bar foo self
  • Rust 中的“Supertraits”或“superclass constraints”被写成trait Sub: Super,而 Haskell 中的为class Super self => Sub self
  • Rust 禁止无主实例,导致 Rust 中的一致性规则与 Haskell 不同。
  • Rust 的impl解析在决定两个impl是否重叠或在潜在的impl之间进行选择时,会考虑相关的where条款和特质约束条件。Haskell 只考虑instance声明中的约束,不考虑其他地方提供的任何约束。
  • Rust 的 traits 的一个子集(“对象安全”的 traits)可以通过 trait 对象用于动态调度。同样的功能在 Haskell 中通过 GHC 的“ExistentialQuantification”可用。

Documentation

为什么 Stack Overflow 上有这么多 Rust 的答案是错误的?

Rust 语言已经存在了很多年,在 2015 年 5 月才达到 1.0 版本。在这之前的时间里,语言发生了很大的变化,而 Stack Overflow 的一些答案是在语言的旧版本时给出的。

随着时间的推移,越来越多的答案将提供给当前的版本,从而改善这个问题,因为过时的答案的比例减少了。

我在哪里报告 Rust 文档中的问题?

你可以在 Rust 编译器issue tracker上报告 Rust 文档中的问题。请务必先阅读贡献指南

我如何查看我的项目所依赖的库的 Rustdoc 文档?

当你使用cargo doc为你自己的项目生成文档时,它也会为活动的依赖版本生成文档。这些文档会被放到你的项目的target/doc目录下。使用cargo doc --open来打开这些文档,或者自己打开target/doc/index.html