本书跟随原文截止日期:2024-06-15
前言
试水版
本译作是译者自己在学习 Rust 时,有感于国内 Rust 中文学习资源贫乏的激情之作。
译者在阅读和练习完了 Rust 的入门经典 The Rust Programming Language 和 Rust by Example 之后,发现自己对 Rust 非常感兴趣,就一发不可收拾地继续投入精力来深入学习。在这个继续学习的过程中译者发现自己在不断的翻阅官网的 参考手册(Reference)。
直接查看英语原文的确对理解 Rust 的基础概念很有帮助,但毕竟看英文效率太低;并且原文作为专业的工具书在读者设定上对阅读者做了较高的基础知识储备和 Rust 领域知识的假设;以及由于原文并未正式完工,还在不断增补中,所以其为追求效率导致其使用的英语语法比较简略随意,对基础知识和背景知识的省略严重,导致对译者这样的非英语母语者阅读起来体验并不好。所以译者就萌生了自己对阅读理解做翻译笔记的想法。
在做翻译笔记的时候发现很多技术坎要过,需要核对一下自己的理解到底对不对,就想偷懒看看中文网内有没有业内大佬把这些资源翻译为中文的,或者这本书的直接翻译版。很遗憾,Rust 在国内不怎么流行,中文资源不多,又很零碎分散,所以很长一段时间之内,译者并没有发现,后来通过 Rust语言中文社区发现国内站点-芽之家上有Rust Reference的翻译版。小激动了一番之后发现此版本的翻译者在翻译完前几章后中途弃坑了。
此发现虽有遗憾,但也激发了译者的勇气,既然有人敢开头,那我就敢给他接力翻译几篇。但遗憾的是译者发现芽之家的译本对译者来说仍不是很好理解,也不能及时的和 Rust 官网上的 参考手册(Reference)保持同步。所以译者就依仗着有道翻译决定单干,所以本书的前期的格式和部分名词的译法参照了此版本。所以译者在此特别感谢此版本的翻译者给译者的勇气。
因为译者本身非计算机专科出身,又只是 Rust 的入门学习者,又是首次尝试这么大的翻译量,所以书中难免有不少翻译错误或者译者理解错误的地方,但译者的对 Rust 的热爱还在,对 Rust 学习的热情还在,所以就直接厚着脸皮拿出来献丑。译者这样做一是致敬 Rust,一是希望为国内 Rust 社区的繁荣尽些热心,更是为了提高译者自己的学习水平。当然也希望能激发更多学习者的热情;同时也希望读者能帮助译者进步,不止于帮助译者改进本书。
最后,因为译者实在是水平有限,对本书附录的 Macro Follow-Set Ambiguity Formal Specification 实在是无能为力了,只能翻译一半停下,希望读者中的高手能帮忙继续翻译,或者直接重新翻译,或者给译者指点一下也行。
抢救版
由于自己的才疏学浅,本译作发布之后,发现了不少翻译错误和译者对 Rust 知识点的理解错误,所以译者不得不再次鼓起勇气来检查和改正这些错误和失误。经过半个月的时间紧急抢救,现在译者终于有信心再次宣布——本译作救活了,可以一看了!
翻译说明
翻译风格
本书基本是忠于原文的直译,但针对比较晦涩的地方,本书采用了译者自己的理解的意译。但译者本身也是 Rust 的一个入门学习者,所以这里难免出错,所以这里恳请读者及时指正,以免译者贻害其他读者。
也因为大部分是直译,所以行文并不是很符合中文的阅读习惯,比如同一句话里的定语过多,比如前后两句话没有明显的承接关系等。开始时译者也想把这些修改为符合中文阅读的行文方式,但一是发现工作量有些大,再则,也是主要的,就是在修改的过程中难免加入译者过多的演绎,这样很可能偏离原作的语义,所以暂时没改。其实根据后来译者的检查阅读体验发现,这样的行文反倒是有助于提高阅读时的专注力,比如,停下来对一句话里“...的...的”划分反倒是能让读者更深入理解语义。所以译者愿意相信这百分之十几的阅读磕顿有助于提升这作为一部工具书的价值。
有时针对同一个英文原文,社区可能存在多种流行度差不多译法或译者认为自己的译法更贴切时,译者会采用格式“A译法/B译法”来同时给出这两种译法,比如,grammar 就翻译为“文法/语法”。
一个问题是译者对原文章节段落标题的翻译。这样做是原文的文档链接仍然有效,这样能有效降低译者对文档链接的维护,也能降低译者对一些名词的专门维护,还能有效提醒读者本章节段落的主题。
再一个问题是译文中的“译者注”,这里原本是译者加入的自我理解,以便降低新手的阅读门槛。但后来发现有些泛滥,并且译者的过分演绎发挥可能影响阅读体验,甚至曲解原意,所以译者删除了一部分,后续翻译也尽量克制了这一冲动。
再有就是对一些名词的翻译,译者基本上是遵循了 LearnKu 上的 Rust 语言术语中英文对照表上的约定,但也有一些译者按自己的理解做了新的翻译,这些下节和本书最后的本书术语翻译对照表中说明。
Rust 的每种句法都有自己独特的句法规则,前期译者对这部分内容做了翻译,但发现翻译后无法保持句法规则的清晰性,严重妨碍读者对其的理解,尤其是其中的 token 和[产生式(production)]如果翻译了就完全丢失了其中蕴含的格式意义,所以这部分译者就放弃了,直接拷贝了原文。
译者最近开始学习编译原理,发现本书的有不少译法跟标准的译法还是有差异,但译者最近有些精力不济了,所以译者准备等编译原理学完之后再统一改,所以再次对各位读者先说声抱歉,也欢迎各位读者提交 PR 先行修改。
待续
约定
如果一些词译者觉得只使用中文行文可能会导致读者迷惑,那译者就会使用半角的圆括号把原英文带出,比如:标称类型(nominal type)。
但有一类虽然比较浅显,译者还是采用了这种格式,比如:结构体(struct
),这类是其实是原文直接使用了 Rust 文法中的简单token 来指代相关名词的方便做法。这类做法的另一个含义是这这类词代表了某种特定和关键的意义,并有可能直接出现源码中。所以译者的这种翻译方法也是为了不遗失这部分含义。
还有一类是针对比较生僻或非常专业的原文,译者可能较多的采用了意译,比如 “Macro Kleene Matcher”,可能译为“宏克林闭包匹配器”比较专业,但译者意译为“可匹配空的宏匹配器”,专业性是降低了,但也可能照顾了部分非专业读者。
译文中还有许多全角圆括号,这些有可能是原文行文中就有的括号,也可能是译者为中文行文方便添加进来的,也有是译者为补充语义、方便阅读特意添加进来的(这部分主要是译者注太多了,译者看着也烦)。
再有就是翻译难免加上译者的理解,尤其是译者有强烈的愿望希望在不降低此书作为手册之外,能尽可能的降低此书的阅读门槛,所以译者除了多处采用意译、补充原文省略词外,还采用了了在部分晦涩或译者认为需要提醒的地方采用了右上角标注跳转链接的方式,这里跳转处的解释很多都是译者自己的理解,特提请读者注意辨别。
一些不确定该如何翻译的名词,译者也遵循一般原则没有强翻。但如果一个专有名词又有中文又有英文,译者就有意没在这两个次词/字中间留有空格,例如:“trait对象”。尤其是那些原文带有 token 含义的名词,译者也一般把它们“紧密结合”在一起,比如:let
语句、cfg
属性等。
每章节译文开头,译者都把其对应的原文链接地址和本译文对应的 commit id 给放上了。同时也把译者最后维护的时间给放上。这些是希望将来的其他贡献者能共同遵守。
本翻译说明的开头还附上了本书最后一次维护时同步原文的时间。
待续
常用词翻译
- lifetime,社区一般译为“生命周期”,但在本书原文中有行文用到 lifecycle,本译作为何此词译义分开,选择翻译为“生存期”。并且译者在翻译中发现使用“生存期”在中文中行文更合适。
- Syntax,这个词很多译者的随意的翻译为“语法”,但在本书里它大部分时候特指单一语言部件的产生式格式,所以本书统一翻译为“句法”。在它之上一层的语言组织方式才翻译为“文法”,有时也翻译为“语法”。
- nominal type:标称类型。这是译者自创的一个翻译方法,首先 nominal type 在目前国内 Rust 社区里还没有合适的翻译先例,所以译者这里就蹭机器学习的热点翻译为“标称类型”,这种行径虽然降低了Rust的逼格,但在目前状态下,收益可能还是值得的。
- item,这个一般翻译为“项”或“条目”,译者期初为了读起来更符合习惯,选择了“项”的翻译方法,但后来发现这个方法太容易引起歧义,无法体现出其作为语法单元的特殊含义,所以又给它添了两个字,变成了“程序项”。发布后,听从网友“无聊”的建议,又统一改为“程序项”。而其他名词复合时,一般会简化翻译为“XX项”,如:“static item”,译者就翻译为“静态项”。
- size,社区内常把这个词在表示 Rust 数据类型的位宽时译做“大小”,但在中文语境中,有时无法把其和数值大小,以及比较意义的大小,或长度等含义快速区分开来,所以本文在表示 Rust 数据类型的位宽时统一译做“内存宽度”。
- bit,这个通常译为“位”,但有时它会和中文中的“两位数”的“位”混淆,所以在代表二进制位宽时译者干脆保持不译。
- alignment,这个词在表示一种布局属性时,通常译作“对齐”,但本书由于体量较大,行文中很难把其和动词的“对齐”,以及普通意义上的“对齐”区分开来,所以在特指布局属性中的 alignment 时,统一翻译为“对齐量”。出于同样的考虑,representation 在表示类型的布局属性时统一翻译为“表形”。
- 更多常用词汇的翻译对照见本书术语翻译对照表。
阅读建议
本译作的英文原文主语省略很严重,但基本规律是省略的主语大部分是本节的标题,所以译者也很多时候继承了这个风格,所以阅读某一具体章节的时候,请读者主动把语义范围缩小为当前讲述的话题点上。
建议:译者希望本书的读者中,那些有一定水平或有能力直接进行英文阅读的读者还是要读一读阅读英文原文,顺便也请这部分读者在对比阅读的同时本书提高翻译质量。
待续
贡献力量
译者欢迎各种形式的贡献和指导。
待续
版权说明
Nightly 版本放在国内 Gitee 上,采用 MulanPSL-2.0;stable 版放在 github 上,采用 MIT。
Introduction
介绍
introduction.md
commit: 6ab78176d305f1fe9b5186a940676293c1ad31ef
本章译文最后维护日期:2021-06-19
本书是 Rust 编程语言的主要参考手册,本书提供了3类资料:
- 一些章节非正式地介绍了该语言的各种语言结构及其用法。
- 一些章节非正式地介绍了该语言的内存模型、并发模型、运行时服务、链接模型,以及调试工具。-
- 附录章节提供了一些对 Rust 语言有影响的编程原理和参考资料。
警告:此书尚未完成,记录 Rust 的所有内容需要花些时间。有关本书未记录的内容,请查阅 GitHub issues。
Rust releases
Rust 发行版
Rust 每六周发布一种新的版本。
该语言的第一个稳定版本是 Rust 1.0.0,然后是 Rust 1.1.0,以此类推。
相应的工具(rustc
、’cargo
,等)和文档(标准库、本书,等等)与语言版本一起发布。
本书的最新版本,与最新的 Rust 版本相匹配,它总是在 https://doc.rust-lang.org/reference/ 等你。
通过在此链接的“reference”路径段之前添加相应的 Rust版本号,可以找到以往的版本。 比如,Rust 1.49.0版在 https://doc.rust-lang.org/1.49.0/reference/ 下。
What The Reference is Not
参考手册 并非——
这本书不是对这门语言的入门介绍。本书假设您熟悉该语言。若您需要学习该语言的基础知识,请阅读 Rust程序设计语言。
这本书也不作为 Rust 语言发行版中包含的标准库的参考资料。Rust 的库文档是从其源代码文件中提取的文档属性。此外,有许多可能被可能认为是语言自带特性(features)的特性其实都是 Rust 的标准库的特性,所以您要寻找的特性可能在那里,而不是在这里。
类似地,本书通常不能作为记录 rustc 或者 Cargo 细节的工具书。rustc 有自己专门的书 rustc book,Cargo 也有一本书 cargo book,该书中包含了 Cargo 的[参考手册] cargo reference。本书也涉及了少量和它们的相关知识,比如链接的章节,介绍了 rustc 是如何工作的。
本书仅作为稳定版 Rust 的参考资料存在,关于尚在开发中的非稳定特性,请参阅 Unstable Book。
Rust编译器(包括 rustc
)将执行编译优化,但本参考手册不指导这些编译器的优化工作,所以只能把编译后的程序看作一个黑盒子。如果想侦测它的优化方式,只能通过运行它、给它提供输入并观察它的输出来推断。所有的编译器优化结果和程序的运行效果都必须和本参考手册所说的保持一致。
最后,本书并非 Rust 语言规范。它可能包含特定于 rustc
的细节,这不应该被当作 Rust 语言的规范。我们打算以后出版这样一本书,但在那之前,本手册是最接近的东西。
How to Use This Book
如何使用此书
本书不会假定您是按顺序阅读本书。本书的每一章一般都可以独立阅读,但会交叉链接到其他章节,以了解它们所相关的内容,但不会进行讨论。
阅读本书有两种主要方式。
第一是寻找特定问题的答案。如果您知道回答问题的章节,您可以直接从目录跳入该章节进行阅读。否则,您可以按 s
键或单击顶部栏上的放大镜来搜索与问题相关的关键字(译者注:目前本翻译版还不支持这种搜索功能)。例如,假设您想知道在 let
语句中创建的临时值何时被销毁;同时,假设您还不知道表达式那一章中定义了临时对象的生存期,那么可以搜索 “temporary let” ,第一个搜索结果将带您去阅读该部分。
第二是提高您对此语言某一方面的认知。在这种情况下,只需浏览目录,直到看到您想了解的内容,然后点开阅读。阅读中如果某个链接看起来很有帮助,那就点击并阅读该部分内容。
也就是说,本书没有错误的阅读方式。您觉得怎样读对您最有帮助就怎样读。
Conventions
约定
像所有技术书籍一样,在如何展示信息方面,本书有一些约定。这些约定记录如下。
-
定义术语的语句中,术语写为斜体。当术语在其定义章节之外使用时,通常会有一个链接来指向该术语定义的章节。
示例术语 这是一个定义术语的示例。
-
编译 crate 所使用的版次之间的语言差异用一个块引用表示,以粗体的“版次差异:”开头。
版次差异:此句法(syntax)在 2015 版次有效,2018 版次起不允许使用。
-
一些有用信息,比如有关本书状态,或指出有用但大多超出本书范围的信息,大都位于以粗体的“注:”开头的块注释中。
注:这是一个注释示例。
-
有关对语言的不健全(sound)行为,或者针对易于混淆的语言特性的警告,记录在特殊的警告框里。
警告:这是一个示例警告。
-
文本中内联的代码片段在
<code>
标签里。较长的代码示例放在突出显示句法(syntax)的框中,该框的右上角有用于复制、执行和显示隐藏行的控件
// 这是隐藏行。 fn main() { println!("这是一段示例代码。"); }
除非另有说明,否则所有示例均使用最新版本的语法和编译检查。
-
文法和词法结构放在块引用中,第一行为粗体上标的 词法 或 句法。
句法
ExampleGrammar:
~
Expression
|box
Expression查阅表义符(notation)以获取更多细节。
Contributing
贡献力量
我们欢迎各种形式的贡献。
您可以通过开启议题或向 Rust 参考手册仓库发送 PR 来为本书做出贡献。如果这本书没有回答您的问题,并且您认为它的答案应该在本书的范围内,请不要犹豫,提交议题或在 Zulip 的 t-lang/doc
流频道上询问。知道人们最喜欢用这本书来做什么将有助于引导我们的注意力来让这些部分变得更好。我们也希望此手册尽可能地规范,所以如果你看到任何错误或非规范的地方,但没有明确指出,也请[提交议题]。
Notation
表义符/符号
notation.md
commit: dd1b9c331eb14ea7047ed6f2b12aaadab51b41d6
本章译文最后维护日期:2020-11-7
Grammar
文法/语法
下表中的各种表义符会在本书中标有 词法 和 句法 的文法片段中用到:
表义符 | 示例 | 释义 |
---|---|---|
CAPITAL | KW_IF, INTEGER_LITERAL | 由词法分析生成的单一 token |
ItalicCamelCase | LetStatement, Item | 句法产生式(syntactical production) |
string | x , while , * | 确切的字面字符(串) |
\x | \n, \r, \t, \0 | 转义字符 |
x? | pub ? | 可选项 |
x* | OuterAttribute* | x 重复零次或多次 |
x+ | MacroMatch+ | x 重复一次或多次 |
xa..b | HEX_DIGIT1..6 | x 重复 a 到 b 次 |
| | u8 | u16 , Block | Item | 或 |
[ ] | [b B ] | 列举的任意字符 |
[ - ] | [a -z ] | a 到 z 范围内的任意字符(包括 a 和 z) |
~[ ] | ~[b B ] | 列举范围外的任意字符(序列) |
~string | ~\n , ~*/ | 此字符序列外的任意字符(序列) |
( ) | (, Parameter)? | 程序项分组 |
String table productions
字符串表产生式
文法中的一些规则 — 比如一元运算符,二元运算符和关键字 — 会以简化形式:作为可打印字符串的列表形式(在本书的相关章节的头部的各种产生式定义/句法规则里)给出。这些规则构成了关于 token规则的规则子集,并且它们被假定为源码编译时的词法分析阶段的结果被再次输入给解析器,然后由一个DFA驱动,对此字符串表里的所有条目(string table entries)进行析取(disjunction)操作(来进行句法分析)。
本书还约定,当文法表中出现如 monospace
这样的字符串时,它代表对这些产生式中的单一 token 成员的隐式引用。查阅 tokens 以获取更多信息。
词法结构
notation.md
commit: 4a2bdf896cd2df370a91d14cb8ba04e326cd21db
本章译文最后维护日期:2020-10-17
Input format
输入格式
notation.md
commit: 0b153cb607e981bfa64ec6fcbfc92cefc891d4ed
本章译文最后维护日期:2024-04-06
本章介绍如何将源文件解释为一系列 token。
有关程序如何组织成文件的描述,请参见crate 和源文件。
Source encoding
源文件编码方式
每个源文件都被解释为以 UTF-8编码的 Unicode字符序列。 如果文件不是有效的 UTF-8编码方式,则报错。
Byte order mark removal
BOM移除
如果字符序列中的第一个字符是 U+FEFF
(BYTE ORDER MARK),则会将其移除。
CRLF normalization
CRLF归一化
U+000D
(CR) 后紧跟着 U+000A
(LF) 这样的字符对都被一个 U+000A
(LF) 所代替。
其他时候出现的字符U+000D
(CR) 则保留在原位(它们被视为空白符)。
Shebang removal
Shebang 移除
如果字符序列以字符 #!
起行,那直到并包括第一个 U+000A
(LF) 的字符将被从此字符序列中移除。
例如,以下文件的第一行将被忽略:
#!/usr/bin/env rustx
fn main() {
println!("Hello!");
}
作为例外,如果 #!
字符后面跟着一个 [
token(此时需要忽略中间的[comments][注释]或空白符),则不会删除任何内容。
这样可以防止删除了源文件开头的内部属性。
注意: 标准库的
include!
宏读取的文件会采用BOM移除、CRLF归一化和 shebang删除的处理方式。但include_str!
和include_bytes!
宏没有采用。
Tokenization
token化
源文件被前面步骤处理后的的字符序列将如本章剩余部分所述那样被转换为 token。
Keywords
关键字
keywords.md
commit: 089f6e12c2d557b02caf545604db93e20a2c99b3 本章译文最后维护日期:2023-05-12
Rust 将关键字分为三类:
Strict keywords
严格关键字
这类关键字只能在正确的上下文中使用。它们不能用作以下名称:
词法分析:
KW_AS :as
KW_BREAK :break
KW_CONST :const
KW_CONTINUE :continue
KW_CRATE :crate
KW_ELSE :else
KW_ENUM :enum
KW_EXTERN :extern
KW_FALSE :false
KW_FN :fn
KW_FOR :for
KW_IF :if
KW_IMPL :impl
KW_IN :in
KW_LET :let
KW_LOOP :loop
KW_MATCH :match
KW_MOD :mod
KW_MOVE :move
KW_MUT :mut
KW_PUB :pub
KW_REF :ref
KW_RETURN :return
KW_SELFVALUE :self
KW_SELFTYPE :Self
KW_STATIC :static
KW_STRUCT :struct
KW_SUPER :super
KW_TRAIT :trait
KW_TRUE :true
KW_TYPE :type
KW_UNSAFE :unsafe
KW_USE :use
KW_WHERE :where
KW_WHILE :while
以下关键字从 2018 版开始启用。
词法分析 2018+
KW_ASYNC :async
KW_AWAIT :await
KW_DYN :dyn
Reserved keywords
保留关键字
这类关键字目前还没有被使用,但是它们被保留以备将来使用。它们具有与严格关键字相同的限制。这样做的原因是通过禁止当前程序使用这些关键字,从而使当前程序能兼容 Rust 的未来版本。
词法分析
KW_ABSTRACT :abstract
KW_BECOME :become
KW_BOX :box
KW_DO :do
KW_FINAL :final
KW_MACRO :macro
KW_OVERRIDE :override
KW_PRIV :priv
KW_TYPEOF :typeof
KW_UNSIZED :unsized
KW_VIRTUAL :virtual
KW_YIELD :yield
以下关键字从 2018 版开始成为保留关键字。
词法分析 2018+
KW_TRY :try
Weak keywords
弱关键字
这类关键字只有在特定的上下文中才有特殊的意义。例如,可以声明名为 union
的变量或方法。
-
macro_rules
用于创建自定义宏。 -
union
用于声明联合体(union
),它只有在联合体声明中使用时才是关键字。 -
'static
用于静态生存期,不能用作通用泛型生存期参数和循环标签// error[E0262]: invalid lifetime parameter name: `'static` fn invalid_lifetime_parameter<'static>(s: &'static str) -> &'static str { s }
-
在 2015 版次中,当
dyn
用在非::
开头的路径限定的类型前时,它是关键字。从 2018 版开始,
dyn
被提升为一个严格关键字。
词法分析
KW_MACRO_RULES :macro_rules
KW_UNION :union
KW_STATICLIFETIME :'static
词法分析 2015
KW_DYN :dyn
Identifiers
标识符
identifiers.md
commit: 1f9de62e04e6c79922ab78062407a658f4c3fa6a
本章译文最后维护日期:2022-10-22
词法分析:
IDENTIFIER_OR_KEYWORD :
XID_Start XID_Continue*
|_
XID_Continue+RAW_IDENTIFIER :
r#
IDENTIFIER_OR_KEYWORD 排除crate
,self
,super
,Self
NON_KEYWORD_IDENTIFIER : IDENTIFIER_OR_KEYWORD *排除严格关键字和保留关键字 *
IDENTIFIER :
NON_KEYWORD_IDENTIFIER | RAW_IDENTIFIER
标识符遵循 Unicode标准附录31 中针对 Unicode 15.0版的规范,后面所述内容在此版本中均有备述。 这里举一些标识符的示例:
foo
_identifier
r#true
Москва
東京
其中 UAX #31 中要求标识符使用的(产生式)参数如下:
- 起始字符 :=
XID_Start
,外加一个下划线 (U+005F) - 后续字符 :=
XID_Continue
- 中间字符 := 空
还有一个附加约束,即单个下划线字符不是标识符。
注意: 以下划线开头的标识符通常用于表示有意不会被实际使用的标识符,且会使
rustc
对未被使用的警告静音。
如果标识符没有下面原生标识符章节中描述的 r#
前缀,那它不能是严格关键字或保留关键字。
标识符中不允许使用零宽度非连接符(ZWNJ U+200C)和零宽度连接符(ZWJ U+200D)。
在下列情况下,标识符仅限于 XID_Start
和 XID_Continue
的ASCII子集:
Normalization
标准化
标识符使用Unicode标准附录15中定义的规范化形式C(NFC)进行规范化。如果两个标识符的 NFC形式相等,那么它们就是等价的。
[过程宏][proc macro]和声明宏在其输入中接受规范化的标识符。
Raw identifiers
原生标识符
除了有形式前缀 r#
修饰外,原生标识符与普通标识符类似。(注意形式前缀 r#
不包括在实际标识符中。)与普通标识符不同,原生标识符可以是除上面列出的 RAW_IDENTIFIER
之外的任何严格关键字或保留关键字。
Comments
注释
comments.md
commit: fa56fdba0e9dba35eb29d11c95c7a009ed67cb35
本章译文最后维护日期:2024-03-09
词法分析
LINE_COMMENT :(译者注:行注释)
/*
(~[*
!
\n
] |**
| BlockCommentOrDoc) |//
BLOCK_COMMENT :(译者注:块注释)
/*
(~[*
!
] |**
| BlockCommentOrDoc) (BlockCommentOrDoc | ~*/
)**/
|/**/
|/***/
INNER_LINE_DOC :(译者注:内部行文档型注释)
//!
~[\n
IsolatedCR]*INNER_BLOCK_DOC :(译者注:内部块文档型注释)
/*!
( BlockCommentOrDoc | ~[*/
IsolatedCR] )**/
OUTER_LINE_DOC :(译者注:外部行文档型注释)
///
(~/
~[\n
IsolatedCR]*)?OUTER_BLOCK_DOC :(译者注:外部块文档型注释)
/**
(~*
| BlockCommentOrDoc ) (BlockCommentOrDoc | ~[*/
IsolatedCR])**/
BlockCommentOrDoc :(译者注:块注释或文档型注释)
BLOCK_COMMENT
| OUTER_BLOCK_DOC
| INNER_BLOCK_DOCIsolatedCR :
\r
Non-doc comments
非文档型注释
Rust 代码中的注释一般遵循 C++ 风格的行(//
)和块(/* ... */
)注释形式,也支持嵌套的块注释。
非文档型注释(Non-doc comments)被解释为某种形式的空白符。
Doc comments
文档型注释
以三个斜线(///
)开始的行文档型注释,以及块文档型注释(/** ... */
),均为外部文档型注释。它们被当做 doc
属性的特殊句法解析。也就是说,它们等同于把注释内容写入 #[doc="..."]
里。例如:/// Foo
等同于 #[doc="Foo"]
,/** Bar */
等同于 #[doc="Bar"]
。
以 //!
开始的行文档型注释,以及 /*! ... */
形式的块文档型注释属于注释体所在对象的文档型注释,而非注释体之后的程序项的。也就是说,它们等同于把注释内容写入 #![doc="..."]
里。//!
注释通常用于标注模块位于的文件。
文档型注释中不允许出现字符U+000D
(CR)。
注意:
U+000D
(CR)后直跟U+000A
(LF) 的字符序列会预先被转换为U+000A
(LF)。
示例
#![allow(unused)] fn main() { //! 应用于此 crate 的隐式匿名模块的文档型注释 pub mod outer_module { //! - 内部行文档型注释 //!! - 仍是内部行文档型注释 (但是这样开头会更具强调性) /*! - 内部块文档型注释 */ /*!! - 仍是内部块文档型注释 (但是这样开头会更具强调性) */ // - 普通注释 /// - 外部行文档型注释 (以 3 个 `///` 开始) //// - 普通注释 /* - 普通注释 */ /** - 外部块文档型注释 (exactly) 2 asterisks */ /*** - 普通注释 */ pub mod inner_module {} pub mod nested_comments { /* 在 Rust 里 /* 我们可以 /* 嵌套注释 */ */ */ // 所有这三种类型的块注释都可以包含或嵌套在任何其他类型的 // 注释中: /* /* */ /** */ /*! */ */ /*! /* */ /** */ /*! */ */ /** /* */ /** */ /*! */ */ pub mod dummy_item {} } pub mod degenerate_cases { // 空内部行文档型注释 //! // 空内部块文档型注释 /*!*/ // 空行注释 // // 空外部行文档型注释 /// // 空块注释 /**/ pub mod dummy_item {} // 空的两个星号的块注释不是一个文档块,它是一个块注释。 /***/ } /* 下面这个是不允许的,因为外部文档型注释需要一个 接收该文档的程序项 */ /// 我的程序项呢? mod boo {} } }
Whitespace
空白符
whitespace.md
commit: 716e04f203c10387bd66aa3bcf1663e75ce208cf
本章译文最后维护日期:2022-08-21
空白符是非空字符串,它里面只包含具有 Pattern_White_Space
属性的 Unicode 字符,即:
U+0009
(水平制表符,'\t'
)U+000A
(换行符,'\n'
)U+000B
(垂直制表符)U+000C
(换页符)U+000D
(回车符,'\r'
)U+0020
(空格符,' '
)U+0085
(下一行标记符)U+200E
(从左到右标记符)U+200F
(从右到标左记符)U+2028
(行分隔符)U+2029
(段分隔符)
Rust是一种“格式自由(free-form)”的语言,这意味着所有形式的空白符在文法中仅用于分隔 tokens 的作用,没有语义意义。
Rust 程序中,如果将一个空白符元素替换为任何其他合法的空白符元素(例如单个空格字符),它们仍有相同的意义。
Tokens
tokens.md
commit: 5afb503a4c1ea3c84370f8f4c08a1cddd1cdf6ad
本章译文最后维护日期:2024-03-09
token 是采用非递归方式的正则文法(regular languages)定义的基本语法产生式(primitive productions)。Rust 源码输入可以被分解成以下几类 token:
在本文档中,“简单”token 会直接在(相关章节头部的)[字符串表产生式(production)][string table production]表单中给出,并以 monospace
字体显示。(译者注:本译作的原文中,在文法表之外的行文中也会大量出现这种直接使用简单token 来替代相关名词的做法,一般此时如果译者觉得这种 token 需要翻译时,会使用诸如:结构体(struct
) 这种形式来翻译。读者需要意识到“struct”是文法里的一个 token,能以其字面形式直接出现在源码里。)
Literals
字面量
字面量是字面量表达式中使用的各种 token。
Examples
示例
Characters and strings
字符和字符串
举例 | # 号的数量1 | 字符集 | 转义 | |
---|---|---|---|---|
字符 | 'H' | 0 | 全部 Unicode | 引号 & ASCII & Unicode |
字符串 | "hello" | 0 | 全部 Unicode | 引号 & ASCII & Unicode |
原生字符串 | r#"hello"# | <256 | 全部 Unicode | N/A |
字节 | b'H' | 0 | 全部 ASCII | 引号 & 字节 |
字节串 | b"hello" | 0 | 全部 ASCII | 引号 & 字节 |
原生字节串 | br#"hello"# | <256 | 全部 ASCII | N/A |
C语言风格的字符串 | c"hello" | 0 | 全部 Unicode | Quote & Byte & Unicode |
原生C语言风格的字符串 | cr#"hello"# | <256 | 全部 Unicode | N/A |
字面量两侧的 #
数量必须相同。
注意: 字符和字符串字面量token 不会包括
U+000D
(CR)后紧跟U+000A
(LF) 的字符序列:这对字符会在编译器读取源文件时被转换为单个U+000A
(LF)字符。
ASCII escapes
ASCII 转义
名称 | |
---|---|
\x41 | 7-bit 字符编码(2位数字,最大值为 0x7F ) |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\\ | 反斜线 |
\0 | Null |
Byte escapes
字节转义
名称 | |
---|---|
\x7F | 8-bit 字符编码(2位数字) |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\\ | 反斜线 |
\0 | Null |
Unicode escapes
unicode 转义
名称 | |
---|---|
\u{7FFF} | 24-bit Unicode 字符编码(最多6个数字) |
Quote escapes
引号转义
Name | |
---|---|
\' | 单引号 |
\" | 双引号 |
Numbers
数字
所有数字字面量允许使用 _
作为可视分隔符,比如:1_234.0E+18f64
Suffixes
后缀
后缀是字面量主体部分后面的字符序列(它们之间不能含有空格),其形式与非原生标识符或关键字相同。
词法
SUFFIX : IDENTIFIER_OR_KEYWORD
SUFFIX_NO_E : SUFFIX not beginning withe
orE
任何带有后缀的字面量(如字符串、整型等)都可以作为有效的 token。
带有后缀的字面量token 可以传递给宏而不会产生错误。
宏自己决定如何解释这种 token,以及是否该报错。
特别是,声明宏的 literal
段指示符匹配带有任意后缀的字面量token。
#![allow(unused)] fn main() { macro_rules! blackhole { ($tt:tt) => () } macro_rules! blackhole_lit { ($l:literal) => () } blackhole!("string"suffix); // OK blackhole_lit!(1suffix); // OK }
但是,那些被解析为字面量表达式或模式的字面量token 的后缀是受限的。 非数字文字标记上的任何后缀都将被拒绝,数字文字标记仅接受以下列表中的后缀。
整数 | 浮点数 |
---|---|
u8 , i8 , u16 , i16 , u32 , i32 , u64 , i64 , u128 , i128 , usize , isize | f32 , f64 |
Character and string literals
字符和字符串字面量
Character literals
字符字面量
词法
CHAR_LITERAL :
'
( ~['
\
\n \r \t] | QUOTE_ESCAPE | ASCII_ESCAPE | UNICODE_ESCAPE )'
SUFFIX?QUOTE_ESCAPE :
\'
|\"
ASCII_ESCAPE :
\x
OCT_DIGIT HEX_DIGIT
|\n
|\r
|\t
|\\
|\0
UNICODE_ESCAPE :
\u{
( HEX_DIGIT_
* )1..6}
字符字面量是位于两个 U+0027
(单引号 '
)字符内的单个 Unicode 字符。当它是 U+0027
自身时,必须前置转义字符 U+005C
(\
)。
String literals
字符串字面量
词法
STRING_LITERAL :
"
(
~["
\
IsolatedCR] (译者注:IsolatedCR:后面没有跟\n
的\r
,首次定义见注释)
| QUOTE_ESCAPE
| ASCII_ESCAPE
| UNICODE_ESCAPE
| STRING_CONTINUE
)*"
SUFFIX?STRING_CONTINUE :
\
后跟 \n
字符串字面量是位于两个 U+0022
(双引号 "
)字符内的任意 Unicode 字符序列。当它是 U+0022
自身时,必须前置转义字符 U+005C
(\
)。
字符串字面量允许使用字符U+000A
(LF) 的形式来换行书写。
但当非转义的字符 U+005C
(\
)后面紧跟着一个换行符时,换行符并不会出现在字符串中。
详细信息请参见字符串接续符转义。
字符U+000D
(CR) 除了作为这种字符串接续符转义的一部分之外,不能出现在字符串字面量中。
Character escapes
字符转义
不管是字符字面量,还是非原生字符串字面量,Rust 都为其提供了额外的转义功能。转义以一个 U+005C
(\
)开始,并后跟如下形式之一:
- 7-bit 码点转义以
U+0078
(x
)开头,后面紧跟两个十六进制数字,其最大值为0x7F
。它表示 ASCII 字符,其码值就等于字面提供的十六进制值。不允许使用更大的值,因为不能确定其是 Unicode 码点还是字节值(byte values)。 - 24-bit 码点转义以
U+0075
(u
)开头,后跟多达六位十六进制数字,位于花括号U+007B
({
)和U+007D
(}
)之间。这表示(需转义到的)Unicode 字符的码点等于花括号里的十六进制值。 - 空白符转义是
U+006E
(n
)、U+0072
(r
) 或者U+0074
(t
) 之一,依次分别表示 Unicode 码点U+000A
(LF),U+000D
(CR),或者U+0009
(HT)。 - null转义 是字符
U+0030
(0
),表示 Unicode 码点U+0000
(NUL)。 - 反斜线转义 是字符
U+005C
(\
),反斜线必须通过转义才能表示其自身。
Raw string literals
原生字符串字面量
词法
RAW_STRING_LITERAL :
r
RAW_STRING_CONTENT SUFFIX?RAW_STRING_CONTENT :
"
( ~ IsolatedCR )* (非贪婪模式)"
|#
RAW_STRING_CONTENT#
原生字符串字面量不做任何转义。它以字符 U+0072
(r
)后跟小于256个字符 U+0023
(#
),以及一个字符 U+0022
(双引号 "
),这样的字符组合开始。
中间原生字符串主体部分可包含除了 U+000D
(CR) 之外的任意 Unicode 字符序列。
最后再后跟另一个 U+0022
(双引号 "
)以及跟与字符串主体前的那段字符组合中的同等数量的 U+0023
(#
)的字符来表示字符串主体的结束。
所有包含在原生字符串文本主体中的 Unicode 字符都代表他们自身:字符 U+0022
(双引号 "
)(除非后跟的纯 U+0023
(#
)字符串与文本主体开始前的对称相等)或字符 U+005C
(\
)此时都没有特殊含义。
字符串字面量示例:
#![allow(unused)] fn main() { "foo"; r"foo"; // foo "\"foo\""; r#""foo""#; // "foo" "foo #\"# bar"; r##"foo #"# bar"##; // foo #"# bar "\x52"; "R"; r"R"; // R "\\x52"; r"\x52"; // \x52 }
Byte and byte string literals
字节和字节串字面量
Byte literals
字节字面量
词法
BYTE_LITERAL :
b'
( ASCII_FOR_CHAR | BYTE_ESCAPE )'
SUFFIX?ASCII_FOR_CHAR :
任何 ASCII 字符 (0x00 到 0x7F), 排除'
,\
, \n, \r 或者 \tBYTE_ESCAPE :
\x
HEX_DIGIT HEX_DIGIT
|\n
|\r
|\t
|\\
|\0
|\'
|\"
字节字面量是单个 ASCII 字符(码值在 U+0000
到 U+007F
区间内)或一个转义字节作为字节字面量的真实主体跟在表示形式意义的字符 U+0062
(b
)和字符 U+0027
(单引号 '
)组合之后,然后再后接字符 U+0027
。如果字符 U+0027
本身要出现在字面量中,它必须经由前置字符 U+005C
(\
)转义。字节字面量等价于一个 u8
8-bit 无符号整型数字字面量。
Byte string literals
字节串字面量
词法
BYTE_STRING_LITERAL :
b"
( ASCII_FOR_STRING | BYTE_ESCAPE | STRING_CONTINUE )*"
SUFFIX?ASCII_FOR_STRING :
任何 ASCII 字符(码值位于 0x00 到 0x7F 之间), 排除"
,\
和 IsolatedCR
非原生字节串字面量是 ASCII 字符和转义字符组成的字符序列,形式是以字符 U+0062
(b
)和字符 U+0022
(双引号 "
)组合开头,以字符 U+0022
结尾。如果字面量中包含字符 U+0022
,则必须由前置的 U+005C
(\
)转义。
或者,字节串字面量也可以是原生字节串字面量(下面有其定义)。
字节串字面量允许使用字符U+000A
(LF) 的形式来换行书写。
但当非转义的字符 U+005C
(\
)后面紧跟着一个换行符时,换行符并不会出现在字符串中。
详细信息请参见字符串接续符转义。
字符U+000D
(CR) 除了作为这种字符串接续符转义的一部分之外,不能出现在字节串字面量中。
一些额外的转义可以在字节或非原生字节串字面量中使用,转义以 U+005C
(\
)开始,并后跟如下形式之一:
- 字节转义以
U+0078
(x
)开始,后跟恰好两位十六进制数字来表示十六进制值代表的字节。 - 空白符转义是字符
U+006E
(n
)、U+0072
(r
),或U+0074
(t
)之一,分别表示字节值0x0A
(ASCII LF)、0x0D
(ASCII CR),或0x09
(ASCII HT)。 - null转义是字符
U+0030
(0
),表示字节值0x00
(ASCII NUL)。 - 反斜线转义是字符
U+005C
(\
),必须被转义以表示其 ASCII 编码0x5C
。
Raw byte string literals
原生字节串字面量
词法
RAW_BYTE_STRING_LITERAL :
br
RAW_BYTE_STRING_CONTENT SUFFIX?RAW_BYTE_STRING_CONTENT :
"
ASCII_FOR_RAW* (非贪婪模式)"
|#
RAW_BYTE_STRING_CONTENT#
ASCII_FOR_RAW :
除了 IsolatedCR 外的任何 ASCII 字符(0x00 到 0x7F)
原生字节串字面量不做任何转义。它们以字符 U+0062
(b
)后跟 U+0072
(r
),再后跟小于256个字符 U+0023
(#
)及字符 U+0022
(双引号 "
),这样的字符组合开始。
中间是原生字节串主体,这部分可包含除了 U+000D
(CR) 外的任意的 ASCII 字符序列。
最后再后跟另一个 U+0022
(双引号 "
)以及跟与字符串主体前的那段字符组合中的同等数量的 U+0023
(#
)的字符来表示字符串主体的结束。
原生字节串字面量不能包含任何非 ASCII 字节。
原生字节串文本主体中的所有字符都代表它们自身的 ASCII 编码,字符 U+0022
(双引号 "
)(除非后跟的纯 U+0023
(#
)字符串与文本主体开始前的对称相等)或字符 U+005C
(\
)此时都没有特殊含义。
字节串字面量示例:
#![allow(unused)] fn main() { b"foo"; br"foo"; // foo b"\"foo\""; br#""foo""#; // "foo" b"foo #\"# bar"; br##"foo #"# bar"##; // foo #"# bar b"\x52"; b"R"; br"R"; // R b"\\x52"; br"\x52"; // \x52 }
C string and raw C string literals
C语言风格的字符串字面量和原生C语言风格的字符串字面量
C string literals
C语言风格的字符串字面量
词法
C_STRING_LITERAL :
c"
(
~["
\
IsolatedCR NUL]
| BYTE_ESCAPE except\0
or\x00
| UNICODE_ESCAPE except\u{0}
,\u{00}
, …,\u{000000}
| STRING_CONTINUE
)*"
SUFFIX?
C语言风格的字符串字面量_是通过前面是字符U+0063
(c
) 和 U+0022
(双引号),后面是字符U+0022
转义过的 Unicode字符序列。如果字面量中存在字符U+0022
,则其前面必须用U+005C
(\
)字符进行转义。
或者,C语言风格的字符串字面量可以是下面定义的_原生C语言风格的字符串字面量。
C语言风格的字符串由字节0x00
隐式终止,因此C语言风格的字符串字面量c""
等同于从字节字符串文字b"\x00"
来手动构造一个&CStr
。除了隐式终止符之外,C语言风格的字符串中不允许再使用字节0x00
。
C语言风格的字符串字面量允许使用字符U+000A
(LF) 的形式来换行书写。
但当非转义的字符 U+005C
(\
)后面紧跟着一个换行符时,换行符并不会出现在字符串中。
详细信息请参见字符串接续符转义。
字符U+000D
(CR) 除了作为这种字符串接续符转义的一部分之外,不能出现在C语言风格的字符串字面量中。
一些额外的转义符在非原生C语言风格的字符串字面量中可用。转义以U+005C
(\
)开头,并后继以下形式之一:
- 一个_字节转义符_以
U+0078
(x
)开头,后面正好跟两个_十六进制数字_。它表示等于所提供的十六进制值的字节序。 - 一个_24位码点转义符_以
U+0075
(u
) 开头,后面最多有六个由大括号U+007B
({
)和U+007D
(}
)包围的_十六进制数字_。它表示由这些十六进制数值通过的UTF-8编码的Unicode码点值。 - _空白转义符_是字符
U+006E
(n
)、U+0072
(r
) 或U+0074
(t
) 之一,分别表示字节0x0A
(ASCII LF)、0x0D
(ASCII CR) 或0x09
(ASCII HT)。 - _反斜杠转义符_就是字符
U+005C
(\
),必须对其进行转义才能表示其ASCII编码的0x5C
。
C语言风格的字符串本身表示没有定义编码类型的字节序,但 C语言风格的字符串字面量又可能包含U+007F
以上的 Unicode字符。这种情况下这些字符将被替换为该字符的UTF-8表示形式的字节。
以下 C语言风格的字符串字面量表达形式等效:
#![allow(unused)] fn main() { c"æ"; // 小写的拉丁字符 AE (U+00E6) c"\u{00E6}"; c"\xC3\xA6"; }
版次差异: 2021版或更高版本中可以使用 C语言风格的字符串字面量。在更低的版次中,
c""
token 会被词法解析器解析为c ""
。
Raw C string literals
原生C语言风格的字符串字面量
Lexer
RAW_C_STRING_LITERAL :
cr
RAW_C_STRING_CONTENT SUFFIX?RAW_C_STRING_CONTENT :
"
( ~ IsolatedCR NUL )* (non-greedy)"
|#
RAW_C_STRING_CONTENT#
原生C语言风格的字符串字面量不处理任何转义。它们以字符U+0063
(c
)开头,然后跟U+0072
(r
),然后再跟少于256个的字符U+0023
(#
)和一个 U+0022
(双引号)字符(记作开头引号)。
中间_原生C语言风格的字符串本体_可以包含除 U+0000
(NUL) 和 U+000D
(CR) 之外的任何 Unicode字符序列。
最后再后跟另一个 U+0022
(双引号 "
)以及跟与字符串主体前的那段字符组合中的同等数量的 U+0023
(#
)的字符来表示字符串主体的结束。
原生C语言风格的字符串本体中包含的所有字符都以 UTF-8编码形式表示。字符U+0022
(双引号)(后面跟有至少与用于开始原生C语言风格的字符串字面量的数量相同的 U+0023
(#
)字符时除外)或 U+005C
(\
) 没有任何特殊含义。
版次差异: 2021版或更高版次可以使用原生C语言风格的字符串字面量。在更低的版次中,
cr""
token 会被词法解析器解析为cr ""
,cr#""#
被解析为cr #""#
(这是不符合语法的)。
Examples for C string and raw C string literals
C语言风格的字符串字面量和原生C语言风格的字符串字面量的示例
#![allow(unused)] fn main() { c"foo"; cr"foo"; // foo c"\"foo\""; cr#""foo""#; // "foo" c"foo #\"# bar"; cr##"foo #"# bar"##; // foo #"# bar c"\x52"; c"R"; cr"R"; // R c"\\x52"; cr"\x52"; // \x52 }
Number literals
数字字面量
数字字面量可以是整型字面量,也可以是浮点型字面量,识别这两种字面量的文法是混合在一起的。
Integer literals
整型字面量
词法
INTEGER_LITERAL :
( DEC_LITERAL | BIN_LITERAL | OCT_LITERAL | HEX_LITERAL ) SUFFIX_NO_E?DEC_LITERAL :
DEC_DIGIT (DEC_DIGIT|_
)*BIN_LITERAL :
0b
(BIN_DIGIT|_
)* BIN_DIGIT (BIN_DIGIT|_
)*OCT_LITERAL :
0o
(OCT_DIGIT|_
)* OCT_DIGIT (OCT_DIGIT|_
)*HEX_LITERAL :
0x
(HEX_DIGIT|_
)* HEX_DIGIT (HEX_DIGIT|_
)*BIN_DIGIT : [
0
-1
]OCT_DIGIT : [
0
-7
]DEC_DIGIT : [
0
-9
]HEX_DIGIT : [
0
-9
a
-f
A
-F
]
整型字面量具备下述 4 种形式之一:
- 十进制字面量以十进制数字开头,后跟十进制数字和*下划线(
_
)*的任意组合。 - 十六进制字面量以字符序列
U+0030
U+0078
(0x
)开头,后跟十六进制数字和下划线的任意组合(至少一个数字)。 - 八进制字面量以字符序列
U+0030
U+006F
(0o
)开头,后跟八进制数字和下划线的任意组合(至少一个数字)。 - 二进制字面量以字符序列
U+0030
U+0062
(0b
)开头,后跟二进制数字和下划线的任意组合(至少一个数字)。
与其它字面量一样,整型字面量后面可紧跟(没有空格)一个上述的后缀。
后缀不能以 e
或 E
开头,因为这将被解析为浮点字面量的指数。
参见整型字面量表达式以了解这些后缀的功能效果。
被正确解析为整型字面量的示例:
#![allow(unused)] fn main() { #![allow(overflowing_literals)] 123; 123i32; 123u32; 123_u32; 0xff; 0xff_u8; 0x01_f32; // 注意这是整数 7986, 不是浮点数 1.0 0x01_e3; // 注意这是整数 483, 不是浮点数 1000.0 0o70; 0o70_i16; 0b1111_1111_1001_0000; 0b1111_1111_1001_0000i64; 0b________1; 0usize; // 下面这些对它们的类型来说太大了,但仍被认为是字面量表达式 128_i8; 256_u8; // 这是一个整型字面量,但被解析器接受为浮点型字面量表达式 5f32; }
注意对于 -1i8
这样的,其实它被分析为两个 token: -
后跟 1i8
。
不被承认为合法字面量表达式的整型字面量:
#![allow(unused)] fn main() { #[cfg(FALSE)] { 0invalidSuffix; 123AFB43; 0b010a; 0xAB_CD_EF_GH; 0b1111_f32; } }
Tuple index
元组索引
词法
TUPLE_INDEX:
INTEGER_LITERAL
元组索引直接与字面量token 进行比较。元组索引以 0
开始,每个后续索引的值以十进制的 1
递增。因此,元组索引只能匹配十进制值,并且该值不能用 0
做前缀字符。
#![allow(unused)] fn main() { let example = ("dog", "cat", "horse"); let dog = example.0; let cat = example.1; // 下面的示例非法. let cat = example.01; // 错误:没有 `01` 字段 let horse = example.0b10; // 错误:没有 `0b10` 字段 }
注意: 元组的索引可能包含一些特定的后缀,但是这不是被故意设计为有效的,可能会在将来的版本中被移除。 更多信息请参见https://github.com/rust-lang/rust/issues/60210。
Floating-point literals
浮点型字面量
词法
FLOAT_LITERAL :
DEC_LITERAL.
_(紧跟着的不能是.
,_
或者 XID_Start类型的字符)__
| DEC_LITERAL.
DEC_LITERAL SUFFIX_NO_E?
| DEC_LITERAL (.
DEC_LITERAL)? FLOAT_EXPONENT SUFFIX?FLOAT_EXPONENT :
(e
|E
) (+
|-
)? (DEC_DIGIT|_
)* DEC_DIGIT (DEC_DIGIT|_
)*
浮点型字面量有如下两种形式:
- 十进制字面量后跟句点字符
U+002E
(.
)。后面可选地跟着另一个十进制数字,还可以再接一个可选的指数。 - 十进制字面量后跟一个指数。
如同整型字面量,浮点型字面量也可后跟一个后缀,但在后缀之前,浮点型字面量部分不以 U+002E
(.
)结尾。
如果字面量不包含指数,则后缀不能以 e
或 E
开头。
参见浮点型字面量表达式以了解这类后缀的功能效果。
各种形式的浮点型字面量示例:
#![allow(unused)] fn main() { 123.0f64; 0.1f64; 0.1f32; 12E+99_f64; let x: f64 = 2.; }
最后一个例子稍显不同,因为不能对一个以句点结尾的浮点型字面量使用后缀句法,2.f64
会尝试在 2
上调用名为 f64
的方法。
请注意,像 -1.0
这样的会被分析为两个 token: -
后跟 1.0
。
不被认为是合法的字面量表达式的浮点型字面量的示例:
#![allow(unused)] fn main() { #[cfg(FALSE)] { 2.0f80; 2e5f80; 2e5e6; 2.0e5e6; 1.3e10u64; } }
Reserved forms similar to number literals
类似于数字字面量的保留形式
Lexer
RESERVED_NUMBER :
BIN_LITERAL [2
-9
]
| OCT_LITERAL [8
-9
]
| ( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL ).
(不能直接后跟.
,_
或一个 XID_Start类型的字符)
| ( BIN_LITERAL | OCT_LITERAL ) (e
|E
)
|0b
_
* end of input or not BIN_DIGIT
|0o
_
* end of input or not OCT_DIGIT
|0x
_
* end of input or not HEX_DIGIT
| DEC_LITERAL ( . DEC_LITERAL)? (e
|E
) (+
|-
)? end of input or not DEC_DIGIT
后面词法形式和数字字面量差不多的保留形式。 由于这些可能会引起歧义,它们会被 token转化器(tokenizer)拒绝,而不是被解释为单独的 token。
-
不带后缀的二进制或八进制字面量,不插入空格的后跟一个超出其进制数字字符范围的十进制数字。
-
不带后缀的二进制、八进制或十六进制字面量,不插入空格的后跟一个句点字符(句点后面的内容与浮点数字面量相同)。
-
不带前缀的二进制或八进制字面量,不加空格的后跟字符
e
或E
。 -
以一个进制数前缀开始的输入,但又不是有效的二进制、八进制或十六进制字面量(因为它没包含数字)。
-
具有浮点型字面量形式且指数中没有数字的输入。
这些保留形式的示例:
#![allow(unused)] fn main() { 0b0102; // 这可不是 `0b010` 后跟 `2` 0o1279; // 这可不是 `0o127` 后跟 `9` 0x80.0; // 这可不是 `0x80` 后跟 `.` and `0` 0b101e; // 这不是一个带后缀的字面量,也不是 `0b101` 后跟 `e` 0b; // 这不是一个整型字面量,也不是 `0` 后跟 `b` 0b_; // 这不是一个整型字面量,也不是 `0` 后跟 `b_` 2e; // 这不是一个浮点型字面量,也不是 `2` 后跟 `e` 2.0e; // 这不是一个浮点型字面量,也不是 `2.0` 后跟 `e` 2em; // 这不是一个带后缀的字面量,也不是 `2` 后跟 `em` 2.0em; // 这不是一个带后缀的字面量,也不是 `2.0` 后跟 `em` }
Lifetimes and loop labels
生存期和循环标签
词法
LIFETIME_TOKEN :
'
IDENTIFIER_OR_KEYWORD (后面没有紧跟'
)
|'_
(后面没有紧跟'
)LIFETIME_OR_LABEL :
'
NON_KEYWORD_IDENTIFIER (后面没有紧跟'
)
生存期参数和循环标签使用 LIFETIME_OR_LABEL 类型的 token。(尽管 LIFETIME_OR_LABEL 是 LIFETIME_TOKEN 的子集,但)任何符合 LIFETIME_TOKEN 约定的 token 也都能被上述词法分析规则所接受,比如 LIFETIME_TOKEN 类型的 token 在宏中就可以畅通无阻的使用。
Punctuation
标点符号
为了完整起见,这里列出了(Rust 里)所有的标点符号的 symbol token。它们各自的用法和含义在链接页面中都有定义。
符号 | 名称 | 使用方法 |
---|---|---|
+ | Plus | 算术加法, trait约束, 可匹配空的宏匹配器(Macro Kleene Matcher) |
- | Minus | 算术减法, 取反 |
* | Star | 算术乘法, 解引用, 裸指针, 可匹配空的宏匹配器, use 通配符 |
/ | Slash | 算术除法 |
% | Percent | 算术取模 |
^ | Caret | 位和逻辑异或 |
! | Not | 位和逻辑非, 宏调用, 内部属性, never型, 否定实现 |
& | And | 位和逻辑与, 借用, 引用, 引用模式 |
| | Or | 位和逻辑或, 闭包, match 中的模式, if let, 和 while let |
&& | AndAnd | 短路与, 借用, 引用, 引用模式 |
|| | OrOr | 短路或, 闭包 |
<< | Shl | 左移位, 嵌套泛型 |
>> | Shr | 右移位, 嵌套泛型 |
+= | PlusEq | 加法及赋值 |
-= | MinusEq | 减法及赋值 |
*= | StarEq | 乘法及赋值 |
/= | SlashEq | 除法及赋值 |
%= | PercentEq | 取模及赋值 |
^= | CaretEq | 按位异或及赋值 |
&= | AndEq | 按位与及赋值 |
|= | OrEq | 按位或及赋值 |
<<= | ShlEq | 左移位及赋值 |
>>= | ShrEq | 右移位及赋值, 嵌套泛型 |
= | Eq | 赋值, 属性, 各种类型定义 |
== | EqEq | 等于 |
!= | Ne | 不等于 |
> | Gt | 大于, 泛型, 路径 |
< | Lt | 小于, 泛型, 路径 |
>= | Ge | 大于或等于, 泛型 |
<= | Le | 小于或等于 |
@ | At | 子模式绑定 |
_ | Underscore | 通配符模式, 自动推断型类型, 常量项中的非命名程序项, 外部 crate, 和 use声明,和解构赋值 |
. | Dot | 字段访问, 元组索引 |
.. | DotDot | 区间, 结构体表达式, 模式,区间模式 |
... | DotDotDot | 可变参数函数, 区间模式 |
..= | DotDotEq | 闭区间, 区间模式 |
, | Comma | 各种分隔符 |
; | Semi | 各种程序项和语句的结束符, 数组类型 |
: | Colon | 各种分隔符 |
:: | PathSep | [路径分隔符]路径 |
-> | RArrow | 函数返回类型, 闭包返回类型, 数组指针类型 |
=> | FatArrow | 匹配臂, 宏 |
<- | LArrow | 左箭头符号在Rust 1.0之后就没有再使用过,但它仍然被视为一个单独的 token |
# | Pound | 属性 |
$ | Dollar | 宏 |
? | Question | 问号运算符, 非确定性内存宽度, 可匹配空的宏匹配器 |
~ | Tilde | 从 Rust 1.0 开始,波浪号操作符就弃用了,但其 token 可能仍在使用 |
Delimiters
定界符
括号用于文法的各个部分,左括号必须始终与右括号配对。括号以及其内的 token 在宏中被称作“token树(token trees)”。括号有三种类型:
括号 | 类型 |
---|---|
{ } | 花/大括号 |
[ ] | 方/中括号 |
( ) | 圆/小括号 |
Reserved prefixes
保留前缀
词法 2021+
RESERVED_TOKEN_DOUBLE_QUOTE : ( IDENTIFIER_OR_KEYWORD 排除b
或c
或r
或br
或cr
|_
)"
RESERVED_TOKEN_SINGLE_QUOTE : ( IDENTIFIER_OR_KEYWORD 排除b
|_
)'
RESERVED_TOKEN_POUND : ( IDENTIFIER_OR_KEYWORD 排除r
或br
或cr
|_
)#
这种被称为 保留前缀 的词法形式是保留供将来使用的。
如果输入的源代码在词法上被解释为非原生标识符(或关键字或 _
),且紧接着的字符是 #
、'
或 "
(没有空格),则将其标识为保留前缀。(译者注:例如 prefix#identifier, prefix"string", prefix'c', 和 prefix#123(其中 prefix可以是任何标识符(但不能是原生标识符))这样词法形式被标识为保留词法,以供将来拓展语法使用)
请注意,原生标识符、原生字符串字面量和原生字节字符串字面量可能包含 #
字符,但不会被解释为包含保留前缀。
类似地,原生字符串字面量、字节字面量、字节字符串字面量、原生字节字符串字面量、C语言风格的字符串字面量和原生C语言风格的字符串字面量中使用的r
、b
、 br
、 c
和 cr
前缀也不会解释为保留前缀。
版次差异:从2021版开始,保留前缀被词法解释器报告为错误(特别是,它们不能再传递给宏了)。
在2021版之前,保留前缀可以被词法解释器接受并被解释为多个 token(例如,后接
#
的标识符或关键字)。在所有的版次中都可以被编译器接受的示例:
#![allow(unused)] fn main() { macro_rules! lexes {($($_:tt)*) => {}} lexes!{a #foo} lexes!{continue 'foo} lexes!{match "..." {}} lexes!{r#let#foo} // 3个 tokens: r#let # foo }
在 2021之前的版次中可以被编译器接受,但之后会被拒绝的示例:
#![allow(unused)] fn main() { macro_rules! lexes {($($_:tt)*) => {}} lexes!{a#foo} lexes!{continue'foo} lexes!{match"..." {}} }
Macros
宏
macros.md
commit: 93f9325701a4c8beba06cb439c1fa88b26893844
本章译文最后维护日期:2022-08-21
可以使用称被为宏的自定义句法形式来扩展 Rust 的功能和句法。宏需要被命名,并通过一致的句法去调用:some_extension!(...)
。
定义新宏有两种方式:
- 声明宏(Macros by Example)以更高级别的声明性的方式定义了一套新句法规则。
- 过程宏(Procedural Macros)使用操作输入的token的函数来定义类函数宏、自定义派生和自定义属性。
Macro Invocation
宏调用
句法
MacroInvocation :
SimplePath!
DelimTokenTreeDelimTokenTree :
(
TokenTree*)
|[
TokenTree*]
|{
TokenTree*}
TokenTree :
Token排除 定界符(delimiters) | DelimTokenTreeMacroInvocationSemi :
SimplePath!
(
TokenTree*)
;
| SimplePath!
[
TokenTree*]
;
| SimplePath!
{
TokenTree*}
宏调用是在编译时来扩展宏的,并用扩展结果替换该调用。可以在下述情况里调用宏:
当宏调用被用作程序项或语句时,此时它应用的 MacroInvocationSemi 句法规则要求它如果不使用花括号,则在结尾处须添加分号。在宏调用或宏(macro_rules
)定义之前不允许使用可见性限定符。
#![allow(unused)] fn main() { // 作为表达式使用. let x = vec![1,2,3]; // 作为语句使用. println!("Hello!"); // 在模式中使用. macro_rules! pat { ($i:ident) => (Some($i)) } if let pat!(x) = Some(1) { assert_eq!(x, 1); } // 在类型中使用. macro_rules! Tuple { { $A:ty, $B:ty } => { ($A, $B) }; } type N2 = Tuple!(i32, i32); // 作为程序项使用. use std::cell::RefCell; thread_local!(static FOO: RefCell<u32> = RefCell::new(1)); // 作为关联程序项使用. macro_rules! const_maker { ($t:ty, $v:tt) => { const CONST: $t = $v; }; } trait T { const_maker!{i32, 7} } // 宏内调用宏 macro_rules! example { () => { println!("Macro call in a macro!") }; } // 外部宏 `example` 展开后, 内部宏 `println` 才会展开. example!(); }
Macros By Example
声明宏
macros-by-example.md
commit: 01c8196e0120f0577f6aa05ada9d962f0019a86c
本章译文最后维护日期:2024-05-26
句法
MacroRulesDefinition :
macro_rules
!
IDENTIFIER MacroRulesDefMacroRulesDef :
(
MacroRules)
;
|[
MacroRules]
;
|{
MacroRules}
MacroRules :
MacroRule (;
MacroRule )*;
?MacroRule :
MacroMatcher=>
MacroTranscriberMacroMatcher :
(
MacroMatch*)
|[
MacroMatch*]
|{
MacroMatch*}
MacroMatch :
Token排除$
和定界符
| MacroMatcher
|$
( IDENTIFIER_OR_KEYWORD 排除crate
| RAW_IDENTIFIER |_
):
MacroFragSpec
|$
(
MacroMatch+)
MacroRepSep? MacroRepOpMacroFragSpec :
block
|expr
|ident
|item
|lifetime
|literal
|meta
|pat
|pat_param
|path
|stmt
|tt
|ty
|vis
MacroRepSep :
Token排除 定界符 和 MacroRepOpMacroRepOp :
*
|+
|?
MacroTranscriber :
DelimTokenTree
macro_rules
允许用户以声明性的(declarative)方式定义句法扩展。我们称这种扩展形式为“声明宏(macros by example)”或简称“宏”。
每个声明宏都有一个名称和一条或多条规则。每条规则都有两部分:一个匹配器(matcher),描述它匹配的句法;一个转码器(transcriber),描述成功匹配后将执行的替代调用句法。匹配器和转码器都必须由定界符(delimiter)包围。宏可以扩展为表达式、语句、程序项(包括 trait、impl 和外来程序项)、类型或模式。
Transcribing
转码
当宏被调用时,宏扩展器(macro expander)按名称查找宏调用,并依次尝试此宏中的每条宏规则。宏会根据第一个成功的匹配进行转码;如果当前转码结果导致错误,不会再尝试进行后续匹配。在匹配时,不会执行预判;如果编译器不能明确地确定如何一个 token 一个 token 地解析宏调用,则会报错。在下面的示例中,编译器不会越过标识符,去提前查看后跟的 token 是 )
,尽管这能帮助它明确地解析调用:
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $j:ident) => { }; } ambiguity!(error); // 错误: 局部歧义(local ambiguity) }
在匹配器和转码器中,token $
用于从宏引擎中调用特殊行为(下文元变量和重复元中有详述)。不属于此类调用的 token 将按字面意义进行匹配和转码,除了一个例外。这个例外是匹配器的外层定界符将匹配任何一对定界符。因此,比如匹配器 (())
将匹配 {()}
,而 {{}}
不行。字符 $
不能按字面意义匹配或转码。
Forwarding a matched fragment
转发匹配段
当将当前匹配的匹配段转发给另一个声明宏时,第二个宏中的匹配器看到的将是此匹配段类型的不透明抽象句法树(opaque AST)。第二个宏不能使用字面量token 来匹配匹配器中的这个匹配段,唯一可看到/使用的就是此匹配段类型一样的匹配段选择器(fragment specifier)。但匹配段类型 ident
、lifetime
、和 tt
是几个例外,它们可以通过字面量token 进行匹配。下面示例展示了这一限制:(译者注:匹配段选择器和匹配段,以及宏中各部件的定义可以凑合着看看译者未能翻译完成的宏定义规范)
#![allow(unused)] fn main() { macro_rules! foo { ($l:expr) => { bar!($l); } // ERROR: ^^ no rules expected this token in macro call } macro_rules! bar { (3) => {} } foo!(3); }
以下示例展示了 tt
类型的匹配段在成功匹配(转码)一次之后生成的 tokens 如何能够再次直接匹配:
#![allow(unused)] fn main() { // 成功编译 macro_rules! foo { ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3); }
Metavariables
元变量
在匹配器中,$
名称:
匹配段选择器 这种句法格式匹配符合指定句法类型的 Rust 句法段,并将其绑定到元变量 $
名称 上。有效的匹配段选择器包括:
item
: 程序项block
: 块表达式stmt
: 语句,注意此选择器不匹配句尾的分号(如果匹配器中提供了分号,会被当做分隔符),但碰到分号是自身的一部分的程序项语句的情况又会匹配。pat_param
: 顶层无or模式的模式pat
: 目前至少可以匹配任意顶层无or模式的模式, 具体匹配细节或许更依赖于具体的版次expr
: 表达式ty
: 类型ident
: 标识符或关键字或裸标识符path
: 类型表达式 形式的路径tt
: token树 (单个 token 或宏匹配定界符()
、[]
或{}
中的标记)meta
: 属性,属性中的内容lifetime
: 生存期tokenvis
: 可能为空的可见性限定符literal
: 匹配-
?字面量表达式
因为匹配段类型已在匹配器中指定了,则在转码器中,元变量只简单地用 $
名称 这种形式来指代就行了。元变量最终将被替换为跟它们匹配上的句法元素。元变量关键字 $crate
可以用来指代当前的 crate(请参阅后面的卫生性(hygiene)章节)。元变量可以被多次转码,也可以完全不转码。
出于向后兼容性的原因,尽管 _
也是一个表达式,expr
段指示符并不会与单独的下划线匹配。但是,_
作为子表达式出现时,它却可以与 expr
段指示符相匹配。
出于相同的原因,expr
段指示符也不能与独立的常量块相匹配,但常量块做为子表达式时是可以与expr
段指示符匹配的。
版次差异:从 2021版次开始,段指示符
pat
匹配顶层的or模式(也就是说,它能接受全部形态的模式)。在 2021版次之前,它和
pat_param
匹配的段是完全一样的(也就是说它至接受顶层无or模式的模式)。相关版次为
macro_rules!
定义的有效版次。
Repetitions
重复元
在匹配器和转码器中,重复元被表示为:将需要重复的 token 放在 $(
…)
内,然后后跟一个重复运算符(repetition operator),这两者之间可以放置一个可选的分隔符(separator token)。分隔符可以是除定界符或重复运算符之外的任何 token,其中分号(;
)和逗号(,
)最常见。例如: $( $i:ident ),*
表示用逗号分隔的任何数量的标识符。嵌套的重复元是合法的。
重复运算符为:
*
— 表示任意数量的重复元。+
— 表示至少有一个重复元。?
— 表示一个可选的匹配段,可以出现零次或一次。
因为 ?
表示最多出现一次,所以它不能与分隔符一起使用。
通过分隔符的分隔,重复的匹配段都会被匹配和转码为指定的数量的匹配段。元变量就和这些每个段中的重复元相匹配。例如,之前示例中的 $( $i:ident ),*
将 $i
去匹配列表中的所有标识符。
在转码过程中,重复元会受到额外的限制,以便于编译器知道该如何正确地扩展它们:
- 在转码器中,元变量必须与它在匹配器中出现的次数、指示符类型以及其在重复元内的嵌套顺序都完全相同。因此,对于匹配器
$( $i:ident ),*
,转码器=> { $i }
,=> { $( $( $i)* )* }
和=> { $( $i )+ }
都是非法的,但是=> { $( $i );* }
是正确的,它用分号分隔的标识符列表替换了逗号分隔的标识符列表。 - 转码器中的每个重复元必须至少包含一个元变量,以便确定扩展多少次。如果在同一个重复元中出现多个元变量,则它们必须绑定到相同数量的匹配段上,不能有的多,有的少。例如,
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))
里,绑定到$j
的匹配段的数量必须与绑定到$i
上的相同。这意味着用(a, b, c; d, e, f)
调用这个宏是合法的,并且可扩展到((a,d), (b,e), (c,f))
,但是(a, b, c; d, e)
是非法的,因为前后绑定的数量不同。此要求适用于嵌套的重复元的每一层。
Scoping, Exporting, and Importing
作用域、导出以及导入
由于历史原因,声明宏的作用域并不完全像各种程序项那样工作。宏有两种形式的作用域:文本作用域(textual scope)和基于路径的作用域(path-based scope)。文本作用域基于宏在源文件中(定义和使用所)出现的顺序,或是跨多个源文件出现的顺序,文本作用域是默认的作用域。(后本节面将进一步解释这个。)基于路径的作用域与其他程序项作用域的运行方式相同。宏的作用域、导出和导入主要由其属性控制。
当声明宏被非限定标识符(unqualified identifier)(非多段路径段组成的限定性路径)调用时,会首先在文本作用域中查找。如果文本作用域中没有任何结果,则继续在基于路径的作用域中查找。如果宏的名称由路径限定,则只在基于路径的作用域中查找。
use lazy_static::lazy_static; // 基于路径的导入.
macro_rules! lazy_static { // 文本定义.
(lazy) => {};
}
lazy_static!{lazy} // 首先通过文本作用域来查找我们的宏.
self::lazy_static!{} // 忽略文本作用域查找,直接使用基于路径的查找方式找到一个导入的宏.
Textual Scope
文本作用域
文本作用域很大程度上取决于宏本身在源文件中的出现顺序,其工作方式与用 let
语句声明的局部变量的作用域类似,只不过它可以直接位于模块下。当使用 macro_rules!
定义宏时,宏在定义之后进入其作用域(请注意,这不影响宏在定义中递归调用自己,因为宏调用的入口还是在定义之后的某次调用点上,此点开始的宏名称递归查找一定有效),在封闭它的作用域(通常是模块)结束时离开。文本作用域可以覆盖/进入子模块,甚至跨越多个文件:
//// src/lib.rs
mod has_macro {
// m!{} // 报错: m 未在作用域内.
macro_rules! m {
() => {};
}
m!{} // OK: 在声明 m 后使用.
mod uses_macro;
}
// m!{} // Error: m 未在作用域内.
//// src/has_macro/uses_macro.rs
m!{} // OK: m 在上层模块文件 src/lib.rs 中声明后使用
多次定义宏并不报错;除非超出作用域,否则最近的宏声明将屏蔽前一个。
#![allow(unused)] fn main() { macro_rules! m { (1) => {}; } m!(1); mod inner { m!(1); macro_rules! m { (2) => {}; } // m!(1); // 报错: 没有设定规则来匹配 '1' m!(2); macro_rules! m { (3) => {}; } m!(3); } m!(1); }
宏也可以在函数内部声明和使用,其工作方式类似:
#![allow(unused)] fn main() { fn foo() { // m!(); // 报错: m 未在作用域内. macro_rules! m { () => {}; } m!(); } // m!(); // Error: m 未在作用域内. }
The macro_use
attribute
macro_use
属性
macro_use
属性有两种用途。首先,它可以通过作用于模块的方式让模块内的宏的作用域在模块关闭时不结束:
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); }
其次,它可以用于从另一个 crate 里来导入宏,方法是将它附加到当前 crate 根模块中的 extern crate
声明前。以这种方式导入的宏会被导入到macro_use
预导入包里,而不是直接文本导入,这意味着它们可以被任何其他同名宏屏蔽。虽然可以在导入语句之前使用 #[macro_use]
导入宏,但如果发生冲突,则最后导入的宏将胜出。可以使用可选的 MetaListIdents元项属性句法指定要导入的宏列表;当将 #[macro_use]
应用于模块上时,则不支持此指定操作。
#[macro_use(lazy_static)] // 或者使用 #[macro_use] 来导入所有宏.
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // 报错: lazy_static 没在 `self` 中定义
要用 #[macro_use]
导入宏必须先使用 #[macro_export]
导出,下文会有讲解。
Path-Based Scope
基于路径的作用域
默认情况下,宏没有基于路径的作用域。但是如果该宏带有 #[macro_export]
属性,则相当于它在当前 crate 的根作用域的顶部被声明,它通常可以这样引用:
#![allow(unused)] fn main() { self::m!(); m!(); // OK: 基于路径的查找发现 m 在当前模块中有声明. mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
标有 #[macro_export]
的宏始终是 pub
的,以便可以通过路径或前面所述的 #[macro_use]
方式让其他 crate 来引用。
Hygiene
卫生性
默认情况下,宏中引用的所有标识符都按原样展开,并在宏的调用位置上去查找。如果宏引用的程序项或宏不在调用位置的作用域内,则这可能会导致问题。为了解决这个问题,可以替代在路径的开头使用元变量 $crate
,强制在定义宏的 crate 中进行查找。
//// 在 `helper_macro` crate 中.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // 这可能会导致错误,因为 'helper' 在当前作用域之后才定义.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// 在另一个 crate 中使用.
// 注意没有导入 `helper_macro::helper`!
use helper_macro::helped;
fn unit() {
helped!();
}
请注意,由于 $crate
指的是当前的($crate
源码出现的)crate,因此在引用非宏程序项时,它必须与全限定模块路径一起使用:
#![allow(unused)] fn main() { pub mod inner { #[macro_export] macro_rules! call_foo { () => { $crate::inner::foo() }; } pub fn foo() {} } }
此外,尽管 $crate
允许宏在扩展时引用其自身 crate 中的程序项,但它的使用对可见性没有影响(,或者说它的使用仍受可见性条件的约束)。引用的程序项或宏必须仍然在调用位置处可见。在下面的示例中,任何试图从此 crate 外部调用 call_foo!()
的行为都将失败,因为 foo()
不是公有的。
#![allow(unused)] fn main() { #[macro_export] macro_rules! call_foo { () => { $crate::foo() }; } fn foo() {} }
(译者注:原文给出的这个例子是能正常调用的,原文并没有给出在 crate 外部调用的例子)
版本&版次差异:在 Rust 1.30 之前,
$crate
和local_inner_macros
(后面会讲)不受支持。从该版本开始,它们与基于路径的宏导入(前面讲过)一起被添加进来,用以确保不需要在当前 crate 已经使用导入了某导出宏的情况下再额外手动导入此导出宏下面用到的辅助宏。如果要让 Rust 的早期版本编写的 crate 要使用辅助宏,需要修改为使用$crate
或local_inner_macros
,以便与基于路径的导入一起工作。
当一个宏被导出时,可以在 #[macro_export]
属性里添加 local_inner_macros
属性值,用以自动为该属性修饰的宏内包含的所有宏调用自动添加 $crate::
前缀。这主要是作为一个工具来迁移那些在引入 $crate
之前的版本编写的 Rust 代码,以便它们能与 Rust 2018 版中基于路径的宏导入一起工作。在使用新版本编写的代码中不鼓励使用它。
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // 自动转码为 $crate::helper!(). } #[macro_export] macro_rules! helper { () => { () } } }
Follow-set Ambiguity Restrictions
随集歧义限制(译者注:该节还需要继续校对打磨,主要难点还是因为附录的宏定义规范译者还没有能全部搞懂)
宏系统使用的解析器相当强大,但是为了防止其在 Rust 的当前或未来版本中出现二义性解析,因此对它做出了限制。特别地,在消除二义性展开的基本规则之外又增加了一条:元变量匹配的非终结符(nonterminal)必须后跟一个已经确定为可以用来安全分隔匹配段的分隔符。
例如,像 $i:expr [ , ]
这样的宏匹配器在现今的 Rust 中理论上是可以接受的,因为现在 [,]
不可能是合法表达式的一部分,因此解析始终是明确的。但是,由于 [
可以开始一个尾随表达式(trailing expressions),因此 [
不是一个可以安全排除在表达式后面出现的字符。如果在接下来的 Rust 版本中接受了 [,]
,那么这个匹配器就会产生歧义或是错误解析,破坏正常代码。但是,像$i:expr,
或 $i:expr;
这样的匹配符始终是合法的,因为 ,
和;
是合法的表达式分隔符。目前规范中的规则是:(译者注:下面的规则不是绝对的,因为宏的基础理论还在发展中。)
-
expr
和stmt
只能后跟一个:=>
、,
、;
。 -
pat_param
只能后跟一个:=>
、,
、=
、|
、if
或in
。 -
pat
只能后跟一个:=>
,,
,=
,if
或in
。 -
path
和ty
只能后跟一个:=>
、,
、=
、|
、;
、:
、>
、>>
、[
、{
、as
、where
、块(block
)型非终结符(block nonterminals)。 -
vis
只能后跟一个:,
、非原生字符串priv
以外的任何标识符和关键字、可以表示类型开始的任何 token、ident
或ty
或path
型非终结符。(译者注:可以表示类型开始的 token 有:{
(
,[
,!
,\*
,&
,&&
,?
, 生存期,>
,>>
,::
, 非关键字标识符,super
,self
,Self
,extern
,crate
,$crate
,_
,for
,impl
,fn
,unsafe
,typeof
,dyn
}。注意这个列表也不一定全。) -
其它所有的匹配段选择器没有限制。
Edition Differences: 在2021版次之前,
pat
也可后跟|
。
当涉及到重复元时,随集歧义限制适用于所有可能的展开次数,注意需将重复元中的分隔符考虑在内。这意味着:
- 如果重复元包含分隔符,则分隔符必须能够跟随重复元的内容重复。
- 如果重复元可以重复多次(
*
或+
),那么重复元的内容必须能自我重复。 - 重复元前后内容必须严格匹配匹配器中指定的前后内容。
- 如果重复元可以匹配零次(
*
或?
),那么它后面的内容必须能够直接跟在它前面的内容后面。
有关更多详细信息,请参阅正式规范。
Procedural Macros
过程宏
procedural-macros.md
commit: a7a86824fa90172340e20053be5e6f217cc466fe
本章译文最后维护日期:2024-04-06
过程宏允许在执行函数时创建句法扩展。过程宏有三种形式:
- 类函数宏(function-like macros) -
custom!(...)
- 派生宏(derive macros)-
#[derive(CustomDerive)]
- 属性宏(attribute macros) -
#[CustomAttribute]
过程宏允许在编译时运行对 Rust 句法进行操作的代码,它可以在消费掉一些 Rust 句法输入的同时产生新的 Rust 句法输出。可以将过程宏想象成是从一个 AST 到另一个 AST 的函数映射。
过程宏必须在 crate 类型为 proc-macro
的 crate 中定义。
注意: 使用 Cargo 时,定义过程宏的 crate 的配置文件里要使用
proc-macro
键做如下设置:[lib] proc-macro = true
作为函数,它们要么有返回句法,要么触发 panic,要么永无休止地循环。返回句法根据过程宏的类型替换或添加句法;panic 会被编译器捕获,并将其转化为编译器错误;无限循环不会被编译器不会捕获,但会导致编译器被挂起。
过程宏在编译时运行,因此具有与编译器相同的环境资源。例如,它可以访问的标准输入、标准输出和标准错误输出等,这些编译器可以访问的资源。类似地,文件访问也是一样的。因此,过程宏与 Cargo构建脚本 具有相同的安全考量。
过程宏有两种报告错误的方法。首先是 panic;第二个是发布 compile_error
性质的宏调用。
The proc_macro
crate
过程宏类型的 crate 几乎总是会去链接编译器提供的 proc_macro
crate。proc_macro
crate 提供了编写过程宏所需的各种类型和工具来让编写更容易。
此 crate 主要包含了一个 TokenStream
类型。过程宏其实是在 *token流(token streams)*上操作,而不是在某个或某些 AST 节点上操作,因为这对于编译器和过程宏的编译目标来说,这是一个随着时间推移要稳定得多的接口。token流大致相当于 Vec<TokenTree>
,其中 TokenTree
可以大致视为词法 token。例如,foo
是标识符(Ident
)类型的 token,.
是一个标点符号(Punct
)类型的 token,1.2
是一个字面量(Literal
)类型的 token。不同于 Vec<TokenTree>
的是 TokenStream
的克隆成本很低。
所有类型的 token 都有一个与之关联的 Span
。
Span
是一个不透明的值,不能被修改,但可以手工创建。
Span
表示程序内的源代码范围,主要用于错误报告。
虽然不能修改 Span
本身,但可以更改与 token 关联的 Span
,例如通过其他 token 来获取 Span
。
Procedural macro hygiene
过程宏的卫生性
过程宏是非卫生的(unhygienic)。这意味着它的行为就好像它输出的 token流是被简单地内联写入它周围的代码中一样。这意味着它会受到外部程序项的影响,也会影响外部导入。
鉴于此限制,宏作者需要小心地确保他们的宏能在尽可能多的上下文中正常工作。这通常包括对库中程序项使用绝对路径(例如,使用 ::std::option::Option
而不是 Option
),或者确保生成的函数具有不太可能与其他函数冲突的名称(如 __internal_foo
,而不是 foo
)。
Function-like procedural macros
类函数过程宏
类函数过程宏是使用宏调用运算符(!
)调用的过程宏。
这种宏是由一个带有 proc_macro
属性和 (TokenStream) -> TokenStream
签名的 公有可见性函数定义。输入 TokenStream
是由宏调用的定界符界定的内容,输出 TokenStream
将替换整个宏调用。
例如,下面的宏定义忽略它的输入,并将函数 answer
输出到它的作用域。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
然后我们用它在一个二进制 crate 里打印 “42” 到标准输出。
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
类函数过程宏可以在任何宏调用位置调用,这些位置包括语句、表达式、模式、类型表达式、程序项可以出现的位置(包括extern
块里、固有(inherent)实现里和 trait实现里、以及 trait声明里)。
Derive macros
派生宏
派生宏为派生(derive
)属性定义新输入。这类宏在给定输入结构体(struct
)、枚举(enum
)或联合体(union
) token流的情况下创建新程序项。它们也可以定义派生宏辅助属性。
自定义派生宏由带有 proc_macro_derive
属性和 (TokenStream) -> TokenStream
签名的公有可见性函数定义。
输入 TokenStream
是带有 derive
属性的程序项的 token流。输出 TokenStream
必须是一组程序项,然后将这组程序项追加到输入 TokenStream
中的那条程序项所在的模块或块中。
下面是派生宏的一个示例。它没有对输入执行任何有用的操作,只是追加了一个函数 answer
。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
然后使用这个派生宏:
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
Derive macro helper attributes
派生宏辅助属性
派生宏可以将额外的属性添加到它们所在的程序项的作用域中。这些属性被称为派生宏辅助属性。这些属性是惰性的,它们存在的唯一目的是将这些属性在使用现场获得的属性值反向输入到定义它们的派生宏中。也就是说所有该宏的宏应用都可以看到它们。
定义辅助属性的方法是在 proc_macro_derive
宏中放置一个 attributes
键,此键带有一个使用逗号分隔的标识符列表,这些标识符是辅助属性的名称。
例如,下面的派生宏定义了一个辅助属性 helper
,但最终没有用它做任何事情。
#![crate_type="proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
然后在一个结构体上使用这个派生宏:
#[derive(HelperAttr)]
struct Struct {
#[helper] field: ()
}
Attribute macros
属性宏
属性宏定义可以附加到程序项上的新的外部属性,这些程序项包括外部(extern
)块、固有实现、trate实现,以及 trait声明中的各类程序项。
属性宏由带有 proc_macro_attribute
属性和 (TokenStream, TokenStream) -> TokenStream
签名的公有可见性函数定义。签名中的第一个 TokenStream
是属性名称后面的定界 token树(delimited token tree)(不包括外层定界符)。如果该属性作为裸属性(bare attribute)给出,则第一个 TokenStream
值为空。第二个 TokenStream
是程序项的其余部分,包括该程序项的其他属性。输出的 TokenStream
将此属性宏应用的程序项替换为任意数量的程序项。
例如,下面这个属性宏接受输入流并按原样返回,实际上对属性并无操作。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
下面示例显示了属性宏看到的字符串化的 TokenStream
。输出将显示在编译时的编译器输出窗口中。(具体格式是以 "out:"为前缀的)输出内容也都在后面每个示例函数后面的注释中给出了。
// my-macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{attr}\"");
println!("item: \"{item}\"");
item
}
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams;
// 示例: 基础函数
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() {}"
// 示例: 带输入参数的属性
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
// 示例: 输入参数中有多个 token 的
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
// 示例:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"
Declarative macro tokens and procedural macro tokens
声明宏的 token 和过程宏的 token
在 token (或者更确切地说是 token树的)层面,声明性宏和过程性宏的使用比较类似,但它们的定义却不相同。
token树在声明宏macro_rules
(对应于 tt
类型的匹配器)中被定义为:
- 组分界(
(...)
,{...}
等) - 此语言支持的所有操作符,不论是单字符操作符或多字符操作符(
+
,+=
)。- 注意这里的操作符集合中不包括单引号
'
。
- 注意这里的操作符集合中不包括单引号
- 字面量 (
"string"
,1
等)- 注意负号(例如
-1
)永远不会是字面量token的一部分,它只是另外一个操作符token。
- 注意负号(例如
- 标识符,包括关键字(
ident
,r#ident
,fn
) - 生存期(
'ident
) macro_rules
中元变量的替代项(metavariable substitutions)(例如:macro_rules! mac { ($my_expr: expr) => { $my_expr } }
在mac
扩展之后,其中的$my_expr
的替换项,此时无论传入的表达式是什么,它都将被视为一个 token树)
token树在过程宏中被定义为:
- 组分界(
(...)
,{...}
等) - 此语言支持的运算符中使用的所有标点字符(punctuation characters)(包括
+
,但不包括+=
),单引号'
字符也包括(典型的是在生存期中使用,有关生存期的拆分和合并行为,请参见下文) - 字面量 (
"string"
,1
等)- 负号(例如
-1
)是整型或浮点型字面量的一部分。
- 负号(例如
- 标识符,包括关键字(
ident
,r#ident
,fn
)
当 token流从过程宏中传入和传出时,需要考虑这两个定义之间的错配问题。 请注意,下面的转换可能是惰性的,因此如果没有实际去检测 token流,它们可能不会发生。
当传入给一个过程宏:
- 所有的多字符操作符被拆分为单字符操作符。
- 生存期被拆分符号
'
和一个标识符。 - 所有元变量的替换都会被表示为它们的底层 token流。
- 当需要保留解析优先级时,这些 token流可能会被包装进带有隐式分隔符(
Delimiter::None
)的分组(Group
)中。 tt
和ident
类型的元变量的替换项永远不会被包装进这样的分组中,而是会始终表示为它们的底层 token树。
- 当需要保留解析优先级时,这些 token流可能会被包装进带有隐式分隔符(
当从一个过程宏中传出时:
- 当需要时,单个标点字符(punctuation characters)会被粘结成多字符操作符。
- 单引号
'
和标识符一起被粘结成生存期。 - 当需要保留解析优先级时,负值字面量会被转换为两个 token(
-
和 一个字面量),可能还会被包装进带有隐式分隔符(Delimiter::None
)的分组(group
)。
请注意,声明宏和过程性宏都不支持文档注释标记(例如/// Doc
),因此它们在传递给宏时总是转换为等效的 #[doc = r"str"]
属性的 token流。
crate 和源文件
crates-and-source-files.md
commit: 824b9156b221d6e051cca6f634e28515f30055e6
本章译文最后维护日期:2024-04-06
句法
Crate :
InnerAttribute*
Item*
注意:尽管像任何其他语言一样,Rust 也都可以通过解释器和编译器实现,但现在唯一存在的实现是编译器,并且该语言也是一直被设计为可编译的。因为这些原因,所以本章节所有的讨论都是基于编译器这条路径的。
Rust 的语义有编译时和运行时之间的阶段差异(phase distinction)。1 其中静态解释的语义规则控制编译的成败,而动态解释的语义规则控制程序在运行时的行为。
编译模型以 crate 为中心。每次编译都以源码的形式处理单个的 crate,如果成功,将生成二进制形式的单个 crate:可执行文件或某种类型的库文件。2
crate 是编译和链接的单元,也是版本控制、版本分发和运行时加载的基本单元。一个 crate 包含一个嵌套的带作用域的模块树。这个树的顶层是一个匿名的模块(从模块内部路径的角度来看),并且一个 crate 中的任何程序项都有一个规范的模块路径来表示它在 crate 的模块树中的位置。
Rust 编译器总是使用单个源文件作为输入来开启编译过程的,并且总是生成单个输出 crate。对输入源文件的处理可能导致其他源文件作为模块被加载进来。源文件的扩展名为 .rs
。
Rust 源文件描述了一个模块,其名称和(在当前 crate 的模块树中的)位置是从源文件外部定义的:要么通过引用源文件中的显式模块(Module)项,要么由 crate 本身的名称定义。每个源文件都是一个模块,但并非每个模块都需要自己的源文件:多个模块定义可以嵌套在同一个文件中。
每个源文件包含一个由零个或多个程序项定义组成的代码序列,并且这些源文件都可选地从应用于其内部模块的任意数量的属性开始,大部分这些属性都会会影响编译器行为。匿名的 crate 根模块可附带一些应用于整个 crate 的属性。
注意:文件的内容前面可能有一个 shebang。
#![allow(unused)] fn main() { // 指定 crate 名称. #![crate_name = "projx"] // 指定编译输出文件的类型 #![crate_type = "lib"] // 打开一种警告 // 这句可以放在任何模块中, 而不是只能放在匿名 crate 模块里。 #![warn(non_camel_case_types)] }
Preludes and no_std
预导入包和 no_std
本节内容已经移入预导入包那章里了。
Main Functions
main函数
包含 main
函数的 crate 可以被编译成可执行文件。如果一个 main
函数存在,它必须不能有参数,不能对其声明任何 trait约束或生存期约束,不能有任何 where子句,并且它的返回类型必须实现 Termination
trait:
fn main() {}
fn main() -> ! { std::process::exit(0); }
fn main() -> impl std::process::Termination { std::process::ExitCode::SUCCESS }
main
主函数也可以是一个导入项,例如从外部crate 或当前crate 中导入。
#![allow(unused)] fn main() { mod foo { pub fn bar() { println!("Hello, world!"); } } use foo::bar as main; }
注意: 标准库中,实现
Termination
trait 的类型包括:
()
!
Infallible
ExitCode
Result<T, E> where T: Termination, E: Debug
The no_main
attribute
no_main
属性
可在 crate 层级使用 *no_main
属性*来禁止对可执行二进制文件发布 main
symbol,即禁止当前 crate 的 main 函数的执行。当链接的其他对象定义了 main
函数时,这很有用。
The crate_name
attribute
crate_name
属性
可在 crate 层级应用 crate_name
属性,并通过使用 MetaNameValueStr元项属性句法来指定 crate 的名称。
#![allow(unused)] #![crate_name = "mycrate"] fn main() { }
crate 名称不能为空,且只能包含 [Unicode字母数字]或字符 _
(U+005F)。
这种区别也存在于解释器中。静态检查,如语法分析、类型检查和 lint检查,都应该在程序执行之前进行,而不要去管程序何时执行。
crate 有点类似于 ECMA-335 CLI 模型中的 assembly、SML/NJ 编译管理器中的 library、Owens 和 Flatt 模块系统中的 unit, 或 Mesa 中的 configuration。
Conditional compilation
条件编译
conditional-compilation.md
commit: bcd45dd389c8c7a2abfe2bbee800ef8192e729b6
本章译文最后维护日期:2024-06-15
句法
ConfigurationPredicate :
ConfigurationOption
| ConfigurationAll
| ConfigurationAny
| ConfigurationNotConfigurationOption :
IDENTIFIER (=
(STRING_LITERAL | RAW_STRING_LITERAL))?ConfigurationAll
all
(
ConfigurationPredicateList?)
ConfigurationAny
any
(
ConfigurationPredicateList?)
ConfigurationNot
not
(
ConfigurationPredicate)
ConfigurationPredicateList
ConfigurationPredicate (,
ConfigurationPredicate)*,
?
根据某些条件, *条件性编译的源代码(Conditionally compiled source code)*可以被认为是 crate 源代码的一部分,也可以不被认为是 crate 源代码的一部分。可以使用属性 cfg
和 cfg_attr
以及内置的 cfg
macro 来有条件地对源代码进行编译。这些条件可以基于被编译的 crate 的目标架构、传递给编译器的值,以及下面将详细描述的一些其他事项。
每种形式的编译条件都有一个计算结果为真或假的配置谓词(configuration predicate)。谓词是以下内容之一:
- 一个配置选项。如果设置了该选项,则为真,如果未设置则为假。
all()
这样的配置谓词列表,列表内的配置谓词以逗号分隔。如果至少有一个谓词为假,则为假。如果没有谓词,则为真。any()
这样的配置谓词列表,列表内的配置谓词以逗号分隔。如果至少有一个谓词为真,则为真。如果没有谓词,则为假。- 带一个配置谓词的
not()
模式 。如果此谓词为假,整个配置它为真;如果此谓词为真,整个配置为假。
配置选项可以是名称,也可以是键值对,它们可以设置,也可以不设置。名称以单个标识符形式写入,例如 unix
。键值对被写为标识符后跟 =
,然后再跟一个字符串。例如,target_arch=“x86_64”
就是一个配置选项。
注意:
=
周围的空白符将被忽略。foo="bar"
和foo = "bar"
是等价的配置选项。
键在键值对配置选项列表中不是唯一的。例如,feature = "std"
and feature = "serde"
可以同时设置。
Set Configuration Options
设置配置选项
设置哪些配置选项是在 crate 编译期时就静态确定的。一些选项属于编译器设置集(compiler-set),这部分选项是编译器根据相关编译数据设置的。其他选项属于任意设置集(arbitrarily-set),这部分设置必须从代码之外传参给编译器来自主设置。无法在正在编译的 crate 的源代码中设置编译配置选项。
注意: 对于
rustc
,任意配置集的配置选项要使用命令行参数--cfg
来设置。
注意: 键名为
feature
的配置选项一般被 Cargo 约定用于指定编译期(用到的各种编译)选项和可选依赖项。
警告:任意配置集的配置选项可能与编译器设置集的配置选项设置相同的值。例如,在编译一个 Windows 目标时,可以执行命令行 rustc --cfg "unix" program.rs
,这样就同时设置了 unix
和 windows
配置选项。但实际上这样做是不明智的。
target_arch
此键值对选项用于一次性设置编译目标的 CPU 架构。该值类似于平台的目标三元组(target triple)1中的第一个元素,但也不完全相同。
示例值:
"x86"
"x86_64"
"mips"
"powerpc"
"powerpc64"
"arm"
"aarch64"
target_feature
此键值对选项用于设置当前编译目标的可用平台特性。
示例值:
"avx"
"avx2"
"crt-static"
"rdrand"
"sse"
"sse2"
"sse4.1"
有关可用特性的更多细节,请参见 target_feature
属性。此外,编译器还为 target_feature
选项提供了一个额外的 crt-static
特性,它表示需要链接一个可用的静态C运行时。
target_os
此键值对选项用于一次性设置编译目标的操作系统类型。该值类似于平台目标三元组中的第二和第三个元素。
示例值:
"windows"
"macos"
"ios"
"linux"
"android"
"freebsd"
"dragonfly"
"openbsd"
"netbsd"
"none"
(主要用于嵌入式目标架构)
target_family
此键值对选项提供了对具体目标平台更通用化的描述,比如编译目标操作系统或架构。可以设置任意数量的键值对。 最多设置一次,用于设置编译目标的操作系统类别。
示例值:
"unix"
"windows"
"wasm"
unix
and windows
unix
和 windows
如果设置了 target_family = "unix"
则谓词 unix
为真;如果设置了 target_family = "windows"
则谓词 windows
为真。
target_env
设置此键值对选项可用于进一步消除编译目标平台上因为要使用特定 ABI 或 libc
而带来的歧义。由于历史原因,仅当实际需要消除歧义时,才将此值定义为非空字符串。因此,例如在许多 GNU 平台上,此值将为空。该值类似于平台目标三元组的第四个元素,但也有区别。一个区别是在嵌入式 ABI 上,比如在目标为嵌入式系统时,gnueabihf
会简单地将 target_env
定义为 "gnu"
。
示例值:
""
"gnu"
"msvc"
"musl"
"sgx"
target_abi
键值对选项是为了设置目标ABI的信息以进一步消除 target_env
可能引起的歧义。由于历史原因,此值仅在实际需要消除歧义时定义为非空字符串。因此,例如,在许多GNU平台上,该值将为空。
示例值:
""
"llvm"
"eabihf"
"abi64"
"sim"
"macabi"
target_endian
此键值对选项根据编译目标的 CPU 的字节序(endianness)属性一次性设置值为 “little” 或 “big”。
target_pointer_width
此键值对选项用于一次性设置编译目标的指针位宽(pointer width in bits)。
示例值:
"16"
"32"
"64"
target_vendor
此键值对选项用于一次性设置编译目标的供应商。
示例值:
"apple"
"fortanix"
"pc"
"unknown"
target_has_atomic
此键值对选项用于设置目标平台上的每个原子操作的位宽,这些原子操作包括原子加载、原子存储、原子比较和原子交换。
当cfg存在时,core::sync::atomic
下所有的稳定版的API 类型都可用来指定相关的原子位宽。
可能的值有:
"8"
"16"
"32"
"64"
"128"
"ptr"
test
在编译测试套件时启用。通过在 rustc
里使用 --test
命令行参数来完成此启用。请参阅测试章节来获取更多和测试支持相关的信息。
debug_assertions
在进行非优化编译时默认启用。这可以用于在开发中启用额外的代码调试功能,但不能在生产中启用。例如,它控制着标准库的 debug_assert!
宏(是否可用)。
proc_macro
当须要指定当前 crate 的编译输出文件类型(crate-type)为 proc_macro
时设置。
panic
根据恐慌策略设置键值选项。请注意,将来可能会添加更多选项值。
示例选项值:
"abort"
"unwind"
Forms of conditional compilation
条件编译的形式
The cfg
attribute
cfg
属性
句法
CfgAttrAttribute :
cfg
(
ConfigurationPredicate)
cfg
属性根据配置谓词有条件地包括它所附加的东西。
它被写成 cfg
后跟一个 (
,再跟一个配置谓词,最后是一个 )
。
如果谓词为真,则重写该部分代码,使其上没有 cfg
属性。如果谓词为假,则直接从源代码中删除该该部分代码。
当一个应用在 crate级别的 cfg
有一个假谓词时,行为略有不同:cfg
之前的任何 crate属性都会保留,cfg
之后的任何 crate属性都会删除。这样我们即使是已经使用 #![cfg(...)]
移除了整个 crate,仍允许使用 #![no_std]
和 #![no_core]
这样的 crate属性来避免链接 std
/core
。
在函数上的一些例子:
#![allow(unused)] fn main() { // 该函数只会在编译目标为 macOS 时才会包含在构建中 #[cfg(target_os = "macos")] fn macos_only() { // ... } // 此函数仅在定义了 foo 或 bar 时才会被包含在构建中 #[cfg(any(foo, bar))] fn needs_foo_or_bar() { // ... } // 此函数仅在编译目标是32位体系架构的类unix 系统时才会被包含在构建中 #[cfg(all(unix, target_pointer_width = "32"))] fn on_32bit_unix() { // ... } // 此函数仅在没有定义 foo 时才会被包含在构建中 #[cfg(not(foo))] fn needs_not_foo() { // ... } // 仅当恐慌策略设置为 unwind 时,目标程序才会包括此函数 #[cfg(panic = "unwind")] fn when_unwinding() { // ... } }
cfg
属性允许在任何允许属性的地方上使用。
The cfg_attr
attribute
cfg_attr
属性
句法
CfgAttrAttribute :
cfg_attr
(
ConfigurationPredicate,
CfgAttrs?)
当配置谓词为真时,此属性展开为谓词后列出的属性。例如,下面的模块可以在 linux.rs
或 windows.rs
中都能找到。
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(windows, path = "windows.rs")]
mod os;
可以列出零个、一个或多个属性。多个属性将各自展开为单独的属性。例如:
#[cfg_attr(feature = "magic", sparkles, crackles)]
fn bewitched() {}
// 当启用了 `magic` 特性时, 上面的代码将会被展开为:
#[sparkles]
#[crackles]
fn bewitched() {}
注意:
cfg_attr
能展开为另一个cfg_attr
。比如:
#[cfg_attr(target_os = "linux", cfg_attr(feature = "multithreaded", some_other_attribute))]
这个是合法的。这个示例等效于:
#[cfg_attr(all(target_os = "linux", feature ="multithreaded"), some_other_attribute)]
.
cfg_attr
属性允许在任何允许属性的地方上使用。
The cfg
macro
cfg
宏
内置的 cfg
宏接受单个配置谓词,当谓词为真时计算为 true
字面量,当谓词为假时计算为 false
字面量。
例如:
#![allow(unused)] fn main() { let machine_kind = if cfg!(unix) { "unix" } else if cfg!(windows) { "windows" } else { "unknown" }; println!("I'm running on a {} machine!", machine_kind); }
首先给出 目标三元组 的参考资料地址:https://www.bookstack.cn/read/rCore_tutorial_doc/d997e9cbdfeef7d4.md。
接下来为防止该地址失效,我用自己的理解简单重复一下我对这个名词的理解:
目标三元组可以理解为我们常说的平台信息,包含这些信息:第一项元素:CPU 架构;第二项元素:供应商;第三项元素:操作系统;第四项元素:ABI。
Rust 下查看当前平台的三元组属性可以用 rustc --version --verbose
命令行。比如我在我工作机下下执行这行命令行的输出的 host
信息为:host: x86_64-unknown-linux-gnu
,那我都工作机的目标三元组的信息就是:CPU 架构为 x86_64 ,供应商为 unknown ,操作系统为 linux ,ABI 为 gnu 。
我另一台 windows 机器为:host: x86_64-pc-windows-msvc
,那这台的目标三元组的信息为:CPU 架构为 x86_64 ,供应商为 pc ,操作系统为 windows ,ABI 为 msvc 。
Rust 官方对一些平台提供了默认的目标三元组,我们可以通过 rustc --print target-list
命令来查看完整列表。
Items
程序项
items.md
commit: 524b9b1efdfdef603d9617d8e1476b66b99a6349
本章译文最后维护日期:2024-06-15
句法:
Item:
OuterAttribute*
VisItem
| MacroItemVisItem:
Visibility?
(
Module
| ExternCrate
| UseDeclaration
| Function
| TypeAlias
| Struct
| Enumeration
| Union
| ConstantItem
| StaticItem
| Trait
| Implementation
| ExternBlock
)MacroItem:
MacroInvocationSemi
| MacroRulesDefinition
*程序项*是 crate 的组成单元。程序项由一套嵌套的模块被组织在一个 crate 内。每个 crate 都有一个“最外层”的匿名模块;crate 中所有的程序项都在其 crate 的模块树中自己的路径。
程序项在编译时就完全确定下来了,通常在执行期间保持结构稳定,并可以驻留在只读内存中。
有以下几类程序项:
程序项可以声明在crate的根层中、模块中或一个块表达式中。 关联程序项,程序项的的一种子集,可以声明在 traits 和 实现中。 外部程序项,程序项的的一种子集,它可以声明在外部块中。
程序项可以以任意顺序定义,但 macro_rules
例外,它有自己特有的作用域行为表象。
程序项名称的名称解析规则允许在模块或块中引用程序项的位置之前或之后定义该程序项。
有关程序项的作用域规则的信息,请参阅程序项作用域章节。
Modules
模块
modules.md
commit: 83f725f1b9dda6166589d7b715b75b7f54143b8e
本章译文最后维护日期:2021-07-31
句法:
Module :
unsafe
?mod
IDENTIFIER;
|unsafe
?mod
IDENTIFIER{
InnerAttribute*
Item*
}
模块是零个或多个程序项的容器。
模块项是一个用花括号括起来的,有名称的,并以关键字 mod
作为前缀的模块。模块项将一个新的具名模块引入到组成 crate 的模块树中。模块可以任意嵌套。
模块的一个例子:
#![allow(unused)] fn main() { mod math { type Complex = (f64, f64); fn sin(f: f64) -> f64 { /* ... */ unimplemented!(); } fn cos(f: f64) -> f64 { /* ... */ unimplemented!(); } fn tan(f: f64) -> f64 { /* ... */ unimplemented!(); } } }
模块和类型共享相同的命名空间。禁止在同一个作用域中声明与此作用域下模块同名的具名类型(named type):也就是说,类型定义、trait、结构体、枚举、联合体、类型参数或 crate 不能在其作用域中屏蔽此作用域中也生效的模块名称,反之亦然。使用 use
引入到当前作用域的程序项也受这个限制。
在句法上,关键字 unsafe
允许出现在关键字 mod
之前,但是在语义层面却会被弃用。这种设计允许宏在将关键字 unsafe
从 token流中移除之前利用此句法来使用此关键字。
Module Source Filenames
模块的源文件名
没有代码体的模块是从外部文件加载的。当模块没有 path
属性限制时,文件的路径和逻辑上的模块路径互为镜像。祖先模块的路径组件(path component)是此模块文件的目录,而模块的内容存在一个以该模块名为文件名,以 .rs
为扩展文件名的文件中。例如,下面的示例可以反映这种模块结构和文件系统结构相互映射的关系:
模块路径 | 文件系统路径 | 文件内容 |
---|---|---|
crate | lib.rs | mod util; |
crate::util | util.rs | mod config; |
crate::util::config | util/config.rs |
当一个目录下有一个名为 mod.rs
的源文件时,模块的文件名也可以和这个目录互相映射。上面的例子也可以用一个承载同一源码内容的名为 util/mod.rs
的实体文件来表达模块路径 crate::util
。注意不允许 util.rs
和 util/mod.rs
同时存在。
注意:在
rustc
1.30 版本之前,使用文件mod.rs
是加载嵌套子模块的方法。现在鼓励使用新的命名约定,因为它更一致,并且可以避免在项目中搞出许多名为mod.rs
的文件。
The path
attribute
path
属性
用于加载外部文件模块的目录和文件可以受 path
属性的影响。(或者说可以联合使用 path
属性来重新指定那种没有代码体的模块声明的加载对象的文件路径。)
对于不在内联模块(inline module)块内的模块上的 path
属性,此属性引入的文件的路径为相对于当前源文件所在的目录。例如,下面的代码片段将使用基于其所在位置的路径:
#[path = "foo.rs"]
mod c;
Source File | c 's File Location | c 's Module Path |
---|---|---|
src/a/b.rs | src/a/foo.rs | crate::a::b::c |
src/a/mod.rs | src/a/foo.rs | crate::a::c |
对于处在内联模块块内的 path
属性,此属性引入的文件的路径取决于 path
属性所在的源文件的类型。(先对源文件进行分类,)“mod-rs”源文件是根模块(比如是 lib.rs
或 main.rs
)和文件名为 mod.rs
的模块,“非mod-rs”源文件是所有其他模块文件。(那)在 mod-rs 文件中,内联模块块内的 path
属性(引入的文件的)路径是相对于 mod-rs 文件的目录(该目录包括作为目录的内联模块组件名)。对于非mod-rs 文件,除了路径以此模块名为目录前段外,其他是一样的。例如,下面的代码片段将使用基于其所在位置的路径:
mod inline {
#[path = "other.rs"]
mod inner;
}
Source File | inner 's File Location | inner 's Module Path |
---|---|---|
src/a/b.rs | src/a/b/inline/other.rs | crate::a::b::inline::inner |
src/a/mod.rs | src/a/inline/other.rs | crate::a::inline::inner |
在内联模块和其内嵌模块上混合应用上述 path
属性规则的一个例子(mod-rs 和非mod-rs 文件都适用):
#[path = "thread_files"]
mod thread { // 译者注:有模块要内联进来的内联模块
// 从相对于当前源文件的目录下的 `thread_files/tls.rs` 文件里载 `local_data` 模块。
#[path = "tls.rs"]
mod local_data; // 译者注:内嵌模块
}
Attributes on Modules
模块上的属性
模块和所有程序项一样能接受外部属性。它们也能接受内部属性:可以在带有代码体的模块的 {
之后,也可以在模块源文件的开头(但须在可选的 BOM 和 shebang 之后)。
在模块中有意义的内置属性是 cfg
、deprecated
、doc
、lint检查类属性、path
和 no_implicit_prelude
。模块也能接受宏属性。
Extern crate declarations
外部crate声明
extern-crates.md
commit: 8ca80a14d359cc21b0e9c10dde7d45fe1fcb9af8
本章译文最后维护日期:2022-12-04
句法:
ExternCrate :
extern
crate
CrateRef AsClause?;
CrateRef :
IDENTIFIER |self
AsClause :
as
( IDENTIFIER |_
)
外部crate(extern crate
)声明指定了对外部 crate 的依赖关系。(这种声明让)外部的 crate 作为外部crate(extern crate
)声明中提供的标识符被绑定到当前声明的作用域中。此外,如果 extern crate
出现在 crate的根模块中,那么此 crate名称也会被添加到外部预导入包中,以便使其自动出现在所有模块的作用域中。as
子句可用于将导入的 crate 绑定到不同的名称上。
外部crate 在编译时被解析为一个特定的 soname
1, 并且一个到此 soname
的运行时链接会传递给链接器,以便在运行时加载此 soname
。soname
在编译时解析,方法是扫描编译器的库文件路径,匹配外部crate 的 crate_name
。因为crate_name
是在编译时通过可选的 crate_name
属性声明的,所以如果外部 crate 没有提供 crate_name
, 则默认拿该外部crate 的 name
属性值来和外部crate(extern crate
)声明中的[标识符]绑定。
导入 self
crate 会创建到当前 crate 的绑定。在这种情况下,必须使用 as
子句指定要绑定到的名称。
三种外部crate(extern crate
)声明的示例:
extern crate pcre;
extern crate std; // 等同于: extern crate std as std;
extern crate std as ruststd; // 使用其他名字去链接 'std'
当给 Rust crate 命名时,不允许使用连字符(-
)。然而 Cargo 包却可以使用它们。在这种情况下,当 Cargo.toml
文件中没有指定 crate 名称时, Cargo 将透明地将 -
替换为 _
以供 Rust 源文件内的外部crate(extern crate
)声明引用 (详见 RFC 940)。
这有一个示例:
// 导入 Cargo 包 hello-world
extern crate hello_world; // 连字符被替换为下划线
Extern Prelude
外部预导入包
本节内容已经移入预导入包 — 外部预导入包中了。
Underscore Imports
下划线导入
外部的 crate依赖可以通过使用带有下划线形如 extern crate foo as _
的形式来声明,而无需将其名称绑定到当前作用域内。这种声明方式对于只需要 crate 被链接进来,但 crate 从不会被当前代码引用的情况可能很有用,并且还可以避免未使用的 lint 提醒。
下划线导入不会影响 macro_use
属性的正常使用,macro_use
属性仍会将各种宏名正常导入到 macro_use
预导入包中。
The no_link
attribute
no_link
属性
可以在外部项(extern crate
item)上指定使用 no_link
属性,以防止此 crate 被链接到编译输出中。这通常用于加载一个 crate 而只访问它的宏。
译者注:这里的 soname
是 linux系统里的动态库文件的 soname。
Use declarations
Use声明
use-declarations.md
commit: 868850de68713cb7e1dc2ac8031d8978d801bddf
本章译文最后维护日期:2022-06-17
句法:
UseDeclaration :
use
UseTree;
UseTree :
(SimplePath?::
)?*
| (SimplePath?::
)?{
(UseTree (,
UseTree )*,
?)?}
| SimplePath (as
( IDENTIFIER |_
) )?
use声明用来创建一个或多个与程序项路径同义的本地名称绑定。通常使用 use
声明来缩短引用模块所需的路径。这些声明可以出现在模块和块中,但通常在作用域顶部。
use声明支持多种便捷方法:
- 使用带有花括号的 glob-like(
::
) 句法use a::b::{c, d, e::f, g::h::i};
来同时绑定一个系列有共同前缀的路径。 - 使用关键字
self
,例如use a::b::{self, c, d::e};
,来同时绑定一系列有共同前缀和共同父模块的路径。 - 使用句法
use p::q::r as x;
将编译目标名称重新绑定为新的本地名称。这种也可以和上面两种方法一起使用:use a::b::{self as ab, c as abc}
。 - 使用星号通配符句法
use a::b::*;
来绑定与给定前缀匹配的所有路径。 - 将前面的方法嵌套重复使用,例如
use a::b::{self as ab, c, d::{*, e::f}};
。
use
声明的一个示例:
use std::collections::hash_map::{self, HashMap}; fn foo<T>(_: T){} fn bar(map1: HashMap<String, usize>, map2: hash_map::HashMap<String, usize>){} fn main() { // use声明也可以存在于函数体内 use std::option::Option::{Some, None}; // 等价于 'foo(vec![std::option::Option::Some(1.0f64), std::option::Option::None]);' foo(vec![Some(1.0f64), None]); // `hash_map` 和 `HashMap` 在当前作用域内都有效. let map1 = HashMap::new(); let map2 = hash_map::HashMap::new(); bar(map1, map2); }
use
Visibility
use
可见性
与其他程序项一样,默认情况下,use
声明对包含它的模块来说是私有的。同样的,如果使用关键字 pub
进行限定,use
声明也可以是公有的。use
声明可用于*重导出(re-expor)*名称。因此,公有的 use
声明可以将某些公有名称重定向到不同的目标定义中:甚至是位于不同模块内具有私有可见性的规范路径定义中。如果这样的重定向序列形成一个循环或不能明确地解析,则会导致编译期错误。
重导出的一个示例:
mod quux { pub use self::foo::{bar, baz}; pub mod foo { pub fn bar() {} pub fn baz() {} } } fn main() { quux::bar(); quux::baz(); }
在本例中,模块 quux
重导出了在模块 foo
中定义的两个公共名称。
use
Paths
use
路径
注意:本章节内容还不完整。
一些正常和不正常的使用 use
程序项的例子:
#![allow(unused_imports)] use std::path::{self, Path, PathBuf}; // good: std 是一个 crate 名称 use crate::foo::baz::foobaz; // good: foo 在当前 crate 的第一层 mod foo { pub mod example { pub mod iter {} } use crate::foo::example::iter; // good: foo 在当前 crate 的第一层 // use example::iter; // 在 2015 版里不行,2015 版里相对路径必须以 `self` 开头; 2018 版这样写没问题 use self::baz::foobaz; // good: `self` 指的是 'foo' 模块 use crate::foo::bar::foobar; // good: foo 在当前 crate 的第一层 pub mod bar { pub fn foobar() { } } pub mod baz { use super::bar::foobar; // good: `super` 指的是 'foo' 模块 pub fn foobaz() { } } } fn main() {}
:
版次差异: 在 2015 版中,
use
路径也允许访问 crate 根模块中的程序项。继续使用上面的例子,那以下use
路径的用法在 2015 版中有效,在 2018 版中就无效了:mod foo { pub mod example { pub mod iter {} } pub mod baz { pub fn foobaz() {} } } use foo::example::iter; use ::foo::baz::foobaz; fn main() {}
2015 版不允许用 use声明来引用外部预导入包里的 crate。因此,在2015 版中仍然需要使用
extern crate
声明,以便在 use声明中去引用外部 crate。从 2018 版开始,use声明可以像extern crate
一样指定外部 crate 依赖关系。在 2018 版中,如果本地程序项与外部的 crate 名称相同,那么使用该 crate 名称需要一个前导的
::
来明确地选择该 crate 名称。这种做法是为了与未来可能发生的更改保持兼容。// use std::fs; // 错误, 这样有歧义. use ::std::fs; // 从`std` crate 里导入, 不是下面这个 mod. use self::std::fs as self_fs; // 从下面这个 mod 导入. mod std { pub mod fs {} } fn main() {}
Underscore Imports
下划线导入
通过使用形如为 use path as _
的带下划线的 use声明,可以在不绑定名称的情况下导入程序项。这对于导入一个 trait 特别有用,这样就可以在不导入 trait 的 symbol 的情况下使用这个 trait 的方法,例如,如果 trait 的 symbol 可能与另一个 symbol 冲突。再一个例子是链接外部的 crate 而不导入其名称。
使用星号全局导入(Asterisk glob imports,即 ::*
)句法将以 _
的形式导入能匹配到的所有程序项,但这些程序项在当前作用域中将处于不可命名的状态。
mod foo { pub trait Zoo { fn zoo(&self) {} } impl<T> Zoo for T {} } use self::foo::Zoo as _; struct Zoo; // 下划线形式的导入就是为了避免和这个程序项在名字上起冲突 fn main() { let z = Zoo; z.zoo(); }
在宏扩展之后会创建一些惟一的、不可命名的 symbols,这样宏就可以安全地扩展出(emit)对 _
导入的多个引用。例如,下面代码不应该产生错误:
#![allow(unused)] fn main() { macro_rules! m { ($item: item) => { $item $item } } m!(use std as _;); // 这会扩展出: // use std as _; // use std as _; }
Functions
函数
functions.md
commit: 9e6a8c029e142810b0f1dd7421c8aacab399aec6
本章译文最后维护日期:2022-10-22
句法
Function :
FunctionQualifiersfn
IDENTIFIER GenericParams?
(
FunctionParameters?)
FunctionReturnType? WhereClause?
( BlockExpression |;
)FunctionQualifiers :
const
?async
1?unsafe
? (extern
Abi?)?Abi :
STRING_LITERAL | RAW_STRING_LITERALFunctionParameters :
SelfParam,
?
| (SelfParam,
)? FunctionParam (,
FunctionParam)*,
?SelfParam :
OuterAttribute* ( ShorthandSelf | TypedSelf )ShorthandSelf :
(&
|&
Lifetime)?mut
?self
TypedSelf :
mut
?self
:
TypeFunctionParam :
OuterAttribute* ( FunctionParamPattern |...
| Type 2 )FunctionParamPattern :
PatternNoTopAlt:
( Type |...
)FunctionReturnType :
->
Type1限定符
async
不能在 2015版中使用。2在2015版中,只有类型的函数参数只允许出现在trait项的关联函数中。
函数由一个块,以及一个名称、一组参数和一个返回类型组成。
除了名称,其他的都是可选的。
函数使用关键字 fn
声明。
函数可以声明一组输入变量作为参数,调用者通过它向函数传递参数,函数完成后,它再将带有输出类型的结果值返回给调用者。
如果返回类型没有显式声明,则默认返回单元类型。
当一个函数被引用时,该函数会产生一个相应的零内存宽度函数项类型(function item type)的一等(first-class)值,调用该值就相当于直接调用该函数。
举个简单的定义函数的例子:
#![allow(unused)] fn main() { fn answer_to_life_the_universe_and_everything() -> i32 { return 42; } }
Function parameters
函数参数
函数参数是不可反驳型模式,所以任何在 else-less形式(译者注:此种形式就是那种不关心条件为false的情况)的 let
绑定中有效的模式都可以有效应用在函数参数上:
#![allow(unused)] fn main() { fn first((value, _): (i32, i32)) -> i32 { value } }
如果第一个参数是一个SelfParam类型的参数,这表明该函数是一个方法。带 self参数的函数只能在 trait 或实现中作为关联函数出现。
带有 ...
token 的参数表示此函数是一个可变参数函数,...
只能作为外部块里的函数的最后一个参数使用。可变参数可以有一个可选标识符,例如 args: ...
。
Function body
函数体
函数的块在概念上被包装进在一个块中,该块绑定该函数的参数模式,然后返回(return
)该函数的块的值。这意味着如果轮到块的*尾部表达式(tail expression)*被求值计算了,该块将结束,求得的值将被返回给调用者。通常,程序执行时如果流碰到函数体中的显式返回表达式(return expression),就会截断那个隐式的最终表达式的执行。
例如,上面例子里函数的行为就像下面被改写的这样:
// argument_0 是调用者真正传过来的第一个参数
let (value, _) = argument_0;
return {
value
};
没有主体块的函数以分号结束。这种形式只能出现在 trait 或外部块中。
Generic functions
泛型函数
泛型函数允许在其签名中出现一个或多个参数化类型(parameterized types)。每个类型参数必须在函数名后面的尖括号封闭逗号分隔的列表中显式声明。
#![allow(unused)] fn main() { // foo 是建立在 A 和 B 基础上的泛型函数 fn foo<A, B>(x: A, y: B) { } }
在函数签名上和函数体内部,类型参数的名称可以被用作类型名。可以为类型参数指定 trait约束,以允许对该类型的值调用这些 trait 的方法。这种约束是使用 where
句法指定的:
#![allow(unused)] fn main() { use std::fmt::Debug; fn foo<T>(x: T) where T: Debug { } }
当使用泛型函数时,它的(类型参数的)类型会基于调用的上下文被实例化。例如,这里调用 foo
函数:
#![allow(unused)] fn main() { use std::fmt::Debug; fn foo<T>(x: &[T]) where T: Debug { // 省略细节 } foo(&[1, 2]); }
将用 i32
实例化类型参数 T
。
类型参数也可以在函数名之后的末段路径组件(trailing path component,即 ::<type>
)中显式地提供。如果没有足够的上下文来确定类型参数,那么这可能是必要的。例如:mem::size_of::<u32>() == 4
。
Extern function qualifier
外部函数限定符
外部函数限定符(extern
)可以提供那些能通过特定 ABI 才能调用的函数的定义:
extern "ABI" fn foo() { /* ... */ }
这些通常与提供了函数声明的外部块程序项一起使用,这样就可以调用此函数而不必同时提供此函数的定义:
extern "ABI" {
fn foo(); /* no body */
}
unsafe { foo() }
当函数的 FunctionQualifiers
句法规则中的 "extern" Abi?*
选项被省略时,会默认使用 "Rust"
类型的 ABI。例如:
#![allow(unused)] fn main() { fn foo() {} }
等价于:
#![allow(unused)] fn main() { extern "Rust" fn foo() {} }
使用 "Rust"
之外的 ABI 可以让在 Rust 中声明的函数被其他编程语言调用。比如下面声明了一个可以从 C 中调用的函数:
#![allow(unused)] fn main() { // 使用 "C" ABI 声明一个函数 extern "C" fn new_i32() -> i32 { 0 } // 使用 "stdcall" ABI声明一个函数 #[cfg(target_arch = "x86_64")] extern "stdcall" fn new_i32_stdcall() -> i32 { 0 } }
与外部块一样,当使用关键字 extern
而省略 "ABI"
时,ABI 默认使用的是 "C"
。也就是说这个:
#![allow(unused)] fn main() { extern fn new_i32() -> i32 { 0 } let fptr: extern fn() -> i32 = new_i32; }
等价于:
#![allow(unused)] fn main() { extern "C" fn new_i32() -> i32 { 0 } let fptr: extern "C" fn() -> i32 = new_i32; }
非 "Rust"
的 ABI 函数不支持与 Rust 函数完全相同的展开(unwind)方式。因此展开进程过这类 ABI 函数的尾部时就会导致该进程被终止。(译者注:展开是逆向的。)
注意:
rustc
背后的 LLVM 是通过执行一个非法指令来实现中止进程的功能的。
Const functions
常量函数
使用关键字 const
限定的函数是常量(const)函数,元组结构体构造函数和元组变体构造函数也是如此。可以在常量上下文中调用常量函数。
常量函数可以使用 extern
函数修饰符来修饰,但只能使用 "Rust"
和 "C"
ABI。
常量函数不允许是 async类型的,
Async functions
异步函数
函数可以被限定为异步的,这还可以与 unsafe
限定符结合在一起使用:
#![allow(unused)] fn main() { async fn regular_example() { } async unsafe fn unsafe_example() { } }
异步函数在调用时不起作用:相反,它们将参数捕获进一个 future。当该函数被轮询(polled)时,该 future 将执行该函数的函数体。
一个异步函数大致相当于返回一个以 async move
块为代码体的 impl Future
的函数:
#![allow(unused)] fn main() { // 源代码 async fn example(x: &str) -> usize { x.len() } }
大致等价于:
#![allow(unused)] fn main() { use std::future::Future; // 脱糖后的 fn example<'a>(x: &'a str) -> impl Future<Output = usize> + 'a { async move { x.len() } } }
实际的脱糖(desugaring)过程相当复杂:
- 脱糖过程中的返回类型被假定捕获了
async fn
声明中的所有的生存期参数(包括省略的)。这可以在上面的脱糖示例中看到:(我们)显式地给它补上了一个生存期参数'a
,因此捕捉到'a
。 - 代码体中的 [
async move
块][async blocks]捕获所有函数参数,包括未使用或绑定到_
模式的参数。参数销毁方面,除了销毁动作需要在返回的 future 完全执行(fully awaited)完成后才会发生外,可以确保异步函数参数的销毁顺序与函数非异步时的顺序相同。
有关异步效果的详细信息,请参见 async
块。
版次差异: 异步函数只能从 Rust 2018 版才开始可用。
Combining async
and unsafe
async
和 unsafe
的联合使用
声明一个既异步又非安全(unsafe
)的函数是合法的。调用这样的函数是非安全的,并且(像任何异步函数一样)会返回一个 future。这个 future 只是一个普通的 future,因此“await”它不需要一个 unsafe
的上下文:
#![allow(unused)] fn main() { // 等待这个返回的 future 相当于解引用 `x`。 // // 安全条件: 在返回的 future 执行完成前,`x` 必须能被安全解引用 async unsafe fn unsafe_example(x: *const i32) -> i32 { *x } async fn safe_example() { // 起初调用上面这个函数时需要一个非安全(`unsafe`)块: let p = 22; let future = unsafe { unsafe_example(&p) }; // 但是这里非安全(`unsafe`)块就没必要了,这里能正常读到 `p`: let q = future.await; } }
请注意,此行为是对返回 impl Future
的函数进行脱糖处理的结果——在本例中,我们脱糖处理生成的函数是一个非安全(unsafe
)函数,这个非安全(unsafe
)函数返回值与原始定义的函数的返回值仍保持一致。
非安全限定在异步函数上的使用方式与它在其他函数上的使用方式完全相同:只是它表示该函数会要求它的调用者提供一些额外的义务/条件来确保该函数的健全性(soundness)。与任何其他非安全函数一样,这些条件可能会超出初始调用本身——例如,在上面的代码片段中, 函数 unsafe_example
将指针 x
作为参数,然后(在执行 await 时)解引用了对该指针的引用。这意味着在 future 完成执行之前,x
必须是有效的,调用者有责任确保这一点。
Attributes on functions
函数上的属性
在函数上允许使用外部属性,也允许在函数体中的 {
后面直接放置内部属性。
下面这个例子显示了一个函数的内部属性。该函数的文档中只会出现单词“Example”。
#![allow(unused)] fn main() { fn documented() { #![doc = "Example"] } }
注意:除了 lint检查类属性,函数上一般惯用的还是外部属性。
在函数上有意义的属性是 cfg
、cfg_attr
、deprecated
、doc
、export_name
、link_section
、no_mangle
、lint检查类属性、must_use
、过程宏属性、测试类属性和优化提示类属性。函数也可以接受属性宏。
Attributes on function parameters
函数参数上的属性
函数参数允许使用外部属性,允许的内置属性仅限于 cfg
、cfg_attr
、allow
、warn
、deny
和 forbid
。
#![allow(unused)] fn main() { fn len( #[cfg(windows)] slice: &[u16], #[cfg(not(windows))] slice: &[u8], ) -> usize { slice.len() } }
应用于程序项的过程宏属性所使用的惰性辅助属性也是允许的,但是要注意不要在最终(输出)的 TokenStream
中包含这些惰性属性。
例如,下面的代码定义了一个未在任何地方正式定义的惰性属性 some_inert_attribute
,而 some_proc_macro_attribute
过程宏负责检测它的存在,并从输出 token流 TokenStream
中删除它。
#[some_proc_macro_attribute]
fn foo_oof(#[some_inert_attribute] arg: u8) {
}
Type aliases
类型别名
type-aliases.md
commit: 3c8acda52ffcf5f7ca410c76f4fddf0e129592e2
本章译文最后维护日期:2022-10-22
句法
TypeAlias :
type
IDENTIFIER GenericParams? (:
TypeParamBounds )? WhereClause? (=
Type WhereClause?)?;
类型别名为现有的类型定义一个新名称。类型别名用关键字 type
声明。每个值都是一个唯一的特定的类型,但是可以实现几个不同的 trait,或者兼容几个不同的类型约束。
例如,下面将类型 Point
定义为类型 (u8, u8)
的同义词/别名:
#![allow(unused)] fn main() { type Point = (u8, u8); let p: Point = (41, 68); }
元组结构体或单元结构体的类型别名不能用于充当该类型的构造函数:
#![allow(unused)] fn main() { struct MyStruct(u32); use MyStruct as UseAlias; type TypeAlias = MyStruct; let _ = UseAlias(5); // OK let _ = TypeAlias(5); // 无法工作 }
类型别名不用作关联类型时,必须包含type规范,而不能包含TypeParamBounds规范。
类型别名用作 trait 中的关联类型时,不能包含type规范,但可以包括TypeParamBounds规范。
类型别名用作 trait impl 中的关联类型时,必须包含type规范,而不能包含TypeParamBounds规范。
trait impl 中类型别名上等号前的 Where子句(如 type TypeAlias<T> where T: Foo = Bar<T>
)已被弃用。目前推荐使用等号后面的 Where子句(如type TypeAlias<T> = Bar<T> where T: Foo
)。
Structs
结构体
structs.md
commit: 9d0aa172932ed15ec1b13556e6809b74bc58a02b
本章译文最后维护日期:2021-1-17
句法
Struct :
StructStruct
| TupleStructStructStruct :
struct
IDENTIFIER GenericParams? WhereClause? ({
StructFields?}
|;
)TupleStruct :
struct
IDENTIFIER GenericParams?(
TupleFields?)
WhereClause?;
StructFields :
StructField (,
StructField)*,
?StructField :
OuterAttribute*
Visibility?
IDENTIFIER:
TypeTupleFields :
TupleField (,
TupleField)*,
?TupleField :
OuterAttribute*
Visibility?
Type
结构体是一个使用关键字 struct
定义的标称型(nominal)结构体类型。
结构体(struct
)程序项的一个示例和它的使用方法:
#![allow(unused)] fn main() { struct Point {x: i32, y: i32} let p = Point {x: 10, y: 11}; let px: i32 = p.x; }
元组结构体是一个标称型(nominal)元组类型,也是用关键字 struct
定义的。例如:
#![allow(unused)] fn main() { struct Point(i32, i32); let p = Point(10, 11); let px: i32 = match p { Point(x, _) => x }; }
*单元结构体(unit-like struct)*是没有任何字段的结构体,它的定义完全不包含字段(fields)列表。这样的结构体隐式定义了其类型的同名常量(值)。例如:
#![allow(unused)] fn main() { struct Cookie; let c = [Cookie, Cookie {}, Cookie, Cookie {}]; }
等价于
#![allow(unused)] fn main() { struct Cookie {} const Cookie: Cookie = Cookie {}; let c = [Cookie, Cookie {}, Cookie, Cookie {}]; }
结构体的精确内存布局还没有规范下来。目前可以使用 repr
属性来指定特定的布局。
Enumerations
枚举
enumerations.md
commit: 4f32346c1ace326fe3d699f20bbd7a77680e830c
本章译文最后维护日期:2023-01-15
句法
Enumeration :
enum
IDENTIFIER GenericParams? WhereClause?{
EnumItems?}
EnumItem :
OuterAttribute* Visibility?
IDENTIFIER ( EnumItemTuple | EnumItemStruct )? EnumItemDiscriminant?EnumItemTuple :
(
TupleFields?)
EnumItemStruct :
{
StructFields?}
EnumItemDiscriminant :
=
Expression
枚举,英文为 enumeration,常见其简写形式 enum,它同时定义了一个标称型(nominal)枚举类型和一组构造器,这可用于创建或使用模式来匹配相应枚举类型的值。
枚举使用关键字 enum
来声明。
enum
程序项的一个示例和它的使用方法:
#![allow(unused)] fn main() { enum Animal { Dog, Cat, } let mut a: Animal = Animal::Dog; a = Animal::Cat; }
枚举构造器可以带有具名字段或未具名字段:
#![allow(unused)] fn main() { enum Animal { Dog(String, f64), Cat { name: String, weight: f64 }, } let mut a: Animal = Animal::Dog("Cocoa".to_string(), 37.2); a = Animal::Cat { name: "Spotty".to_string(), weight: 2.7 }; }
在这个例子中,Cat
是一个类结构体枚举变体(struct-like enum variant),而 Dog
则被简单地称为枚举变体。
变体都不带字段的枚举被称为*无字段枚举*。比如下面这个就是无字段枚举:
#![allow(unused)] fn main() { enum Fieldless { Tuple(), Struct{}, Unit, } }
如果无字段枚举枚举的变体全是单元体变体,这类枚举也被称为*单元体枚举*。比如:
#![allow(unused)] fn main() { enum Enum { Foo = 3, Bar = 2, Baz = 1, } }
Discriminants
判别值
每个枚举实例都有一个判别值:一个逻辑上与其关联的整数,用于确定它持有的是哪个变体。
在默认表型下,判别值被解释为一个 isize
的值。然而,编译器允许在其实际内存布局中使用内存尺寸更小的类型(或其他区分变体的方法)。
Assigning discriminant values
指定判别值的值
Explicit discriminants
显式判别值
在两种情况下,变体的判别值可以通过在变量名称后面加上 =
和常量表达式来显式设置:
-
如果枚举变体全是"单元体".
-
如果使用了原语表型。例如:
#![allow(unused)] fn main() { #[repr(u8)] enum Enum { Unit = 3, Tuple(u16), Struct { a: u8, b: u16, } = 1, } }
Implicit discriminants
隐式判别值
如果未指定变体的判别值,则将其的判别值设置为比当前声明中前一个变体的判别值加一。如果声明中第一个变体的判别值未指定,则将其设置为零。
#![allow(unused)] fn main() { enum Foo { Bar, // 0 Baz = 123, // 123 Quux, // 124 } let baz_discriminant = Foo::Baz as u32; assert_eq!(baz_discriminant, 123); }
同一枚举中,两个变体使用相同的判别值是错误的。
#![allow(unused)] fn main() { enum SharedDiscriminantError { SharedA = 1, SharedB = 1 } enum SharedDiscriminantError2 { Zero, // 0 One, // 1 OneToo = 1 // 1 (和前值冲突!) } }
当前一个变体的判别值是当前表形允许的的最大值时,再使用默认判别值就也是错误的。
#![allow(unused)] fn main() { #[repr(u8)] enum OverflowingDiscriminantError { Max = 255, MaxPlusOne // 应该是256,但枚举溢出了 } #[repr(u8)] enum OverflowingDiscriminantError2 { MaxMinusOne = 254, // 254 Max, // 255 MaxPlusOne // 应该是256,但枚举溢出了。 } }
Accessing discriminant
读取判别值
Via mem::discriminant
通过 mem::discriminant
mem::discriminant
返回对枚举值的判别值(可用于比较)的不透明引用。但它不能用于获得判别式的值。
转换
如果枚举是单元体枚举(没有元组和结构体变体),则可以使用数字类型转换直接访问其判别值;例如。:
#![allow(unused)] fn main() { enum Enum { Foo, Bar, Baz, } assert_eq!(0, Enum::Foo as isize); assert_eq!(1, Enum::Bar as isize); assert_eq!(2, Enum::Baz as isize); }
如果没有显式判别值,或者只有单元体变体是显式指定的,则无字段枚举也可以转换。
#![allow(unused)] fn main() { enum Fieldless { Tuple(), Struct{}, Unit, } assert_eq!(0, Fieldless::Tuple() as isize); assert_eq!(1, Fieldless::Struct{} as isize); assert_eq!(2, Fieldless::Unit as isize); #[repr(u8)] enum FieldlessWithDiscrimants { First = 10, Tuple(), Second = 20, Struct{}, Unit, } assert_eq!(10, FieldlessWithDiscrimants::First as u8); assert_eq!(11, FieldlessWithDiscrimants::Tuple() as u8); assert_eq!(20, FieldlessWithDiscrimants::Second as u8); assert_eq!(21, FieldlessWithDiscrimants::Struct{} as u8); assert_eq!(22, FieldlessWithDiscrimants::Unit as u8); }
Pointer casting
指针转换
如果枚举指定应用了原语表型,则可以通过 unsafe指针转换访问判别值:
#![allow(unused)] fn main() { #[repr(u8)] enum Enum { Unit, Tuple(bool), Struct{a: bool}, } impl Enum { fn discriminant(&self) -> u8 { unsafe { *(self as *const Self as *const u8) } } } let unit_like = Enum::Unit; let tuple_like = Enum::Tuple(true); let struct_like = Enum::Struct{a: false}; assert_eq!(0, unit_like.discriminant()); assert_eq!(1, tuple_like.discriminant()); assert_eq!(2, struct_like.discriminant()); }
Zero-variant enums
无变体枚举
没有变体的枚举称为零变体枚举/无变体枚举。因为它们没有有效的值,所以不能被实例化。
#![allow(unused)] fn main() { enum ZeroVariants {} }
零变体枚举与 never类型等效,但它不能被强转为其他类型。
#![allow(unused)] fn main() { enum ZeroVariants {} let x: ZeroVariants = panic!(); let y: u32 = x; // 类型不匹配错误 }
Variant visibility
变体的可见性
依照句法规则,枚举变体是允许有自己的[可见性(visibility)][Visibility]限定/注解(annotation)的,但当枚举被(句法分析程序)验证(validate)通过后,可见性注解又被弃用。因此,在源码解析层面,允许跨不同的上下文对其中不同类型的程序项使用统一的句法规则进行解析。
#![allow(unused)] fn main() { macro_rules! mac_variant { ($vis:vis $name:ident) => { enum $name { $vis Unit, $vis Tuple(u8, u16), $vis Struct { f: u8 }, } } } // 允许空 `vis`. mac_variant! { E } // 这种也行,因为这段代码在被验证通过前会被移除。 #[cfg(FALSE)] enum E { pub U, pub(crate) T(u8), pub(super) T { f: String } } }
Unions
联合体
unions.md
commit: b10666f10f6a731cfffbfc3c42841c59f8f76b58
本章译文最后维护日期:2024-06-15
句法
Union :
union
IDENTIFIER GenericParams? WhereClause?{
StructFields?}
除了用 union
代替 struct
外,联合体声明使用和结构体声明相同的句法。
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
联合体的关键特性是联合体的所有字段共享同一段存储。因此,对联合体的一个字段的写操作会覆盖其他字段,而联合体的内存宽度由其内存宽度最大的字段的内存宽度所决定。
联合体字段类型仅限于以下类型子集:
Copy
类型- 引用 (任意
T
上的&T
和&mut T
) ManuallyDrop<T>
(任意T
)- 仅包含了以上被允许的联合体字段类型的元组或数组
这个限制专门用来确保联合体的字段永远不需要销毁操作。与结构体和枚举一样,可以对联合体使用 impl Drop
来手动定义被销毁时发生的操作。
编译器不接受没有任何字段的联合体,但宏可以接受。
Initialization of a union
联合体的初始化
可以使用与结构体类型相同的句法创建联合体类型的值,但必须只能指定一个字段:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; }
上面的表达式创建了一个类型为 MyUnion
的值,并使用字段 f1
初始化了其存储。可以使用与结构体字段相同的句法访问联合体:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; let f = unsafe { u.f1 }; }
Reading and writing union fields
读写联合体字段
联合体没有“活跃成员字段(active field)”的概念。
相反,每次访问联合体只是将此联合体的存储解释为此访问的字段类型。
读取联合体的字段就是以当前读取字段的类型来解读此联合体的存储位。
字段可以有一个非零的偏移量存在(使用 C表型的除外);在这种情况下,读取将从字段的相对偏移量的 bit 开始。
程序员有责任确保此数据在当前字段类型下有效。
否则会导致未定义行为(undefined behavior)。
例如,在 bool
类型的字段下读取到数值 3
是未定义行为。
实际上,对一个 C表型的联合体进行写操作,然后再从中读取,就好比从用于写入的类型到用于读取的类型的 transmute
操作。
因此,所有的联合体字段的读取必须放在 unsafe
块里:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; } }
通常,那些用到联合体的程序代码会先在非安全的联合体字段访问操作上提供一层安全包装,然后再使用。
相反,对联合体字段的写入操作是安全的,因为它们只是覆盖任意数据,不会导致未定义行为。(请注意,联合体字段类型不会关联到 drop操作,因此联合字段的写入永远不会隐式销毁任何内容。)
Pattern matching on unions
联合体上的模式匹配
访问联合体字段的另一种方法是使用模式匹配。联合体字段上的模式匹配与结构体上的模式匹配使用相同的句法,只是这种模式只能一次指定一个字段。因为模式匹配就像使用特定字段来读取联合体,所以它也必须被放在非安全(unsafe
)块中。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } fn f(u: MyUnion) { unsafe { match u { MyUnion { f1: 10 } => { println!("ten"); } MyUnion { f2 } => { println!("{}", f2); } } } } }
模式匹配可以将联合体作为更大的数据结构的一个字段进行匹配。特别是,当使用 Rust 联合体通过 FFI 实现 C标签联合体(C tagged union)时,这允许同时在标签和相应字段上进行匹配:
#![allow(unused)] fn main() { #[repr(u32)] enum Tag { I, F } #[repr(C)] union U { i: i32, f: f32, } #[repr(C)] struct Value { tag: Tag, u: U, } fn is_zero(v: Value) -> bool { unsafe { match v { Value { tag: Tag::I, u: U { i: 0 } } => true, Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true, _ => false, } } } }
References to union fields
引用联合体字段
由于联合体字段共享存储,因此拥有对联合体一个字段的写访问权就同时拥有了对其所有其他字段的写访问权。因为这一事实,引用的借用检查规则必须调整。因此,如果联合体的一个字段是被出借,那么在相同的生存期内它的所有其他字段也都处于出借状态。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } // 错误: 不能同时对 `u` (通过 `u.f2`)拥有多余一次的可变借用 fn test() { let mut u = MyUnion { f1: 1 }; unsafe { let b1 = &mut u.f1; // ---- 首次可变借用发生在这里 (通过 `u.f1`) let b2 = &mut u.f2; // ^^^^ 二次可变借用发生在这里 (通过 `u.f2`) *b1 = 5; } // - 首次借用在这里结束 assert_eq!(unsafe { u.f1 }, 5); } }
如您所见,在许多方面(除了布局、安全性和所有权),联合体的行为与结构体完全相同,这很大程度上是因为联合体继承使用了结构体的句法的结果。对于 Rust 语言未明确提及的许多方面(比如隐私性(privacy)、名称解析、类型推断、泛型、trait实现、固有实现、一致性、模式检查等等)也是如此。
这句译者简单理解就是对已经初始化的变量再去覆写的时候要先去读一下这个变量代表的地址上的值的状态,如果有值,并且允许覆写,那 Rust 为防止内存泄漏就先执行那变量的析构行为(drop()),清空那个地址上的关联堆数据,再写入。我们这里对联合体的预设条件是此联合体值有 Copy特性,有 Copy特性了,对值的直接覆写不会造成内存泄漏,就不必调用析构行为,也不需要事先的非安全读操作了。对于这个问题nomicon的“未初始化内存”章有讲述,博主CrLF0710的两篇“学一点 Rust 内存模型会发生什么呢?”里也都有精彩讲解。
Constant items
常量项
constant-items.md
commit: 021889f26215721860a153e692909eda7cfd7a6e
本章译文最后维护日期:2023-05-03
句法
ConstantItem :
const
( IDENTIFIER |_
):
Type (=
Expression )?;
常量项是一个可选的具名 常量值,它与程序中的具体内存位置没有关联。无论常量在哪里使用,它们本质上都是内联的,这意味着当它们被使用时,都是直接被拷贝到相关的上下文中来使用的。这包括使用非拷贝(non-Copy
)类型的值和来自外部的 crate 的常量。对相同常量的引用不保证它们引用的是相同的内存地址。
常量必须显式指定数据类型。类型必须具有 'static
生存期:程序初始化器(initializer)中的任何引用都必须具有 'static
生存期。
常量可以引用其他常量的地址,在这种情况下,如果适用,该地址将具有省略的生存期,否则(在大多数情况下)默认为 'static
生存期。(请参阅静态生存期省略。)但是,编译器仍有权多次调整转移该常量,因此引用的地址可能并不固定。
#![allow(unused)] fn main() { const BIT1: u32 = 1 << 0; const BIT2: u32 = 1 << 1; const BITS: [u32; 2] = [BIT1, BIT2]; const STRING: &'static str = "bitstring"; struct BitsNStrings<'a> { mybits: [u32; 2], mystring: &'a str, } const BITS_N_STRINGS: BitsNStrings<'static> = BitsNStrings { mybits: BITS, mystring: STRING, }; }
常量表达式只能在trait定义中省略。
Constants with Destructors
常量与析构函数
常量可以包含析构函数。析构函数在值超出作用域时运行。1
#![allow(unused)] fn main() { struct TypeWithDestructor(i32); impl Drop for TypeWithDestructor { fn drop(&mut self) { println!("Dropped. Held {}.", self.0); } } const ZERO_WITH_DESTRUCTOR: TypeWithDestructor = TypeWithDestructor(0); fn create_and_drop_zero_with_destructor() { let x = ZERO_WITH_DESTRUCTOR; // x 在函数的结尾处通过调用 drop 方法被销毁。 // 打印出 "Dropped. Held 0.". } }
Unnamed constant
未命名常量
不同于关联常量,自由常量(free constant)可以使用下划线来命名。例如:
#![allow(unused)] fn main() { const _: () = { struct _SameNameTwice; }; // OK 尽管名称和上面的一样: const _: () = { struct _SameNameTwice; }; }
与下划线导入一样,宏可以多次安全地在同一作用域中扩展出相同的未具名常量。例如,以下内容不应该产生错误:
#![allow(unused)] fn main() { macro_rules! m { ($item: item) => { $item $item } } m!(const _: () = ();); // 这会展开出: // const _: () = (); // const _: () = (); }
Evaluation
求值
自由常量总是在编译时进行计算,以消除 panics。即使在未使用的函数中也会发生这种情况:
#![allow(unused)] fn main() { // Compile-time panic const PANIC: () = std::unimplemented!(); fn unused_generic_function<T>() { // A failing compile-time assertion const _: () = assert!(usize::BITS == 0); } }
在程序退出前,析构销毁的只是其中的一份拷贝;这句还有另一层含义是常量在整个程序结束时会调用析构函数。
Static items
静态项
static-items.md
commit: bbb69d1e29ff3dc3acaa3f0011bf9b1c9991322c
本章译文最后维护日期:2021-07-04
句法
StaticItem :
static
mut
? IDENTIFIER:
Type (=
Expression )?;
静态项类似于常量项,除了它在程序中表示一个精确的内存位置。所有对静态项的引用都指向相同的内存位置。静态项拥有 'static
生存期,它比 Rust 程序中的所有其他生存期都要长。静态项不会在程序结束时调用析构动作 drop
。
静态初始化器是在编译时求值的常量表达式。静态初始化器可以引用其他静态项。
包含非内部可变类型的非 mut
静态项可以放在只读内存中。
所有访问静态项的操作都是安全的,但对静态项有一些限制:
- 静态项的数据类型必须有
Sync
trait约束,这样才可以让线程安全地访问。 - 常量项不能引用静态项。
必须为自由静态项提供初始化表达式,但在外部块中静态项必须省略初始化表达式。
Statics & generics
静态项和泛型
在泛型作用域中(例如在包覆实现或默认实现中)定义的静态项将只会定义一个静态项,就好像该静态项定义是从当前作用域中拉入到模块内一样。不会出现程序项在单态化的过程中各自出现自己的独有静态项。
例如:
use std::sync::atomic::{AtomicUsize, Ordering}; trait Tr { fn default_impl() { static COUNTER: AtomicUsize = AtomicUsize::new(0); println!("default_impl: counter was {}", COUNTER.fetch_add(1, Ordering::Relaxed)); } fn blanket_impl(); } struct Ty1 {} struct Ty2 {} impl<T> Tr for T { fn blanket_impl() { static COUNTER: AtomicUsize = AtomicUsize::new(0); println!("blanket_impl: counter was {}", COUNTER.fetch_add(1, Ordering::Relaxed)); } } fn main() { <Ty1 as Tr>::default_impl(); <Ty2 as Tr>::default_impl(); <Ty1 as Tr>::blanket_impl(); <Ty2 as Tr>::blanket_impl(); }
以上会打印如下:
default_impl: counter was 0
default_impl: counter was 1
blanket_impl: counter was 0
blanket_impl: counter was 1
Mutable statics
可变静态项
如果静态项是用关键字 mut
声明的,则它允许被程序修改。Rust 的目标之一是使尽可能的避免出现并发 bug,那允许修改可变静态项显然是竞态(race conditions)或其他 bug 的一个重要来源。因此,读取或写入可变静态项变量时需要引入非安全(unsafe
)块。应注意确保对可变静态项的修改相对于运行在同一进程中的其他线程来说是安全的。
尽管有这些缺点,可变静态项仍然非常有用。它们可以与 C库一起使用,也可以在外部(extern
)块中从 C库中来绑定它。
#![allow(unused)] fn main() { fn atomic_add(_: &mut u32, _: u32) -> u32 { 2 } static mut LEVELS: u32 = 0; // 这违反了不共享状态的思想,而且它在内部不能防止竞争,所以这个函数是非安全的(`unsafe`) unsafe fn bump_levels_unsafe1() -> u32 { let ret = LEVELS; LEVELS += 1; return ret; } // 这里我们假设有一个返回旧值的 atomic_add 函数,这个函数是“安全的(safe)”, // 但是返回值可能不是调用者所期望的,所以它仍然被标记为 `unsafe` unsafe fn bump_levels_unsafe2() -> u32 { return atomic_add(&mut LEVELS, 1); } }
除了可变静态项的类型不需要实现 Sync
trait 之外,可变静态项与普通静态项具有相同的限制。
Using Statics or Consts
使用常量项或静态项
应该使用常量项还是应该使用静态项可能会令人困惑。一般来说,常量项应优先于静态项,除非以下情况之一成立:
- 存储大量数据
- 需要静态项的存储地址不变的特性。
- 需要内部可变性。
Trait
traits.md
commit: 46ed38d89d162b9bb5f7be2390c17d9ef3f0a985
本章译文最后维护日期:2023-12-30
句法
Trait :
unsafe
?trait
IDENTIFIER GenericParams? (:
TypeParamBounds? )? WhereClause?{
InnerAttribute*
AssociatedItem*
}
trait 描述类型可以实现的抽象接口。这类接口由三种关联程序项(associated items)组成,它们分别是:
所有 trait 都定义了一个隐式类型参数 Self
,它指向“实现此接口的类型”。trait 还可能包含额外的类型参数。这些类型参数,包括 Self
在内,都可能会跟正常类型参数一样受到其他 trait 的约束。
trait 需要具体的类型去实现,具体的实现方法是通过该类型的各种独立实现(implementations)来完成的。
trait函数可以通过使用分号代替函数体来省略函数体。这表明此 trait的实现必须去定义实现该函数。如果 trait函数定义了一个函数体,那么这个定义就会作为任何不覆盖它的实现的默认函数实现。类似地,关联常量可以省略等号和表达式,以指示相应的实现必须定义该常量值。关联类型不能定义类型,只能在实现中指定类型。
#![allow(unused)] fn main() { // 有定义和没有定义的相关联trait项的例子 trait Example { const CONST_NO_DEFAULT: i32; const CONST_WITH_DEFAULT: i32 = 99; type TypeNoDefault; fn method_without_default(&self); fn method_with_default(&self) {} } }
Trait函数不能是 const
类型的。
Trait bounds
trait约束
泛型程序项可以使用 trait 作为其类型参数的约束。
Generic Traits
泛型trait
可以为 trait 指定类型参数来使该 trait 成为泛型trait/泛型类型。这些类型参数出现在 trait 名称之后,使用与泛型函数相同的句法。
#![allow(unused)] fn main() { trait Seq<T> { fn len(&self) -> u32; fn elt_at(&self, n: u32) -> T; fn iter<F>(&self, f: F) where F: Fn(T); } }
Object Safety
对象安全条款
对象安全的 trait 可以是 trait对象的底层 trait。如果 trait 符合以下限定条件(在 RFC 255 中定义),则认为它是对象安全的(object safe):
- 所有的超类traitsupertraits 也必须也是对象安全的。
- 超类trait 中不能有
Sized
。也就是说不能有Self: Sized
约束。 - 它必须没有任何关联常量。
- 它必须没有任何带泛型的关联类型。
- 所有关联函数必须可以从 trait对象调度分派,或者是显式不可调度分派:
- 可调度分派函数必须:
- 不能有类型参数(尽管生存期参数可以有)。
- 作为方法时,
Self
只能出现在方法的接受者(receiver)的类型里,其它地方不能使用Self
。 - 方法的接受者的类型必须是以下类型之一:
- 没有不透明的返回类型;也就是说,
- 不能是一个
async fn
(这是一个隐形的Future
类型)。 - 不能是一个返回位置的
impl Trait
类型 (fn example(&self) -> impl Trait
)。
- 不能是一个
- 没有
where Self: Sized
约束(即接受者的类型Self
(例如:self
) 不能有Sized
约束)。
- 显式不可调度分派函数要求:
- 有
where Self: Sized
约束(即接受者的类型Self
(例如:self
) 有Sized
约束)。
- 有
- 可调度分派函数必须:
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::pin::Pin; // 对象安全的 trait 方法。 trait TraitMethods { fn by_ref(self: &Self) {} fn by_ref_mut(self: &mut Self) {} fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} fn with_lifetime<'a>(self: &'a Self) {} fn nested_pin(self: Pin<Arc<Self>>) {} } struct S; impl TraitMethods for S {} let t: Box<dyn TraitMethods> = Box::new(S); }
#![allow(unused)] fn main() { // 此 trait 是对象安全的,但不能在 trait对象上分发(dispatch)使用这些方法。 trait NonDispatchable { // 非方法不能被分发。 fn foo() where Self: Sized {} // 在运行之前 Self 类型未知。 fn returns(&self) -> Self where Self: Sized; // `other` 可能是另一具体类型的接受者。 fn param(&self, other: Self) where Self: Sized {} // 泛型与虚函数指针表(Virtual Function Pointer Table, vtable)不兼容。 fn typed<T>(&self, x: T) where Self: Sized {} } struct S; impl NonDispatchable for S { fn returns(&self) -> Self where Self: Sized { S } } let obj: Box<dyn NonDispatchable> = Box::new(S); obj.returns(); // 错误: 不能调用带有 Self 返回类型的方法 obj.param(S); // 错误: 不能调用带有 Self 类型的参数的方法 obj.typed(1); // 错误: 不能调用带有泛型类型参数的方法 }
#![allow(unused)] fn main() { use std::rc::Rc; // 非对象安全的 trait trait NotObjectSafe { const CONST: i32 = 1; // 错误: 不能有关联常量 fn foo() {} // 错误: 关联函数没有 `Sized` 约束 fn returns(&self) -> Self; // 错误: `Self` 在返回类型中 fn typed<T>(&self, x: T) {} // 错误: 泛型类型参数 fn nested(self: Rc<Box<Self>>) {} // 错误: 嵌套接受者还未被完全支持。(译者注:有限支持见上面的补充规则。) } struct S; impl NotObjectSafe for S { fn returns(&self) -> Self { S } } let obj: Box<dyn NotObjectSafe> = Box::new(S); // 错误 }
#![allow(unused)] fn main() { // Self: Sized trait 非对象安全的。 trait TraitWithSize where Self: Sized {} struct S; impl TraitWithSize for S {} let obj: Box<dyn TraitWithSize> = Box::new(S); // 错误 }
#![allow(unused)] fn main() { // 如果有 `Self` 这样的泛型参数,那 trait 就是非对象安全的 trait Super<A> {} trait WithSelf: Super<Self> where Self: Sized {} struct S; impl<A> Super<A> for S {} impl WithSelf for S {} let obj: Box<dyn WithSelf> = Box::new(S); // 错误: 不能使用 `Self` 作为类型参数 }
Supertraits
超类trait
超类trait 是类型为了实现某特定 trait 而需要一并实现的 trait。此外,在任何地方,如果泛型或 trait对象被某个 trait约束,那这个泛型或 trait对象就可以访问这个超类trait 的关联程序项。
超类trait 是通过 trait 的 Self
类型上的 trait约束来声明的,并且通过这种声明 trait约束的方式来传递这种超类trait 关系。一个 trait 不能是它自己的超类trait。
有超类trait 的 trait 称其为其超类trait 的子trait(subtrait)。
下面是一个声明 Shape
是 Circle
的超类trait 的例子。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } }
下面是同一个示例,除了改成使用 where子句来等效实现。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle where Self: Shape { fn radius(&self) -> f64; } }
下面例子通过 Shape
的 area
函数为 radius
提供了一个默认实现:
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle where Self: Shape { fn radius(&self) -> f64 { // 因为 A = pi * r^2,所以通过代数推导得:r = sqrt(A / pi) (self.area() /std::f64::consts::PI).sqrt() } } }
下一个示例调用了一个泛型参数的超类trait 上的方法。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } fn print_area_and_radius<C: Circle>(c: C) { // 这里我们调用 `Circle` 的超类trait 的 area 方法。 println!("Area: {}", c.area()); println!("Radius: {}", c.radius()); } }
类似地,这里是一个在 trait对象上调用超类trait 的方法的例子。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } struct UnitCircle; impl Shape for UnitCircle { fn area(&self) -> f64 { std::f64::consts::PI } } impl Circle for UnitCircle { fn radius(&self) -> f64 { 1.0 } } let circle = UnitCircle; let circle = Box::new(circle) as Box<dyn Circle>; let nonsense = circle.radius() * circle.area(); }
Unsafe traits
非安全trait
以关键字 unsafe
开头的 trait程序项表示实现该 trait 可能是非安全的。使用正确实现的非安全trait 是安全的。trait实现也必须以关键字 unsafe
开头。
Parameter patterns
参数模式
(trait 中)没有代码体的函数声明或方法声明(的参数模式)只允许使用标识符/IDENTIFIER模式 或 _
通配符模式。当前 mut
IDENTIFIER 还是允许的,但已被弃用,未来将成为一个硬编码错误(hard error)。
在 2015 版中,trait 的函数或方法的参数模式是可选的:
#![allow(unused)] fn main() { // 2015版 trait T { fn f(i32); // 不需要参数的标识符. } }
所有的参数模式被限制为下述之一:
- IDENTIFIER
mut
IDENTIFIER_
&
IDENTIFIER&&
IDENTIFIER
(跟普通函数一样,)从 2018 版开始,(trait 中)函数或方法的参数模式不再是可选的。同时,也跟普通函数一样,(trait 中)函数或方法只要有代码体,其参数模式可以是任何不可反驳型模式。但如果没有代码体,上面列出的限制仍然有效。
#![allow(unused)] fn main() { trait T { fn f1((a, b): (i32, i32)) {} fn f2(_: (i32, i32)); // 没有代码体不能使用元组模式。 } }
Item visibility
程序项的可见性
依照句法规定,trait程序项在语法上允许使用 Visibility句法的注释,但是当 trait 被(句法法分析程序)验证(validate)后,该可见性注释又被弃用。因此,在源码解析层面,可以在使用程序项的不同上下文中使用统一的语法对这些程序项进行解析。例如,空的 vis
宏匹配段选择器可以用于 trait程序项,而在其他允许使用非空可见性的情况下,也可使用这同一套宏规则。
macro_rules! create_method { ($vis:vis $name:ident) => { $vis fn $name(&self) {} }; } trait T1 { // 只允许空 `vis`。 create_method! { method_of_t1 } } struct S; impl S { // 这里允许使用非空可见性。 create_method! { pub method_of_s } } impl T1 for S {} fn main() { let s = S; s.method_of_t1(); s.method_of_s(); }
两点提醒:所有 trait 都定义了一个隐式类型参数 Self
,它指向“实现此接口的类型”;trait 的 Self 默认满足:Self: ?Sized
Implementations
实现
implementations.md
commit: b5ae8364e361152059760576b60f180f5ed2276d
本章译文最后维护日期:2021-11-05
句法
Implementation :
InherentImpl | TraitImplInherentImpl :
impl
GenericParams? Type WhereClause?{
InnerAttribute*
AssociatedItem*
}
TraitImpl :
unsafe
?impl
GenericParams?!
? TypePathfor
Type
WhereClause?
{
InnerAttribute*
AssociatedItem*
}
实现是将程序项与*实现类型(implementing type)*关联起来的程序项。实现使用关键字 impl
定义,它包含了属于当前实现的类型的实例的函数,或者包含了当前实现的类型本身的静态函数。
有两种类型的实现:
- 固有实现(inherent implementations)
- trait实现(trait implementations)
Inherent Implementations
固有实现
固有实现被定义为一段由关键字 impl
,泛型类型声明,指向标称类型(nominal type)的路径,一个 where子句和一对花括号括起来的一组*类型关联项(associable items)*组成的序列。
(这里)标称类型也被称作实现类型(implementing type);类型关联项(associable items)可理解为实现类型的各种关联程序项(associated items)。
固有实现将其包含的程序项与其的实现类型关联起来。固有实现可以包含关联函数(包括方法)和关联常量。固有实现不能包含关联类型别名。
关联程序项的路径是其实现类型的所有(形式的)路径中的任一种,然后再拼接上这个关联程序项的标识符来作为整个路径的末段路径组件(final path component)。
类型可以有多个固有实现。但作为原始类型定义的实现类型必须与这些固有实现处在同一个 crate 里。
pub mod color { // 译者添加的注释:这个结构体是 Color 的原始类型,是一个标称类型,也是后面两个实现的实现类型 pub struct Color(pub u8, pub u8, pub u8); // 译者添加的注释:这个实现是 Color 的固有实现 impl Color { // 译者添加的注释:类型关联项(associable items) pub const WHITE: Color = Color(255, 255, 255); } } mod values { use super::color::Color; // 译者添加的注释:这个实现也是 Color 的固有实现 impl Color { // 译者添加的注释:类型关联项(associable items) pub fn red() -> Color { Color(255, 0, 0) } } } pub use self::color::Color; fn main() { // 实现类型 和 固有实现 在同一个模块下。 color::Color::WHITE; // 固有实现和类型声明不在同一个模块下,此时对通过固有实现关联进的程序项的存取仍通过指向实现类型的路径 color::Color::red(); // 实现类型重导出后,使用这类快捷路径效果也一样。 Color::red(); // 这个不行, 因为 `values` 非公有。 // values::Color::red(); }
Trait Implementations
trait实现
trait实现的定义与固有实现类似,只是在可选的泛型类型声明后须跟一个 trait关键字,再后跟关键字 for
,之后再跟一个指向某个标称类型的路径。
这里讨论的 trait 也被称为被实现trait(implemented trait)。实现类型去实现该被实现trait。
trait实现必须去定义被实现trait 声明里的所有非默认关联程序项,可以重新定义被实现trait 定义的默认关联程序项,但不能定义任何其他程序项。
关联程序项的完整路径为 <
后跟实现类型的路径,再后跟 as
,然后是指向 trait 的路径,再后跟 >
,这整体作为一个路径组件,然后再后接关联程序项自己的路径组件。
非安全(unsafe) trait 需要 trait实现以关键字 unsafe
开头。
#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct Point {x: f64, y: f64}; type Surface = i32; struct BoundingBox {x: f64, y: f64, width: f64, height: f64}; trait Shape { fn draw(&self, s: Surface); fn bounding_box(&self) -> BoundingBox; } fn do_draw_circle(s: Surface, c: Circle) { } struct Circle { radius: f64, center: Point, } impl Copy for Circle {} impl Clone for Circle { fn clone(&self) -> Circle { *self } } impl Shape for Circle { fn draw(&self, s: Surface) { do_draw_circle(s, *self); } fn bounding_box(&self) -> BoundingBox { let r = self.radius; BoundingBox { x: self.center.x - r, y: self.center.y - r, width: 2.0 * r, height: 2.0 * r, } } } }
Trait Implementation Coherence
trait实现的一致性
一个 trait实现如果未通过孤儿规则(orphan rules)检查或有 trait实现重叠(implementations overlap)发生,则认为 trait实现不一致(incoherent)。
当两个实现各自的 trait接口集之间存在非空交集时即为这两个 trait实现 重叠了,(这种常情况下)这两个 trait实现可以用相同的类型来实例化。
Orphan rules
孤儿规则
给定 impl<P1..=Pn> Trait<T1..=Tn> for T0
,只有以下至少一种情况为真时,此 impl
才能成立:
-
Trait
是一个本地 trait -
以下所有
(译者注:为理解上面两条规则,举几个例子、
impl<T> ForeignTrait<LocalType> for ForeignType<T>
这样的实现也是被允许的,而impl<Vec<T>> ForeignTrait<LocalType> for ForeignType<T>
和impl<T> ForeignTrait<Vec<T>> for ForeignType<T>
不被允许。)
这里只有无覆盖类型参数的外观被限制(译者注:为方便理解“无覆盖类型参数”,译者提醒读者把它想象成上面译者举例中的 T
)。注意,理解“无覆盖类型参数”时需要注意:为了保持一致性,基本类型虽然外观形式特殊,但仍不认为是有覆盖的,比如 Box<T>
中的 T
就不认为是有覆盖的,Box<LocalType>
这样的就被认为是本地的。
Generic Implementations
泛型实现
实现可以带有泛型参数,这些参数可用在此实现中的其他地方。实现里的泛型参数直接写在关键字 impl
之后。
#![allow(unused)] fn main() { trait Seq<T> { fn dummy(&self, _: T) { } } impl<T> Seq<T> for Vec<T> { /* ... */ } impl Seq<bool> for u32 { /* 将整数处理为 bits 序列 */ } }
如果泛型参数在以下情况下出现过,则其就能约束某个实现:
- 需要被实现的 trait中存在泛型参数
- 实现类型中存在泛型参数
- 作为类型的约束中的[关联类型],该类型包含另一个约束实现的形参。
类型和常量参数必须能在相应实现里体现出约束逻辑。如果关联类型中有生存期参数在使用,则该生存期的约束意义也必须在实现中体现。
约束必须被传递实现的例子:
#![allow(unused)] fn main() { trait Trait{} trait GenericTrait<T> {} trait HasAssocType { type Ty; } struct Struct; struct GenericStruct<T>(T); struct ConstGenericStruct<const N: usize>([(); N]); // T 通过作为 GenericTrait 的参数来体现约束 impl<T> GenericTrait<T> for i32 { /* ... */ } // T 是作为 GenericStruct 的参数来体现约束 impl<T> Trait for GenericStruct<T> { /* ... */ } // 同样,N 作为 ConstGenericStruct 的参数来体现约束 impl<const N: usize> Trait for ConstGenericStruct<N> { /* ... */ } // T 通过类型 `U` 内的关联类型来体现约束,而 `U` 本身就是一个trait的泛型参数 impl<T, U> GenericTrait<U> for u32 where U: HasAssocType<Ty = T> { /* ... */ } // 这个和前面一样,除了类型变为 `(U, isize)`。`U` 出现在包含 `T` 的类型中,而不是类型本身。 impl<T, U> GenericStruct<U> where (U, isize): HasAssocType<Ty = T> { /* ... */ } }
约束未能被传递实现的例子:
#![allow(unused)] fn main() { // 下面的都是错误的,因为它们的类型或常量参数没能被传递和实现约束 // T没有实现约束,因为实现内部根本就没有出现 T impl<T> Struct { /* ... */ } // 同样 N 也没有可能被实现 impl<const N: usize> Struct { /* ... */ } // 在实现中使用了 T,但却并不约束此实现 impl<T> Struct { fn uses_t(t: &T) { /* ... */ } } // 在 U 的约束中,T 被用作关联类型,但 U 本身在 Struct 里无法被约束 impl<T, U> Struct where U: HasAssocType<Ty = T> { /* ... */ } // T 是在约束中使用了,但不是作为关联类型使用的,所以它没有实现约束 impl<T, U> GenericTrait<U> for u32 where U: GenericTrait<T> {} }
允许的非约束生存期参数的示例:
#![allow(unused)] fn main() { struct Struct; impl<'a> Struct {} }
不允许的非约束生存期参数的示例:
#![allow(unused)] fn main() { struct Struct; trait HasAssocType { type Ty; } impl<'a> HasAssocType for Struct { type Ty = &'a Struct; } }
Attributes on Implementations
实现上的属性
实现可以在关键字 impl
之前引入外部属性,在包含了各种关联程序项的代码体内引入内部属性。内部属性必须位于任何关联程序项之前。这里有意义的属性有 cfg
、deprecated
、doc
和 lint检查类属性。
External blocks
外部块
external-blocks.md
commit: 845baeedae9beb16e98f11fe7173a463e934363e
本章译文最后维护日期:2024-06-15
句法
ExternBlock :
unsafe
?extern
Abi?{
InnerAttribute*
ExternalItem*
}
ExternalItem :
OuterAttribute* (
MacroInvocationSemi
| ( Visibility? ( StaticItem | Function ) )
)
外部块提供未在当前 crate 中定义的程序项的声明,外部块是 Rust 外部函数接口的基础。这其实是某种意义上的不受安全检查的导入入口。
外部块里允许存在两种形式的程序项声明:函数和静态项。只有在非安全(unsafe
)上下文中才能调用在外部块中声明的函数或访问在外部块中声明的静态项。
在句法上,关键字 unsafe
允许出现在关键字 extern
之前,但是在语义层面却会被弃用。这种设计允许宏在将关键字 unsafe
从 token流中移除之前利用此句法来使用此关键字。
Functions
函数
外部块中的函数与其他 Rust函数的声明方式相同,但这里的函数不能有函数体,取而代之的是直接以分号结尾。外部块中的函数的参数不允许使用模式,只能使用标识符(IDENTIFIER) 或 _
。函数限定符(const
、async
、unsafe
和 extern
)也不允许在这里使用。
外部块中的函数可以被 Rust 代码调用,就跟调用在 Rust 中定义的函数一样。Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行转换。
在外部块中声明的函数隐式为非安全(unsafe
)的。当强转为函数指针时,外部块中声明的函数的类型就为 unsafe extern "abi" for<'l1, ..., 'lm> fn(A1, ..., An) -> R
,其中 'l1
,…'lm
是其生存期参数,A1
,…,An
是该声明的参数的类型,R
是该声明的返回类型。
Statics
静态项
静态项在外部块内部与在外部块之外的声明方式相同,只是在外部块内部声明的静态项没有对应的初始化表达式。访问外部块中声明的静态项是 unsafe
的,不管它是否可变,因为没有任何保证来确保静态项的内存位模式(bit pattern)对声明它的类型是有效的,因为初始化这些静态项的可能是其他的任意外部代码(例如 C)。
就像外部块之外的静态项,外部静态项可以是不可变的,也可以是可变的。在执行任何 Rust 代码之前,不可变外部静态项必须被初始化。也就是说,对于外部静态项,仅在 Rust 代码读取它之前对它进行初始化是不够的。
ABI
不指定 ABI 字符串的默认情况下,外部块会假定使用指定平台上的标准 C ABI 约定来调用当前的库。其他的 ABI 约定可以使用字符串 abi
来指定,具体如下所示:
#![allow(unused)] fn main() { // 到 Windows API 的接口。(译者注:指定使用 stdcall调用约定去调用 Windows API) extern "stdcall" { } }
有三个 ABI 字符串是跨平台的,并且保证所有编译器都支持它们:
extern "Rust"
-- 在任何 Rust 语言中编写的普通函数fn foo()
默认使用的 ABI。extern "C"
-- 这等价于extern fn foo()
;无论您的 C编译器支持什么默认 ABI。extern "system"
-- 在 Win32 平台之外,中通常等价于extern "C"
。在 Win32 平台上,应该使用"stdcall"
,或者其他应该使用的 ABI 字符串来链接它们自身的 Windows API。
还有一些特定于平台的 ABI 字符串:
extern "cdecl"
-- 通过 FFI 调用 x86_32 C 资源所使用的默认调用约定。extern "stdcall"
-- 通过 FFI 调用 x86_32架构下的 Win32 API 所使用的默认调用约定extern "win64"
-- 通过 FFI 调用 x86_64 Windows 平台下的 C 资源所使用的默认调用约定。extern "sysv64"
-- 通过 FFI 调用 非Windows x86_64 平台下的 C 资源所使用的默认调用约定。extern "aapcs"
--通过 FFI 调用 ARM 接口所使用的默认调用约定extern "fastcall"
--fastcall
ABI——对应于 MSVC 的__fastcall
和 GCC 以及 clang 的__attribute__((fastcall))
。extern "vectorcall"
--vectorcall
ABI ——对应于 MSVC 的__vectorcall
和 clang 的__attribute__((vectorcall))
。extern "thiscall"
-- MSVC 下调用 C++ 成员函数的默认约定 -- 对应与 MSVC 下的__thiscall
,以及 GCC 和 clang 的__attribute__((thiscall))
调用约定extern "efiapi"
-- 调用 UEFI 函数所使用的 ABI。
Variadic functions
可变参数函数
可以在外部块内的函数的参数列表中的一个或多个具名参数后通过引入 ...
来让该函数成为可变参数函数。可变参数可以以可选的方式通过标识符来指定:
#![allow(unused)] fn main() { extern "C" { fn foo(...); fn bar(x: i32, ...); fn with_name(format: *const u8, args: ...); } }
Attributes on extern blocks
外部块上的属性
下面列出的属性可以控制外部块的行为。
The link
attribute
link
属性
link
属性为外部(extern
)块中的程序项指定编译器应该链接的本地库的名称。它使用 MetaListNameValueStr元项属性句法指定其输入参数。name
键指定要链接的本地库的名称。kind
键是一个可选值,它指定具有以下可选值的库类型:
dylib
— 表示要链接的库类型是动态库。如果没有指定kind
,这是默认值。static
— 表示要链接的库类型是静态库。framework
— 表示要链接的库类型是 macOS 框架。这只对 macOS 目标平台有效。raw-dylib
— 表示要链接的库类型是动态库,但具体要链接到哪个动态库,链接器会使用本次编译出的库作为导入库来定位识别链接,(相关详细信息,请参阅下面的dylib
vsrawdylib
)。此属性仅对 Windows目标平台有效。
如果指定了 kind
键,则必须有 name
键。
可选的 modifiers
参数提供了一种为库指定链接修饰符的方法。
修饰符被指定为以逗号分隔的字符串,每个修饰符的前缀都是 +
或 -
,分别表示修饰符处于启用或禁用状态。
当前不支持在单个 link
属性中指定多个 modifiers
参数,或在同一个 modifiers
参数中指定多个相同的修饰符。
例如: #[link(name = "mylib", kind = "static", modifiers = "+whole-archive")]
。
当从主机环境导入 symbols 时,wasm_import_module
键可用于为外部(extern
)块中的程序项指定 WebAssembly模块名称。如果未指定 wasm_import_module
,则默认模块名为 env
。
#[link(name = "crypto")]
extern {
// …
}
#[link(name = "CoreFoundation", kind = "framework")]
extern {
// …
}
#[link(wasm_import_module = "foo")]
extern {
// …
}
在空外部块上添加 link
属性是有效的。可以用这种方式来满足代码中其他地方的外部块的链接需求(包括上游 crate),而不必向每个外部块都添加此属性。
Linking modifiers: bundle
链接修饰符: bundle
这个修饰符只在静态链接模式下适用。 在其他链接模式下会导致编译错误。
在构建 rlib 或 staticlib 时,+bundle
意味着本地静态库将被打包到 rlib 或者 staticlib类型的归档文件中,之后最终的二进制文件在在链接时,将从此处来检索。
当构建 rlib时,-bundle
意味着本机静态库会被“按名称”注册为该 rlib 的依赖项,并且其中的对象文件仅在最终的二进制文件的链接过程中才被包含进来,其中最终的链接过程会执行按该名称的文件搜索来定位那些对象文件。
当构建 staticlib时,-bundle
意味着本机静态库根本不会被包括在归档文件中,一些层次更高或则说是排序更靠后的构建系统会在之后的链接最终二进制文件的过程中才来添加它。
在构建其他目标(如可执行文件或动态库)时,此修饰符无效。
此修饰符的默认形式是 +bundle
。
有关此修饰符的更多实现细节,请参见rustc文档中的 bundle
。
Linking modifiers: whole-archive
链接修饰符: whole-archive
此修饰符仅兼容静态(static
)链接类型。
使用任何其他链接类型都会引起编译器报错。
+whole-archive
意味着静态库会被链接为一个完整的 archive 文件,而不丢弃任何对象文件。
此修饰符的默认配置是 -whole-archive
。
有关此修饰符的更多实现细节,请参见rustc文档中的 whole-archive
。
Linking modifiers: verbatim
链接修饰符: verbatim
此修饰符兼容任何链接类型。
+verbatim
意味着 rustc 本身不会在库名称中添加任何目标平台指定的库前缀或后缀(如 lib
或 .a
),并且会尽力向链接器请求相同的内容。
-verbatim
意味着 rustc 在将库名称传递给链接器之前,将在库名称中添加特定于目标平台的前缀和后缀,或者不会阻止链接器隐式添加它。
此修饰符的默认值为 -verbatim
。
关于这个修饰符的更多实现细节可以在 rustc 的verbatim
文档 中找到。
dylib
versus raw-dylib
dylib
vs raw-dylib
在 Windows 上,链接动态库需要先向链接器提供一个导入库:这是一个特殊的静态库,它声明了此动态库导出的所有符号,这样链接器就知道它们必须在运行时动态加载。
指定 kind = "dylib"
将指示 Rust编译器根据 name
键链接导入库。然后,链接器将使用其正常的库解析逻辑来查找导入库。或者,使用 kind = "raw-dylib"
来指示编译器在编译期间生成一个导入库,并将其提供给链接器。
raw dylib
仅在 Windows 上受支持。在针对其他平台时使用它将导致编译器错误。
The import_name_type
key
import_name_type
键
在 x86 Windows上,函数的名称是“被修饰过的”(比如被添加了特定的前缀和/或后缀),以指示其支持的调用约定。例如,名为 fn1
且没有参数的 stdcall
调用约定函数将被修饰为_fn1@0
。然而,PE格式也允许名称没有前缀或不加修饰。此外,MSVC和GNU工具链对相同的调用约定使用不同的装饰,这意味着,默认情况下,一些 Win32函数不能通过 GNU工具链使用 raw-dylib
链接类型进行调用。
为了照顾到这些差异,在使用 raw-dylib
链接类型时,你可以通过指定 import_name_type
键使用下面的(某一)值来更改这些函数在生成的库文件中的命名方式:
decorated
:函数名称将使用 MSVC工具链格式进行完全修饰。noprefix
:函数名称将使用 MSVC工具链格式进行修饰,但跳过前导的?
、@
或者可选地_
。undecorated
:函数名称将不会被修饰。
如果未指定 import_name_type
键,则函数名称将使用目标工具链的格式进行完全修饰。
变量从不会被修饰,因此 import_name_type
键对它们在生成的库文件的命名方式没有影响。
import_name_type
键仅在x86 Windows上受支持。在针对其他平台时使用它将导致编译器错误。
The link_name
attribute
link_name
属性
可以在外部(extern
)块内的程序项声明上指定 link_name
属性,可以用它来指示要为给定函数或静态项导入的具体 symbol。它使用 MetaNameValueStr元项属性句法指定 symbol 的名称。
#![allow(unused)] fn main() { extern { #[link_name = "actual_symbol_name"] fn name_in_rust(); } }
此属性和 link_ordinal
属性同时使用会导致编译器报错。
The link_ordinal
attribute
link_ordinal
属性
link_ordinal
属性可以应用于外部(extern
)块内的各种声明上,用以给当前编译生成的链接导入库在链接时要使用的数字序号。Windows 上的动态库导出的每个符号都有的唯一编号,当加载库时可以使用相应序号查找对应的符号,而不必按名称查找。
警告:link_ordinal
只能在此符号的序号已稳定了的情况下使用:如果在包含某符号的二进制文件构建时,此符号对应的序号未明确设置,则此构建将自动为其分配一个序号,并且在后续的库构建时此序号可能会在二进制文件生成之间还会发生变化。
#[link(name = "exporter", kind = "raw-dylib")]
extern "stdcall" {
#[link_ordinal(15)]
fn imported_function_stdcall(i: i32);
}
此属性仅用于 raw-dylib
链接类型。
使用任何其他类型都会导致编译器报错。
此属性和 link_name
属性同时使用会导致编译器报错。
Attributes on function parameters
函数参数上的属性
外部函数参数上的属性遵循与常规函数参数相同的规则和限制。
Type and Lifetime Parameters
类型参数和生存期参数
generics.md
commit: aeda9bc3f65dfce916b1c1e4f1cab5fcddd8cd40
本章译文最后维护日期:2024-06-15
句法
GenericParams :
<
>
|<
(GenericParam,
)* GenericParam,
?>
GenericParam :
OuterAttribute* ( LifetimeParam | TypeParam | ConstParam )LifetimeParam :
LIFETIME_OR_LABEL (:
LifetimeBounds )?TypeParam :
IDENTIFIER (:
TypeParamBounds? )? (=
Type )?ConstParam:
const
IDENTIFIER:
Type (=
Block | IDENTIFIER | -?LITERAL )?
函数、类型别名、结构体、枚举、联合体、trait 和实现可以通过类型参数、常量参数和生存期参数达到参数化配置的的效果。这些参数在尖括号(<...>
)中列出,通常都是紧跟在程序项名称之后和程序项的定义之前。对于实现,因为它没有名称,那它们就直接位于关键字 impl
之后。
泛型参数的顺序被严格限定为生存期参数必须在前,然后是类型参数和常量参数混合。
同一参数名不能在 GenericParams 列表中声明多次。
下面给出一些带类型参数、常量参数和生存期参数的程序项的示例:
#![allow(unused)] fn main() { fn foo<'a, T>() {} trait A<U> {} struct Ref<'a, T> where T: 'a { r: &'a T } struct InnerArray<T, const N: usize>([T; N]); struct EitherOrderWorks<const N: bool, U>(U); }
泛型参数在声明它们的程序项定义的范围内有效。它们不是函数体中声明的程序项,这个在程序项声明中有讲述。 更多细节请参阅[范型参数的作用域][generic parameter scopes]。
引用、裸指针、数组、切片、元组和函数指针也有生存期参数或类型参数,但这些程序项不能使用路径句法去引用。
Const generics
常量泛型
常量泛型参数允许程序项在常量值上泛型化。const标识符为常量参数引入了一个名称,并且该程序项的所有实例必须用给定类型的值去实例化该参数。
常量参数类型值允许为:u8
, u16
, u32
, u64
, u128
, usize
, i8
, i16
, i32
, i64
, i128
, isize
, char
和 bool
这些类型。
常量参数可以在任何可以使用常量项的地方使用,但在类型或数组定义中的重复表达式中使用时,必须如下所述是独立的。也就是说,它们可以在以下地方上允许:
- 可以用于类型内部,用它来构成所涉及的程序项签名的一部分。
- 作为常量表达式的一部分,用于定义关联常量项,或作为关联类型的形参。
- 作为程序项里的任何函数体中的任何运行时表达式中的值。
- 作为程序项中任何函数体中使用到的任何类型的参数。
- 作为程序项中任何字段类型的一部分使用。
#![allow(unused)] fn main() { // 可以使用常量泛型参数的示例。 // 用于程序项本身的签名 fn foo<const N: usize>(arr: [i32; N]) { // 在函数体中用作类型。 let x: [i32; N]; // 用作表达。 println!("{}", N * 2); } // 用作结构体的字段 struct Foo<const N: usize>([i32; N]); impl<const N: usize> Foo<N> { // 用作关联常数 const CONST: usize = N * 4; } trait Trait { type Output; } impl<const N: usize> Trait for Foo<N> { // 用作关联类型 type Output = [i32; N]; } }
#![allow(unused)] fn main() { // 不能使用常量泛型参数的示例 fn foo<const N: usize>() { // 能在函数体中的程序项定义中使用 const BAD_CONST: [usize; N] = [1; N]; static BAD_STATIC: [usize; N] = [1; N]; fn inner(bad_arg: [usize; N]) { let bad_value = N * 2; } type BadAlias = [usize; N]; struct BadStruct([usize; N]); } }
作为进一步的限制,常量只能作为类型或数组定义中的重复表达式中的独立实参出现。在这种上下文限制下,它们只能以单段路径表达式的形式使用(例如 N
或以块{N}
的形式出现)。也就是说,它们不能与其他表达式结合使用。
#![allow(unused)] fn main() { // 不能使用常量参数的示例。 // 不允许在类型中的表达式中组合使用,例如这里的返回类型中的算术表达式 fn bad_function<const N: usize>() -> [u8; {N + 1}] { // 同样的,这种情况也不允许在数组定义里的重复表达式中使用 [1; {N + 1}] } }
路径中的常量实参指定了该程序项使用的常量值。实参必须是常量形参所属类型的常量表达式。常量表达式必须是块表达式(用花括号括起来),除非它是单独路径段(一个标识符)或一个字面量(此字面量可以是以 -
打头的 token)。
注意:这种句法限制是必要的,用以避免在解析类型内部的表达式时可能会导致无限递归(infinite lookahead)。
#![allow(unused)] fn main() { fn double<const N: i32>() { println!("doubled: {}", N * 2); } const SOME_CONST: i32 = 12; fn example() { // 常量参数的使用示例。 double::<9>(); double::<-123>(); double::<{7 + 8}>(); double::<SOME_CONST>(); double::<{ SOME_CONST + 5 }>(); } }
当存在歧义时,如果泛型参数可以同时被解析为类型或常量参数,那么它总是被解析为类型。在块表达式中放置实参可以强制将其解释为常量实参。
#![allow(unused)] fn main() { type N = u32; struct Foo<const N: usize>; // 下面用法是错误的,因为 `N` 被解释为类型别名。 fn foo<const N: usize>() -> Foo<N> { todo!() } // ERROR // 可以使用花括号来强制将 `N` 解释为常量形参。 fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok }
与类型参数和生存期参数不同,常量参数可以声明而不必在被它参数化的程序项中使用,但和泛型实现关联的实现例外:
#![allow(unused)] fn main() { // ok struct Foo<const N: usize>; enum Bar<const M: usize> { A, B } // ERROR: 参数未使用 struct Baz<T>; struct Biz<'a>; struct Unconstrained; impl<const N: usize> Unconstrained {} }
当处理 trait约束时,在确定是否满足相关约束时,不会考虑常量参数的所有实现的穷尽性。例如,在下面的例子中,即使实现了 bool
类型的所有可能的常量值,仍会报错提示 trait约束不满足。
#![allow(unused)] fn main() { struct Foo<const B: bool>; trait Bar {} impl Bar for Foo<true> {} impl Bar for Foo<false> {} fn needs_bar(_: impl Bar) {} fn generic<const B: bool>() { let v = Foo::<B>; needs_bar(v); // ERROR: trait约束 `Foo<B>: Bar` 未被满足 } }
Where clauses
where子句
句法
WhereClause :
where
( WhereClauseItem,
)* WhereClauseItem ?WhereClauseItem :
LifetimeWhereClauseItem
| TypeBoundWhereClauseItemLifetimeWhereClauseItem :
Lifetime:
LifetimeBoundsTypeBoundWhereClauseItem :
ForLifetimes? Type:
TypeParamBounds?
where子句提供了另一种方法来为类型参数和生存期参数指定约束(bound),甚至可以为非类型参数的类型指定约束。
关键字for
可以用来引入高阶生存期参数。它只允许在 LifetimeParam 参数上使用。
#![allow(unused)] fn main() { struct A<T> where T: Iterator, // 可以用 A<T: Iterator> 来替代 T::Item: Copy, // 在一个关联类型上设置约束 String: PartialEq<T>, // 使用类型参数来在字段`String`上设置约束 i32: Default, // 允许,但没什么用 { f: T, } }
Attributes
属性
泛型生存期参数和泛型类型参数允许属性,但在目前这个位置还没有任何任何有意义的内置属性,但用户可能可以通过自定义的派生属性来设置一些有意义的属性。
下面示例演示如何使用自定义派生属性修改泛型参数的含义。
// 假设 MyFlexibleClone 的派生项将 `my_flexible_clone` 声明为它可以理解的属性。
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
a: *const H
}
Associated Items
关联程序项/关联项
associated-items.md
commit: 6b9e4ffd539af7db5e27b6c829f120f827ef69a4
本章译文最后维护日期:2022-10-22
句法
AssociatedItem :
OuterAttribute* (
MacroInvocationSemi
| ( Visibility? ( TypeAlias | ConstantItem | Function ) )
)
关联程序项是在 traits 中声明或在实现中定义的程序项。之所以这样称呼它们,是因为它们是被定义在一个相关联的类型(即实现里指定的类型)上的。关联程序项是那些可在模块中声明的程序项的子集。具体来说,有[关联函数][associated functions](包括方法)、[关联类型][associated types]和[关联常量][associated constants]。
当关联程序项与被关联程序项在逻辑上相关时,关联程序项就非常有用。例如,Option
上的 is_some
方法内在逻辑定义上就与 Option枚举类型相关,所以它应该和 Option 关联在一起。
每个关联程序项都有两种形式:(包含实际实现的)定义和(为定义声明签名的)声明。1
正是这些声明构成了 trait 的契约(contract)以及其泛型参数中的可用内容。
Associated functions and methods
关联函数和方法
关联函数是与一个类型相关联的函数。
关联函数声明为关联函数定义声明签名。它的书写格式和函数项一样,除了函数体被替换为 ;
。
标识符是关联函数的名称。关联函数的泛型参数、参数列表、返回类型 和 where子句必须与它们在关联函数声明中声明的格式一致。
关联函数定义定义与另一个类型相关联的函数。它的编写方式与函数项相同。
常见的关联函数的一个例子是 new
函数,它返回此关联函数所关联的类型的值。
struct Struct { field: i32 } impl Struct { fn new() -> Struct { Struct { field: 0i32 } } } fn main () { let _struct = Struct::new(); }
当关联函数在 trait 上声明时,此函数也可以通过一个指向 trait,再后跟函数名的路径来调用。当发生这种情况时,可以用 trait 的实际路径和关联函数的标识符按 <_ as Trait>::function_name
这样的形式来组织实际的调用路径。
#![allow(unused)] fn main() { trait Num { fn from_i32(n: i32) -> Self; } impl Num for f64 { fn from_i32(n: i32) -> f64 { n as f64 } } // 在这个案例中,这4种形式都是等价的。 let _: f64 = Num::from_i32(42); let _: f64 = <_ as Num>::from_i32(42); let _: f64 = <f64 as Num>::from_i32(42); let _: f64 = f64::from_i32(42); }
Methods
方法
如果关联函数的参数列表中的第一个参数名为 self
2,则此关联函数被称为方法,方法可以使用方法调用操作符(.
)来调用,例如 x.foo()
,也可以使用常用的函数调用形式进行调用。
如果名为 self
的参数的类型被指定了,它就通过以下文法(其中 'lt
表示生存期参数)来把此指定的参数限制解析成此文法中的一个类型:
P = &'lt S | &'lt mut S | Box<S> | Rc<S> | Arc<S> | Pin<P>
S = Self | P
此文法中的 Self
终结符(terminal)表示解析为实现类型(implementing type)的类型。这种解析包括解析上下文中的类型别名 Self
、其他类型别名、或使用投射解析把(self 的类型中的)关联类型解析为实现类型。3
译者注:原谅译者对上面这句背后知识的模糊理解,那首先给出原文:
TheSelf
terminal in this grammar denotes a type resolving to the implementing type. This can also include the contextual type aliasSelf
, other type aliases, or associated type projections resolving to the implementing type.
译者在此先邀请读者中的高手帮忙翻译清楚。感谢感谢。另外译者还是要啰嗦以下译者对这句话背后知识的理解,希望有人能指出其中的错误,以让译者有机会进步:
首先终结符(terminal)就是不能再更细分的词法单元,可以理解它是一个 token,这里它代表 self(即方法接受者)的类型的基础类型。上面句法中的 P 代表一个产生式,它内部定义的规则是并联的,就是自动机在应用这个产生式时碰到任意符合条件的输入就直接进入终态。S 代表有限自动机从 S 这里开始读取 self 的类型。这里 S 是 Self 和 P 的并联,应该表示是:如果 self 的类型直接是 Self,那就直接进入终态,即返回 Self,即方法接收者的直接类型就是结果类型;如果 self 的类型是 P 中的任一种,就返回那一种,比如 self 的类型是一个 box指针,那么就返回Box<S>
。
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::pin::Pin; // 结构体 `Example` 上的方法示例 struct Example; type Alias = Example; trait Trait { type Output; } impl Trait for Example { type Output = Example; } impl Example { fn by_value(self: Self) {} fn by_ref(self: &Self) {} fn by_ref_mut(self: &mut Self) {} fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} fn explicit_type(self: Arc<Example>) {} fn with_lifetime<'a>(self: &'a Self) {} fn nested<'a>(self: &mut &'a Arc<Rc<Box<Alias>>>) {} fn via_projection(self: <Example as Trait>::Output) {} } }
(方法的首参)可以在不指定类型的情况下使用简写句法,具体对比如下:
简写模式 | 等效项 |
---|---|
self | self: Self |
&'lifetime self | self: &'lifetime Self |
&'lifetime mut self | self: &'lifetime mut Self |
注意: (方法的)生存期也能,其实也经常是使用这种方式来省略。
如果 self
参数以 mut
为前缀,它就变成了一个可变的变量,类似于使用 mut
标识符模式的常规参数。例如:
#![allow(unused)] fn main() { trait Changer: Sized { fn change(mut self) {} fn modify(mut self: Box<Self>) {} } }
以下是一个关于 trait 的方法的例子,现给定如下内容:
#![allow(unused)] fn main() { type Surface = i32; type BoundingBox = i32; trait Shape { fn draw(&self, surface: Surface); fn bounding_box(&self) -> BoundingBox; } }
这里定义了一个带有两个方法的 trait。当此 trait 被引入当前作用域内后,所有此 trait 的实现的值都可以调用此 trait 的 draw
和 bounding_box
方法。
#![allow(unused)] fn main() { type Surface = i32; type BoundingBox = i32; trait Shape { fn draw(&self, surface: Surface); fn bounding_box(&self) -> BoundingBox; } struct Circle { // ... } impl Shape for Circle { // ... fn draw(&self, _: Surface) {} fn bounding_box(&self) -> BoundingBox { 0i32 } } impl Circle { fn new() -> Circle { Circle{} } } let circle_shape = Circle::new(); let bounding_box = circle_shape.bounding_box(); }
版次差异: 在 2015 版中, 使用匿名参数来声明 trait方法是可能的 (例如:
fn foo(u8)
)。在 2018 版中,这已被弃用,再用会导致编译错误。新版次中所有的参数都必须有参数名。
Attributes on method parameters
方法参数上的属性
方法参数上的属性遵循与常规函数参数上相同的规则和限制。
Associated Types
关联类型
关联类型是与另一个类型关联的类型别名(type aliases) 。关联类型不能在固有实现中定义,也不能在 trait 中给它们一个默认实现。
关联类型声明为关联类型声明其签名。
书写形式为下面这几种类型,其中 Assoc
是关联类型的名称,Params
是逗号分割的由类型、生存期或常量组成的参数列表,Bounds
是由加号分割的且此关联类型必须受约的 trait约束表,WhereBounds
是逗号分割的且此关联类型必须受约的 trait约束表。
type Assoc;
type Assoc: Bounds;
type Assoc<Params>;
type Assoc<Params>: Bounds;
type Assoc<Params> where WhereBounds;
type Assoc<Params>: Bounds where WhereBounds;
关联类型里的标识符是其声明的类型的别名的名称;其可选的 trait约束必须由此类型别名的实现(implementations)来履行实现。
关联类型上有一个隐式的 Sized
约束,可以使用 ?Sized
放宽此约束。
关联类型定义定义了用于在类型上实现特定trait 的类型别名。它们的书写形式类似于关联类型声明,但不能包含类似于上面的 Bounds
,并且必须包含一个类似于下面的 Type
:
type Assoc = Type;
type Assoc<Params> = Type; // 这里的 `Type` 的类型可以引用 `Params` 作为类型参数
type Assoc<Params> = Type where WhereBounds;
type Assoc<Params> where WhereBounds = Type; // 已经不推荐这种写法
如果类型 `Item` 上有一个来自 trait `Trait`的关联类型 `Assoc`,则表达式 `<Item as Trait>::Assoc` 也是一个类型,具体就是*关联类型定义*中指定的类型的一个别名。此外,如果 `Item` 是类型参数,则 `Item::Assoc` 也可以在类型参数中使用。
关联类型可包括[泛型参数][generic parameters]和 [where子句][where clauses];这些通常被称为*泛型关联类型*或 *GATs*。如果类型`Thing` 带有一个从 带有泛型参数`<'a>` 的`Trait`中继承过来的关联类型`Item`,则该类型可以命名为 `<Thing as Trait>::Item<'x>`,其中 `'x` 是作用域`'a` 内的某个生存期。此时,在此关联类型定义的impls 中出现的 `'a` 将会被 `'x` 替换。
```rust
trait AssociatedType {
// 关联类型声明
type Assoc;
}
struct Struct;
struct OtherStruct;
impl AssociatedType for Struct {
// 关联类型定义
type Assoc = OtherStruct;
}
impl OtherStruct {
fn new() -> OtherStruct {
OtherStruct
}
}
fn main() {
// 使用 <Struct as AssociatedType>::Assoc 来引用关联类型 OtherStruct
let _other_struct: OtherStruct = <Struct as AssociatedType>::Assoc::new();
}
下面是一个带有泛型和 where子句的关联类型的示例:
struct ArrayLender<'a, T>(&'a mut [T; 16]); trait Lend { // 泛型关联类型生命 type Lender<'a> where Self: 'a; fn lend<'a>(&'a mut self) -> Self::Lender<'a>; } impl<T> Lend for [T; 16] { // 泛型关联类型定义 type Lender<'a> = ArrayLender<'a, T> where Self: 'a; fn lend<'a>(&'a mut self) -> Self::Lender<'a> { ArrayLender(self) } } fn borrow<'a, T: Lend>(array: &'a mut T) -> <T as Lend>::Lender<'a> { array.lend() } fn main() { let mut array = [0usize; 16]; let lender = borrow(&mut array); }
Associated Types Container Example
示例展示容器类型的关联类型
下面给出一个 Container
trait 示例。请注意,该类型可用在方法签名内:
#![allow(unused)] fn main() { trait Container { type E; fn empty() -> Self; fn insert(&mut self, elem: Self::E); } }
为了能让实现类型来实现此 trait,实现类型不仅必须为每个方法提供实现,而且必须指定类型 E
。下面是一个为标准库类型 Vec
实现了此 Container
的实现:
#![allow(unused)] fn main() { trait Container { type E; fn empty() -> Self; fn insert(&mut self, elem: Self::E); } impl<T> Container for Vec<T> { type E = T; fn empty() -> Vec<T> { Vec::new() } fn insert(&mut self, x: T) { self.push(x); } } }
Relationship between Bounds
and WhereBounds
Bounds
和 WhereBounds
之间的关系
在下面这个例子里:
#![allow(unused)] fn main() { use std::fmt::Debug; trait Example { type Output<T>: Ord where T: Debug; } }
假定有一个对关联类型的引用,如 <X as Example>::Output<Y>
,关联类型本身必须受 Ord
的约束,而类型Y
必须受 Debug
的约束。
Required where clauses on generic associated types
泛型关联类型中的 where子句的前提条件
trait 上的的泛型关联类型声明目前可能需要一个 where子句列表,这取决于 trait 中的函数以及 GAT 如何被使用。这些规则将来可能会放宽;可以在泛型关联类型初创提案仓库中找到最新信息。
简言之,where子句是必需的,这种书写方式可以最有可能的在各类 impls 中定义关联类型。要做到这一点,当 GAT 作为函数的输入或输出时,此函数里提供的任何合法的子句在之前的 GAT 中必须也写一遍。
#![allow(unused)] fn main() { trait LendingIterator { type Item<'x> where Self: 'x; fn next<'a>(&'a mut self) -> Self::Item<'a>; } }
在上面的 next
函数中,我们提供了一个 Self: 'a
子句,因为有 &'a mut self
限制;因此,我们必须在 GAT 本身上写一个等价的约束:where Self: 'x
。
当 trait 中有多个函数使用同一个 GAT 时,则此 GAT 使用这些函数的约束的交集,而不是并集。(译者注:注意生存期的长短关系和父子关系是逆向的)
#![allow(unused)] fn main() { trait Check<T> { type Checker<'x>; fn create_checker<'a>(item: &'a T) -> Self::Checker<'a>; fn do_check(checker: Self::Checker<'_>); } }
在本例中,type Checker<'a>;
不需要任何约束。虽然我们知道 create_checker
上有 T: 'a
,但我们不知道 do_check
上有什么约束。但是,如果 do_check
被注释掉,则 Checker
上需要做 where T: 'x
约束。
关联类型上的约束还会传播其所需的where子句。
#![allow(unused)] fn main() { trait Iterable { type Item<'a> where Self: 'a; type Iterator<'a>: Iterator<Item = Self::Item<'a>> where Self: 'a; fn iter<'a>(&'a self) -> Self::Iterator<'a>; } }
这里, 因为函数iter
,Item
需要 where Self: 'a
。但是,在 Iterator
中,因为 Item
被用到了,那么 Item
上的 where Self: 'a
约束也要在这里声明一遍。
最后,在 trait 中,明确使用 'static
的 GAT 不需要明确声明其约束条件。
#![allow(unused)] fn main() { trait StaticReturn { type Y<'a>; fn foo(&self) -> Self::Y<'static>; } }
Associated Constants
关联常量
关联常量是与具体类型关联的常量。
关联常量声明为关联常量定义声明签名。书写形式为:先是 const
开头,然后是标识符,然后是 :
,然后是一个类型,最后是一个 ;
。
这里标识符是(外部引用)路径中使用的常量的名称;类型是(此关联常量的)定义必须实现的类型。
关联常量定义定义了与类型关联的常量。它的书写方式与常量项相同。
关联常量定义仅在引用时进行常量求值。此外,包含泛型参数的关联常量定义在单态化后才进行求值。
struct Struct; struct GenericStruct<const ID: i32>; impl Struct { // 定义不会立即执行求值操作 const PANIC: () = panic!("compile-time panic"); } impl<const ID: i32> GenericStruct<ID> { // 定义不会立即执行求值操作 const NON_ZERO: () = if ID == 0 { panic!("contradiction") }; } fn main() { // 引用 Struct::PANIC 导致编译错误 let _ = Struct::PANIC; // 可以编译通过, ID 非 0 let _ = GenericStruct::<1>::NON_ZERO; // 编译错误,因为使用 ID=0 来求值计算 NON_ZERO let _ = GenericStruct::<0>::NON_ZERO; }
Associated Constants Examples
示例展示关联常量
基本示例:
trait ConstantId { const ID: i32; } struct Struct; impl ConstantId for Struct { const ID: i32 = 1; } fn main() { assert_eq!(1, Struct::ID); }
使用默认值:
trait ConstantIdDefault { const ID: i32 = 1; } struct Struct; struct OtherStruct; impl ConstantIdDefault for Struct {} impl ConstantIdDefault for OtherStruct { const ID: i32 = 5; } fn main() { assert_eq!(1, Struct::ID); assert_eq!(5, OtherStruct::ID); }
固有实现中声明和定义是在一起的。
把简写形式转换成等价的标准形式。
结合下面的示例理解。
Attributes
属性
attributes.md
commit: 51817951d0d213a0011f82b62aae02c3b3f2472e
本章译文最后维护日期:2024-05-02
句法
InnerAttribute :
#
!
[
Attr]
OuterAttribute :
#
[
Attr]
Attr :
SimplePath AttrInput?AttrInput :
DelimTokenTree
|=
Expression
属性是一种通用的、格式自由的元数据(free-form metadatum),这种元数据会(被编译器/解释器)依据名称、约定、语言和编译器版本进行解释。(Rust 语言中的)属性是根据 ECMA-335 标准中的属性规范进行建模的,其语法来自 ECMA-334 (C#)。
*内部属性(Inner attributes)*以 #!
开头的方式编写,应用于它在其中声明的程序项。*外部属性(Outer attributes)*以不后跟感叹号的(!
)的 #
开头的方式编写,应用于属性后面的内容。
属性由指向属性的路径和路径后跟的可选的带定界符的 token树(delimited token tree)(其解释由属性定义)组成。除了宏属性之外,其他属性的输入也允许使用等号(=
)后跟表达式的格式。更多细节请参见下面的元项属性句法(meta item syntax)。
属性可以分为以下几类:
属性可以应用于语言中的许多场景:
- 所有的程序项声明都可接受外部属性,同时外部块、函数、实现和模块都可接受内部属性。
- 大多数语句都可接受外部属性(参见表达式属性,了解表达式语句的限制)。
- 块表达式也可接受外部属性和内部属性,但只有当它们是另一个表达式语句的外层表达式时,或是另一个块表达式的最终表达式(final expression)时才有效。
- 枚举(
enum
)变体和结构体(struct
)、联合体(union
)的字段可接受外部属性。 - 匹配表达式的匹配臂(arms)可接受外部属性。
- 泛型生存期(Generic lifetime)或类型参数可接受外部属性。
- 表达式在有限的情况下可接受外部属性,详见表达式属性。
- 函数、闭包和函数指针的参数可接受外部属性。这包括函数指针和外部块中用
...
表示的可变参数上的属性。
属性的一些例子:
#![allow(unused)] fn main() { // 应用于当前模块或 crate 的一般性元数据。 #![crate_type = "lib"] // 标记为单元测试的函数 #[test] fn test_foo() { /* ... */ } // 一个条件编译模块 #[cfg(target_os = "linux")] mod bar { /* ... */ } // 用于静音 lint检查后报告的告警和错误提醒 #[allow(non_camel_case_types)] type int8_t = i8; // 适用于整个函数的内部属性 fn some_unused_variables() { #![allow(unused_variables)] let x = (); let y = (); let z = (); } }
Meta Item Attribute Syntax
元项/元程序项属性句法
“元项(meta item)”是遵循 Attr 产生式(见本章头部)的句法,Rust 的大多数内置属性(built-in attributes)都使用了此句法。它有以下文法格式:
句法
MetaItem :
SimplePath
| SimplePath=
Expression
| SimplePath(
MetaSeq?)
MetaSeq :
MetaItemInner (,
MetaItemInner )*,
?MetaItemInner :
MetaItem
| Expression
元项中的表达式必须能宏展开为字面量表达式,且字面量表达式不得包含整型或浮点型后缀。现在,非字面量表达式的表达式可以在语法上被接受(并且可以传递给过程宏)了,但在解析后却被丢弃。
请注意,如果该属性出现在另一个宏中,它将在该外部宏展开之后再展开。例如,下面的代码将首先展开为名为 Serialize
的过程宏,在第一次展开时,它必须保留 include_str!
调用,以便将来再次展开:
#[derive(Serialize)]
struct Foo {
#[doc = include_str!("x.md")]
x: u32
}
此外,对与某个程序项来说,如果它的属性中用到了宏,那这个宏仅在所有其他相关属性都展开生效之后才展开:
#[macro_attr1] // 展开排序为第一
#[doc = mac!()] // `mac!` 展开排序为第四
#[macro_attr2] // 展开排序为第二
#[derive(MacroDerive1, MacroDerive2)] // 展开排序为第三
fn foo() {}
各种内置属性使用元项句法的不同子集来指定它们的输入。下面的文法规则展示了一些常用的使用形式:
句法
MetaWord:
IDENTIFIERMetaNameValueStr:
IDENTIFIER=
(STRING_LITERAL | RAW_STRING_LITERAL)MetaListPaths:
IDENTIFIER(
( SimplePath (,
SimplePath)*,
? )?)
MetaListIdents:
IDENTIFIER(
( IDENTIFIER (,
IDENTIFIER)*,
? )?)
MetaListNameValueStr:
IDENTIFIER(
( MetaNameValueStr (,
MetaNameValueStr)*,
? )?)
元项句法的一些例子是:
形式 | 示例 |
---|---|
MetaWord | no_std |
MetaNameValueStr | doc = "example" |
MetaListPaths | allow(unused, clippy::inline_always) |
MetaListIdents | macro_use(foo, bar) |
MetaListNameValueStr | link(name = "CoreFoundation", kind = "framework") |
Active and inert attributes
活跃属性和惰性属性
属性要么是活跃的,要么是惰性的。在属性处理过程中,活跃属性将自己从它们所在的对象上移除,而惰性属性依然保持原位置不变。
cfg
和 cfg_attr
属性是活跃的。test
属性在为测试所做的编译形式中是惰性的,在其他编译形式中是活跃的。宏属性是活跃的。所有其他属性都是惰性的。
Tool attributes
外部工具的属性
编译器可能允许和具体外部工具相关联的属性,但这些工具在编译和检查过程中必须存在并驻留在编译器提供的工具类预导入包下对应的命名空间中(才能让这些属性生效)。这种属性的(命名空间)路径的第一段是工具的名称,后跟一个或多个工具自己解释的附加段。
当工具在编译期不可用时,该工具的属性将被静默接受而不提示警告。当工具可用时,该工具负责处理和解释这些属性。
如果使用了 no_implicit_prelude
属性,则外部工具属性不可用。
#![allow(unused)] fn main() { // 告诉rustfmt工具不要格式化以下元素。 #[rustfmt::skip] struct S { } // 控制clippy工具的“圈复杂度(cyclomatic complexity)”极限值。 #[clippy::cyclomatic_complexity = "100"] pub fn f() {} }
注意:
rustc
目前能识别 “clippy” 、“rustfmt” 和 "diagnostic" 这些工具。
Built-in attributes index
内置属性的索引表
下面是所有内置属性的索引表:
- 条件编译(Conditional compilation)
- 测试(Testing)
test
— 将函数标记为测试函数。ignore
— 禁止测试此函数。should_panic
— 表示测试应该产生 panic。
- 派生(Derive)
derive
— 自动部署 trait实现automatically_derived
— 用在由derive
创建的实现上的标记。
- 宏(Macros)
macro_export
— 导出声明宏(macro_rules
宏),用于跨 crate 的使用。macro_use
— 扩展宏可见性,或从其他 crate 导入宏。proc_macro
— 定义类函数宏。proc_macro_derive
— 定义派生宏。proc_macro_attribute
— 定义属性宏。
- 诊断(Diagnostics)
allow
、warn
、deny
、forbid
— 更改默认的 lint检查级别。deprecated
— 生成弃用通知。must_use
— 为未使用的值生成 lint 提醒。diagnostic::on_unimplemented
— 如个某个 trait 没有被实现,则提醒编译器发射一个特定的错误消息。
- ABI、链接(linking)、符号(symbol)、和 FFI
link
— 指定要与外部(extern
)块链接的本地库。link_name
— 指定外部(extern
)块中的函数或静态项的符号(symbol)名。link_ordinal
— 指定外部(extern
)块中函数或静态符号的序号。no_link
— 防止链接外部crate。repr
— 控制类型的布局。crate_type
— 指定 crate 的类别(库、可执行文件等)。no_main
— 禁止发布main
符号(symbol)。export_name
— 指定函数或静态项导出的符号(symbol)名。link_section
— 指定用于函数或静态项的对象文件的部分。no_mangle
— 禁用对符号(symbol)名编码。used
— 强制编译器在输出对象文件中保留静态项。crate_name
— 指定 crate名。
- 代码生成(Code generation)
inline
— 内联代码提示。cold
— 提示函数不太可能被调用。no_builtins
— 禁用某些内置函数。target_feature
— 配置特定于平台的代码生成。track_caller
- 将父调用位置传递给std::panic::Location::caller()
。instruction_set
- 指定用于生成函数代码的指令集S
- 文档(Documentation)
doc
— 指定文档。更多信息见 The Rustdoc Book。Doc注释会被转换为doc
属性。
- 预导入包(Preludes)
no_std
— 从预导入包中移除 std。no_implicit_prelude
— 禁用模块内的预导入包查找。
- 模块(Modules)
path
— 指定模块的源文件名。
- 极限值设置(Limits)
recursion_limit
— 设置某些编译时操作的最大递归限制。type_length_limit
— 设置多态类型(polymorphic type)单态化过程中构造具体类型时所做的最大类型替换次数。
- 运行时(Runtime)
panic_handler
— 设置处理 panic 的函数。global_allocator
— 设置全局内存分配器。windows_subsystem
— 指定要链接的 windows 子系统。
- 特性(Features)
feature
— 用于启用非稳定的或实验性的编译器特性。参见 The Unstable Book 了解在rustc
中实现的特性。
- 类型系统(Type System)
non_exhaustive
— 表明一个类型将来会添加更多的字段/变体。
- 调试器
debugger_visualizer
— 嵌入一个文件,该文件指定类型的调试器输出。collapse_debuginfo
— 控制宏调用在调试信息中的编码方式。
Testing attributes
测试类属性
testing.md
commit: c126440392be42d9dd3906478111cc7b52473d89
本章译文最后维护日期:2022-10-22
以下属性用于指定函数来执行测试。在“测试(test)”模式下编译 crate 可以构建测试函数以及构建用于执行测试(函数)的测试套件(test harness)。启用测试模式还会启用 test
条件编译选项。
The test
attribute
test
属性
test
属性标记一个用来执行测试的函数。这些函数只在测试模式下编译。测试函数必须是自由函数和单态函数,不能有参数,返回类型必须实现 Termination
trait,例如:
()
Result<T, E> where T: Termination, E: Debug
!
注意:测试模式是通过将
--test
参数选项传递给rustc
或使用cargo test
来启用的。
返回 ()
的测试只要结束(terminate)且没有触发 panic 就会通过。返回 Result<(), E>
的测试只要它们返回 Ok(())
就算通过。不结束的测试既不(计为)通过也不(计为)失败。
测试工具调用返回值的 report
方法,并根据表示程序是否成功结束(termination)的返回码 ExitCode
来判定将本次测试为通过或失败。
需要特别提醒的是:
- 只要程序结束且没有 panic,那么返回
()
的测试就可以算是通过。 - 只要返回
Ok(())
,那么返回结果为Result<(), E>
的测试就算通过。 - 返回
ExitCode::SUCCESS
的测试算是通过,返回ExitCode::FAILURE
的测试算是失败。 - 不结束的测试既不通过也不失败。
#![allow(unused)] fn main() { use std::io; fn setup_the_thing() -> io::Result<i32> { Ok(1) } fn do_the_thing(s: &i32) -> io::Result<()> { Ok(()) } #[test] fn test_the_thing() -> io::Result<()> { let state = setup_the_thing()?; // 预期成功 do_the_thing(&state)?; // 预期成功 Ok(()) } }
The ignore
attribute
ignore
属性
被 test
属性标注的(annotated with)函数也可以被 ignore
属性标注。ignore
属性告诉测试套件不要将该函数作为测试执行。但在测试模式下,这类函数仍然会被编译。
ignore
属性可以选择使用 MetaNameValueStr元项属性句法来说明测试被忽略的原因。
#![allow(unused)] fn main() { #[test] #[ignore = "not yet implemented"] fn mytest() { // … } }
注意:
rustc
的测试套件支持使用--include-ignored
参数选项来强制运行那些被忽略测试的函数。
The should_panic
attribute
should_panic
属性
被 test
属性标注并返回 ()
的函数也可以被 should_panic
属性标注。should_panic
属性使测试函数只有在实际发生 panic 时才算通过。
should_panic
属性可选输入一条出现在 panic消息中的字符串。如果在 panic消息中找不到该字符串,则测试将失败。可以使用 MetaNameValueStr元项属性句法或带有 expected
字段的 MetaListNameValueStr元项属性句法来传递字符串。
#![allow(unused)] fn main() { #[test] #[should_panic(expected = "值未匹配上")] fn mytest() { assert_eq!(1, 2, "值未匹配上"); } }
Derive
派生
derive.md
commit: ea7ba21c879c5cf58d7a2dffcf74f93a6c0933c4
本章译文最后维护日期:2022-10-22
derive
属性允许为数据结构自动生成新的程序项。它使用 MetaListPaths元项属性句法(为程序项)指定一系列要实现的 trait 或指定要执行的派生宏的路径。
例如,下面的派生属性将为结构体 Foo
创建一个实现 PartialEq
trait 和 Clone
trait 的实现(impl
item),类型参数 T
将被派生出的实现(impl
)加上 PartialEq
或1 Clone
约束:
#![allow(unused)] fn main() { #[derive(PartialEq, Clone)] struct Foo<T> { a: i32, b: T, } }
上面代码为 PartialEq
生成的实现(impl
)等价于
#![allow(unused)] fn main() { struct Foo<T> { a: i32, b: T } impl<T: PartialEq> PartialEq for Foo<T> { fn eq(&self, other: &Foo<T>) -> bool { self.a == other.a && self.b == other.b } } }
可以通过过程宏为自定义的 trait 实现自动派生(derive
)功能。
The automatically_derived
attribute
automatically_derived
属性
automatically_derived
属性会被自动添加到由 derive
属性为一些内置trait 自动派生的实现中。它对派生出的实现没有直接影响,但是工具和诊断lint 可以使用它来检测这些自动派生的实现。
原文后半句是:"and the type parameter T
will be given the PartialEq
or Clone
constraints for the appropriate impl
:",这里译者也搞不清楚为什么 PartialEq
和 Clone
之间用了"or",而不是"and"?这里译者就先采用直译。
Diagnostic attributes
诊断属性
diagnostics.md
commit: 52874b8312ccbc28710a2532f82032876a08911b
本章译文最后维护日期:2024-04-06
以下属性用于在编译期间控制或生成诊断消息。
Lint check attributes
lint检查类属性
(译者注:lint在原文里有时当名词用,有时当动词用,本文统一翻译成名词,意思就是一种被命名的 lint检查模式)
lint检查(lint check)系统命名了一些潜在的不良编码模式,(这些被命名的 lint检查就是一个一个的lint,)例如编写了不可能执行到的代码,就被命名为 unreachable-code lint,编写未提供文档的代码就被命名为 missing_docs lint。allow
、warn
、deny
和 forbid
这些能调整代码检查级别的属性被称为 lint级别属性,它们使用 MetaListPaths元项属性句法来指定接受此级别的各种 lint。代码实体应用了这些带了具体 lint 列表的 lint级别属性,编译器或相关代码检查工具就可以结合这两层属性对这段代码执行相应的代码检查和检查报告。
对任何名为 C
的 lint 来说:
allow(C)
会压制对C
的检查,那这样的违规行为就不会被报告,warn(C)
警告违反C
的,但继续编译。deny(C)
遇到违反C
的情况会触发编译器报错(signals an error),forbid(C)
与deny(C)
相同,但同时会禁止以后再更改 lint级别,
注意:可以通过
rustc -W help
找到所有rustc
支持的 lint,以及它们的默认设置,也可以在 rustc book 中找到相关文档。
#![allow(unused)] fn main() { pub mod m1 { // 这里忽略未提供文档的编码行为 #[allow(missing_docs)] pub fn undocumented_one() -> i32 { 1 } // 未提供文档的编码行为在这里将触发编译器警告 #[warn(missing_docs)] pub fn undocumented_too() -> i32 { 2 } // 未提供文档的编码行为在这里将触发编译器报错 #[deny(missing_docs)] pub fn undocumented_end() -> i32 { 3 } } }
Lint属性可以覆盖上一个属性指定的级别,但该级别不能更改禁止性的 Lint。这里的上一个属性是指语法树中更高级别的属性,或者是同一实体上的前一个属性,按从左到右的源代码顺序列出。
此示例展示了如何使用 allow
和 warn
来打开和关闭一个特定的 lint检查:
#![allow(unused)] fn main() { #[warn(missing_docs)] pub mod m2 { #[allow(missing_docs)] pub mod nested { // 这里忽略未提供文档的编码行为 pub fn undocumented_one() -> i32 { 1 } // 尽管上面允许,但未提供文档的编码行为在这里将触发编译器警告 #[warn(missing_docs)] pub fn undocumented_two() -> i32 { 2 } } // 未提供文档的编码行为在这里将触发编译器警告 pub fn undocumented_too() -> i32 { 3 } } }
此示例展示了如何使用 forbid
来禁止对该 lint 使用 allow
:
#![allow(unused)] fn main() { #[forbid(missing_docs)] pub mod m3 { // 试图切换到 allow级别将触发编译器报错 #[allow(missing_docs)] /// 返回 2。 pub fn undocumented_too() -> i32 { 2 } } }
注意:
rustc
允许在命令行上设置 lint级别,也支持在setting caps中设置 lint报告中的caps。
Lint groups
lint分组
lint可以被组织成不同命名的组,以便相关lint的等级可以一起调整。使用命名组相当于给出该组中的 lint。
#![allow(unused)] fn main() { // 这允许 "unused"组下的所有lint。 #[allow(unused)] // 这个禁止动作将覆盖前面 "unused"组中的 "unused_must_use" lint。 #[deny(unused_must_use)] fn example() { // 这里不会生成警告,因为 "unused_variables" lint 在 "unused"组下 let x = 1; // 这里会产生报错信息,因为 "unused_must_use" lint 被标记为"deny",而这行的返回结果未被使用 std::fs::remove_file("some_file"); // ERROR: unused `Result` that must be used } }
有一个名为 “warnings” 的特殊组,它包含 “warn”级别的所有 lint。“warnings”组会忽略属性的顺序,并对实体执行所有警告lint 检查。
#![allow(unused)] fn main() { unsafe fn an_unsafe_fn() {} // 这里的两个属性的前后顺序无所谓 #[deny(warnings)] // unsafe_code lint 默认是 "allow" 的。 #[warn(unsafe_code)] fn example_err() { // 这里会编译报错,因为 `unsafe_code`警告的 lint 被提升为 "deny"级别了。 unsafe { an_unsafe_fn() } // ERROR: usage of `unsafe` block } }
Tool lint attributes
工具类lint属性
可以为 allow
、warn
、deny
或 forbid
这些调整代码检查级别的 lint属性添加/输入基于特定工具的 lint。注意该工具在当前作用域内可用才会起效。
工具类lint 只有在相关工具处于活动状态时才会做相应的代码模式检查。如果一个 lint级别属性,如 allow
,引用了一个不存在的工具类lint,编译器将不会去警告不存在该 lint类,只有使用了该工具(,才会报告该 lint类不存在)。
在其他方面,这些工具类lint 就跟像常规的 lint 一样:
// 将 clippy 的整个 `pedantic` lint组设置为警告级别 #![warn(clippy::pedantic)] // 使来自 clippy的 `filter_map` lint 的警告静音 #![allow(clippy::filter_map)] fn main() { // ... } // 使 clippy的 `cmp_nan` lint 静音,仅用于此函数 #[allow(clippy::cmp_nan)] fn foo() { // ... }
The deprecated
attribute
deprecated
属性
deprecated
属性将程序项标记为已弃用。rustc
将对被 #[deprecated]
限定的程序项的行为发出警告。rustdoc
将特别展示已弃用的程序项,包括展示 since
版本和 note
提示(如果可用)。
deprecated
属性有几种形式:
deprecated
— 发布一个通用的弃用消息。deprecated = "message"
— 在弃用消息中包含给定的字符串。- MetaListNameValueStr元项属性句法形式里带有两个可选字段:
since
— 指定程序项被弃用时的版本号。rustc
目前不解释此字符串,但是像 Clippy 这样的外部工具可以检查此值的有效性。note
— 指定一个应该包含在弃用消息中的字符串。这通常用于提供关于不推荐的解释和推荐首选替代。
deprecated
属性可以应用于任何程序项,trait项,枚举变体,结构体字段,外部块项,或宏定义。它不能应用于 trait实现项。当应用于包含其他程序项的程序项时,如模块或实现,所有子程序项都继承此 deprecation属性。
这有个例子:
#![allow(unused)] fn main() { #[deprecated(since = "5.2.0", note = "foo was rarely used. Users should instead use bar")] pub fn foo() {} pub fn bar() {} }
RFC 内有动机说明和更多细节。
The must_use
attribute
must_use
属性
must_use
属性 用于在值未被“使用”时发出诊断警告。它可以应用于用户定义的复合类型(结构体(struct
)、枚举(enum
) 和 联合体(union
))、函数和 trait。
must_use
属性可以使用MetaNameValueStr元项属性句法添加一些附加消息,如 #[must_use = "example message"]
。该字符串将出现在警告消息里。
当用户定义的复合类型上使用了此属性,如果有该类型的表达式正好是表达式语句的表达式,那就违反了 unused_must_use
这个 lint。
#![allow(unused)] fn main() { #[must_use] struct MustUse { // 一些字段 } impl MustUse { fn new() -> MustUse { MustUse {} } } // 违反 `unused_must_use` lint。 MustUse::new(); }
当函数上使用了此属性,如果此函数被当作表达式语句的表达式的调用表达式,那就违反了 unused_must_use
lint。
#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 违反 unused_must_use lint。 five(); }
当在 trait声明中使用了此属性时,如果表达式语句的调用表达式返回了此 trait 的 trait实现(impl trait)或 trait对象(dyn trait) ,那这种情况也违反 unused_must_use
lint。
#![allow(unused)] fn main() { #[must_use] trait Critical {} impl Critical for i32 {} fn get_critical() -> impl Critical { 4i32 } // 违反 `unused_must_use` lint。 get_critical(); }
当 trait声明中的一函数上使用了此属性时,如果调用表达式是此 trait 的某个实现中的该函数时,该行为同样违反 unused_must_use
lint。
#![allow(unused)] fn main() { trait Trait { #[must_use] fn use_me(&self) -> i32; } impl Trait for i32 { fn use_me(&self) -> i32 { 0i32 } } // 违反 `unused_must_use` lint。 5i32.use_me(); }
当在 trait实现里的函数上使用 must_use
属性时,此属性将被忽略。
注意:包含了此(属性应用的程序项产生的值)的普通空操作(no-op)表达式不会违反该 lint。例如,将此类值包装在没有实现
Drop
的类型中,然后不使用该类型,并成为未使用的块表达式的最终表达式(final expression)。#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 这些都不违反 unused_must_use lint。 (five(),); Some(five()); { five() }; if true { five() } else { 0i32 }; match true { _ => five() }; }
注意:当一个必须使用的值被故意丢弃时,使用带有模式
_
的let语句是惯用的方法。#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 不违反 unused_must_use lint。 let _ = five(); }
The diagnostic
tool attribute namespace
diagnostic
属性的命名空间
属性#[diagnostic]
的命名空间是影响编译期错误消息的根属性。
Rust 不保证这些属性提供的提示信息会被使用。
此命名空间可接受未知属性,但这样可能会触发有属性未被使用这样的警告。
此外,对已知属性的无效输入通常只是一个警告(有关详细信息,请参阅属性定义)。
这意味着允许添加或丢弃属性,并在未来更改输入,以允许进行更改,而无需为了保持程序正常工作而不得不选择无意义的属性或选项。
The diagnostic::on_unimplemented
attribute
diagnostic::on_unimplemented
属性
#[diagnostic::on_unimplemented]
属性会给编译器的一个提示,该提示通常用于要求某trait 必须在场,但使用的具体类型上并未实现此trait 的场景中来生成错误消息。
该属性应用在trait申明上,尽管它放在其他位置上并不会报错。
该属性使用MetaListNameValueStr元项属性句法来指定其输入,但该属性的任何格式错误的输入都不会被视为错误,此目的是为了提供向前和向后的兼容性。
其下的选项具有以下给定的含义:
message
— 顶层错误消息的文本模板。label
— 错误消息中错误代码内联显示的文本标签。note
— 提供额外的注释。
其中 note
选项可以出现多次,这将导致编译器发出多条注释消息。
如果其他选项中的任何一个出现了多次,则相关选项的首次定义会被认定为实际使用的值。
任何其他情况都会产生 lint 警告。
对于任何其他不存在的选项,也都会生成 lint 警告。
所有三个选项都接受字符串作为模板格式参数,使用与 std::fmt
字符串相同的格式进行解释。
使用给定命名参数的格式参数将被替换为以下文本:
{Self}
— 实现 trait 的类型的名称。{
GenericParameterName}
— 给定泛型参数的泛型参数的类型名。
任何其他格式参数都将生成警告,但会原样包含在字符串中。
无效的格式字符串可能会产生警告,但通常是被允许的,但可能不会按预期显示。 格式说明符可能会产生警告,但可以会被忽略。
示例:
#[diagnostic::on_unimplemented( message = "My Message for `ImportantTrait<{A}>` implemented for `{Self}`", label = "My Label", note = "Note 1", note = "Note 2" )] trait ImportantTrait<A> {} fn use_my_trait(_: impl ImportantTrait<i32>) {} fn main() { use_my_trait(String::new()); }
编译器可能会生成如下所示的错误消息:
error[E0277]: My Message for `ImportantTrait<i32>` implemented for `String`
--> src/main.rs:14:18
|
14 | use_my_trait(String::new());
| ------------ ^^^^^^^^^^^^^ My Label
| |
| required by a bound introduced by this call
|
= help: the trait `ImportantTrait<i32>` is not implemented for `String`
= note: Note 1
= note: Note 2
Code generation attributes
代码生成属性
codegen.md
commit: 5854fcc286557ad3ab34d325073d11d8118096b6
本章译文最后维护日期:2024-05-02
下述属性用于控制代码生成。
Optimization hints
优化提示
cold
属性和 inline
属性给出了某种代码生成方式的提示建议,这种方式可能比没有此提示时更快。这些属性只是提示,可能会被忽略。
这两个属性都可以在函数上使用。当这类属性应用于 trait 中的函数上时,它们只在那些没有被 trait实现所覆盖的默认函数上生效,而不是所有 trait实现中用到的函数上都生效。这两属性对 trait 中那些没有函数体的函数没有影响。
The inline
attribute
内联(inline
)属性
inline
属性 的意义是暗示在调用者(caller)中放置此(属性限定的)函数的副本,而不是在定义此(属性限定的)函数的地方生此函数的代码,然后去让别处代码来调用此函数。
注意:
rustc
编译器会根据启发式算法(internal heuristics)1自动内联函数。不正确的内联函数会使程序变慢,所以应该小心使用此属性。
使用内联(inline
)属性有三种方法:
#[inline]
暗示执行内联扩展。#[inline(always)]
暗示应该一直执行内联扩展。#[inline(never)]
暗示应该从不执行内联扩展。
注意:
#[inline]
在每种形式中都是一个提示,不是必须要在调用者放置此属性限定的函数的副本。
The cold
attribute
cold
属性
cold
属性 暗示此(属性限定的)函数不太可能被调用。
The no_builtins
attribute
no_builtins
属性
no_builtins
属性 可以应用在 crate 级别,用以禁用对假定存在的库函数调用的某些代码模式优化。2
The target_feature
attribute
target_feature
属性
target_feature
[属性] 可应用于函数上,用来为特定的平台架构特性(platform architecture features)启用该函数的代码生成功能。它使用 MetaListNameValueStr元项属性句法格式来启用(该平台支持的)特性,但这次要求这个句法里只能有一个 enable
键,其对应值是一个逗号分隔的由平台特性名字组成的符串。
#![allow(unused)] fn main() { #[cfg(target_feature = "avx2")] #[target_feature(enable = "avx2")] unsafe fn foo_avx2() {} }
每个目标架构都有一组可以被启用的特性。为不是当前 crate 的编译目标下的CPU架构指定需启用的特性是错误的。
调用启用了当前运行代码的平台不支持的特性编译的函数将导致未定义行为,除非此平台明确声明此调用是安全的。
应用了 target_feature
的函数不会内联到不支持给定特性的上下文中。#[inline(always)]
属性不能与 target_feature
属性一起使用。
Available features
可用特性
下面是可用的特性的名称列表。
x86
or x86_64
即便是同为 x86
或 x86_64
平台,并不是所有平台都支持下述特性,而在未启用特定特性的平台下执行带有此平台的特性的代码会导致未定义行为。
因此在这类平台下,#[target_feature]
只能应用于非安全(unsafe
)函数。
特性 | 隐式启用 | 描述 | 中文描述 |
---|---|---|---|
adx | ADX — Multi-Precision Add-Carry Instruction Extensions | 多精度进位指令扩展 | |
aes | sse2 | AES — Advanced Encryption Standard | 高级加密标准 |
avx | sse4.2 | AVX — Advanced Vector Extensions | 高级矢量扩展指令集 |
avx2 | avx | AVX2 — Advanced Vector Extensions 2 | 高级矢量扩展指令集2 |
bmi1 | BMI1 — Bit Manipulation Instruction Sets | 位操作指令集 | |
bmi2 | BMI2 — Bit Manipulation Instruction Sets 2 | 位操作指令集2 | |
cmpxchg16b | cmpxchg16b - Compares and exchange 16 bytes (128 bits) of data atomically | 原子地比较和交换16字节(128位)的数据 | |
f16c | avx | F16C — 16-bit floating point conversion instructions | 16位浮点转换指令 |
fma | avx | FMA3 — Three-operand fused multiply-add | 三操作乘加指令 |
fxsr | fxsave and fxrstor — Save and restore x87 FPU, MMX Technology, and SSE State | 保存/恢复 x87 FPU、MMX技术,SSE状态 | |
lzcnt | lzcnt — Leading zeros count | 前导零计数 | |
movbe | movbe - Move data after swapping bytes | 交换字节后移动数据 | |
pclmulqdq | sse2 | pclmulqdq — Packed carry-less multiplication quadword | 压缩的四字(16字节)无进位乘法,主用于加解密处理 |
popcnt | popcnt — Count of bits set to 1 | 位1计数,即统计有多少个“为1的位” | |
rdrand | rdrand — Read random number | 从芯片上的硬件随机数生成器中获取随机数 | |
rdseed | rdseed — Read random seed | 从芯片上的硬件随机数生成器中获取为伪随机数生成器设定的种子 | |
sha | sse2 | SHA — Secure Hash Algorithm | 安全哈希算法 |
sse | SSE — Streaming SIMD Extensions | 单指令多数据流扩展指令集 | |
sse2 | sse | SSE2 — Streaming SIMD Extensions 2 | 单指令多数据流扩展指令集2 |
sse3 | sse2 | SSE3 — Streaming SIMD Extensions 3 | 单指令多数据流扩展指令集3 |
sse4.1 | ssse3 | SSE4.1 — Streaming SIMD Extensions 4.1 | 单指令多数据流扩展指令集4.1 |
sse4.2 | sse4.1 | SSE4.2 — Streaming SIMD Extensions 4.2 | 单指令多数据流扩展指令集4.2 |
ssse3 | sse3 | SSSE3 — Supplemental Streaming SIMD Extensions 3 | 增补单指令多数据流扩展指令集3 |
xsave | xsave — Save processor extended states | 保存处理器扩展状态 | |
xsavec | xsavec — Save processor extended states with compaction | 压缩保存处理器扩展状态 | |
xsaveopt | xsaveopt — Save processor extended states optimized | xsave 指令集的优化版 | |
xsaves | xsaves — Save processor extended states supervisor | 保存处理器扩展状态监视程序 |
aarch64
该目标平台要求 #[target_feature]
属性仅适用于 unsafe
函数
关于这些特性的更多文档可以在 [developer.arm.com] 上的 [ARM架构参考手册][ARM Architecture Reference Manual]中或 [developer.arm.com] 上的其他地方找到。 [ARM Architecture Reference Manual]: https://developer.arm.com/documentation/ddi0487/latest [developer.arm.com]: https://developer.arm.com
注意: 如果要使用的话,
paca
和pacg
这对特性应同时标记为启用或禁用,因为目前 LLVM 将其作为一个特性来实现的。
特性 | 隐式启用 | 特性名称 |
---|---|---|
aes | neon | FEAT_AES & FEAT_PMULL - 高级 SIMD AES & PMULL 指令 |
bf16 | FEAT_BF16 - BFloat16 指令 | |
bti | FEAT_BTI - 分支目标识别 | |
crc | FEAT_CRC - CRC32 校验和指令 | |
dit | FEAT_DIT - 与数据无关的定时指令 | |
dotprod | FEAT_DotProd - Advanced SIMD Int8 dot product instructions | |
dpb | FEAT_DPB - 数据持久点之后的缓存清理 | |
dpb2 | FEAT_DPB2 - 数据深度持久点之后的缓存清理 | |
f32mm | sve | FEAT_F32MM - SVE单精度 FP矩阵乘法指令 |
f64mm | sve | FEAT_F64MM - SSVE双精度 FP矩阵乘法指令 |
fcma | neon | FEAT_FCMA - 浮点复数支持 |
fhm | fp16 | FEAT_FHM - 半精度 FP FMLAL 指令 |
flagm | FEAT_FlagM - 条件标志操作 | |
fp16 | neon | FEAT_FP16 - 半精度 FP数据处理 |
frintts | FEAT_FRINTTS - 浮点到整型的辅助转换指令 | |
i8mm | FEAT_I8MM - Int8 的矩阵乘法 | |
jsconv | neon | FEAT_JSCVT - JavaScript 转换指令 |
lse | FEAT_LSE - Large System Extension | |
lor | FEAT_LOR - Limited Ordering Regions extension | |
mte | FEAT_MTE & FEAT_MTE2 - 内存标记扩展 | |
neon | FEAT_FP & FEAT_AdvSIMD - 浮点和高级SIMD扩展 | |
pan | FEAT_PAN - Privileged Access-Never extension | |
paca | FEAT_PAuth - 指针身份验证(地址身份验证) | |
pacg | FEAT_PAuth - 指针身份验证(通用身份验证) | |
pmuv3 | FEAT_PMUv3 - 性能监视器扩展(v3) | |
rand | FEAT_RNG - 随机数发生器 | |
ras | FEAT_RAS & FEAT_RASv1p1 - 可靠性、可用性和可维护性扩展 | |
rcpc | FEAT_LRCPC - Release consistent Processor Consistent | |
rcpc2 | rcpc | FEAT_LRCPC2 - 带即时偏移的rcpc |
rdm | FEAT_RDM - Rounding Double Multiply accumulate | |
sb | FEAT_SB - Speculation Barrier | |
sha2 | neon | FEAT_SHA1 & FEAT_SHA256 - 高级 SIMD SHA 指令 |
sha3 | sha2 | FEAT_SHA512 & FEAT_SHA3 - 高级 SIMD SHA 指令 |
sm4 | neon | FEAT_SM3 & FEAT_SM4 - 高级 SIMD SM3/4 指令 |
spe | FEAT_SPE - 统计分析扩展 | |
ssbs | FEAT_SSBS & FEAT_SSBS2 - Speculative Store Bypass Safe | |
sve | fp16 | FEAT_SVE - 可伸缩向量扩展 |
sve2 | sve | FEAT_SVE2 - 可伸缩向量扩展2 |
sve2-aes | sve2 , aes | FEAT_SVE_AES - SVE AES 指令 |
sve2-sm4 | sve2 , sm4 | FEAT_SVE_SM4 - SVE SM4 指令 |
sve2-sha3 | sve2 , sha3 | FEAT_SVE_SHA3 - SVE SHA3 指令 |
sve2-bitperm | sve2 | FEAT_SVE_BitPerm - SVE位置换 |
tme | FEAT_TME - 事务内存扩展 | |
vh | FEAT_VHE - 虚拟化主机扩展 |
riscv32
or riscv64
此类目标平台要求 #[target_feature]
属性只能应用在 unsafe
函数上。
有关这些功能的进一步文档可以在其各自的规范中找到。可以在 RISC-V ISA手册或 RISC-V GitHub账户上的手册中参阅相关规范细节。
特性 | 隐式启用 | 描述 |
---|---|---|
a | A — 原子指令 | |
c | C — 压缩指令 | |
m | M — 整数乘除法指令 | |
zb | zba , zbc , zbs | Zb — 位操作指令 |
zba | Zba — 地址生成指令 | |
zbb | Zbb — 基本位操作 | |
zbc | Zbc — 无进位乘法指令 | |
zbkb | Zbkb — 加密算法下的位操作指令 | |
zbkc | Zbkc — 加密算法下的无进位乘法指令 | |
zbkx | Zbkx — 交叉排列 | |
zbs | Zbs — 单比特指令 | |
zk | zkn , zkr , zks , zkt , zbkb , zbkc , zkbx | Zk — 标量加密 |
zkn | zknd , zkne , zknh , zbkb , zbkc , zkbx | Zkn — NIST算法套件扩展 |
zknd | Zknd — NIST算法套件: AES解密 | |
zkne | Zkne — NIST算法套件: AES加密 | |
zknh | Zknh — NIST算法套件: 哈希函数指令 | |
zkr | Zkr — 熵源扩展 | |
zks | zksed , zksh , zbkb , zbkc , zkbx | Zks — ShangMi算法套件 |
zksed | Zksed — ShangMi算法套件: SM4分组密码指令 | |
zksh | Zksh — ShangMi算法套件: SM3哈希函数指令 | |
zkt | Zkt — Data Independent Execution Latency Subset |
wasm32
or wasm64
在这两个平台下,安全函数和非安全函数均可启用 #[target_feature]
特性。不可能经由 #[target_feature]
特性导致未定义行为,因为尝试使用 Wasm引擎不支持的指令将在加载时就失败,而不会有被以不同于编译器预期的方式来解释编译后的代码的风险。
特性 | 描述 |
---|---|
bulk-memory | WebAssembly 大容量内存操作提案 |
extended-const | WebAssembly 扩展常量表达式提案 |
mutable-globals | WebAssembly 可变全局变量提案 |
nontrapping-fptoint | WebAssembly 非捕获式浮点到整型转换提案 |
sign-ext | WebAssembly 有符号型扩展运算符提案 |
simd128 | WebAssembly simd 提案 |
Additional information
附加信息
请参阅 target_feature
-条件编译选项,了解如何基于编译时的设置来有选择地启用或禁用对某些代码的编译。注意,条件编译选项不受 target_feature
属性的影响,只是被整个 crate 启用的特性所驱动。
请参阅标准库中的 is_x86_feature_detected
或 is_aarch64_feature_detected
这两个宏,它们可以用来检测平台上的运行时特性。
注意:
rustc
为每个编译目标和 CPU 启用了一组默认特性。编译时,可以使用命令行参数-C target-cpu
选择目标 CPU。可以通过命令行参数-C target-feature
来为整个 crate 启用或禁用某些单独的特性。
The track_caller
attribute
track_caller
属性
track_caller
属性可以应用于除程序入口函数 fn main
之外的任何带有 "Rust"
ABI 的函数。当此属性应用于 trait声明中的函数或方法时,该属性将应用在其所有的实现上。如果 trait 本身提供了带有该属性的默认函数实现,那么该属性也应用于其覆盖实现(override implementations)。
当应用于外部(extern
)块中的函数上时,该属性也必须应用于此函数的任何链接实现(linked implementations)上,否则将导致未定义行为。当此属性应用在一个外部(extern
)块内可用的函数上时,该外部(extern
)块中的对该函数的声明也必须带上此属性,否则将导致未定义行为。
Behavior
表现
将此属性应用到函数 f
上将允许 f
内的代码获得 f
被调用时建立的调用栈的“最顶层”的调用的位置(Location
)信息的提示。从观察的角度来看,此属性的实现表现地就像从 f
所在的帧向上遍历调用栈,定位找到最近的有非此属性限定的调用函数 outer
,并返回 outer
调用时的位置(Location
)信息。
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } }
注意:
core
提供了core::panic::Location::caller
来观察调用者的位置。它封装(wrap)了由rustc
实现的内部函数(intrinsic)core::intrinsics::caller_location
。
注意:由于结果
Location
是一个提示,所以具体实现可能会提前终止对堆栈的遍历。请参阅限制以了解重要的注意事项。
Examples
示例
当 f
直接被 calls_f
调用时,f
中的代码观察其在 calls_f
内的调用位置:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } fn calls_f() { f(); // <-- f() 将打印此处的位置信息 } }
f
被另一个有此属性限定的函数 g
调用,g
又被 calls_g
' 调用,f
和 g
内的代码又同时观察 g
在 calls_g
内的调用位置:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } #[track_caller] fn g() { println!("{}", std::panic::Location::caller()); f(); } fn calls_g() { g(); // <-- g() 将两次打印此处的位置信息,一次是它自己,一次是此 f() 里来的 } }
当g
又被另一个有此属性限定的函数 h
调用,而g
又被 calls_h
' 调用,f
、g
和 h
内的代码又同时观察 h
在 calls_h
内的调用位置:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } #[track_caller] fn g() { println!("{}", std::panic::Location::caller()); f(); } #[track_caller] fn h() { println!("{}", std::panic::Location::caller()); g(); } fn calls_h() { h(); // <-- 将三次打印此处的位置信息,一次是它自己,一次是此 g() 里来,一次是从 f() 里来的 } }
以此类推。
Limitations
限制
track_caller
属性获取的信息是只是一个提示信息,实现不需要维护它。
特别是,将带有 #[track_caller]
的函数自动强转为函数指针会创建一个填充对象,该填充对象在观察者看来似乎是在此(属性限定的)函数的定义处调用的,从而在这层虚拟调用中丢失了实际的调用者信息。这种自动强转情况的一个常见示例是创建方法被此属性限定的 trait对象3 。
注意:前面提到的函数指针填充对象是必需的,因为
rustc
会通过向函数的 ABI 附加一个隐式参数来实现代码生成(codegen)上下文中的track_caller
,但这种添加是不健壮的(unsound),因为该隐式参数不是函数类型的一部分,那给定的函数指针类型可能引用也可能不引用具有此属性的函数。这里创建一个填充对象会对函数指针的调用方隐藏隐式参数,从而保持可靠性。
可字面理解为内部反复试探。
默认情况下,Rust 编译器会默认某些标准库函数在编译时可用,编译器也会把当前编译的代码往这些库函数可用的方向去优化。
因为 trait对象的值不能直接使用,只能自动强转为指针引用,那这里的调用就无法观察到真实的调用位置。
The instruction_set
attribute
instruction_set
属性
instruction_set
属性 可以应用于函数,用以控制将为哪个指令集生成函数。
这允许在单个程序中混合使用多个它支持的 CPU架构指令集。
它使用 MetaListPath语法,以及由体系结构系列名称和指令集名称组成的路径。
在不支持的 instruction_set
属性的目标架构上使用该属性会报编译错误。
On ARM
在 ARMv4T
和 ARMv5te
架构下, 支持使用:
arm::a32
- 生成 A32 "ARM" 指令风格的函数。arm::t32
- 生成 T32 "Thumb" 指令风格的函数
#[instruction_set(arm::a32)]
fn foo_arm_code() {}
#[instruction_set(arm::t32)]
fn bar_thumb_code() {}
使用 instruction_set
属性会带来以下副作用:
*如果将函数的地址作为函数指针,则地址的低位将根据指令集设置为 0(arm)或 1(thumb)。 *函数中的任何内联程汇编指令都必须使用指定的指令集,而不是目标默认值。
Limits
极值设置
limits.md
commit: 064e68d9f878d6e98e12776562fa9306ef851f10
本章译文最后维护日期:2021-10-17
以下属性影响部分编译期参数的极限值设置。
The recursion_limit
attribute
recursion_limit
属性
recursion_limit
属性可以应用于 crate 级别,为可能无限递归的编译期操作(如宏扩展或自动解引用)设置最大递归深度。它使用 MetaNameValueStr元项属性句法来指定递归深度。
注意:
rustc
中这个参数的默认值是128。
#![allow(unused)] #![recursion_limit = "4"] fn main() { macro_rules! a { () => { a!(1); }; (1) => { a!(2); }; (2) => { a!(3); }; (3) => { a!(4); }; (4) => { }; } // 这无法扩展,因为它需要大于4的递归深度。 a!{} }
#![allow(unused)] #![recursion_limit = "1"] fn main() { // 这里的失败是因为需要两个递归步骤来自动解引用 (|_: &u8| {})(&&&1); }
The type_length_limit
attribute
type_length_limit
属性
type_length_limit
属性限制在单态化过程中构造具体类型时所做的最大类型替换次数。它应用于 crate 级别,并使用 MetaNameValueStr元项属性句法来设置类型替换数量的上限。
注意:
rustc
中这个参数的默认值是 1048576。
#![type_length_limit = "4"]
fn f<T>(x: T) {}
// 这里的编译失败是因为单态化 `f::<((((i32,), i32), i32), i32)>` 需要大于4个类型元素。
f(((((1,), 2), 3), 4));
Type system attributes
类型系统属性
type_system.md
commit: 076a798583ecb450dbb27d46c2e1558228d0fcf1
本章译文最后维护日期:2024-05-02
以下属性用于改变类型的使用方式。
The non_exhaustive
attribute
non_exhaustive
属性
non_exhaustive
属性表示类型或变体将来可能会添加更多字段或变体。它可以应用在结构体(struct
)上、枚举(enum
)上 和 枚举变体上。
non_exhaustive
属性使用 MetaWord元项属性句法,因此不接受任何输入。
在当前(non_exhaustive
限制的类型的)定义所在的 crate 内,non_exhaustive
没有效果。
#![allow(unused)] fn main() { #[non_exhaustive] pub struct Config { pub window_width: u16, pub window_height: u16, } #[non_exhaustive] pub struct Token; #[non_exhaustive] pub struct Id(pub u64); #[non_exhaustive] pub enum Error { Message(String), // 译者注:此变体为元组变体 Other, } pub enum Message { #[non_exhaustive] Send { from: u32, to: u32, contents: String }, #[non_exhaustive] Reaction(u32), #[non_exhaustive] Quit, } // 非穷尽结构体可以在定义它的 crate 中正常构建。 let config = Config { window_width: 640, window_height: 480 }; let token = Token; let id = Id(4); // 非穷尽结构体可以在定义它的 crate 中进行详尽匹配 let Config { window_width, window_height } = config; let Token = token; let Id(id_number) = id; let error = Error::Other; let message = Message::Reaction(3); // 非穷尽枚举可以在定义它的 crate 中进行详尽匹配 match error { Error::Message(ref s) => { }, Error::Other => { }, } match message { // 非穷尽变体可以在定义它的 crate 中进行详尽匹配 Message::Send { from, to, contents } => { }, Message::Reaction(id) => { }, Message::Quit => { }, } }
在定义所在的 crate之外,标注为 non_exhaustive
的类型须在添加新字段或变体时保持向后兼容性。
非穷尽类型(non-exhaustive types)不能在定义它的 crate 之外构建:
- 非穷尽变体(结构体(
struct
)或枚举变体(enum
variant))不能用 StructExpression句法(包括函数式更新(functional update)句法)构建。 - 类单元结构体隐式定义的同名常量或 元组结构体里隐含的和元组结构体同名的构造函数的可见性不大于
pub(crate)
。 也就是说,如果结构的可见性是pub
,则这种常量或构造函数的可见性是pub(crate)
,否则两类程序项的可见性是相同的(就像没有#[non_exhaustive]
的情况一样)。 - 枚举(
enum
)实例能被构建。
当超出其定义的 crate 时,以下构造示例不能编译:
示例:(译者注:本例把上例看成本例的 upstream
)
// 这些类型(`Config`、`Error` `Message`)是在上游 crate 中定义的类型,这些类型已被标注为 `#[non_exhaustive]`。
use upstream::{Config, Error, Message};
// 不能构造 `Config` 的实例,如果在 `upstream` 的新版本中添加了新字段,则本地编译会失败,因此不允许这样做。
let config = Config { window_width: 640, window_height: 480 };
// 无法构造 `Token` 的实例;如果添加了新字段,那么它将不再是类单元结构体,因此由它作为类单元结构体创建的同名常量在此crate 外则不再是公共可见的;这段代码无法编译。
let token = Token;
// 无法构造 `Id` 的实例;如果添加了新字段,则其构造函数签名将发生变化,因此其构造函数在此crate 外不再是公共可见的;这段代码无法编译。
let id = Id(5);
// 可以构造 `Error` 的实例;引入的新变体不会导致编译失败。
let error = Error::Message("foo".to_string());
// 无法构造 `Message::Send` 或 `Message::Reaction` 的实例,
// 如果在 `upstream` 的新版本中添加了新字段,则本地编译失败,因此不允许。
let message = Message::Send { from: 0, to: 1, contents: "foo".to_string(), };
let message = Message::Reaction(0);
// 无法构造 `Message::Quit` 的实例,
// 如果 `upstream` 内的 `Message::Quit` 的因为添加字段变成元组变体(tuple-variant/tuple variant)后,则本地编译失败。
let message = Message::Quit;
在定义所在的 crate 之外对非穷尽类型进行匹配,有如下限制:
- 当模式匹配一个非穷尽变体(结构体(
struct
)或枚举变体(enum
variant))时,必须使用 StructPattern句法进行匹配,其匹配臂必须有一个为..
。元组变体的构造函数的可见性降低的不会比pub(crate)
大。 - 当模式匹配在一个非穷尽的枚举(
enum
)上时,增加对单个变体的匹配无助于匹配臂需满足枚举变体的穷尽性(exhaustiveness)的这一要求。
在定义的 crate 之外时,以下匹配示例不能被编译:
示例:(译者注:可以把上上例看成本例的 upstream
)
// 这些类型(`Config`、`Error` `Message`)是在上游 crate 中定义的类型,这些类型已被标注为 `#[non_exhaustive]`。
use upstream::{Config, Error, Message};
// 不包含通配符匹配臂,无法匹配非穷尽枚举。
match error {
Error::Message(ref s) => { },
Error::Other => { },
// 加上 `_ => {},` 就能编译通过
}
// 不包含通配符匹配臂,无法匹配非穷尽结构体
if let Ok(Config { window_width, window_height }) = config {
// 加上 `..` 就能编译通过
}
// 无法匹配非穷尽类单元结构体或元组结构体,除非使用带通配符的语法。
// 使用 `let Token { .. } = token;` 这样的表达方式则可以通过编译
let Token = token;
// 使用 `let Id { 0: id_number, .. } = id;` 这样的表达方式则可以通过编译
let Id(id_number) = id;
match message {
// 没有通配符,无法匹配非穷尽(结构体/枚举内的)变体
Message::Send { from, to, contents } => { },
// 无法匹配非穷尽元组或单元枚举变体(unit enum variant)。
Message::Reaction(type) => { },
Message::Quit => { },
}
也不允许对外部 crate 的非穷尽类型做强转(case)操作。
use othercrate::NonExhaustiveEnum;
// 不能对非本地crate里的非穷尽枚举类型做cast
let _ = NonExhaustiveEnum::default() as u8;
非穷尽类型最好放在下游 crate 里。
Debugger attributes
调试器属性
debugger.md
commit: 2d51a2aec405dd54a617f5ee1b27cef326f30ced
本章译文最后维护日期:2024-05-02
以下属性用于在使用 GDB 或 WinDbg 等第三方调试器时增强调试体验。
The debugger_visualizer
attribute
debugger_visualizer
属性
debugger_visualizer
属性可用于将可视化的调试器工具文件嵌入到调试信息中。
这样可以改善调试器在调试过程中显示值时的体验。
它使用MetaListNameValueStr句法来指定其输入,并且必须指定为 crate属性。
Using debugger_visualizer
with Natvis
将 debugger_visualizer
与 Natvis 一起使用
Natvis 是一个用于 Microsoft调试器(如 Visual Studio 和 WinDbg)的基于 XML 的框架,它使用声明性规则来自定义类型的显示。 有关 Natvis格式的详细信息,请参阅 Microsoft 的Natvis文档。
此属性仅支持在 -windows-msvc
目标平台上嵌入 Natvis文件。
Natvis文件的路径由 natvis_file
键指定,该键是相对于 crate源文件的路径:
#![debugger_visualizer(natvis_file = "Rectangle.natvis")]
struct FancyRect {
x: f32,
y: f32,
dx: f32,
dy: f32,
}
fn main() {
let fancy_rect = FancyRect { x: 10.0, y: 10.0, dx: 5.0, dy: 5.0 };
println!("set breakpoint here");
}
其中 Rectangle.natvis
包含:
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="foo::FancyRect">
<DisplayString>({x},{y}) + ({dx}, {dy})</DisplayString>
<Expand>
<Synthetic Name="LowerLeft">
<DisplayString>({x}, {y})</DisplayString>
</Synthetic>
<Synthetic Name="UpperLeft">
<DisplayString>({x}, {y + dy})</DisplayString>
</Synthetic>
<Synthetic Name="UpperRight">
<DisplayString>({x + dx}, {y + dy})</DisplayString>
</Synthetic>
<Synthetic Name="LowerRight">
<DisplayString>({x + dx}, {y})</DisplayString>
</Synthetic>
</Expand>
</Type>
</AutoVisualizer>
当在 WinDbg 下查看时, fancy_rect
变量将显示如下信息:
> Variables:
> fancy_rect: (10.0, 10.0) + (5.0, 5.0)
> LowerLeft: (10.0, 10.0)
> UpperLeft: (10.0, 15.0)
> UpperRight: (15.0, 15.0)
> LowerRight: (15.0, 10.0)
Using debugger_visualizer
with GDB
debugger_visualizer
与 GDB 一起使用
GDB 支持使用一个结构化的 Python脚本,称为 靓化打印输出(pretty printer),该脚本描述了如何在调试器视图中打印输出可视化类型。 有关靓化打印输出的详细信息,请参阅GDB的靓化打印输出文档。
在 GDB 下调试二进制文件时,嵌入式的靓化打印输出不会被自动加载。
有两种方法可以启用自动加载的嵌入的式靓化打印输出:
1.使用额外的参数启动 GDB,将文件夹或二进制文件显式地添加到自动加载安全路径:GDB-iex“add auto-load safe path path path/to/binary”path/to/binary
有关更多信息,请参阅GDB的[自动加载文档]。
1.在 $HOME/.config/gdb
下创建一个名为 gdbinit
的文件(如果该文件夹还不存在,则可能需要创建该文件夹)。在该文件中添加以下行:add-auto-load-safe-path path/to/binary
。
这些脚本是使用 gdb_script_file
键嵌入的,该键是相对于 crate源文件的路径。
#![debugger_visualizer(gdb_script_file = "printer.py")]
struct Person {
name: String,
age: i32,
}
fn main() {
let bob = Person { name: String::from("Bob"), age: 10 };
println!("set breakpoint here");
}
其中 printer.py
包含:
import gdb
class PersonPrinter:
"Print a Person"
def __init__(self, val):
self.val = val
self.name = val["name"]
self.age = int(val["age"])
def to_string(self):
return "{} is {} years old.".format(self.name, self.age)
def lookup(val):
lookup_tag = val.type.tag
if lookup_tag is None:
return None
if "foo::Person" == lookup_tag:
return PersonPrinter(val)
return None
gdb.current_objfile().pretty_printers.append(lookup)
当此 crate的可调试的二进制文件被传递到 GDB1 时,print bob
将显示:
"Bob" is 10 years old.
注意:这里假设你使用的是 rust-gdb
脚本,该脚本使用了 String
之类的标准库里的类型来配置靓化打印输出。
The collapse_debuginfo
attribute
collapse_debuginfo
属性
*collapse_debuginfo
attribute*控制在为调用此宏的代码生成调试信息时,宏定义中的代码位置是否折叠到与宏的调用位置相关联的单个位置。
该属性使用MetaListIdents句法格式来指定其输入,并且只能应用于宏定义。
该属性可接受的选项有:
#[collapse_debuginfo(yes)]
— 调试信息中的代码位置表示的是宏调用点的位置。#[collapse_debuginfo(no)]
— 调试信息中的代码位置表示的不是宏调用点的位置。#[collapse_debuginfo(external)]
— 仅当宏来至于不同的 crate 时,调试信息中的代码位置才表示的是宏调用点的位置。
没有此属性的宏的默认行为是 external
,除非它们是内置宏。内置宏的默认行为是 yes
。
注意:
rustc
有一个-C collapse-macro-debuginfo
命令行选项,用于屏蔽默认折叠行为和#[collapse_debuginfo]
属性。
#![allow(unused)] fn main() { #[collapse_debuginfo(yes)] macro_rules! example { () => { println!("hello!"); }; } }
Statements and expressions
语句和表达式
statements-and-expressions.md
commit: 45c47cd38654388910a8a92f5435570355ea2762
本章译文最后维护日期:2022-10-22
Rust 基本上是一种表达式语言。这意味着大多数形式的求值或生成表达效果的计算的都是由表达式的统一句法类别来指导的。每一种表达式通常都可以内嵌到另一种表达式中,表达式的求值规则包括指定表达式产生的值和指定其各个子表达式的求值顺序。
对比之下,Rust 中的语句则主要用于包含表达式求值,以及显式地安排表达式的求值顺序。
Statements
语句
statements.md
commit: 82517788e162791dad3305a8c8d8d20d49510ad6
本章译文最后维护日期:2024-06-15
句法
Statement :
;
| Item
| LetStatement
| ExpressionStatement
| MacroInvocationSemi
语句是块(block)1的一个组件,反过来,块又是其外层表达式或函数的组件。
Rust 有两种语句:声明语句(declaration statements)和表达式语句(expression statements)。
Declaration statements
声明语句
声明语句是在它自己封闭的语句块的内部引入一个或多个名称的语句。 声明的名称可以表示新变量或新的程序项。
这两种声明语句就是程序项声明语句和 let声明语句。
Item declarations
程序项声明语句
程序项声明语句的句法形式与模块中的程序项声明的句法形式相同。 在语句块中声明的程序项会将其作用域限制为包含该语句的块。 这类程序项以及在其内声明子项(sub-items)都没有给定的规范路径。 例外的是,只要程序项和 trait(如果有的话)的可见性允许,在(程序项声明语句内定义的和此程序项或 trait 关联的)实现中定义的关联项在外层作用域内仍然是可访问的。 除了这些区别外,它与在模块中声明的程序项的意义也是相同的。
程序项声明语句不会隐式捕获包含它的函数的泛型参数、参数和局部变量。如下,inner
不能访问 outer_var
。
outer_var
.
#![allow(unused)] fn main() { fn outer() { let outer_var = true; fn inner() { /* outer_var 的作用域不包括这里 */ } inner(); } }
let
statements
let
语句
句法
LetStatement :
OuterAttribute*let
PatternNoTopAlt (:
Type )? (=
Expression † (else
BlockExpression) ? ) ?;
† 当指定了
else
块时, Expression 不能是 LazyBooleanExpression,也不能以}
结尾。
let
语句通过一个不可反驳型模式引入了一组新的变量,变量由该模式给定。模式后面有一个可选的类型标注(annotation),再后面是一个可选的初始化表达式。当没有给出类型标注时,编译器将自行推断类型,如果没有足够的信息来执行有限次的类型推断,则将触发编译器报错。由变量声明引入的任何变量从声明点到封闭块作用域的结束都是可见的,除非它们被另一个变量声明遮蔽。
let
语句使用模式引入了一组新的变量。
模式后面有一个可选的类型标注(annotation),之后表达式要么直接结束,要么是一个初始化表达式附加一个可选的 else
块。
当没有给出类型标注时,编译器将自行推断类型,如果没有足够的信息来执行有限次的类型推断,则将触发报错。
由变量声明引入的任何变量从声明点到封闭块作用域的结束都是可见的,除非它们被另一个变量声明遮蔽。
如果没有 else
块存在,那此模式一定是不可反驳的。
如果有 else
块存在,那此模式可以是可反驳的。
如果模式没有被匹配上(这种情况需要模式是可反驳的),else
块会被执行。
else
块的返回必须是发散的(就是被求值为!)。
#![allow(unused)] fn main() { let (mut v, w) = (vec![1, 2, 3], 42); // 绑定可以是 mut 或 const let Some(t) = v.pop() else { // 可反驳模式需要一个 else块 panic!(); // else块的返回必须时发散的 }; let [u, v] = [v[0], v[1]] else { // 此模式时不可反驳的,所以这里的 else块会被编译器当作冗余给 lint 掉。 panic!(); }; }
Expression statements
表达式语句
句法
ExpressionStatement :
ExpressionWithoutBlock;
| ExpressionWithBlock;
?
表达式语句是对表达式求值并忽略其结果的语句。通常,表达式语句存在的目的是触发对其内部的表达式的求值时的效果。
仅由块表达式或控制流表达式组成的表达式,如果它们在允许使用语句的上下文中使用时,是可以省略其后面的分号的。这有可能会导致解析歧义,因为它可以被解析为独立语句,也可以被解析为另一个表达式的一部分;下例中的控制流表达式被解析为一个语句。注意 ExpressionWithBlock 形式的表达式用作语句时,其类型必须是单元类型(()
)。
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3]; v.pop(); // 忽略从 pop 返回的元素 if v.is_empty() { v.push(5); } else { v.remove(0); } // 分号可以省略。 [1]; // 单独的表达式语句,而不是索引表达式。 }
当省略后面的分号时,结果必须是 ()
类型。
#![allow(unused)] fn main() { // bad: 下面块的类型是i32,而不是 `()` // Error: 预期表达式语句的返回值是 `()` // if true { // 1 // } // good: 下面块的类型是i32,(加`;`后的语句的返回值就是 `()`了) if true { 1 } else { 2 }; }
Attributes on Statements
语句上的属性
语句可以有外部属性。在语句中有意义的属性是 cfg
和 lint检查类属性。
本书原文还有 block of code
的写法,这种有些类似于我们口语中说的那种任意的代码段的“代码块”。原文中 block of code
的典型情况是非安全(unsafe
)块。
Expressions
表达式
expressions.md
commit: 01c8196e0120f0577f6aa05ada9d962f0019a86c
本章译文最后维护日期:2024-05-26
句法
Expression :
ExpressionWithoutBlock
| ExpressionWithBlockExpressionWithoutBlock :
OuterAttribute*†
(
LiteralExpression
| PathExpression
| OperatorExpression
| GroupedExpression
| ArrayExpression
| AwaitExpression
| IndexExpression
| TupleExpression
| TupleIndexingExpression
| StructExpression
| CallExpression
| MethodCallExpression
| FieldExpression
| ClosureExpression
| AsyncBlockExpression
| ContinueExpression
| BreakExpression
| RangeExpression
| ReturnExpression
| UnderscoreExpression
| MacroInvocation
)ExpressionWithBlock :
OuterAttribute*†
(
BlockExpression
| ConstBlockExpression
| UnsafeBlockExpression
| LoopExpression
| IfExpression
| IfLetExpression
| MatchExpression
)
一个表达式可以扮演两个角色:它总是能产生一个值;它还有可能表达出效果(effects)(也被称为“副作用(side effects)”)。 表达式会被 求值/计算为(evaluates to) 为值,并在 求值(evaluation) 期间表达出效果。 很多表达式都会包含子表达式,此时子表达式也被称为此表达式的操作数。 每种表达式都表达了以下几点含义:
- 在对表达式求值时是否对操作数求值
- 对操作数求值的顺序
- 如何组合操作数的值来获取表达式的值
基于对这几种含义的实现要求,表达式通过其内在结构规定了其执行结构。 块只是另一种表达式,所以块、语句和表达式可以递归地彼此嵌套到任意深度。
注意:我们给表达式的操作数做了(如上)命名,以便于我们去讨论它们,但这些名称并没有最终稳定下来,以后可能还会更改。
Expression precedence
表达式的优先级
Rust 运算符和表达式的优先级顺序如下,从强到弱。 具有相同优先级的二元运算符按其结合(associativity)顺序做了分组。
运算符/表达式 | 结合性 |
---|---|
Paths(路径) | |
Method calls(方法调用) | |
Field expressions (字段表达式) | 从左向右 |
Function calls, array indexing(函数调用,数组索引) | |
? | |
Unary(一元运算符) - * ! & &mut | |
as | 从左向右 |
* / % | 从左向右 |
+ - | 从左向右 |
<< >> | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
| | 从左向右 |
== != < > <= >= | 需要圆括号 |
&& | 从左向右 |
|| | 从左向右 |
.. ..= | 需要圆括号 |
= += -= *= /= %= &= |= ^= <<= >>= | 从右向左 |
return break closures(返回、中断、闭包) |
Evaluation order of operands
操作数的求值顺序
下面的表达式列表都以相同的方式计算它们的操作数,具体列表后面也有详述。 其他表达式要么不接受操作数,要么按照各自约定(后续章节会有讲述)的条件进行求值。
- 解引用表达式(Dereference expression)
- 错误传播表达式(Error propagation expression)
- 取反表达式(Negation expression)
- 算术和二进制逻辑运算(Arithmetic and logical binary operators)
- 比较运算(Comparison operators)
- 类型转换表达式(Type cast expression)
- 分组表达式(Grouped expression)
- 数组表达式(Array expression)
- 等待表达式(Await expression)
- 索引表达式(Index expression)
- 元组表达式(Tuple expression)
- 元组索引表达式(Tuple index expression)
- 结构体表达式(Struct expression)
- 调用表达式(Call expression)
- 方法调用表达式(Method call expression)
- 字段表达式(Field expression)
- 中断表达式(Break expression)
- 区间表达式(Range expression)
- 返回表达式(Return expression)
在实现执行这些表达式的效果之前,会先对这些表达式的操作数进行求值。 拥有多个操作数的表达式会按照源代码书写的顺序从左到右计算。
注意:子表达式是一个表达式的操作数时,此子表达式内部的求值顺序是由根据前面的章节规定的优先级来确定的。
例如,下面两个 next
方法的调用总是以相同的顺序调用:
#![allow(unused)] fn main() { // 使用 vec 代替 array 来避免引用,因为在编写本例时,拥有内部元素的所有权的数组迭代器还没有稳定下来。 let mut one_two = vec![1, 2].into_iter(); assert_eq!( (1, 2), (one_two.next().unwrap(), one_two.next().unwrap()) ); }
注意:由于表达式是递归执行的,那这些表达式也会从最内层到最外层逐层求值,忽略兄弟表达式,直到没有(未求值的)内部子表达式为止。
Place Expressions and Value Expressions
位置表达式和值表达式
表达式分为两大类:位置表达式和值表达式。跟在每个表达式中一样,操作数可以出现在位置上下文,也可出现在值上下文中。 表达式的求值既依赖于它自己的类别,也依赖于它所在的上下文。 表达式分为两大类别:位置表达式和值表达式;还有第三个小类表达式,称为 assignee表达式。 在每类表达式中,操作数可以出现在位置上下文中,也可以出现在值上下文中。 表达式的求值既取决于其自身的类别,也取决于其处于的上下文。
位置表达式是表示内存位置的表达式。语言中表现为指向局部变量、静态变量、解引用(*expr
)、数组索引表达式(expr[expr]
)、字段引用(expr.f
)、圆括号括起来的位置表达式的路径。
所有其他形式的表达式都是值表达式。
值表达式是代表实际值的表达式。
下面的上下文是位置表达式上下文:
- 复合赋值表达式的左操作数。
- 一元运算符借用、address-of 或解引用的操作数。
- 字段表达式的操作数。
- 数组索引表达式的被索引操作数。
- 任何隐式借用的操作数。
- let语句的初始化器(initializer)。
if let
表达式、while let
表达式或匹配(match
)表达式的检验对象(scrutinee)。- 结构体表达式里的函数式更新(functional update)的基(base)。
注意:历史上,位置表达式被称为 左值/lvalues,值表达式被称为 右值/rvalues。
assignee表达式是出现在赋值表达式的左操作数中的表达式。确切地说,assignee表达式是:
- 位置表达式。
- 下划线.
- 出现在 assignee表达式里的元组。
- 出现在 assignee表达式里的切片。
- 出现在 assignee表达式里的元组结构体 。
- 出现在 assignee表达式(字段名可选)里的结构体 。
- 单元结构体.
assignee表达式允许使用任意层次的圆括号嵌套。
Moved and copied types
移动语义类型和复制语义类型
当位置表达式在值表达式上下文中被求值时,或在模式中被值绑定时,这表示此值会*保存进(held in)*当前表达式代表的内存地址。
如果该值的类型实现了 Copy
,那么该值将被从原来的位置复制一份过来。
如果该值的类型没有实现 Copy
,但实现了 Sized
,那么就有可能把该值从原来的位置移动(move)过来。
从位置表达式里移出值对位置表达式也有要求,只有如下的位置表达式才可能把值从其中移出(move out):
- 当前未被借用的变量。
- 临时变量里的值(Temporary values)。
- 表达式本身就可以被移出值,那它的一些字段也可被移出,但要求这些字段(的类型)没实现
Drop
。 - 对可移出且类型为
Box<T>
的表达式作解引用的结果。
当把值从一个位置表达式里移出到一个局部变量中后,此(表达式代表的)地址将被去初始化(deinitialized),并且该地址在重新初始化之前无法被再次读取。 除以上列出的情况外,任何在值表达式上下文中使用位置表达式都是错误的。
Mutability
可变性
如果一个位置表达式将会被赋值、可变借出、隐式可变借出或被绑定到包含 ref mut
模式上,则该位置表达式必须是可变的(mutable)。我们称这类位置表达式为可变位置表达式。
与之相对,其他位置表达式称为不可变位置表达式。
下面的表达式可以是可变位置表达式上下文:
- 当前未被借出的可变变量。
- 可变静态(
static
)项。 - 临时值。
- 字段:在可变位置表达式上下文中,会对子表达式求值。
- 对
*mut T
指针的解引用。 - 对类型为
&mut T
的变量或变量的字段的解引用。注意:这条是下一条规则的必要条件的例外情况。 - 对实现
DerefMut
的类型的解引用:这就要求在可变位置表达式上下文中来处理这个被解出的值。 - 对实现
IndexMut
的类型做索引,那对此检索出的表达式求值就需要在一个可变位置表达式上下文进行。注意对索引(index)本身的求值不用。
Temporaries
临时位置/临时变量
在大多数位置表达式上下文中使用值表达式时,会创建一个临时的未命名内存位置,并将该位置初始为该值,然后这个表达式的求值结果就为该内存位置。
此过程也有例外,就是把此临时表达式提升为一个静态项(static
)。(译者注:这种情况下表达式将直接在编译时就求值了,求值的结果会根据编译器要求重新选择地址存储)。
临时位置/临时变量的存续作用域(drop scope)通常在包含它的最内层语句的结尾处。
Implicit Borrows
隐式借用
某些特定的表达式可以通过隐式借用一个表达式来将其视为位置表达式。例如,可以直接比较两个非固定内存宽度的切片是否相等,因为 ==
操作符隐式借用了它自身的操作数:
#![allow(unused)] fn main() { let c = [1, 2, 3]; let d = vec![1, 2, 3]; let a: &[i32]; let b: &[i32]; a = &c; b = &d; // ... *a == *b; //译者注:&[i32] 解引用后是一个动态内存宽度类型,理论上两个动态内存宽度类型上无法比较大小的,但这里因为隐式借用此成为可能 // 等价于下面的形式: ::std::cmp::PartialEq::eq(&*a, &*b); }
隐式借用可能会被以下表达式采用:
- 方法调用表达式中的左操作数。
- 字段表达式中的左操作数。
- 调用表达式中的左操作数。
- 数组索引表达式中的左操作数。
- 解引用操作符(
*
)的操作数。 - 比较运算的操作数。
- 复合赋值(compound assignment)的左操作数。
Overloading Traits
重载trait
本书后续章节的许多操作符和表达式都可以通过使用 std::ops
或 std::cmp
中的 trait 被其他类型重载。
这些 trait 也存在于同名的 core::ops
和 core::cmp
中。
Expression Attributes
表达式属性
只有在少数特定情况下,才允许在表达式之前使用外部属性:
在下面情形之前是不允许的:
- 区间(Range)表达式。
- 二元运算符表达式(ArithmeticOrLogicalExpression、ComparisonExpression、LazyBooleanExpression、TypeCastExpression、AssignmentExpression、CompoundAssignmentExpression)。
Literal expressions
字面量表达式
literal-expr.md
commit: 659915cc1169e13329186748e26ec1e5c6a92d4d
本章译文最后维护日期:2024-04-06
词法
LiteralExpression :
[CHAR_LITERAL]
| [STRING_LITERAL]
| [RAW_STRING_LITERAL]
| [BYTE_LITERAL]
| [BYTE_STRING_LITERAL]
| [RAW_BYTE_STRING_LITERAL]
| [C_STRING_LITERAL]
| [RAW_C_STRING_LITERAL]
| [INTEGER_LITERAL]
| [FLOAT_LITERAL]
|true
|false
>
字面表达式是由单一 token(而不是一组 token)组成的表达式,它立即和直接表示其表达式求值的值,而不是通过名称或其他求值规则来引用它。 它直接描述一个数字、字符、字符串或布尔值。
字面量是[常量表达式][constant expression]的一种形式,所以其求值(主要)在编译期。
前面描述的每种词法[字面量][literal tokens]形式都可以组成一个字面量表达式,比如关键字 true
和 false
也可以组成一个字面量表达式。
#![allow(unused)] fn main() { "hello"; // 字符串类型 '5'; // 字符类型 5; // 整型 }
在下面的描述中,token 的 字符串表示 是源自输入的字符序列,该字符序列会与词法语法片段中 token 的产生式进行模式匹配。
注意:此字符串表示从不包括紧跟着
U+000A
(LF) 的字符U+000D
(CR):这一对字符会先被转换为单个字符U+000A
(LF)。
Escapes
转义
下面对文本字面表达式的描述使用了几种形式的 转义。
每种形式的转义的特点是:
- 转义序列: 总是以
U+005C
(\
) 开头的一段字符序列 - 转义值: 单个字符或空的字符序列
在以下关于转义的定义中:
- 八进制数字 是在 [
0
-7
] 区间内的任何字符。 - 十六进制数字 是在 [
0
-9
]、[a
-f
] 或 [A
-F
] 这三个区间内的任何字符。
Simple escapes
简单转义
下表第一列中出现的每个字符序列都是转义序列。
在每种情况下,转义值都是第二列中相应条目中给定的字符。
转义序列 | 转义值 |
---|---|
\0 | U+0000 (NUL) |
\t | U+0009 (HT) |
\n | U+000A (LF) |
\r | U+000D (CR) |
\" | U+0022 (双引号) |
\' | U+0027 (单引号) |
\\ | U+005C (反斜线) |
8-bit escapes
8bit字符值转义
这种转义序列由 \x
后跟两个十六进制数字组成。
转义值是一个字符,其 [Unicode标量值][Unicode scalar value]是将转义序列中的最后两个字符解释为十六进制整数的结果,就好像对这两个字符执行了以16为基数的[u8::from_str_radix
]操作。
注意: 作为结果的转义值因此具有在 [
u8
][numeric types] 数值区间内的 [Unicode标量值][Unicode scalar value]。
7-bit escapes
7bit字符值转义
转义序列由 \x
后跟一个八进制数字和一个十六进制数字组成。
转义值是一个字符,其[Unicode标量值][Unicode scalar value]是将转义序列中的最后两个字符解释为十六进制整数的结果,就好像对这两个字符执行了以16为基数的[u8::from_str_radix
]操作。
Unicode escapes
Unicode字符值转义
转义序列由 \u{
后跟一系列字符组成,每个字符都是十六进制数字或 _
,在后跟一个 }
。
转义值是一个字符,其[Unicode标量值][Unicode scalar value]是将转义序列中包含的十六进制数字解释为十六进制整数的结果,就好像对这一段字符执行了以16为基数的[u32::from_str_radix
]操作。
注意: [CHAR_LITERAL] token 或 [STRING_LITERA] token 的词法定义形式确保存在这样的字符。
String continuation escapes
字符串接续符转义
转义序列由 \
后面紧跟着 U+000A
(LF),以及在下一个非空白符之前的所有后面的空白符组成。
为此,空白符被限定为 U+0009
(HT)、U+000A
(LF)、U+000D
(CR) 和 U+0020
(空格符)。
转义值是一个空的字符序列。
注意: 这种转义形式的效果是字符串延续跳过后面的空白符(包括额外的换行符)。 因此下面
a
、b
和c
是相等的:#![allow(unused)] fn main() { let a = "foobar"; let b = "foo\ bar"; let c = "foo\ bar"; assert_eq!(a, b); assert_eq!(b, c); }
跳过额外的换行符(如上面示例中的变量c 所示)可能会造成潜在混乱和意外。 这种行为将来可能会进行调整。 在做出决定之前,建议避免使用带有多个换行符接续符。 请参阅 this issue,以了解更多信息。
Character literal expressions
字符字面量表达式
字符字面量表达式由单一一个[字符字面量][CHAR_LITERAL]token 组成。
此表达式的类型是 rust的源语类型 [char
][textual types]。
此类token 不能带有后缀。
此类token 的 字面内容 是在此token的字符串表示中第一个 U+0027
('
) 之后,最后一个 U+0027
('
) 之前的字符序列。
此类字面量表达式的 所表示字符 源自字面内容,具体有如下规则:
- 如果字面内容是以下转义序列形式之一,则所表示的字符是转义序列的转义值:
- [简单转义][Simple escapes]
- [7bit转义][7-bit escapes]
- [Unicode转义][Unicode escapes]
- 否则,所表示的字符是构成字面内容的单个字符。
表达式的值是与所表示的字符的[Unicode标量值][Unicode scalar value]相对应的 [char
][text types]类型。
注意: [CHAR_LITERAL] token 的词法定义形式确保会产生单一字符。
字符字面量表达式示例:
#![allow(unused)] fn main() { 'R'; // R '\''; // ' '\x52'; // R '\u{00E6}'; // 拉丁文小写字母æ (U+00E6) }
String literal expressions
字符串字面量表达式
字符串字面量表达式由单一一个[字符串字面量][STRING_LITERAL]token 或[原生字符串字面量][RAW_STRING_LITERAL]token 组成。
此类表达式的类型是对原语类型 [str
][text types] 的共享引用(带有 static
生存期)。
也就是说,类型是 &'static str
。
此类token 不能带有后缀。
此类token 的 字面内容 是此token 的字符串表示中第一个 U+0022
("
) 之后和最后一个 U+0022
("
) 之前的字符序列。
此字面量表达式的 所表示字符串 源自于字符序列的字面内容,具体有如下规则:
-
如果 token 是[STRING_LTERAL]词法所限定的,则字面内容中出现的以下任何形式的转义序列都将被转义序列的转义值替换。
- [简单转义][Simple escapes]
- [7bit转义][7-bit escapes]
- [Unicode转义][Unicode escapes]
- [字符串接续符转义][String continuation escapes]
这些转义替换按从左到右的顺序进行的。 例如,token
"\\x41"
被转换为字符\
x
4
1
。 -
如果 token 是[RAW_STRING_LITERAL]词法所限定的,则所表示的字符串等同于字面内容。
表达式的值是对静态分配的 [str
][text types]的引用,该引用包含所表示字符串的 UTF-8编码形式。
字符串字面量表达式示例:
#![allow(unused)] fn main() { "foo"; r"foo"; // foo "\"foo\""; r#""foo""#; // "foo" "foo #\"# bar"; r##"foo #"# bar"##; // foo #"# bar "\x52"; "R"; r"R"; // R "\\x52"; r"\x52"; // \x52 }
Byte literal expressions
字节字面量表达式
字节字面量表达式由单一一个[字节字面量][BYTE_LITERAL]token 组成。
此表达式的类型是 rust的源语类型 [u8
][numeric types]。
此类token 不能带有后缀。
此类token 的 字面内容 是此token 的字符串表示中第一个 U+0027
('
) 之后和最后一个 U+0027
('
) 之前的字符序列。
此字面量表达式的 所表示字符 源自于字符的字面内容,具体有如下规则:
-
如果字面内容是以下转义序列形式之一,则表示的字符是转义序列的转义值:
- [简单转义][Simple escapes]
- [8bit转义][8-bit escapes]
-
否则,所表示的字符是构成字面内容的单个字符。
此类表达式的值是所表示的字符的[Unicode标量值][Unicode scalar value]。
注意: [BYTE_LTERAL] token 的词法定义形式确保这些规则始终能生成一个 Unicode标量值在 [
u8
][numeric types] 数值区间内的单个字符。
字节字面量表达式的示例:
#![allow(unused)] fn main() { b'R'; // 82 b'\''; // 39 b'\x52'; // 82 b'\xA0'; // 160 }
Byte string literal expressions
字节串字面量表达式
字节串字面量表达式由单一一个[字节串字面量][BYTE_STRING_LITERAL]token 或[原生字节串字面量][RAW_BYTE_STRING_LITERAL]token 组成。
此类表达式的类型是对元素类型为 [u8
][numeric-types]的数组的共享引用(带有 static
生存期)。
也就是说,类型是 &'static [u8; N]
,其中 N
是下文描述的所表示的字符串的字节数。
此类token 不能带有后缀。
此类token 的 字面内容 是此token 的字符串表示中第一个 U+0022
("
) 之后和最后一个 U+0022
("
) 之前的字符序列。
此字面量表达式的 所表示字符串 源自于字符序列的字面内容,具体有如下规则:
-
如果 token 是[BYTE_STRING_LITERAL]词法所限定的,则字面内容中出现的以下任何形式的每个转义序列都将替换为转义序列的转义值。
- [简单转义][Simple escapes]
- [8bit转义][8-bit escapes]
- [字符串接续符转义][String continuation escapes]
这些转义替换按从左到右的顺序进行的。 例如,token
b"\\x41"
被转换为字符\
x
4
1
。 -
如果 token 是[RAW_BYTE_STRING_LITERAL]词法所限定的,则所表示的字符串等同于字面内容。
表达式的值是对静态分配的数组的引用,该数组包含所表示字符串中字符的[Unicode标量值][Unicode scalar values],顺序相同。
注意: [BYTE_STRING_LITERAL] 和 [RAW_BYTE_STRING_LITERAL] 的词法定义形式确保这些规则生成数组元素值始终在 [
u8
][numeric types] 数值区间内。
字节串字面量表达式的示例:
#![allow(unused)] fn main() { b"foo"; br"foo"; // foo b"\"foo\""; br#""foo""#; // "foo" b"foo #\"# bar"; br##"foo #"# bar"##; // foo #"# bar b"\x52"; b"R"; br"R"; // R b"\\x52"; br"\x52"; // \x52 }
C string literal expressions
C语言风格的字符串字面量表达式
C语言风格的字符串字面量表达式由单一一个[C语言风格的字符串字面量][C_STRING_LITERAL] 或 [原生C语言风格的字符串字面量][RAW_C_STRING_LITERAL] token 组成。
此类表达式的类型是对标准库[CStr]类型的共享引用(带有 static
生存期)。
也就是说,类型是 &'static core::ffi::CStr
。
此类token 不能带有后缀。
此类token 的 字面内容 是此token 的字符串表示中第一个 U+0022
("
) 之后和最后一个 U+0022
("
) 之前的字符序列。
此类字面量表达式的 所表示字节序 源自于字节序列的字面内容,具体有如下规则:
-
如果 token 是[C_STRING_LITERAL]词法所限定的,则字面内容被视为一系列内容项(下面有罗列),每个内容项要么是除
\
之外的单个 Unicode字符,要么是[转义][escape]。 内容项序列会被转换为字节序列,具体规则如下所示:- 每个 Unicode字符都有 UTF-8表示形式。
- 每个[简单转义][simple escape]都会被转义为其转义值的[Unicode标量值][Unicode scalar value]。
- 每个[8bit转义][8-bit escape]都会被转义为一个包含其转义值的[Unicode标量值][Unicode scalar value]的单个字节。
- 每个[Unicode转义][unicode escape]都会被转义为其转义值的 UTF-8表示形式。
- 每个[字符串接续符转义][string continuation escape]都不会被转义为任何字节。
-
如果 token [RAW_C_STRING_LITERAL] 词法所限定的,则表示的字节是字面内容的 UTF-8编码。
注意: [C_STRING_LITERAL] 和 [RAW_C_STRING_LITERAL]的词法定义形式确保所表示的字节序不会包含 null字节。
The expression's value is a reference to a statically allocated [CStr] whose array of bytes contains the represented bytes followed by a null byte. 该表达式的值是对静态分配的 [CStr] 的引用,其字节数组就包含了这个后跟一个 null字节的所表示字节序。
C语言风格的字符串字面量表达式示例:
#![allow(unused)] fn main() { c"foo"; cr"foo"; // foo c"\"foo\""; cr#""foo""#; // "foo" c"foo #\"# bar"; cr##"foo #"# bar"##; // foo #"# bar c"\x52"; c"R"; cr"R"; // R c"\\x52"; cr"\x52"; // \x52 c"æ"; // 拉丁文小写字母æ (U+00E6) c"\u{00E6}"; // 拉丁文小写字母æ (U+00E6) c"\xC3\xA6"; // 拉丁文小写字母æ (U+00E6) c"\xE6".to_bytes(); // [230] c"\u{00E6}".to_bytes(); // [195, 166] }
Integer literal expressions
整型字面量表达式
整型字面量表达式由单一一个[整型字面量][INTEGER_LITERAL]token 组成。
如果这种 token 带有[后缀][suffix],那这个后缀的名字必须是[原生整型][numeric types]中的一个:u8
, i8
, u16
, i16
, u32
, i32
, u64
, i64
, u128
, i128
, usize
或 isize
,同时此后缀名也是此表达式的类型。
如果此 token 没有后缀,此表达式的类型将由下面的类型推断规则来确定:
-
如果整型可以从周围的程序上下文中唯一地确定,则表达式具有该类型。
-
如果程序上下文对类型的约束比较宽松,则默认为有符号32位整数
i32
。 -
如果程序上下文对类型的约束过度,则被认为是一个静态类型错误。
整型字面量表达式的示例:
#![allow(unused)] fn main() { 123; // type i32 123i32; // type i32 123u32; // type u32 123_u32; // type u32 let a: u64 = 123; // type u64 0xff; // type i32 0xff_u8; // type u8 0o70; // type i32 0o70_i16; // type i16 0b1111_1111_1001_0000; // type i32 0b1111_1111_1001_0000i64; // type i64 0usize; // type usize }
而表达式的值由 token 的字符串表示形式来确定,规则如下:
-
通过检查字符串的前两个字符来选择整型的进制数,如下所示:
0b
表示进制数为20o
表示进制数为80x
表示进制数为16- 其他表示进制数为10。
-
如果进制数不是10,则从字符串中删除前两个字符。
-
字符串的后缀都会被删除。
-
字符串里的下划线都会被删除。
-
字符串被转换为
u128
类型的值时,比如使用 [u128::from_str_radix
] 时选的进制数标记。 如果该值不能被表示为u128
,将在编译时报错。 -
u128
类型的值通过[数值转换][numeric cast]转换为表达式的类型。
注意: 如果字面量的值不适合表达式的类型,则会对最终强制转换的结果做截断操作。
rustc
包含一个名为overflowing_literals
的 [lint检查][lint check], 默认生效级别为deny
, 这意味着这种情况发生是此表达式会被编译器拒绝。
注意:
-1i8
这样的表达式是对1i8
这个字面量表达式的[取负操作][negation operator],不是一个单一的整型字面量表达式。 请参阅[Overflow],了解有关表示有符号类型的最大负值的说明。
Floating-point literal expressions
浮点型字面量表达式
浮点型字面量表达式由下面两种形式:[浮点型字面量][FLOAT_LITERAL]token 组成。
- 由单一的[浮点型字面量][FLOAT_LITERAL]token 组成
- 由单一的没有后缀和基数指示符的[整型字面量][INTEGER_LITERAL]token 组成
如果这种 token 带有[后缀][suffix],那这个后缀的名字必须是[原生浮点型][floating-point types]中的一个:f32
或 f64
,同时此后缀名也是此表达式的类型。
如果这种 token 没有后缀,此表达式的类型将由下面的类型推断规则来确定:
-
如果浮点型可以从周围的程序上下文中唯一地确定,则表达式具有该类型。
-
如果程序上下文对类型的约束比较宽松,则默认为
f64
。 -
如果程序上下文对类型的约束过度,则被认为是一个静态类型错误。
浮点型字面量表达式的示例:
#![allow(unused)] fn main() { 123.0f64; // type f64 0.1f64; // type f64 0.1f32; // type f32 12E+99_f64; // type f64 5f32; // type f32 let x: f64 = 2.; // type f64 }
而表达式的值由 token 的字符串表示形式来确定,规则如下:
-
字符串的后缀都会被删除。
-
字符串中的下划线都会被删除。
-
字符串被转换为这类表达式类型时,如同直接调用 [
f32::from_str
] 或 [f64::from_str
]。
注意:
-1.0
这样的表达式是对1.0
这个字面量表达式的[取负操作][negation operator],不是一个单一的浮点型字面量表达式。
注意:
inf
和NaN
不是字面量token。 [f32::INFINITY
], [f64::INFINITY
], [f32::NAN
] 和 [f64::NAN
] 这些常量可被用来替代字面量表达式。
在
rustc
里,一个足够大的字面量被计算为无穷时将会触发一个叫做overflowing_literals
的 lint检查。
Boolean literal expressions
布尔型字面量表达式
布尔型字面量表达式由关键字 true
或 false
中的一个组成。
此类表达式的类型是原生[布尔型][boolean type],并且它的值为:
- 如果关键字是
true
,那么值为 true - 如果关键字是
false
,那么值为 false [Escape]: #escapes [Simple escape]: #simple-escapes [Simple escapes]: #simple-escapes [8-bit escape]: #8-bit-escapes [8-bit escapes]: #8-bit-escapes [7-bit escape]: #7-bit-escapes [7-bit escapes]: #7-bit-escapes [Unicode escape]: #unicode-escapes [Unicode escapes]: #unicode-escapes [String continuation escape]: #string-continuation-escapes [String continuation escapes]: #string-continuation-escapes [boolean type]: ../types/boolean.md [constant expression]: ../const_eval.md#constant-expressions [CStr]: https://doc.rust-lang.org/core/ffi/struct.CStr.html [floating-point types]: ../types/numeric.md#floating-point-types [lint check]: ../attributes/diagnostics.md#lint-check-attributes [literal tokens]: ../tokens.md#literals [numeric cast]: operator-expr.md#numeric-cast [numeric types]: ../types/numeric.md [suffix]: ../tokens.md#suffixes [negation operator]: operator-expr.md#negation-operators [overflow]: operator-expr.md#overflow [textual types]: ../types/textual.md [Unicode scalar value]: http://www.unicode.org/glossary/#unicode_scalar_value [Unicode scalar values]: http://www.unicode.org/glossary/#unicode_scalar_value [f32::from_str
]: https://doc.rust-lang.org/core/primitive.f32.html#method.from_str [f32::INFINITY
]: https://doc.rust-lang.org/core/primitive.f32.md#associatedconstant.INFINITY [f32::NAN
]: https://doc.rust-lang.org/core/primitive.f32.md#associatedconstant.NAN [f64::from_str
]: https://doc.rust-lang.org/core/primitive.f64.md#method.from_str [f64::INFINITY
]: https://doc.rust-lang.org/core/primitive.f64.md#associatedconstant.INFINITY [f64::NAN
]: https://doc.rust-lang.org/core/primitive.f64.md#associatedconstant.NAN [u8::from_str_radix
]: https://doc.rust-lang.org/core/primitive.u8.md#method.from_str_radix [u32::from_str_radix
]: https://doc.rust-lang.org/core/primitive.u32.md#method.from_str_radix [u128::from_str_radix
]: https://doc.rust-lang.org/core/primitive.u128.md#method.from_str_radix [CHAR_LITERAL]: ../tokens.md#character-literals [STRING_LITERAL]: ../tokens.md#string-literals [RAW_STRING_LITERAL]: ../tokens.md#raw-string-literals [BYTE_LITERAL]: ../tokens.md#byte-literals [BYTE_STRING_LITERAL]: ../tokens.md#byte-string-literals [RAW_BYTE_STRING_LITERAL]: ../tokens.md#raw-byte-string-literals [C_STRING_LITERAL]: ../tokens.md#c-string-literals [RAW_C_STRING_LITERAL]: ../tokens.md#raw-c-string-literals [INTEGER_LITERAL]: ../tokens.md#integer-literals [FLOAT_LITERAL]: ../tokens.md#floating-point-literals
Path expressions
路径表达式
path-expr.md
commit: d33e4b03f0f810a315915412448a1f73c30e0feb
本章译文最后维护日期:2024-05-26
句法
PathExpression :
PathInExpression
| QualifiedPathInExpression
路径被用做表达式上下文时表示局部变量或程序项。
解析为局部变量或静态变量的路径表达式是位置表达式,其他路径是值表达式。
使用 static mut
变量需在 unsafe
块中。
#![allow(unused)] fn main() { mod globals { pub static STATIC_VAR: i32 = 5; pub static mut STATIC_MUT_VAR: i32 = 7; } let local_var = 3; local_var; globals::STATIC_VAR; unsafe { globals::STATIC_MUT_VAR }; let some_constructor = Some::<i32>; let push_integer = Vec::<i32>::push; let slice_reverse = <[i32]>::reverse; }
关联常量的计算处理方式与[const
块][const
blocks]相同。
Block expressions
块表达式
block-expr.md
commit: 82517788e162791dad3305a8c8d8d20d49510ad6
本章译文最后维护日期:2024-06-15
句法
BlockExpression :
{
InnerAttribute*
Statements?
}
Statements :
Statement+
| Statement+ ExpressionWithoutBlock
| ExpressionWithoutBlock
块表达式或块是一个控制流表达式(control flow expression),同时也是程序项声明和变量声明的匿名空间作用域。
作为控制流表达式,块按顺序执行其非程序项声明的语句组件,最后执行可选的最终表达式(final expression)。
作为一个匿名空间作用域,在本块内声明的程序项只在块本身围成的作用域内有效,而块内由 let
语句声明的变量的作用域为下一条语句到块尾。
更多细节请参见作用域章节。
块的句法规则为:先是一个 {
,后跟内部属性,再后是任意条语句,再后是一个被称为最终操作数(final operand)的可选表达式,最后是一个 }
。
语句之间通常需要后跟分号,但有两个例外: 1、程序项声明语句不需要后跟分号。 2、表达式语句通常需要后面的分号,但它的外层表达式是控制流表达式时不需要。
此外,允许在语句之间使用额外的分号,但是这些分号并不影响语义。
在对块表达式进行求值时,除了程序项声明语句外,每个语句都是按顺序执行的。 如果给出了块尾的可选的最终操作数(final operand),则最后会执行它。
块的类型是最此块的最终操作数(final operand)的类型,但如果省略了最终操作数,则块的类型为 ()
。
#![allow(unused)] fn main() { fn fn_call() {} let _: () = { fn_call(); }; let five: i32 = { fn_call(); 5 }; assert_eq!(5, five); }
注意:作为控制流表达式,如果块表达式是一个表达式语句的外层表达式,则该块表达式的预期类型为
()
,除非该块后面紧跟着一个分号。
块总是值表达式,并会在值表达式上下文中对最后的那个操作数进行求值。
注意:如果确实有需要,块可以用于强制移动值。 例如,下面的示例在调用
consume_self
时失败,因为结构体已经在之前的块表达式里被从s
里移出了。#![allow(unused)] fn main() { struct Struct; impl Struct { fn consume_self(self) {} fn borrow_self(&self) {} } fn move_by_block_expression() { let s = Struct; // 将值从块表达式里的 `s` 里移出。 (&{ s }).borrow_self(); // 执行失败,因为 `s` 里的值已经被移出。 s.consume_self(); } }
async
blocks
async
块
句法
AsyncBlockExpression :
async
move
? BlockExpression
*异步块(async block)*是求值为 future 的块表达式的一个变体。 块的最终表达式(如果存在)决定了 future 的结果值。(译者注:单词 future 对应中文为“未来”。原文可能为了双关的行文效果,经常把作为类型的 future 和字面意义上的 future 经常混用,所以译者基本保留此单词不翻译,特别强调“未来”的意义时也会加上其英文单词。)
执行一个异步块类似于执行一个闭包表达式:它的即时效果是生成并返回一个匿名类型。
类似闭包返回的类型实现了一个或多个 std::ops::Fn
trait,异步块返回的类型实现了 std::future::Future
trait。
此类型的实际数据格式规范还未确定下来。
注意: rustc 生成的 future类型大致相当于一个枚举,rustc 为这个 future 的每个
await
点生成一个此枚举的变体,其中每个变体都存储了对应点再次恢复执行时需要的数据。
版次差异: 异步块从 Rust 2018 版才开始可用。
Capture modes
捕获方式
异步块使用与闭包相同的捕获方式从其环境中捕获变量。
跟闭包一样,当编写 async { .. }
时,每个变量的捕获方式将从该块里的内容中推断出来。
而 async move { .. }
类型的异步块将把所有需要捕获的变量使用移动语义移入(move)到相应的结果 future 中。
Async context
异步上下文
因为异步块构造了一个 future,所以它们定义了一个async上下文,这个上下文可以相应地包含 await
表达式。
异步上下文是由异步块和异步函数的函数体建立的,它们的语义是依照异步块定义的。
Control-flow operators
控制流操作符
异步块的作用类似于函数的边界符来界定函数,或者更类似于闭包。
因此 ?
操作符和 返回(return
)表达式也都能影响 future 的输出,且都不会影响封闭它的函数或其他上下文。
也就是说,return <expr>
在异步块中跟其在 future 的输出是一样的,都是将 <expr>
的计算结果返回。
类似地,如果 <expr>?
传播(propagate)一个错误,这个错误也会被 future 在未来的某个时候作为返回结果被传播出去。
最后,关键字 break
和 continue
不能用于从异步块中跳出分支。
因此,以下内容是非法的:
#![allow(unused)] fn main() { loop { async move { break; // 错误[E0267]: `break` inside of an `async` block } } }
const
blocks
Const块
句法
ConstBlockExpression :
const
BlockExpression
Const块是块表达式的变体,其代码主体在编译时而不是在运行时求值。
Const块允许你直接一个定义常量值,而不必定义新的常量项,因此它们有时也称为内连常量。 Const块还支持类型推理,因此不需要指定类型,这点与常量项不同。
与自由常量项不同,Const块能够引用作用域中的泛型参数。 它们会被脱糖为作用域中带有泛型参数的常量项(类似于关联常量,但没有与它们关联的 trait 或类型)。 例如,下面这些代码:
#![allow(unused)] fn main() { fn foo<T>() -> usize { const { std::mem::size_of::<T>() + 1 } } }
等价于:
#![allow(unused)] fn main() { fn foo<T>() -> usize { { struct Const<T>(T); impl<T> Const<T> { const CONST: usize = std::mem::size_of::<T>() + 1; } Const::<T>::CONST } } }
如果在运行时执行 Const块表达式,则会确保其常量已经被求值,即使其返回值会被忽略: If the const block expression is executed at runtime, then the constant is guaranteed to be evaluated, even if its return value is ignored:
#![allow(unused)] fn main() { fn foo<T>() -> usize { // 如果这段代码被执行,那么断言的值肯定是在编译时就被求值了。 const { assert!(std::mem::size_of::<T>() > 0); } // Here we can have unsafe code relying on the type being non-zero-sized. // 这里,我们就可以放置一些依赖于非零尺寸类型的 unsafe代码。 /* ... */ 42 } }
如果在运行时不会执行 Const块表达式,则编译时可以计算它,也可以不计算它:
#![allow(unused)] fn main() { if false { // 在构建程序时,此panic 有可能触发,也有可能不触发。 const { panic!(); } } }
unsafe
blocks
非安全(unsafe
)块
句法 UnsafeBlockExpression :
unsafe
BlockExpression
参见 unsafe
块 以察看更多相关信息
可以在代码块前面加上关键字 unsafe
以允许非安全操作。
例如:
#![allow(unused)] fn main() { unsafe { let b = [13u8, 17u8]; let a = &b[0] as *const u8; assert_eq!(*a, 13); assert_eq!(*a.offset(1), 17); } unsafe fn an_unsafe_fn() -> i32 { 10 } let a = unsafe { an_unsafe_fn() }; }
Labelled block expressions
带标签的块表达式
带标签的块表达式的相关文档记录在循环和其他可中断表达式的相关章节中。
Attributes on block expressions
在以下上下文中,允许在块表达式的左括号之后直接使用内部属性:
- 函数和方法的代码体。
- 循环体(
loop
,while
,while let
, 和for
)。 - 被用作语句的块表达式。
- 块表达式作为数组表达式、元组表达式、调用表达式、元组结构体表达式和[枚举变体][enum variant]表达式的元素。
- 作为另一个块表达式的尾部表达式(tail expression)的块表达式。
在块表达式上有意义的属性有 cfg
和 lint检查类属性。
例如,下面这个函数在 unix 平台上返回 true
,在其他平台上返回 false
。
#![allow(unused)] fn main() { fn is_unix_platform() -> bool { #[cfg(unix)] { true } #[cfg(not(unix))] { false } } }
操作符/运算符表达式
Operator expressions
operator-expr.md
commit: 97c9ad16e565f2d47593a1ac56c806b2892e4223
本章译文最后维护日期:2024-03-09
句法
OperatorExpression :
BorrowExpression
| DereferenceExpression
| ErrorPropagationExpression
| NegationExpression
| ArithmeticOrLogicalExpression
| ComparisonExpression
| LazyBooleanExpression
| TypeCastExpression
| AssignmentExpression
| CompoundAssignmentExpression
操作符是 Rust 语言为其内建类型定义的。
本文后面的许多操作符都可以使用 std::ops
或 std::cmp
中的 trait 进行重载。
溢出
Overflow
在 debug模式下编译整数运算时,如果发生溢出,会触发 panic。
可以使用命令行参数 -C debug-assertions
和 -C overflow-checks
设置编译器标志位来更直接地控制这个溢出过程。
以下情况被认为是溢出:
- 当
+
、*
或-
(二元运算符的减号) 创建的值大于当前类型可存储的最大值或小于最小值。 - 将
-
(一元运算符的负号) 应用于任何有符号整型可存储的极负值(如i8的极负值为-128),除非此符号的操作数是一个字面量表达式(或独立存在于一个或多个分组表达式中的字面量表达式)。译者注:-128_i8 和 -(128) 不溢出,let j: i8 = -(-128_i8)
溢出。 - 使用
/
或%
,其中左操作数是某类有符号整型的最小整数,右操作数是-1
。注意,即使由于历史原因禁用了-C overflow-checks
选项,这些检查仍会发生。 - 使用
<<
或>>
,其中右操作数大于或等于左操作数类型的二进制位数,或右操作数为负数。
注意: 上面第二点中除非的情况是像
-128_i8
或let j: i8 = -(128)
这样的形式永远不会引起 panic,并且预期值也正常为 -128。因为此时,像128_i8
这样,虽然其内部值已经是 -128 了(因为整型字面量会根据整型的类型标记被截断以适用其类型,见整型字面量中的描述),即便再给其应用了一元运算符-
但仍被例外处理为一个极负值。二进制补码溢出约定:对那些极负值再取负会使值保持不变。
在
rustc
里,这些对极负值取负的表达式也会忽略overflowing_literals
这样的 lint检查。
借用操作符/运算符
Borrow operators
句法
BorrowExpression :
(&
|&&
) Expression
| (&
|&&
)mut
Expression
&
(共享借用)和 &mut
(可变借用)运算符是一元前缀运算符。
当应用于位置表达式上时,此表达式生成指向值所在的内存位置的引用(指针)。
在引用存续期间,该内存位置也被置于借出状态。
对于共享借用(&
),这意味着该位置可能不会发生变化,但可能会被再次读取或共享。
对于可变借用(&mut
),在借用到期之前,不能以任何方式访问该位置。&mut
在可变位置表达式上下文中会对其操作数求值。
如果 &
或 &mut
运算符应用于值表达式上,则会创建一个临时值。
这类操作符不能重载。
#![allow(unused)] fn main() { { // 将创建一个存值为7的临时位置,该该临时位置在此作用域内持续存在 let shared_reference = &7; } let mut array = [-2, 3, 9]; { // 在当前作用域内可变借用了 `array`。那 `array` 就只能通过 `mutable_reference` 来使用。 let mutable_reference = &mut array; } }
尽管 &&
是一个单一 token(惰性与(and
)操作符),但在借用表达式(borrow expressions)上下文中使用时,它是作为两个借用操作符用的:
#![allow(unused)] fn main() { // 意义相同: let a = && 10; let a = & & 10; // 意义相同: let a = &&&& mut 10; let a = && && mut 10; let a = & & & & mut 10; }
裸地址操作符
Raw address-of operators
与借用操作符相关的是操作符的裸地址操作符,这些操作符不是一级语法,但可以通过宏 ptr::addr_of!(expr)
和 ptr::addr_of_mut!(expr)
暴露出来。
此表达式expr
是在位置上下文中求值的。
ptr::addr_of!(expr)
会在给定的位置上创建一个 *const T
类型的常量裸指针,而 ptr::addr_of_mut!(expr)
会创建 *mut T
类型的可变裸指针。
每当位置表达式的计算结果没有正确对齐或没能存储对其类型来说是有效的值时,或者每当创建引用引入了不正确的别名假设时,必须使用裸地址操作符,不能借用操作符。 在这些情况下,使用借用操作符将通过创建无效引用而导致未定义的行为,但仍可以使用裸地址操作符来构造出裸指针。
下面示例通过 packed
结构创建了指向没有地址对齐的裸指针:
#![allow(unused)] fn main() { use std::ptr; #[repr(packed)] struct Packed { f1: u8, f2: u16, } let packed = Packed { f1: 1, f2: 2 }; // `&packed.f2` 会创建一个未对齐的引用,这种引用是一种未定义行为! let raw_f2 = ptr::addr_of!(packed.f2); assert_eq!(unsafe { raw_f2.read_unaligned() }, 2); }
以下是创建不包含有效值的指向某个位置的裸指针的示例:
#![allow(unused)] fn main() { use std::{ptr, mem::MaybeUninit}; struct Demo { field: bool, } let mut uninit = MaybeUninit::<Demo>::uninit(); // `&uninit.as_mut().field` 会创建一个对未初始化为 `bool` 的引用,因此这也是一个未定义行为! let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) }; unsafe { f1_ptr.write(true); } let init = unsafe { uninit.assume_init() }; }
解引用操作符
The dereference operator
句法
DereferenceExpression :
*
Expression
*
(解引用)操作符也是一元前缀操作符。当应用于指针上时,它表示该指针指向的内存位置。
如果表达式的类型为 &mut T
或 *mut T
,并且该表达式是局部变量、局部变量的(内嵌)字段、或是可变的位置表达式,则它代表的内存位置可以被赋值。解引用原始指针需要在非安全(unsafe
)块才能进行。
在不可变位置表达式上下文中对非指针类型作 *x
相当于执行 *std::ops::Deref::deref(&x)
;同样的,在可变位置表达式上下文中这个动作就相当于执行 *std::ops::DerefMut::deref_mut(&mut x)
。
#![allow(unused)] fn main() { let x = &7; assert_eq!(*x, 7); let y = &mut 9; *y = 11; assert_eq!(*y, 11); }
问号操作符
The question mark operator
句法
ErrorPropagationExpression :
Expression?
问号操作符(?
)解包(unwrap)有效值或返回错误值,并将它们传播(propagate)给调用函数。
问号操作符(?
)是一个一元后缀操作符,只能应用于类型 Result<T, E>
和 Option<T>
。
当应用在 Result<T, E>
类型的值上时,它可以传播错误。
如果值是 Err(e)
,那么它实际上将从此操作符所在的函数体或闭包中返回 Err(From::from(e))
。
如果应用到 Ok(x)
,那么它将解包此值以求得 x
。
#![allow(unused)] fn main() { use std::num::ParseIntError; fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // 立即返回一个 Err() Ok(x + y) // 不会执行到这里 } let res = try_to_parse(); println!("{:?}", res); assert!(res.is_err()) }
当应用到 Option<T>
类型的值时,它向调用者传播错误 None
。
如果它应用的值是 None
,那么它将返回 None
。
如果应用的值是 Some(x)
,那么它将解包此值以求得 x
。
#![allow(unused)] fn main() { fn try_option_some() -> Option<u8> { let val = Some(1)?; Some(val) } assert_eq!(try_option_some(), Some(1)); fn try_option_none() -> Option<u8> { let val = None?; Some(val) } assert_eq!(try_option_none(), None); }
操作符 ?
不能被重载。
取反运算符
Negation operators
语法
NegationExpression :
-
Expression
|!
Expression
这是最后两个一元运算符。 下表总结了它们用在基本类型上的表现,同时指出其他类型要重载这些操作符需要实现的 trait。 记住,有符号整数总是用二进制补码形式表示。 所有这些运算符的操作数都在值表达式上下文中被求值,所以这些操作数的值会被移走或复制。
符号 | 整数 | bool | 浮点数 | 用于重载的 trait |
---|---|---|---|---|
- | 符号取反* | 符号取反 | std::ops::Neg | |
! | 按位取反 | 逻辑非 | std::ops::Not |
* 仅适用于有符号整数类型。
下面是这些运算符的一些示例:
#![allow(unused)] fn main() { let x = 6; assert_eq!(-x, -6); assert_eq!(!x, -7); assert_eq!(true, !false); }
算术和逻辑二元运算符
Arithmetic and Logical Binary Operators
句法
ArithmeticOrLogicalExpression :
Expression+
Expression
| Expression-
Expression
| Expression*
Expression
| Expression/
Expression
| Expression%
Expression
| Expression&
Expression
| Expression|
Expression
| Expression^
Expression
| Expression<<
Expression
| Expression>>
Expression
二元运算符表达式都用中缀表示法(infix notation)书写。 下表总结了算术和逻辑二元运算符在原生类型(primitive type)上的行为,同时指出其他类型要重载这些操作符需要实现的 trait。 记住,有符号整数总是用二进制补码形式表示。 所有这些运算符的操作数都在值表达式上下文中求值,因此这些操作数的值会被移走或复制。
符号 | 整数 | bool | 浮点数 | 用于重载此运算符的 trait | 用于重载此运算符的复合赋值(Compound Assignment) Trait |
---|---|---|---|---|---|
+ | 加法 | 加法 | std::ops::Add | std::ops::AddAssign | |
- | 减法 | 减法 | std::ops::Sub | std::ops::SubAssign | |
* | 乘法 | 乘法 | std::ops::Mul | std::ops::MulAssign | |
/ | 除法*† | 取余 | std::ops::Div | std::ops::DivAssign | |
% | 取余**† | Remainder | std::ops::Rem | std::ops::RemAssign | |
& | 按位与 | 逻辑与 | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位或 | 逻辑或 | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位异或 | 逻辑异或 | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移位 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移位*** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法趋零取整。
** Rust使用由截断除法定义的求宇运算。也就是 余数=被除数%除数
,其中余数将与被除数的符号一致。
*** 有符号整数类型算术右移位,无符号整数类型逻辑右移位。
† 对整型来说,除0会导致panic。
下面是使用这些操作符的示例:
#![allow(unused)] fn main() { assert_eq!(3 + 6, 9); assert_eq!(5.5 - 1.25, 4.25); assert_eq!(-5 * 14, -70); assert_eq!(14 / 3, 4); assert_eq!(100 % 7, 2); assert_eq!(0b1010 & 0b1100, 0b1000); assert_eq!(0b1010 | 0b1100, 0b1110); assert_eq!(0b1010 ^ 0b1100, 0b110); assert_eq!(13 << 3, 104); assert_eq!(-10 >> 2, -3); }
比较运算符
Comparison Operators
句法
ComparisonExpression :
Expression==
Expression
| Expression!=
Expression
| Expression>
Expression
| Expression<
Expression
| Expression>=
Expression
| Expression<=
Expression
Rust 还为原生类型以及标准库中的多种类型都定义了比较运算符。
链式比较运算时需要借助圆括号,例如,表达式 a == b == c
是无效的,(但如果逻辑允许)可以写成 (a == b) == c
。
与算术运算符和逻辑运算符不同,重载这些运算符的 trait 通常用于显示/约定如何比较一个类型,并且还很可能会假定使用这些 trait 作为约束条件的函数定义了实际的比较逻辑。 其实标准库中的许多函数和宏都使用了这个假定(尽管不能确保这些假定的安全性)。 与上面的算术和逻辑运算符不同,这些运算符会隐式地对它们的操作数执行共享借用,并在位置表达式上下文中对它们进行求值:
#![allow(unused)] fn main() { let a = 1; let b = 1; a == b; // 等价于: ::std::cmp::PartialEq::eq(&a, &b); }
这意味着不需要将值从操作数移出(moved out of)。
符号 | 含义 | 须重载方法 |
---|---|---|
== | 等于 | std::cmp::PartialEq::eq |
!= | 不等于 | std::cmp::PartialEq::ne |
> | 大于 | std::cmp::PartialOrd::gt |
< | 小于 | std::cmp::PartialOrd::lt |
>= | 大于或等于 | std::cmp::PartialOrd::ge |
<= | 小于或等于 | std::cmp::PartialOrd::le |
下面是使用比较运算符的示例:
#![allow(unused)] fn main() { assert!(123 == 123); assert!(23 != -12); assert!(12.5 > 12.2); assert!([1, 2, 3] < [1, 3, 4]); assert!('A' <= 'B'); assert!("World" >= "Hello"); }
短路布尔运算符
Lazy boolean operators
句法
LazyBooleanExpression :
Expression||
Expression
| Expression&&
Expression
运算符 ||
和 &&
可以应用在布尔类型的操作数上。
运算符 ||
表示逻辑“或”,运算符 &&
表示逻辑“与”。
它们与 |
和 &
的不同之处在于,只有在左操作数尚未确定表达式的结果时,才计算右操作数。
也就是说,||
只在左操作数的计算结果为 false
时才计算其右操作数,而只有在计算结果为 true
时才计算 &&
的操作数。
#![allow(unused)] fn main() { let x = false || true; // true let y = false && panic!(); // false, 不会计算 `panic!()` }
类型转换表达式
Type cast expressions
句法
TypeCastExpression :
Expressionas
TypeNoBounds
类型转换表达式用二元运算符 as
表示。
执行类型转换(as
)表达式将左侧的值显式转换为右侧的类型。
类型转换(as
)表达式的一个例子:
#![allow(unused)] fn main() { fn sum(values: &[f64]) -> f64 { 0.0 } fn len(values: &[f64]) -> i32 { 0 } fn average(values: &[f64]) -> f64 { let sum: f64 = sum(values); let size: f64 = len(values) as f64; sum / size } }
as
可用于显式执行自动强转(coercions),以及下列形式的强制转换。
任何不符合强转规则或不在下表中的转换都会导致编译器报错。
下表中 *T
代表 *const T
或 *mut T
。m
引用类型中代表可选的 mut
或指针类型中的 mut
或 const
。
e 的类型 | U | 通过 e as U 执行转换 |
---|---|---|
整型或浮点型 | 整型或浮点型 | 数字转换 |
枚举 | 整型 | 枚举转换 |
bool 或 char | 整型 | 原生类型到整型的转换 |
u8 | char | u8 到 char 的转换 |
*T | *V where V: Sized * | 指针到指针的转换 |
*T where T: Sized | 数字型(Numeric type) | 指针到地址的转换 |
整型 | *V where V: Sized | 地址到指针的转换 |
&m₁ T | *m₂ T ** | 引用到指针的转换 |
&m₁ [T; n] | *m₂ T ** | 数组到指针的转换 |
函数项 | 函数指针 | 函数到函数指针的转换 |
函数项 | *V where V: Sized | 函数到指针的转换 |
函数项 | 整型 | 函数到地址的转换 |
函数指针 | *V where V: Sized | 函数指针到指针的转换 |
函数指针 | 整型 | 函数指针到地址的转换 |
闭包 *** | 函数指针 | 闭包到函数指针的转换 |
* 或者 T
和V
也可以都是兼容的 unsized 类型,例如,两个都是切片,或者都是同一种 trait对象。
** 仅当 m₁
是 mut
或 m₂
是 const
时, 可变(mut
)引用到 const
指针才会被允许。
*** 仅适用于不捕获(遮蔽(close over))任何环境变量的闭包。
语义
Semantics
数字转换
Numeric cast
- 在两个内存宽度(size)相同的整型数值(例如 i32 -> u32)之间进行转换是一个空操作(no-op) (Rust 使用2的补码表示定长整数的负值)
- 从一个较大内存宽度的整型转换为较小内存宽度的整型(例如 u32 -> u8)将会采用截断(truncate)算法 1
- 从较小内存宽度的整型转换为较大内存宽度的整型(例如 u8 -> u32)将
- 如果源数据是无符号的,则进行零扩展(zero-extend)
- 如果源数据是有符号的,则进行符号扩展(sign-extend)
- 从浮点数转换为整型将使浮点数趋零取整(round the float towards zero)
NaN
将返回0
- 大于转换到的整型类型的最大值时(包括
INFINITY
),取该整型类型的最大值。 - 小于转换到的整型类型的最小值时(包括
INFINITY
),取该整型类型的最小值。
- 从整数强制转换为浮点数将产生最接近的浮点数 *
- 如有必要,舍入采用
roundTiesToEven
模式 *** - 在溢出时,将会产生该浮点型的常量 Infinity(∞)(与输入符号相同)
- 注意:对于当前的数值类型集,溢出只会发生在
u128 as f32
这种转换形式,且数字大于或等于f32::MAX + (0.5 ULP)
时。
- 如有必要,舍入采用
- 从 f32 到 f64 的转换是无损转换
- 从 f64 到 f32 的转换将产生最接近的 f32 **
- 如有必要,舍入采用
roundTiesToEven
模式 *** - 在溢出时,将会产生 f32 的常量 Infinity(∞)(与输入符号相同) * 如果硬件本身不支持这种舍入模式和溢出行为,那么这些整数到浮点型的转换可能会比预期的要慢。
- 如有必要,舍入采用
** 如果硬件本身不支持这种舍入模式和溢出行为,那么这些 f64 到 f32 的转换可能会比预期的要慢。
*** 按照 IEEE 754-2008§4.3.1 的定义:选择最接近的浮点数,如果恰好在两个浮点数中间,则优先选择最低有效位为偶数的那个。
Enum cast
枚举转换
可将枚举类型转换为其判别值,在必要时,后继可以再继续使用数字类型转换。 枚举转换仅限于以下枚举类型:
Primitive to integer cast
原生类型到整型
false
转换为0
,true
转换为1
。char
转换为字符码点,然后在必要时可以使用数字类型转换。
u8
to char
cast
u8
到 char
把为 char
的字符代码点的 u8
值转换为 char
Pointer to address cast
指针到地址
把原始指针转换为整数将产生此指针指向的内存的机器地址。
如果被转换出的整数类型内存宽度小于指针类型的位宽,地址可能会被截断;可以使用类型 usize
来避免这种情况。
Address to pointer cast
地址到指针
从整数转换为原始指针会将整数解释为内存地址,并生成一个引用该内存地址的指针。
警告: 这将与 Rust内存模型交互,需要指出的是该模型仍在开发中。 即便是按位转换后是一个有效的指针,但这种从转换中生成的指针可能会受到一些额外的限制。如果不遵循别名规则,那么解引用这样的指针可能是未定义行为。
一个常见的健壮的地址转换算法的示例:
#![allow(unused)] fn main() { let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); let first_address = p1 as usize; let second_address = first_address + 4; // 4 == size_of::<i32>() let p2 = second_address as *mut i32; unsafe { *p2 += 1; } assert_eq!(values[1], 3); }
Pointer-to-pointer cast
指针到指针的强制转换
*const T
/ *mut T
可以通过以下行为方式强制转换为 *const U
/ *mut U
:
-
如果
T
和U
的尺寸都相同,则返回的指针保持不变。 -
如果
T
和U
都是非固定尺寸(unsized)的,则返回的指针也保持不变。 这两种情况下需要特别指出的是这些指针的元数据也会被准确地保存了下来。例如,从
*const [T]
到*const [U]
的强制转换仍保留了元素的数量。 因此请注意,这种强制转换不一定保留指针所指向对象的尺寸(例如,将*const [u16]
强制转换为*const [u8]
将转换出一个只指向了原始内存宽度一半的裸指针)。 这同样适用于str
和任何尾部为非固定尺寸(unsized)的切片类型的复合类型,如struct Foo(i32, [u8])
或(u64, Foo)
。 -
如果
T
非固定尺寸(unsized)的,而U
是固定尺寸的,则强制转换将丢弃胖指针T
的所有元数据,并生成由该指针的数据部分组成的瘦指针U
。
赋值表达式
Assignment expressions
句法
AssignmentExpression :
Expression=
Expression
赋值表达式会把某个值移入到一个特定的位置。
赋值表达式由一个可变的接收者表达式(assignee expression)(就是被赋值的操作数)后跟一个等号(=
)和一个值表达式(也就是所要赋的值的操作数)组成。
在赋值表达式的基本形式中,接收者表达式是一个位置表达式,这个我们之前已讨论过了。
下文将讨论更一般的解构赋值情况,这种情况的基本规则是等号右边的整个表达式被分解,然后按顺序为左侧的位置表达式对应赋值。
Basic assignments
基本赋值形式
执行赋值表达式要首先对它的操作数先行求值。 这其中,等号右边的值操作数要最先求值,然后是接收者表达式。 对于解构赋值,接受者表达式的各个子表达式会按从左到右的顺序进行求值。
注意:此表达式与其他表达式的求值顺序不同,此表达式的右操作数在左操作数之前被求值。
对赋值表达的位置表达式求值时会先销毁(drop)此位置(如果是未初始化的局部变量或未初始化的局部变量的字段则不会启动这步析构操作),然后将赋值值复制(copy)或移动(move)到此位置中。
赋值表达式总是会生成单元类型值。
示例:
#![allow(unused)] fn main() { let mut x = 0; let y = 0; x = y; }
Destructuring assignments
解构赋值形式
解构赋值可以和变量的解构声明形式(应用了解构模式匹配的变量声明形式)相比较,它允许对拥有复杂结构的变量(如元组或结构)进行赋值。 例如,我们可以交换两个可变变量:
#![allow(unused)] fn main() { let (mut a, mut b) = (0, 1); // 使用解构赋值来交换 `a` 和 `b`。 (b, a) = (a, b); }
与使用 let
来做解构声明不同,由于存在句法二义性,模式可能不会被允许出现在解构赋值的左侧。
如果要这样做,你替代的方法就是让模式相关的这批表达式组成接收者表达式(assignee expression)放在赋值表达式的左侧使用。
然后再将接收者表达式脱糖为具体的单个模式匹配,然后依次赋值。
被脱糖的模式必须是不可反驳的模式:特别是,这意味着只有长度在编译时已知的切片模式,或者全切片模式 [..]
,才被允许进行解构赋值。
对接收者表达式进行脱糖方法简单明了,下面通过示例进行说明。
#![allow(unused)] fn main() { struct Struct { x: u32, y: u32 } let (mut a, mut b) = (0, 0); (a, b) = (3, 4); [a, b] = [3, 4]; Struct { x: a, y: b } = Struct { x: 3, y: 4}; // 被脱糖为: { let (_a, _b) = (3, 4); a = _a; b = _b; } { let [_a, _b] = [3, 4]; a = _a; b = _b; } { let Struct { x: _a, y: _b } = Struct { x: 3, y: 4}; a = _a; b = _b; } }
允许在单个表达式中多次使用标识符。
下划线表达式和空的范围表达式可以用来忽略特定值,以便达到不绑定它们的目的。
注意模式里的默认绑定方式不会对这种脱糖表达式起效。
复合赋值表达式
Compound assignment expressions
句法
CompoundAssignmentExpression :
Expression+=
Expression
| Expression-=
Expression
| Expression*=
Expression
| Expression/=
Expression
| Expression%=
Expression
| Expression&=
Expression
| Expression|=
Expression
| Expression^=
Expression
| Expression<<=
Expression
| Expression>>=
Expression
复合赋值表达式将算术符(以及二进制逻辑操作符)与赋值表达式相结合在一起使用。
比如:
#![allow(unused)] fn main() { let mut x = 5; x += 1; assert!(x == 6); }
复合赋值的句法是可变 位置表达式(被赋值操作数),然后是一个操作符再后跟一个 =
(这两个符号共同作为一个单独的 token),最后是一个值表达式(也叫被复合修改操作数(modifying operand))。
与其他位置操作数不同,被赋值的位置操作数必须是一个位置表达式。 试图使用值表达式将导致编译器报错,而不是将其提升转换为临时位置。
复合赋值表达式的求值取决于操作符的类型。
如果复合赋值表达式了两个操作数的类型都是原生类型,则首先对被复合修改操作数进行求值,然后再对被赋值操作数求值。 最后将被赋值操作数的位置值设置为原被赋值操作数的值和复合修改操作数执行运算后的值。
注意:此表达式与其他表达式的求值顺序不同,此表达式的右操作数在左操作数之前被求值。
此外,这个表达式是调用操作符重载复合赋值trait 的函数的语法糖(见本章前面的表格)。 被赋值操作数必须是可变的。
例如,下面 example
函数中的两个表达式语句是等价的:
#![allow(unused)] fn main() { struct Addable; use std::ops::AddAssign; impl AddAssign<Addable> for Addable { /* */ fn add_assign(&mut self, other: Addable) {} } fn example() { let (mut a1, a2) = (Addable, Addable); a1 += a2; let (mut a1, a2) = (Addable, Addable); AddAssign::add_assign(&mut a1, a2); } }
与赋值表达式一样,复合赋值表达式也总是会生成单元类型值。
警告:复合赋值表达式的操作数的求值顺序取决于操作数的类型:对于原生类型,右边操作数将首先被求值,而对于非原生类型,左边操作数将首先被求值。 建议尽量不要编写依赖于复合赋值表达式中操作数的求值顺序的代码。请参阅这里的测试以获得使用此依赖项的示例。
截断,即一个值范围较大的变量A转换为值范围较小的变量B,如果超出范围,则将A减去B的区间长度。例如,128超出了i8类型的范围(-128,127),截断之后的值等于128-256=-128。
Grouped expressions
圆括号表达式(分组表达式)
grouped-expr.md
commit: 7c5b80d96da3ba16848d6aba6a99aa06efe0bc31
本章译文最后维护日期:2022-10-22
句法
GroupedExpression :
(
Expression)
由圆括号封闭的表达式的求值结果就是在其内的表达式的求值结果。 在表达式内部,圆括号可用于显式地指定表达式内部的求值顺序。
*圆括号表达式(parenthesized expression)包装单个表达式,并对该表达式求值。 圆括号表达式的句法规则就是一对圆括号封闭一个被称为封闭操作数(enclosed operand)*的表达式。
圆括号表达式被求值为其封闭操作数的值。 与其他表达式不同,圆括号表达式可以是位置表达式或值表达式。 当封闭操作数是位置表达式时,它是一个位置表达式;当封闭操作数是一个值表达式是,它是一个值表达式。
圆括号可用于显式修改表达式中的子表达式的优先顺序。
圆括号表达式的一个例子:
#![allow(unused)] fn main() { let x: i32 = 2 + 3 * 4; // 没有圆括号 let y: i32 = (2 + 3) * 4; // 有圆括号 assert_eq!(x, 14); assert_eq!(y, 20); }
当调用结构体的函数指针类型的成员时,必须使用括号,示例如下:
#![allow(unused)] fn main() { struct A { f: fn() -> &'static str } impl A { fn f(&self) -> &'static str { "The method f" } } let a = A{f: || "The field f"}; assert_eq!( a.f (), "The method f"); assert_eq!((a.f)(), "The field f"); }
Array and array index expressions
数组和数组索引表达式
array-expr.md
commit: 15049771545fb6cebec0753fe72d761ede2d0e57
本章译文最后维护日期:2022-08-20
Array expressions
数组表达式
句法
ArrayExpression :
[
ArrayElements?]
ArrayElements :
Expression (,
Expression )*,
?
| Expression;
Expression
数组表达式用来构建数组。 数组表达式有两种形式。
第一种形式是在数组中列举出所有的元素值。 这种形式的句法规则通过在方括号中放置统一类型的、逗号分隔的表达式来表现。 这样编写将生成一个包含这些表达式的值的数组,其中数组元素的顺序就是这些表达式写入的时顺序。
第二种形式的句法规则通过在方括号内放置两用个用分号(;
)分隔的表达式来表现。
分号(;
)前的表达式被称为重复值操作数(repeat operand)
分号(;
)后的表达式被称为数组长度操作数(length operand)
其中,数组长度操作数必须是 usize
类型的,并且必须是常量表达式,比如是字面量或常量项。
此形式的数组表达式创建一个长度为长度操作数值的数组,每个元素都是重复值操作数的副本。
也就是说,[a; b]
这种形式会创建包含 b
个 a
值的数组。
如果数组长度操作数的值大于 1,则要求 a
的类型实现了 Copy
,或 a
自己是一个常量项的路径。
当 [a; b]
形式的重复值操作数 a
是一个常量项时,其将被计算求值数组长度操作数 b
次。
如果数组长度操作数 b
为 0,则常量项根本不会被求值。
对于非常量项的表达式,只计算求值一次,然后将结果复制数组长度操作数 b
次。
#![allow(unused)] fn main() { [1, 2, 3, 4]; ["a", "b", "c", "d"]; [0; 128]; // 内含128个0的数组 [0u8, 0u8, 0u8, 0u8,]; [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 二维数组 const EMPTY: Vec<i32> = Vec::new(); [EMPTY; 2]; }
Array and slice indexing expressions
数组和切片索引表达式
句法
IndexExpression :
Expression[
Expression]
数组和切片类型的值(slice-typed values)可以通过后跟一个由方括号封闭一个类型为 usize
的表达式(索引)的方式来对此数组或切片进行索引检索。
如果数组是可变的,则其检索出的内存位置还可以被赋值。
对于数组和切片类型之外的索引表达式 a[b]
其实相当于执行 *std::ops::Index::index(&a, b)
,或者在可变位置表达式上下文中相当于执行 *std::ops::IndexMut::index_mut(&mut a, b)
。
与普通方法一样,Rust 也将在 a
上反复插入解引用操作,直到查找到对上述方法的实现。
数组和切片的索引是从零开始的。数组访问是一个常量表达式,因此数组索引的越界检查可以在编译时通过检查常量索引值本身进行。 否则,越界检查将在运行时执行,如果此时越界检查未通过,那将把当前线程置于 panicked 状态。
#![allow(unused)] fn main() { // 默认情况下,`unconditional_panic` lint检查会执行 deny 级别的设置, // 即 crate 在默认情况下会有外部属性设置 `#[deny(unconditional_panic)]` // 而像 `(["a", "b"])[n]` 这样的简单动态索引检索会被该 lint 检查出来,而提前报错,导致程序被拒绝编译。 // 因此这里调低 `unconditional_panic` 的 lint 级别以通过编译。 #![warn(unconditional_panic)] ([1, 2, 3, 4])[2]; // 3 let b = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; b[1][2]; // 多维数组索引 let x = (["a", "b"])[10]; // 告警:索引越界 let n = 10; // 译者注:上行可以在 `#![warn(unconditional_panic)]` 被注释的情况下换成 // let n = if true {10} else {0}; // 试试,那下行就不会被 unconditional_panic lint 检查到了 let y = (["a", "b"])[n]; // panic let arr = ["a", "b"]; arr[10]; // 告警:索引越界 }
数组和切片以外的类型可以通过实现 Index trait 和 IndexMut trait 来达成数组索引表达式的效果。
Tuple and tuple indexing expressions
元组和元组索引表达式
tuple-expr.md
commit: 37ca438c9ac58448ecf304b735e71644e8127f3d
本章译文最后维护日期:2021-07-17
Tuple expressions
元组表达式
句法
TupleExpression :
(
TupleElements?)
TupleElements :
( Expression,
)+ Expression?
元组表达式用来构建元组值。
元组表达式的句法规则为:一对圆括号封闭的以逗号分隔的表达式列表,这些表达式被称为元组初始化操作数(tuple initializer operands)。 为了避免和圆括号表达式混淆,一元元组表达式的元组初始化操作数后的逗号不能省略。
元组表达式是一个值表达式,它会被求值计算成一个元组类型的新值。
元组初始化操作数的数量构成元组的元数(arity)。
没有元组初始化操作数的元组表达式生成单元元组(unit tuple)。
对于其他元组表达式,第一个被写入的元组初始化操作数初始化第 0 个元素,随后的操作数依次初始化下一个开始的元素。
例如,在元组表达式 ('a', 'b', 'c')
中,'a'
初始化第 0 个元素的值,'b'
初始化第 1 个元素,'c'
初始化第2个元素。
元组表达式和相应类型的示例:
表达式 | 类型 |
---|---|
() | () (unit) |
(0.0, 4.5) | (f64, f64) |
("x".to_string(), ) | (String, ) |
("a", 4usize, true) | (&'static str, usize, bool) |
Tuple indexing expressions
元组索引表达式
句法
TupleIndexingExpression :
Expression.
TUPLE_INDEX
元组索引表达式被用来存取元组或[元组结构体][tuple structs]的字段。
元组索引表达式的句法规则为:一个被称为*元组操作数(tuple operand)*的表达式后跟一个 .
,最后再后跟一个元组索引。
元组索引的句法规则要求该索引必须写成一个不能有前导零、下划线和后缀的十进制字面量的形式。
例如 0
和 2
是合法的元祖索引,但 01
、0_
、0i32
这些不行。
元组操作数的类型必须是元组类型或[元组结构体][tuple structs]。 元组索引必须是元组操作数类型的字段的名称。(译者注:这句感觉原文表达有问题,这里也给出原文 The tuple index must be a name of a field of the type of the tuple operand.)
对元组索引表达式的求值计算除了能求取其元组操作数的对应位置的值之外没有其他作用。 作为位置表达式,元组索引表达式的求值结果是元组操作数字段的位置,该字段与元组索引同名。
元组索引表达式示例:
#![allow(unused)] fn main() { // 索引检索一个元组 let pair = ("a string", 2); assert_eq!(pair.1, 2); // 索引检索一个元组结构体 struct Point(f32, f32); let point = Point(1.0, 0.0); assert_eq!(point.0, 1.0); assert_eq!(point.1, 0.0); }
注意:与字段访问表达式不同,元组索引表达式可以是调用表达式的函数操作数。 (这之所以可行,)因为元组索引表达式不会与方法调用相混淆,因为方法名不可能是数字。
注意:虽然数组和切片也有元素,但它们必须使用数组或切片索引表达式或切片模式去访问它们的元素。
Struct expressions
结构体表达式
struct-expr.md
commit: 6e6e8b42196d200eaa6b7fe9dad812dc9268985f
本章译文最后维护日期:2023-05-03
句法
StructExpression :
StructExprStruct
| StructExprTuple
| StructExprUnitStructExprStruct :
PathInExpression{
(StructExprFields | StructBase)?}
StructExprFields :
StructExprField (,
StructExprField)* (,
StructBase |,
?)StructExprField :
OuterAttribute *
(
IDENTIFIER
| (IDENTIFIER | TUPLE_INDEX):
Expression
)StructBase :
..
ExpressionStructExprTuple :
PathInExpression(
( Expression (,
Expression)*,
? )?
)
StructExprUnit : PathInExpression
结构体表达式用来创建结构体、枚举或联合体的值。它由指向结构体程序项、枚举变体、联合体程序项的路径,以及与此程序项的字段对应的值组成。 结构体表达式有三种形式:结构体(struct)、元组结构体(tuple)和单元结构体(unit)。
下面是结构体表达式的示例:
#![allow(unused)] fn main() { struct Point { x: f64, y: f64 } struct NothingInMe { } struct TuplePoint(f64, f64); mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } } struct Cookie; fn some_fn<T>(t: T) {} Point {x: 10.0, y: 20.0}; NothingInMe {}; TuplePoint(10.0, 20.0); TuplePoint { 0: 10.0, 1: 20.0 }; // 效果和上一行一样 let u = game::User {name: "Joe", age: 35, score: 100_000}; some_fn::<Cookie>(Cookie); }
Field struct expression
结构体表达式的字段设置
用花括号把字段括起来的结构体表达式允许以任意顺序指定每个字段的值。字段名与值之间用冒号分隔。
联合体类型的值只能使用此句法创建,并且只能指定一个字段。
Functional update syntax
函数式更新句法
构造结构体类型的值的结构体表达式可以以 ..
后跟一个表达式的句法结尾,这种句法表示这是一种函数式更新(functional update)。
..
后跟的表达式(此表达式被称为此函数式更新的基(base))必须与正在构造的新结构体值是同一种结构体类型的。
整个结构体表达式先为已指定的字段使用已给定的值,然后再从基表达式(base expression)里为剩余未指定的字段移动或复制值。 与所有结构体表达式一样,此结构体类型的所有字段必须是可见的,甚至那些没有显式命名的字段也是如此。
#![allow(unused)] fn main() { struct Point3d { x: i32, y: i32, z: i32 } let mut base = Point3d {x: 1, y: 2, z: 3}; let y_ref = &mut base.y; Point3d {y: 0, z: 10, .. base}; // OK, 只有 base.x 获取进来了 drop(y_ref); }
带花括号的结构体表达式不能直接用在循环表达式或 if表达式的头部,也不能直接用在 if let或匹配表达式的检验对象(scrutinee)上。 但是,如果结构体表达式在另一个表达式内(例如在圆括号内),则可以用在这些情况下。
构造元组结构体时其字段名可以是代表索引的十进制整数数值。 这中表达方法还可以与基结构体一起使用来填充其余未指定的索引:
#![allow(unused)] fn main() { struct Color(u8, u8, u8); let c1 = Color(0, 0, 0); // 创建元组结构体的典型方法。 let c2 = Color{0: 255, 1: 127, 2: 0}; // 按索引来指定字段。 let c3 = Color{1: 0, ..c2}; // 使用基的字段值来填写结构体的所有其他字段。 }
Struct field init shorthand
初始化结构体字段的快捷方法
当使用字段的名字(注意不是位置索引数字)初始化某数据结构(结构体、枚举、联合体)时,允许将 fieldname: fieldname
写成 fieldname
这样的简化形式。
这种句法让代码更少重复,更加紧凑。
例如:
For example: For example:
#![allow(unused)] fn main() { struct Point3d { x: i32, y: i32, z: i32 } let x = 0; let y_value = 0; let z = 0; Point3d { x: x, y: y_value, z: z }; Point3d { x, y: y_value, z }; }
Tuple struct expression
元组结构体表达式
用圆括号括起字段的结构体表达式构造出来的结构体为元组结构体。 虽然为了完整起见,也把它作为一个特定的(结构体)表达式列在这里,但实际上它等价于执行元组结构体构造器的调用表达式。例如:
#![allow(unused)] fn main() { struct Position(i32, i32, i32); Position(0, 0, 0); // 创建元组结构体的典型方法。 let c = Position; // `c` 是一个接收3个参数的函数。 let pos = c(8, 6, 7); // 创建一个 `Position` 值。 }
Unit struct expression
单元结构体表达式
单元结构体表达式只是单元结构体程序项(unit struct item)的路径。 也是指向此单元结构体的值的隐式常量。 单元结构体的值也可以用无字段结构体表达式来构造。例如:
#![allow(unused)] fn main() { struct Gamma; let a = Gamma; // Gamma的值。 let b = Gamma{}; // 和`a`的值完全一样。 }
Call expressions
调用表达式
call-expr.md
commit: 9b4969786f828fbe0bf818547ef6549f029899a9
本章译文最后维护日期:2023-06-11
句法
CallExpression :
Expression(
CallParams?)
CallParams :
Expression (,
Expression )*,
?
调用表达式用来调用函数。
调用表达式的句法规则为:一个被称作*函数操作数(function operand)的表达式,后跟一个圆括号封闭的逗号分割的被称为参数操作数(argument operands)*的表达式列表。
如果函数最终返回,则此调用表达式执行完成。
对于非函数类型,表达式 f(...)
会使用 std::ops::Fn
、std::ops::FnMut
或 std::ops::FnOnce
这些 trait 上的某一方法,选择使用哪个要看 f
如何获取其输入的参数,具体就是看是通过引用、可变引用、还是通过获取所有权来获取的。
如有需要,也可通过自动借用。
Rust 也会根据需要自动对 f
作解引用处理。
下面是一些调用表达式的示例:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { 0 } let three: i32 = add(1i32, 2i32); let name: &'static str = (|| "Rust")(); }
Disambiguating Function Calls
函数调用的消歧
为获得更直观的完全限定的句法规则,Rust 对所有函数调都作了糖化(sugar)处理。 根据当前作用域内的程序项调用的二义性,函数调用有可能需要完全限定。
注意:过去,Rust 社区在文档、议题、RFC 和其他社区文章中使用了术语“确定性函数调用句法(Unambiguous Function Call Syntax)”、“通用函数调用句法(Universal Function Call Syntax)” 或 “UFCS”。 但是,这个术语缺乏描述力,可能还会混淆当前的议题。 我们在这里提起这个词是为了便于搜索。
少数几种情况下经常会出现一些导致方法调用或关联函数调用的接受者或引用对象不明确的情况。这些情况可包括:
- 作用域内的多个 trait 为同一类型定义了相同名称的方法
- 自动解引(Auto-
deref
)用搞不定的情况;例如,区分智能指针本身的方法和指针所指对象上的方法 - 不带参数的方法,就像
default()
这样的和返回类型的属性(properties)的,如size_of()
为了解决这种二义性,程序员可以使用更具体的路径、类型或 trait 来明确指代他们想要的方法或函数。
例如:
trait Pretty { fn print(&self); } trait Ugly { fn print(&self); } struct Foo; impl Pretty for Foo { fn print(&self) {} } struct Bar; impl Pretty for Bar { fn print(&self) {} } impl Ugly for Bar { fn print(&self) {} } fn main() { let f = Foo; let b = Bar; // 我们可以这样做,因为对于`Foo`,我们只有一个名为 `print` 的程序项 f.print(); // 对于 `Foo`来说,这样是更明确了,但没必要 Foo::print(&f); // 如果你不喜欢简洁的话,那,也可以这样 <Foo as Pretty>::print(&f); // b.print(); // 错误: 发现多个 `print` // Bar::print(&b); // 仍错: 发现多个 `print` // 必要,因为作用域内的多个程序项定义了 `print` <Bar as Pretty>::print(&b); }
更多细节和动机说明请参考RFC 132。
Method-call expressions
方法调用表达式
method-call-expr.md
commit: e4964a0a951ec7b468992acac50645b443ee4f1d
本章译文最后维护日期:2022-03-14
句法
MethodCallExpression :
Expression.
PathExprSegment(
CallParams?)
方法调用由一个表达式(接受者(receiver))后跟一个单点号(.
)、一个表达式路径段(path segment)和一个圆括号封闭的的表达式列表组成。
方法调用被解析为特定 trait 上的关联方法时,如果点号左边的表达式有确切的已知的 self
类型,则会静态地分发(statically dispatch)给在此类型下查找到的某个同名方法来执行;如果点号左边的表达式是间接的 trait对象,则会采用动态分发(dynamically dispatch)的方式。
#![allow(unused)] fn main() { let pi: Result<f32, _> = "3.14".parse(); let log_pi = pi.unwrap_or(1.0).log(2.72); assert!(1.14 < log_pi && log_pi < 1.15) }
在查找方法调用时,为了调用某个方法,可能会自动对接受者做解引用或借用。 这需要比其他函数更复杂的查找流程,因为这可能需要调用许多可能的方法。具体会用到下述步骤:
第一步是构建候选接受者类型的列表。通过重复对接受者表达式的类型作解引用,将遇到的每个类型添加到列表中,然后在最后再尝试进行一次非固定内存宽度类型自动强转(unsized coercion),如果成功,则将结果类型也添加到此类型列表里。
然后,再在这个列表中的每个候选类型 T
后紧跟着添加 &T
和 &mut T
候选项。
例如,接受者的类型为 Box<[i32;2]>
,则候选类型为 Box<[i32;2]>
,&Box<[i32;2]>
,&mut Box<[i32;2]>
,[i32; 2]
(通过解引用得到),&[i32; 2]
,&mut [i32; 2]
,[i32]
(通过非固定内存宽度类型自动强转得到),&[i32]
,最后是 &mut [i32]
。
然后,对每个候选类型 T
,编译器会它的以下位置上搜索一个可见的同名方法,找到后还会把此方法所属的类型当做接受者:
T
的固有方法(直接在T
上实现的方法)。- 由
T
已实现的可见的 trait 所提供的任何方法。如果T
是一个类型参数,则首先查找由T
上的 trait约束所提供的方法。然后查找作用域内所有其他的方法。
注意:查找是按顺序进行的,这有时会导致出现不太符合直觉的结果。 下面的代码将打印 “In trait impl!”,因为首先会查找
&self
上的方法,在找到结构体Foo
的(接受者类型为)&mut self
的(固有)方法(Foo::bar
)之前先找到(接受者类型为&self
的)trait方法(Bar::bar
)。struct Foo {} trait Bar { fn bar(&self); } impl Foo { fn bar(&mut self) { println!("In struct impl!") } } impl Bar for Foo { fn bar(&self) { println!("In trait impl!") } } fn main() { let mut f = Foo{}; f.bar(); }
如果上面这第二步查找导致了多个可能的候选类型[^译者注],就会导致报错,此时必须将接受者转换为适当的接受者类型再来进行方法调用。
此方法过程不考虑接受者的可变性或生存期,也不考虑方法是否为非安全(unsafe
)方法。
一旦查找到了一个方法,如果由于这些(可变性、生存期或健全性)原因中的一个(或多个)而不能调用,则会报编译错误。
如果某步碰到了存在多个可能性方法的情况,比如泛型方法之间或 trait方法之间被认为是相同的,那么它就会导致编译错误。 这些情况就需要使用函数调用的消歧句法来为方法调用或函数调用消除歧义。
版次差异:在 2021 版次之前,在查找可用的方法时,如果候选的接受者类型是一个数组类型,由标准库提供的
IntoIterator
trait 提供的方法会被忽略。
警告: 对于 trait对象,如果有一个与 trait方法同名的固有方法,那么当尝试在方法调用表达式(method call expression)中调用该方法时,将编译报错。 此时,可以使用消除函数调用歧义的句法来明确调用语义。 在 trait对象上使用消除函数调用歧义的句法,将只能调用 trait方法,无法调用固有方法。 所以只要不在 trait对象上定义和 trait方法同名的固有方法就不会碰到这种麻烦。
[^译者注]:这个应该跟后面说的方法名歧义了一样,如果类型 T
的两个 trait 都有调用的那个方法,那此方法的调用者类型就不能确定了,就需要消歧。
Field access expressions
字段访问表达式
field-expr.md
commit: 08b97f94ccd3c2c835aae402664340894587e39d
本章译文最后维护日期:2023-01-15
句法
FieldExpression :
Expression.
IDENTIFIER
*字段表达式(field expression)*是计算求取结构体或联合体的字段的内存位置的位置表达式。 当操作数可变时,其字段表达式也是可变的。
*字段表达式(field expression)的句法规则为:一个被称为容器操作数(container operand)*的表达式后跟一个单点号(.
),最后是一个标识符。
字段表达式后面不能再紧跟着一个被圆括号封闭起来的逗号分割的表达式列表(这种表示这是一个方法调用表达式)。
因此字段表达式不能是调用表达式的函数调用者。
字段表达式代表结构体(struct
)或联合体(union
)的字段。要调用存储在结构体的字段中的函数,需要在此字段表达式外加上圆括号。
注意:如果要在调用表达式中使用它(来调用函数),要把此字段表达式先用圆括号包装成一个圆括号表达式。
#![allow(unused)] fn main() { struct HoldsCallable<F: Fn()> { callable: F } let holds_callable = HoldsCallable { callable: || () }; // 非法: 会被解析为调用 "callable"方法 // holds_callable.callable(); // 合法 (holds_callable.callable)(); }
示例:
mystruct.myfield;
foo().x;
(Struct {a: 10, b: 20}).a;
(mystruct.function_field)() // 调用表达式里包含一个字段表达式
Automatic dereferencing
自动解引用
如果容器操作数的类型实现了 Deref
或 DerefMut
(这取决于该操作数是否为可变),则会尽可能多次地自动解引用(automatically dereferenced),以使字段访问成为可能。
这个过程也被简称为自动解引用(autoderef)。
Borrowing
借用
当借用时,结构体的各个字段以及对结构体的整体引用都被视为彼此分离的实体。
如果结构体没有实现 Drop
,同时该结构体又存储在局部变量中,(这种各个字段被视为彼此分离的单独实体的逻辑)还适用于每个字段的移出(move out)。
但如果对 Box
化之外的用户自定义类型执行自动解引用,这(种各个字段被视为彼此分离的单独实体的逻辑)就不适用了。
#![allow(unused)] fn main() { struct A { f1: String, f2: String, f3: String } let mut x: A; x = A { f1: "f1".to_string(), f2: "f2".to_string(), f3: "f3".to_string() }; let a: &mut String = &mut x.f1; // x.f1 被可变借用 let b: &String = &x.f2; // x.f2 被不可变借用 let c: &String = &x.f2; // 可以被再次借用 let d: String = x.f3; // 从 x.f3 中移出 }
Closure expressions
闭包表达式
closure-expr.md
commit: 04ce4c4e508797abb7534053c1e8420ce5873c27
本章译文最后维护日期:2022-10-22
句法
ClosureExpression :
move
?
(||
||
ClosureParameters?|
)
(Expression |->
TypeNoBounds BlockExpression)ClosureParameters :
ClosureParam (,
ClosureParam)*,
?ClosureParam :
OuterAttribute* PatternNoTopAlt (:
Type )?
闭包表达式,也被称为 lambda表达式或 lambda,它定义了一个闭包类型,并把此表达式求值计算为该类型的值。
闭包表达式的句法规则为:先是一个可选的 move
关键字,后跟一对管道定界符(|
)封闭的逗号分割的被称为闭包参数(closure parameters)的模式列表(每个闭包参数都可选地通过 :
后跟其类型),再可选地通过 ->
后跟一个返回类型,最后是被称为闭包体操作数(closure body operand) 的表达式。
代表闭包参数的每个模式后面的可选类型是该模式的类型标注(type annotations)。
如果存在返回类型,则闭包体表达式必须是一个普通的块(表达式)。
闭包表达式本质是将一组参数映射到参数后面的表达式的函数。
与 let
绑定一样,闭包参数也是不可反驳型模式的,其类型标注是可选的,如果没有给出,则从上下文推断。
每个闭包表达式都有一个唯一的匿名类型。
特别值得注意的是闭包表达式能捕获它们被定义时的环境中的变量(capture their environment),而普通的函数定义则不能。
如果没有关键字 move
,闭包表达式将[推断它该如何从其环境中捕获每个变量][infers how it captures each variable from its environment],它倾向于通过共享引用来捕获,从而有效地借用闭包体中用到的所有外部变量。
如果有必要,编译器会推断出应该采用可变引用,还是应该从环境中移动或复制值(取决于这些变量的类型)。
闭包可以通过前缀关键字 move
来强制通过复制值或移动值的方式捕获其环境变量。
这通常是为了确保当前闭包的生存期类型为 'static
。
编译器将通过闭包对其捕获的变量的处置方式来确定此闭包类型将实现的[闭包trait][closure traits]。
如果所有捕获的类型都实现了 Send
和/或 Sync
,那么此闭包类型也实现了 Send
和/或 Sync
。
这些存在这些 trait,函数可以通过泛型的方式接受各种闭包,即便闭包的类型名无法被确切指定。
Closure trait implementations
闭包trait 的实现
当前闭包类型实现哪一个闭包trait 依赖于该闭包如何捕获变量和这些变量的类型。
了解闭包如何和何时实现 Fn
、FnMut
和 FnOnce
这三类 trait,请参看调用trait 和自动强转那一章。
如果所有捕获的类型都实现了 Send
和/或 Sync
,那么此闭包类型也实现了 Send
和/或 Sync
。
Example
示例
在下面例子中,我们定义了一个名为 ten_times
的函数,它接受高阶函数参数,然后我们传给它一个闭包表达式作为实参并调用它。
之后又定义了一个使用移动语义从环境中捕获变量的闭包表达式来供该函数调用。
#![allow(unused)] fn main() { fn ten_times<F>(f: F) where F: Fn(i32) { for index in 0..10 { f(index); } } ten_times(|j| println!("hello, {}", j)); // 带类型标注 i32 ten_times(|j: i32| -> () { println!("hello, {}", j) }); let word = "konnichiwa".to_owned(); ten_times(move |j| println!("{}, {}", word, j)); }
Attributes on closure parameters
闭包参数上的属性
闭包参数上的属性遵循与常规函数参数上相同的规则和限制。
Loops and other breakable expressions
循环和其他可中断表达式
loop-expr.md
commit: aa9c70bda63b3ab73b15746609831dafb96f56ff
本章译文最后维护日期:2023-05-03
句法
LoopExpression :
LoopLabel? (
InfiniteLoopExpression
| PredicateLoopExpression
| PredicatePatternLoopExpression
| IteratorLoopExpression
| LabelBlockExpression
)
Rust支持五种循环表达式:
loop
表达式表示一个无限循环。while
表达式不断循环,直到谓词为假。while let
表达式循环测试给定模式。for
表达式从迭代器中循环取值,直到迭代器为空。- 带标签的块表达式 的循环一旦启动,将一直循环,但可以通过使用
break
来提前退出循环。
所有五种类型的循环都支持 break
表达式和循环标签(label).
除了带标签的块表达式都支持 continue
表达式。
只有 loop
循环和带标签的块表达式支持非平凡求值(evaluation to non-trivial values)1。
Infinite loops
无限循环
句法
InfiniteLoopExpression :
loop
BlockExpression
loop
表达式会不断地重复地执行它代码体内的代码:loop { println!("I live."); }
。
没有包含关联的 break
表达式的 loop
表达式是发散的,并且具有类型 !
。
包含相应 break
表达式的 loop
表达式可以结束循环,并且此表达式的类型必须与 break
表达式的类型兼容。
Predicate loops
谓词循环
句法
PredicateLoopExpression :
while
Expression排除结构体表达式 BlockExpression
while
循环从对布尔型的循环条件操作数求值开始。
如果循环条件操作数的求值结果为 true
,则执行循环体块,然后控制流返回到循环条件操作数。如果循环条件操作数的求值结果为 false
,则 while
表达式完成。
举个例子:
#![allow(unused)] fn main() { let mut i = 0; while i < 10 { println!("hello"); i = i + 1; } }
Predicate pattern loops
谓词模式循环
句法
PredicatePatternLoopExpression :
while
let
Pattern=
Scrutinee排除惰性布尔运算符表达式 BlockExpression
while let
循环在语义上类似于 while
循环,但它用 let
关键字后紧跟着一个模式、一个 =
、一个检验对象(scrutinee)表达式和一个块表达式,来替代原来的条件表达式。
如果检验对象表达式的值与模式匹配,则执行循环体块,然后控制流再返回到模式匹配语句。如果不匹配,则 while
表达式执行完成。
#![allow(unused)] fn main() { let mut x = vec![1, 2, 3]; while let Some(y) = x.pop() { println!("y = {}", y); } while let _ = 5 { println!("不可反驳模式总是会匹配成功"); break; } }
while let
循环等价于包含匹配(match
)表达式的 loop
表达式。
如下:
'label: while let PATS = EXPR {
/* loop body */
}
等价于
'label: loop {
match EXPR {
PATS => { /* loop body */ },
_ => break,
}
}
可以使用操作符 |
指定多个模式。
这与匹配(match
)表达式中的 |
具有相同的语义:
#![allow(unused)] fn main() { let mut vals = vec![2, 3, 1, 2, 2]; while let Some(v @ 1) | Some(v @ 2) = vals.pop() { // 打印 2, 2, 然后 1 println!("{}", v); } }
与 if let
表达式的情况一样,检验表达式不能是一个懒惰布尔运算符表达式。
Iterator loops
迭代器循环
句法
IteratorLoopExpression :
for
Patternin
Expression排除结构体表达式 BlockExpression
for
表达式是一个用于在 std::iter::IntoIterator
的某个迭代器实现提供的元素上进行循环的语法结构。
如果迭代器生成一个值,该值将与此 for
表达式提供的不可反驳型模式进行匹配,执行循环体,然后控制流返回到 for
循环的头部。
如果迭代器为空了,则 for
表达式执行完成。
for
循环遍历数组内容的示例:
#![allow(unused)] fn main() { let v = &["apples", "cake", "coffee"]; for text in v { println!("I like {}.", text); } }
for
循环遍历一个整数序列的例子:
#![allow(unused)] fn main() { let mut sum = 0; for n in 1..11 { sum += n; } assert_eq!(sum, 55); }
for
循环等价于如下这样包含了匹配(match
)表达式的 loop
表达式。
'label: for PATTERN in iter_expr {
/* loop body */
}
等价于:
{
let result = match IntoIterator::into_iter(iter_expr) {
mut iter => 'label: loop {
let mut next;
match Iterator::next(&mut iter) {
Option::Some(val) => next = val,
Option::None => break,
};
let PATTERN = next;
let () = { /* loop body */ };
},
};
result
}
这里的 IntoIterator
、Iterator
和 Option
是标准库的程序项(standard library item),不是当前作用域中解析的的任何名称。
变量名 next
、iter
和 val
也仅用于表述需要,实际上它们不是用户可以输入的名称。
注意:上面代码里使用外层
matche
来确保iter_expr
中的任何临时值在循环结束前不会被销毁。next
先声明后赋值是因为这样能让编译器更准确地推断出类型。
Loop labels
循环标签
句法
LoopLabel :
LIFETIME_OR_LABEL:
一个循环表达式可以选择设置一个标签。
这类标签被标记为循环表达式之前的生存期(标签),如 'foo: loop { break 'foo; }
、'bar: while false {}
、'humbug: for _ in 0..0 {}
。
如果循环存在标签,则嵌套在该循环中的带此标签的 break
表达式和 continue
表达式可以退出此标签标记的循环层或将控制流返回至此标签标记的循环层的头部。
具体请参见后面的 break表达式和 continue表达式。
循环标签遵循局部变量的卫生性和遮蔽规则。例如,下面代码将打印 "outer loop":
#![allow(unused)] fn main() { 'a: loop { 'a: loop { break 'a; } print!("outer loop"); break 'a; } }
break
expressions
break
表达式
句法
BreakExpression :
break
LIFETIME_OR_LABEL? Expression?
当遇到 break
时,相关的循环体的执行将立即结束,例如:
#![allow(unused)] fn main() { let mut last = 0; for x in 1..100 { if x > 12 { break; } last = x; } assert_eq!(last, 12); }
break
表达式通常与包含 break
表达式的最内层 loop
、for
或 while
循环相关联,但是可以使用循环标签来指定受影响的循环层(此循环层必须是封闭该 break表达式的循环之一)。
例如:
#![allow(unused)] fn main() { 'outer: loop { while true { break 'outer; } } }
break
表达式只允许在循环体内使用,它有 break
、break 'label
或(参见后面)break EXPR
或 break 'label EXPR
这四种形式。
Labelled block expressions
带标签的块表达式
句法
LabelBlockExpression :
BlockExpression
带标签的块表达式同普通的块表达式完全相同,只是它允许在块中使用 break
表达式。
跟循环不同,标签表达式中的 break
表达式必须带有一个标签(即标签不是可选的)。
同时,带标签的块表达式必须以标签开头。
#![allow(unused)] fn main() { fn do_thing() {} fn condition_not_met() -> bool { true } fn do_next_thing() {} fn do_last_thing() {} let result = 'block: { do_thing(); if condition_not_met() { break 'block 1; } do_next_thing(); if condition_not_met() { break 'block 2; } do_last_thing(); 3 }; }
continue
expressions
continue
表达式
句法
ContinueExpression :
continue
LIFETIME_OR_LABEL?
当遇到 continue
时,相关的循环体的当前迭代将立即结束,并将控制流返回到循环头。
在 while
循环的情况下,循环头是控制循环的条件表达式。
在 for
循环的情况下,循环头是控制循环的调用表达式。
与 break
一样,continue
通常与最内层的循环相关联,但可以使用 continue 'label
来指定受影响的循环层。
continue
表达式只允许在循环体内部使用。
break
and loop values
break
和loop
返回值
当使用 loop
循环时,可以使用 break
表达式从循环中返回一个值,通过形如 break EXPR
或 break 'label EXPR
来返回,其中 EXPR
是一个表达式,它的结果被从 loop
循环中返回。
例如:
#![allow(unused)] fn main() { let (mut a, mut b) = (1, 1); let result = loop { if b > 10 { break b; } let c = a + b; a = b; b = c; }; // 斐波那契数列中第一个大于10的值: assert_eq!(result, 13); }
如果 loop
有关联的 break
,则不认为该循环是发散的,并且 loop
表达式的类型必须与每个 break
表达式的类型兼容。
其后不跟表达式的 break
被认为与后跟 ()
的break
表达式的效果相同。
求得 ()
类型以外的值。
Range expressions
区间表达式
range-expr.md
commit: eb5290329316e96c48c032075f7dbfa56990702b
本章译文最后维护日期:2021-02-21
句法
RangeExpression :
RangeExpr
| RangeFromExpr
| RangeToExpr
| RangeFullExpr
| RangeInclusiveExpr
| RangeToInclusiveExprRangeExpr :
Expression..
ExpressionRangeFromExpr :
Expression..
RangeToExpr :
..
ExpressionRangeFullExpr :
..
RangeInclusiveExpr :
Expression..=
ExpressionRangeToInclusiveExpr :
..=
Expression
..
和 ..=
操作符会根据下表中的规则构造 std::ops::Range
(或 core::ops::Range
)的某一变体类型的对象:
产生式/句法规则 | 句法 | 类型 | 区间语义 |
---|---|---|---|
RangeExpr | start.. end | std::ops::Range | start ≤ x < end |
RangeFromExpr | start.. | std::ops::RangeFrom | start ≤ x |
RangeToExpr | .. end | std::ops::RangeTo | x < end |
RangeFullExpr | .. | std::ops::RangeFull | - |
RangeInclusiveExpr | start..= end | std::ops::RangeInclusive | start ≤ x ≤ end |
RangeToInclusiveExpr | ..= end | std::ops::RangeToInclusive | x ≤ end |
举例:
#![allow(unused)] fn main() { 1..2; // std::ops::Range 3..; // std::ops::RangeFrom ..4; // std::ops::RangeTo ..; // std::ops::RangeFull 5..=6; // std::ops::RangeInclusive ..=7; // std::ops::RangeToInclusive }
下面的表达式是等价的。
#![allow(unused)] fn main() { let x = std::ops::Range {start: 0, end: 10}; let y = 0..10; assert_eq!(x, y); }
区间能在 for
循环里使用:
#![allow(unused)] fn main() { for i in 1..11 { println!("{}", i); } }
if
and if let
expressions
if
和 if let
表达式
if-expr.md
commit: 894afd4f72fdb8682c094d2023f3ec3c6c464824
本章译文最后维护日期:2023-03-04
if
expressions
if
表达式
句法
IfExpression :
if
Expression排除结构体表达式 BlockExpression
(else
( BlockExpression | IfExpression | IfLetExpression ) )?
if
表达式是程序控制中的一个条件分支。if
表达式的句法是一个条件操作数(operand)后紧跟一个块,再后面是任意数量的 else if
条件表达式和块,最后是一个可选的尾部 else
块。
条件操作数的类型必须是布尔型。如果条件操作数的求值结果为 true
,则执行紧跟的块,并跳过后续的 else if
块或 else
块。
如果条件操作数的求值结果为 false
,则跳过紧跟的块,并按顺序求值后续的 else if
条件表达式。
如果所有 if
条件表达式和 else if
条件表达式的求值结果均为 false
,则执行 else
块。
if表达式的求值结果就是所执行的块的返回值,或者如果没有块被求值那 if表达式的求值结果就是 ()
。
if
表达式在所有情况下的类型必须一致。
#![allow(unused)] fn main() { let x = 3; if x == 4 { println!("x is four"); } else if x == 3 { println!("x is three"); } else { println!("x is something else"); } let y = if 12 * 15 > 150 { "Bigger" } else { "Smaller" }; assert_eq!(y, "Bigger"); }
if let
expressions
if let
表达式
句法
IfLetExpression :
if
let
Pattern=
Scrutinee排除惰性布尔运算符表达式 BlockExpression
(else
( BlockExpression | IfExpression | IfLetExpression ) )?
if let
表达式在语义上类似于 if
表达式,但是代替条件操作数的是一个关键字 let
,再后面是一个模式、一个 =
和一个检验对象(scrutinee)操作数。
如果检验对象操作数的值与模式匹配,则执行相应的块。
否则,如果存在 else
块,则继续处理后面的 else
块。和 if
表达式一样,if let
表达式也可以有返回值,这个返回值是由被求值的块确定。
#![allow(unused)] fn main() { let dish = ("Ham", "Eggs"); // 此主体代码将被跳过,因为该模式被反驳 if let ("Bacon", b) = dish { println!("Bacon is served with {}", b); } else { // 这个块将被执行。 println!("No bacon will be served"); } // 此主体代码将被执行 if let ("Ham", b) = dish { println!("Ham is served with {}", b); } if let _ = 5 { println!("不可反驳型的模式总是会匹配成功的"); } }
if
表达式和 if let
表达式能混合使用:
#![allow(unused)] fn main() { let x = Some(3); let a = if let Some(1) = x { 1 } else if x == Some(2) { 2 } else if let Some(y) = x { y } else { -1 }; assert_eq!(a, 3); }
if let
表达式等价于match表达式,例如:
if let PATS = EXPR {
/* body */
} else {
/*else */
}
is equivalent to
match EXPR {
PATS => { /* body */ },
_ => { /* else */ }, // 如果没有 else块,这相当于 `()`
}
可以使用操作符 |
指定多个模式。
这与匹配(match
)表达式中的 |
具有相同的语义:
#![allow(unused)] fn main() { enum E { X(u8), Y(u8), Z(u8), } let v = E::Y(12); if let E::X(n) | E::Y(n) = v { assert_eq!(n, 12); } }
if let
表达式不能是惰性布尔运算符表达式。
使用惰性布尔运算符的效果是不明确的,因为 Rust 里一个新特性(if-let执行链(if-let chains)的实现-请参阅eRFC 2947)正被提上日程。
当确实需要惰性布尔运算符表达式时,可以像下面一样使用圆括号来实现:
// Before...
if let PAT = EXPR && EXPR { .. }
// After...
if let PAT = ( EXPR && EXPR ) { .. }
// Before...
if let PAT = EXPR || EXPR { .. }
// After...
if let PAT = ( EXPR || EXPR ) { .. }
match
expressions
匹配(match
)表达式
match-expr.md
commit: 9b1c9c6be04000a13aa9aa038d3c4c9c162f3061
本章译文最后维护日期:2022-06-17
句法
MatchExpression :
match
Scrutinee{
InnerAttribute*
MatchArms?
}
Scrutinee :
Expression排除结构体表达式MatchArms :
( MatchArm=>
( ExpressionWithoutBlock,
| ExpressionWithBlock,
? ) )*
MatchArm=>
Expression,
?MatchArm :
OuterAttribute* Pattern MatchArmGuard?MatchArmGuard :
if
Expression
匹配(match
)表达式在模式(pattern)上建立代码逻辑分支(branch)。
匹配的确切形式取决于其应用的模式。
一个匹配(match
)表达式带有一个要与模式进行比较的 检验对象(scrutinee)表达式。
检验对象表达式和模式必须具有相同的类型。
根据检验对象表达式是位置表达式或值表达式,匹配(match
)的行为表现会有所不同。
如果检验对象表达式是一个值表达式,则这个表达式首先会在被求值到一个临时内存位置,然后将这个结果值按顺序与匹配臂(arms)中的模式进行比较,直到找到一个成功的匹配。
第一个匹配成功的模式所在的匹配臂会被选中为当前匹配(match
)的分支目标,然后以该模式绑定的变量为中介,(把它从检验对象那里匹配到的变量值)转赋值给该匹配臂的块中的局部变量,然后控制流进入该块。
当检验对象表达式是一个位置表达式时,此匹配(match
)表达式不用先去内存上分配一个临时位置;但是,按值匹配的绑定方式(by-value binding)会复制或移动这个(位置表达式代表的)内存位置里面的值。
如果可能,最好还是在位置表达式上进行匹配,因为这种匹配的生存期继承了该位置表达式的生存期,而不会(让其生存期仅)局限于此匹配的内部。
匹配(match
)表达式的一个示例:
#![allow(unused)] fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), 4 => println!("four"), 5 => println!("five"), _ => println!("something else"), } }
模式中绑定到的变量的作用域可以覆盖到匹配守卫(match guard)和匹配臂的表达式里。 变量绑定方式(移动、复制或引用)取决于使用的具体模式。
可以使用操作符 |
连接多个匹配模式。
每个模式将按照从左到右的顺序进行测试,直到找到一个成功的匹配。
#![allow(unused)] fn main() { let x = 9; let message = match x { 0 | 1 => "not many", 2 ..= 9 => "a few", _ => "lots" }; assert_eq!(message, "a few"); // 演示模式匹配顺序。 struct S(i32, i32); match S(1, 2) { S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1), _ => panic!(), } }
注意:
2..=9
是一个区间(Range)模式,不是一个区间表达式。 因此,只有区间模式支持的区间类型才能在匹配臂中使用。
每个 |
分隔的模式里出现的变量绑定必须出现在匹配臂的所有模式里。
相同名称的绑定变量必须具有相同的类型和相同的变量绑定模式。
Match guards
匹配守卫
匹配臂可以接受*匹配守卫(Pattern guard)*来进一步改进匹配标准。
模式守卫出现在模式的后面,由关键字 if
后面的布尔类型表达式组成。
当模式匹配成功时,将执行匹配守卫表达式。
如果此表达式的计算结果为真,则此模式将进一步被确认为匹配成功。
否则,匹配将测试下一个模式,包括测试同一匹配臂中运算符 |
分割的后续匹配模式。
#![allow(unused)] fn main() { let maybe_digit = Some(0); fn process_digit(i: i32) { } fn process_other(i: i32) { } let message = match maybe_digit { Some(x) if x < 10 => process_digit(x), Some(x) => process_other(x), None => panic!(), }; }
注意:使用操作符
|
的多次匹配可能会导致后跟的匹配守卫必须多次执行的副作用。 例如:#![allow(unused)] fn main() { use std::cell::Cell; let i : Cell<i32> = Cell::new(0); match 1 { 1 | _ if { i.set(i.get() + 1); false } => {} _ => {} } assert_eq!(i.get(), 2); }
匹配守卫可以引用绑定在它们前面的模式里的变量。 在对匹配守卫进行计算之前,将对检验对象内部被模式的变量匹配上的那部分进行共享引用。 在对匹配守卫进行计算时,访问守卫里的这些变量就会使用这个共享引用。只有当匹配守卫最终计算为真时,此共享引用的值才会从检验对象内部移动或复制到相应的匹配臂的变量中。 这使得共享借用可以在守卫内部使用,还不会在守卫不匹配的情况下将值移出检验对象。 此外,通过在计算匹配守卫的同时持有共享引用,也可以防止匹配守卫内部意外修改检验对象。
Attributes on match arms
匹配臂上的属性
在匹配臂上允许使用外部属性,但在匹配臂上只有 cfg
和 lint检查类属性这些属性才有意义。
在允许块表达式上的属性存在的那几种表达式上下文中,可以在匹配表达式的左括号后直接使用内部属性。
return
expressions
返回(return
)表达式
return-expr.md
commit: eb5290329316e96c48c032075f7dbfa56990702b
本章译文最后维护日期:2021-02-21
句法
ReturnExpression :
return
Expression?
返回(return)表达式使用关键字 return
来标识。
对返回(return
)表达式求值会将其参数移动到当前函数调用的指定输出位置,然后销毁当前函数的激活帧(activation frame),并将控制权转移到此函数的调用帧(caller frame)。
一个返回(return
)表达式的例子:
#![allow(unused)] fn main() { fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } }
Await expressions
等待(await)表达式
await-expr.md
commit: bdaa92848c77f4eaadbab5dc753e0ec0b84c5221
本章译文最后维护日期:2022-08-20
句法
AwaitExpression :
Expression.
await
等待(await)表达式挂起当前计算,直到给定的 future 准备好生成值。
等待(await)表达式的句法格式为:一个其类型实现了 Future trait 的表达式(此表达式本身被称为 future操作数)后跟一 .
标记,再后跟一个 await
关键字。
“await”表达式是一种语法构造,用于挂起由“std::future::IntoFuture”实现提供的计算,直到给定的future准备好生成值。
等待表达式的语法是一个表达式,其类型实现了〔‘IntoFuture‘〕特性,称为*future operand*,然后是标记‘,然后是“等待”关键字。
*等待(await)*表达式是一种语法构造,用来挂起由 std::future::IntoFuture
的各类实现提供的计算,直到给定的 future 准备好生成值。
等待表达式的本身有自己的类型,此类型实现了 IntoFuture
trait,表达式本身被称为 future操作数,后跟一 token .
,再后跟一个 await
关键字。
等待(await)表达式仅在异步上下文中才能使用,例如 异步函数(async fn
) 或 异步(async
)块。
更具体地说,等待(await)表达式具有以下效果:
- 通过在 future操作数上调用
IntoFuture::into_future
来创建一个 future。 - 对 future 进行求值,结果保存到一个 future类型的
tmp
中; - 使用
Pin::new_unchecked
固定住(Pin)这个tmp
; - 然后通过调用
Future::poll
方法对这个固定住的 future 进行 poll操作,同时将当前任务上下文传递给它; - 如果
poll
调用返回Poll::Pending
,那么这个 future 就也返回Poll::Pending
,然后以此状态挂起此表达式,当外部异步上下文再次 poll 此表达式时,过程又从步骤3开始执行。 - 如果
poll
调用不返回Poll::Pending
,那必定返回了Poll::Ready
,这种情况下,变体Poll::Ready
中包含的值通常就是await
表达式自己的返回结果。
版次差异: 等待(await)表达式只能从 Rust 2018 版开始才可用 zh |
Task context
任务上下文
任务上下文是指在对异步上下文本身进行 poll 时提供给当前异步上下文的上下文(Context
)。
因为等待(await
)表达式只能在异步上下文中才能使用,所以此时必须有一些任务上下文可用。
Approximate desugaring
近似脱糖
实际上,一个等待(await)表达式大致相当于如下这个非正规的脱糖过程:
match operand.into_future() {
mut pinned => loop {
let mut pin = unsafe { Pin::new_unchecked(&mut pinned) };
match Pin::future::poll(Pin::borrow(&mut pin), &mut current_context) {
Poll::Ready(r) => break r,
Poll::Pending => yield Poll::Pending,
}
}
}
其中,yield
伪代码返回 Poll::Pending
,当再次调用时,从该点继续执行。
变量 current_context
是指从异步环境中获取的上下文。
_
expressions
_
表达式
underscore-expr.md
commit: 7f1e24a45764646504c79176094346eb1925f100
本章译文最后维护日期:2022-12-30
句法
UnderscoreExpression :
_
下划线表达式(使用符号 _
来表示)通常在解构赋值中的充当占位符。
它们只能出现在赋值表达式的左侧。
请注意,这与通配符模式是不同的。
一个 _
表达式的一个示例:
#![allow(unused)] fn main() { let p = (1, 2); let mut a = 0; (_, a) = p; }
Patterns
模式
patterns.md
commit: 82517788e162791dad3305a8c8d8d20d49510ad6
本章译文最后维护日期:2024-06-15
句法
Pattern :
|
? PatternNoTopAlt (|
PatternNoTopAlt )*PatternNoTopAlt :
PatternWithoutRange
| RangePatternPatternWithoutRange :
LiteralPattern
| IdentifierPattern
| WildcardPattern
| RestPattern
| ReferencePattern
| StructPattern
| TupleStructPattern
| TuplePattern
| GroupedPattern
| SlicePattern
| PathPattern
| MacroInvocation
模式基于给定数据结构去匹配值,并可选地将变量和这些结构中匹配到的值绑定起来。 模式也用在变量声明上和函数(包括闭包)的参数上。
下面示例中的模式完成四件事:
- 测试
person
是否在其car
字段中填充了内容。 - 测试
person
的age
字段(的值)是否在 13 到 19 之间,并将其值绑定到给定的变量person_age
上。 - 将对
name
字段的引用绑定到给定变量person_name
上。 - 忽略
person
的其余字段。其余字段可以有任何值,并且不会绑定到任何变量上。
#![allow(unused)] fn main() { struct Car; struct Computer; struct Person { name: String, car: Option<Car>, computer: Option<Computer>, age: u8, } let person = Person { name: String::from("John"), car: Some(Car), computer: None, age: 15, }; if let Person { car: Some(_), age: person_age @ 13..=19, name: ref person_name, .. } = person { println!("{} has a car and is {} years old.", person_name, person_age); } }
模式用于:
Destructuring
解构
模式可用于解构结构体(struct
)、枚举(enum
)和元组。
解构将一个值分解成它的组件组成,使用的句法与创建此类值时的几乎相同。
在检验对象表达式的类型为结构体(struct
)、枚举(enum
)或元组(tuple
)的模式中,占位符(_
) 代表一个数据字段,而通配符 ..
代表特定变量/变体(variant)的所有剩余字段。当使用字段的名称(而不是字段序号)来解构数据结构时,允许将 fieldname
当作 fieldname: fieldname
的简写形式书写。
#![allow(unused)] fn main() { enum Message { Quit, WriteString(String), Move { x: i32, y: i32 }, ChangeColor(u8, u8, u8), } let message = Message::Quit; match message { Message::Quit => println!("Quit"), Message::WriteString(write) => println!("{}", &write), Message::Move{ x, y: 0 } => println!("move {} horizontally", x), Message::Move{ .. } => println!("other move"), Message::ChangeColor { 0: red, 1: green, 2: _ } => { println!("color change, red: {}, green: {}", red, green); } }; }
Refutability
可反驳性
当一个模式有可能与它所匹配的值不匹配时,我们就说它是可反驳型的(refutable)。 也就是说,*不可反驳型(irrefutable)*模式总是能与它们所匹配的值匹配成功。例如:
#![allow(unused)] fn main() { let (x, y) = (1, 2); // "(x, y)" 是一个不可反驳型模式 if let (a, 3) = (1, 2) { // "(a, 3)" 是可反驳型的, 将不会匹配 panic!("Shouldn't reach here"); } else if let (a, 4) = (3, 4) { // "(a, 4)" 是可反驳型的, 将会匹配 println!("Matched ({}, 4)", a); } }
Literal patterns
字面量模式
句法
LiteralPattern :
true
|false
| CHAR_LITERAL
| BYTE_LITERAL
| STRING_LITERAL
| RAW_STRING_LITERAL
| BYTE_STRING_LITERAL
| RAW_BYTE_STRING_LITERAL
| C_STRING_LITERAL
| RAW_C_STRING_LITERAL
|-
? INTEGER_LITERAL
|-
? FLOAT_LITERAL
字面量模式匹配的值与字面量所创建的值完全相同。 由于负数不是字面量,(特设定)字面量模式也接受字面量前的可选负号,它的作用类似于否定运算符。
字面量模式接受 C语言风格的字符串字面量和原始C语言风格的字符串字面量,但 &CStr
没实现结构相等(#[derive(Eq, PartialEq)]
),因此 &CStr
上的任何此类 match
都将被类型错误所拒绝。
字面量模式总是可以反驳型的。
示例:
#![allow(unused)] fn main() { for i in -2..5 { match i { -1 => println!("It's minus one"), 1 => println!("It's a one"), 2|4 => println!("It's either a two or a four"), _ => println!("Matched none of the arms"), } } }
Identifier patterns
标识符模式
句法
IdentifierPattern :
ref
?mut
? IDENTIFIER (@
PatternNoTopAlt ) ?
标识符模式将它们匹配的值绑定到一个变量上。
此标识符在该模式中必须是唯一的。
该变量会在作用域中遮蔽任何同名的变量。
这种绑定的作用域取决于使用模式的上下文(例如 let
绑定或匹配臂(match
arm)1)。
标识符模式只能包含一个标识符(也可能前带一个 mut
),能匹配任何值,并将其绑定到该标识符上。
最常见的标识符模式应用场景就是用在变量声明上和用在函数(包括闭包)的参数上。
#![allow(unused)] fn main() { let mut variable = 10; fn sum(x: i32, y: i32) -> i32 { x + y } }
要将模式匹配到的值绑定到变量上,也可使用句法 variable @ subpattern
。
例如,下面示例中将值 2 绑定到 e
上(不是整个区间(range):这里的区间是一个区间子模式(range subpattern))。
#![allow(unused)] fn main() { let x = 2; match x { e @ 1 ..= 5 => println!("got a range element {}", e), _ => println!("anything"), } }
默认情况下,标识符模式里变量会和匹配到的值的一个拷贝副本绑定,或匹配值自身移动过来和变量完成绑定,具体是使用拷贝语义还是移动语义取决于匹配到的值是否实现了 Copy
。
也可以通过使用关键字 ref
将变量和值的引用绑定,或者使用 ref mut
将变量和值的可变引用绑定。示例:
#![allow(unused)] fn main() { let a = Some(10); match a { None => (), Some(value) => (), } match a { None => (), Some(ref value) => (), } }
在第一个匹配表达式中,值被拷贝(或移动)(到变量 value
上)。
在第二个匹配中,对相同内存位置的引用被绑定到变量上。
之所以需要这种句法,是因为在解构子模式(destructuring subpatterns)里,操作符 &
不能应用在值的字段上。
例如,以下内容无效:
#![allow(unused)] fn main() { struct Person { name: String, age: u8, } let value = Person { name: String::from("John"), age: 23 }; if let Person { name: &person_name, age: 18..=150 } = value { } }
要使其有效,请按如下方式编写代码:
#![allow(unused)] fn main() { struct Person { name: String, age: u8, } let value = Person { name: String::from("John"), age: 23 }; if let Person { name: ref person_name, age: 18..=150 } = value { } }
这里,ref
不是被匹配的一部分。
这里它唯一的目的就是使变量和匹配值的引用绑定起来,而不是潜在地拷贝或移动匹配到的内容。
路径模式(Path pattern)优先于标识符模式。
如果给某个标识符指定了 ref
或 ref mut
,同时该标识符又遮蔽了某个常量,这会导致错误。
如果 @
子模式是不可反驳型的或未指定子模式,则标识符模式是不可反驳型的。
Binding modes
绑定方式
基于人类工程学的考虑,为了让引用和匹配值的绑定更容易一些,模式会自动选择不同的绑定方式。
当引用值与非引用模式匹配时,这将自动地被视为 ref
或 ref mut
绑定方式。
示例:
#![allow(unused)] fn main() { let x: &Option<i32> = &Some(3); if let Some(y) = x { // y 被转换为`ref y` ,其类型为 &i32 } }
*非引用模式(Non-reference patterns)*包括除上面这种单独的标识符绑定模式和后面会讲到的通配符模式(_
)、匹配引用类型的常量(const
)模式和引用模式这些模式以外的所有模式。
如果绑定模式(binding pattern)中没有显式地包含 ref
、ref mut
、mut
,那么它将使用默认绑定方式来确定如何绑定变量。
默认绑定方式以使用移动语义的“移动(move)”方式开始。
当匹配一个模式时,编译器对模式从外到内逐层匹配。每次非引用模式和引用匹配上了时,引用都会自动解引用出最后的值,并更新默认绑定方式,再进行最终的匹配。
此时引用会将默认绑定方式设置为 ref
方式。
可变引用会将模式设置为 ref mut
方式,除非绑定方式已经是 ref
了(在这种情况下它仍然是 ref
方式)。
如果自动解引用解出的值仍然是引用,则会重复解引用。2
移动语义的绑定方式和引用语义的绑定方式可以在同一个模式中混合使用,这样做会导致绑定对象的部分被移走,并且之后无法再使用该对象。 这只适用于类型无法拷贝的情况下。
下面的示例中,name
被移出了 person
,因此如果再试图把 person
作为一个整体使用,或再次使用 person.name
,将会因为*部分移出(partial move)*的问题而报错。
示例:
#![allow(unused)] fn main() { struct Person { name: String, age: u8, } let person = Person{ name: String::from("John"), age: 23 }; // 在 `age` 被引用绑定的情况下,`name` 被从 person 中移出 let Person { name, ref age } = person; }
Wildcard pattern
通配符模式
句法
WildcardPattern :
_
通配符模式(下划线符号)能与任何值匹配。
常用它来忽略那些无关紧要的值。
在其他模式中使用该模式时,它匹配单个数据字段(与和匹配所有其余字段的 ..
相对)。
与标识符模式不同,它不会复制、移动或借用它匹配的值。
示例:
#![allow(unused)] fn main() { let x = 20; let (a, _) = (10, x); // x 一定会被 _ 匹配上 assert_eq!(a, 10); // 忽略一个函数/闭包参数 let real_part = |a: f64, _: f64| { a }; // 忽略结构体的一个字段 struct RGBA { r: f32, g: f32, b: f32, a: f32, } let color = RGBA{r: 0.4, g: 0.1, b: 0.9, a: 0.5}; let RGBA{r: red, g: green, b: blue, a: _} = color; assert_eq!(color.r, red); assert_eq!(color.g, green); assert_eq!(color.b, blue); // 能接收带任何值的任何 Some let x = Some(10); if let Some(_) = x {} }
通配符模式总是不可反驳型的。
Rest patterns
剩余模式
句法
RestPattern :
..
剩余模式(..
token)充当匹配长度可变的模式(variable-length pattern),它匹配之前之后没有匹配的零个或多个元素。
它只能在元组模式、元组结构体模式和切片模式中使用,并且只能作为这些模式中的一个元素出现一次。
当作为标识符模式的子模式时,它也可出现在切片模式里。
剩余模式总是不可反驳型的。
示例:
#![allow(unused)] fn main() { let words = vec!["a", "b", "c"]; let slice = &words[..]; match slice { [] => println!("slice is empty"), [one] => println!("single element {}", one), [head, tail @ ..] => println!("head={} tail={:?}", head, tail), } match slice { // 忽略除最后一个元素以外的所有元素,并且最后一个元素必须是 "!". [.., "!"] => println!("!!!"), // `start` 是除最后一个元素之外的所有元素的一个切片,最后一个元素必须是 “z”。 [start @ .., "z"] => println!("starts with: {:?}", start), // `end` 是除第一个元素之外的所有元素的一个切片,第一个元素必须是 “a” ["a", end @ ..] => println!("ends with: {:?}", end), // 'whole' 是整个切片,`last` 是最后一个元素 whole @ [.., last] => println!("the last element of {:?} is {}", whole, last), rest => println!("{:?}", rest), } if let [.., penultimate, _] = slice { println!("next to last is {}", penultimate); } let tuple = (1, 2, 3, 4, 5); // 剩余模式也可是在元组和元组结构体模式中使用。 match tuple { (1, .., y, z) => println!("y={} z={}", y, z), (.., 5) => println!("tail must be 5"), (..) => println!("matches everything else"), } }
Range patterns
区间模式
句法
RangePattern :
RangeInclusivePattern
| RangeFromPattern
| RangeToInclusivePattern
| ObsoleteRangePatternRangeExclusivePattern :
RangePatternBound..
RangePatternBound
RangeInclusivePattern :
RangePatternBound..=
RangePatternBoundRangeFromPattern :
RangePatternBound..
RangeToInclusivePattern :
..=
RangePatternBoundObsoleteRangePattern :(译者注:废弃的区间模式句法/产生式) \ RangePatternBound
...
RangePatternBoundRangePatternBound :
CHAR_LITERAL
| BYTE_LITERAL
|-
? INTEGER_LITERAL
|-
? FLOAT_LITERAL
| PathExpression
区间模式匹配在区间上下边界内界定的标量值。
它们包括一个符号(..
、..=
或...
中的一个)和一侧或两侧的绑定。
区间模式符号左侧的界限称为下限。
区间模式符号右侧的界限称为上限。
区间模式可是闭区间或半开区间。
同时带有下限和上限的区间模式将匹配其两个边界之间的所有值(包括两个边界)。
它被写为其下限,后跟 ..
来表示本区间为开区间或 ..=
来表示本区间为闭区间,最后是其上限。
区间模式的类型是其上下限的共同类型。
例如:模式 'm'..='p'
只匹配 'm'
, 'n'
, 'o'
和 'p'
这4个字符。
那 'm'..'p'
就只能匹配 'm'
, 'n'
和 'o'
, 'p'
被有意的排除在外。
下限不能比上限大。
因此在 a..=b
里,必须总是有 a ≤ b。
比如,10..=0
这样的区间模式是错误的。
如果区间模式只有上限或下限,则它们是半开的。 它们的类型与其上限或下限相同。
只有下限的区间模式将匹配大于或等于下限的任何值。
它被写为其下限,后跟..
,并且具本模式有与其下限相同的类型。
比如1..
可以匹配 9,或者 9001,或者 9007199254740991(如果此值的内存宽度合适),但是不能匹配到 0,也不能匹配有符号整数的负值。
只有上限的区间模式将匹配小于或等于上限的任何值。
它被写为..=
后跟上限,并且本模式具有与其上限相同的类型。
比如,..=10
将匹配10、1、0;对于有符号整型,除这些外,还包括所有的负值。
只带有一个边界值的区间模式不能用作切片模式中的子模式的顶层模式。
界值的合法形式如下:
- 字面量中的字符、字节、整形或浮点字面量。
-
后跟一个整型或浮点型字面量。- 路径
如果界值被书写为路径形式,则在宏解析之后,路径必须解析为一个 char
、整型或浮点型的常量项。
界值的类型和值取决于它的写入方式。
如果界值是路径形式,则模式必须能以此路径解析到的此路径所代表的那个常量的类型和值。
如果是浮点数区间模式,解析出的常量项不能是一个 NaN
。
如果界值是一个字面量,则界值具有相应的字面量表达式的类型和值。
如果字面量前面有一个 -
字符,则界值的类型与相应的字面量表达式的类型相同,且值为相应字面量表达式的值的相反数 。
示例:
#![allow(unused)] fn main() { let c = 'f'; let valid_variable = match c { 'a'..='z' => true, 'A'..='Z' => true, 'α'..='ω' => true, _ => false, }; let ph = 10; println!("{}", match ph { 0..7 => "acid", 7 => "neutral", 8..=14 => "base", _ => unreachable!(), }); let uint: u32 = 5; match uint { 0 => "zero!", 1.. => "正数!", }; // 使用指向常量值的路径: const TROPOSPHERE_MIN : u8 = 6; const TROPOSPHERE_MAX : u8 = 20; const STRATOSPHERE_MIN : u8 = TROPOSPHERE_MAX + 1; const STRATOSPHERE_MAX : u8 = 50; const MESOSPHERE_MIN : u8 = STRATOSPHERE_MAX + 1; const MESOSPHERE_MAX : u8 = 85; let altitude = 70; println!("{}", match altitude { TROPOSPHERE_MIN..=TROPOSPHERE_MAX => "troposphere", STRATOSPHERE_MIN..=STRATOSPHERE_MAX => "stratosphere", MESOSPHERE_MIN..=MESOSPHERE_MAX => "mesosphere", _ => "outer space, maybe", }); pub mod binary { pub const MEGA : u64 = 1024*1024; pub const GIGA : u64 = 1024*1024*1024; } let n_items = 20_832_425; let bytes_per_item = 12; if let size @ binary::MEGA..=binary::GIGA = n_items * bytes_per_item { println!("这适用并占用{}个字节", size); } trait MaxValue { const MAX: u64; } impl MaxValue for u8 { const MAX: u64 = (1 << 8) - 1; } impl MaxValue for u16 { const MAX: u64 = (1 << 16) - 1; } impl MaxValue for u32 { const MAX: u64 = (1 << 32) - 1; } // 使用限定路径: println!("{}", match 0xfacade { 0 ..= <u8 as MaxValue>::MAX => "fits in a u8", 0 ..= <u16 as MaxValue>::MAX => "fits in a u16", 0 ..= <u32 as MaxValue>::MAX => "fits in a u32", _ => "too big", }); }
当区间模式匹配某固定位宽的整型类型和字符型(char
)的整个值域时,此模式是不可反驳型的。例如,0u8..=255u8
是不可反驳型的。某类整型的值区间是从该类型的最小值到该类型最大值的闭区间。字符型(char
)的值的区间就是那些包含所有 Unicode 标量值的区间,即 '\u{0000}'..='\u{D7FF}'
和 '\u{E000}'..='\u{10FFFF}'
。
版次差异:在2021版之前,在带有上下限的区间模式里,可以使用
...
来表达..=
的语义。
Reference patterns
引用模式
句法
ReferencePattern :
(&
|&&
)mut
? PatternWithoutRange
引用模式对当前匹配的指针做解引用,从而能借用它们:
例如,下面 x: &i32
上的两个匹配是等效的:
#![allow(unused)] fn main() { let int_reference = &3; let a = match *int_reference { 0 => "zero", _ => "some" }; let b = match int_reference { &0 => "zero", _ => "some" }; assert_eq!(a, b); }
引用模式的文法产生式(grammar production)要求必须使用 token &&
来匹配对引用的引用,因为 &&
本身是一个单独的 token,而不是两个 &
token。
译者注:举例
let a = Some(&&10); match a { Some( &&value ) => println!("{}", value), None => {} }
引用模式中添加关键字 mut
可对可变引用做解引用。
引用模式中的可变性标记必须与作为匹配对象的那个引用的可变性匹配。
引用模式总是不可反驳型的。
Struct patterns
结构体模式
句法
StructPattern :
PathInExpression{
StructPatternElements ?
}
StructPatternElements :
StructPatternFields (,
|,
StructPatternEtCetera)?
| StructPatternEtCeteraStructPatternFields :
StructPatternField (,
StructPatternField) *StructPatternField :
OuterAttribute *
(
TUPLE_INDEX:
Pattern
| IDENTIFIER:
Pattern
|ref
?mut
? IDENTIFIER
)StructPatternEtCetera :
OuterAttribute *
..
结构体模式匹配与子模式定义的所有条件匹配的结构体值/枚举值/联合体值。它也被用来解构结构体值/枚举值/联合体值。
在结构体模式中,结构体字段需通过名称、索引(对于元组结构体来说)来指代,或者通过使用 ..
来忽略:
#![allow(unused)] fn main() { struct Point { x: u32, y: u32, } let s = Point {x: 1, y: 1}; match s { Point {x: 10, y: 20} => (), Point {y: 10, x: 20} => (), // 顺序没关系 Point {x: 10, ..} => (), Point {..} => (), } struct PointTuple ( u32, u32, ); let t = PointTuple(1, 2); match t { PointTuple {0: 10, 1: 20} => (), PointTuple {1: 10, 0: 20} => (), // 顺序没关系 PointTuple {0: 10, ..} => (), PointTuple {..} => (), } enum Message { Quit, Move { x: i32, y: i32 }, } let m = Message::Quit; match m { Message::Quit => (), Message::Move {x: 10, y: 20} => (), Message::Move {..} => (), } }
如果没使用 ..
,则需要一个用于匹配结构体的结构体模式来指定所有字段:
#![allow(unused)] fn main() { struct Struct { a: i32, b: char, c: bool, } let mut struct_value = Struct{a: 10, b: 'X', c: false}; match struct_value { Struct{a: 10, b: 'X', c: false} => (), Struct{a: 10, b: 'X', ref c} => (), Struct{a: 10, b: 'X', ref mut c} => (), Struct{a: 10, b: 'X', c: _} => (), Struct{a: _, b: _, c: _} => (), } }
用于匹配联合体的结构体模式必须且只能指定一个字段(请参阅在联合体上使用模式匹配)。
ref
和/或 mut
IDENTIFIER 这样的句法格式能匹配任意值,并将其绑定到与给定字段同名的变量上。
#![allow(unused)] fn main() { struct Struct { a: i32, b: char, c: bool, } let struct_value = Struct{a: 10, b: 'X', c: false}; let Struct{a: x, b: y, c: z} = struct_value; // 解构所有的字段 }
如果 PathInExpression 被解析为带有多个变体的枚举的构造函数,或者它的一个子模式是可反驳型的,则此结构体模式是可反驳型的。
Tuple struct patterns
元组结构体模式
句法
TupleStructPattern :
PathInExpression(
TupleStructItems?)
元组结构体模式匹配元组结构体值和枚举值,这些值将与该模式的子模式定义的所有条件进行匹配。 它还被用于解构元组结构体值或枚举值。
当元组结构体模式的一个子模式是可反驳型的,则该元组结构体模式就是可反驳型的。 如果 PathInExpression 被解析为带有多个变体的枚举的构造函数,或者它的一个子模式是可反驳型的,则该元组结构体模式是可反驳型的。
Tuple patterns
元组模式
句法
TuplePattern :
(
TuplePatternItems?)
TuplePatternItems :
Pattern,
| RestPattern
| Pattern (,
Pattern)+,
?
元组模式匹配与子模式定义的所有条件匹配的元组值。它们还被用来解构元组值。
内部只带有一个剩余模式(RestPattern)的元组句法形式 (..)
是一种内部不需要逗号分割的特殊匹配形式,它可以匹配任意长度的元组。
当元组模式的一个子模式是可反驳型的,那该元组模式就是可反驳型的。
使用元组模式的示例:
#![allow(unused)] fn main() { let pair = (10, "ten"); let (a, b) = pair; assert_eq!(a, 10); assert_eq!(b, "ten"); }
Grouped patterns
分组模式
句法
GroupedPattern :
(
Pattern)
将模式括在圆括号内可用来显式控制复合模式的优先级。例如,像 &0..=5
这样的引用模式和区间模式相邻就会引起歧义,这时可以用圆括号来消除歧义。
#![allow(unused)] fn main() { let int_reference = &3; match int_reference { &(0..=5) => (), _ => (), } }
Slice patterns
切片模式
句法
SlicePattern :
[
SlicePatternItems?]
切片模式可以匹配固定长度的数组和动态长度的切片。
#![allow(unused)] fn main() { // 固定长度 let arr = [1, 2, 3]; match arr { [1, _, _] => "从 1 开始", [a, b, c] => "从其他值开始", }; }
#![allow(unused)] fn main() { // 动态长度 let v = vec![1, 2, 3]; match v[..] { [a, b] => { /* 这个匹配臂不适用,因为长度不匹配 */ } [a, b, c] => { /* 这个匹配臂适用 */ } _ => { /* 这个通配符是必需的,因为长度不是编译时可知的 */ } }; }
在匹配数组时,只要每个元素是不可反驳型的,切片模式就是不可反驳型的。当匹配切片时,只有单个 ..
剩余模式或带有 ..
(剩余模式)作为子模式的标识符模式的情况才是不可反驳型的。
在一个切片中,没有下限和上限的区间模式必须用括号括起来,如(a..)
中所示,以明确其目的是与单个切片元素匹配。
带有下限和上限的区间模式,如 a..=b
不需要用括号括起来。
Path patterns
路径模式
句法
PathPattern :
PathExpression
路径模式是指向(refer to)常量值或指向没有字段的结构体或没有字段的枚举变体的模式。
非限定路径模式可以指向:
- 枚举变体
- 结构体
- 常量
- 关联常量
限定路径模式只能指向关联常量。
当路径模式指向结构体或枚举变体(枚举只有一个变体)或类型为不可反驳型的常量时,该路径模式是不可反驳型的。 当路径模式指向的是可反驳型常量或带有多个变体的枚举时,该路径模式是可反驳型的。
Constant patterns
常量模式
当类型为 T
的常量 C
被用作模式时,我们首先检查 T
是否 T: PartialEq
。
此外,我们要求 C
的值 具有(递归)结构相等性(structural equality),其递归定义如下:
- 整型以及
str
、bool
和char
值总是具有结构相等性的。 - 如果元组、数组和切片的所有字段/元素都具有结构相等性,则它具有结构相等性。
(特别是,
()
和[]
总是具有结构相等性。) - 如果引用指向的值具有结构相等性,则该引用具有结构相等性。
- 如果
struct
或enum
类型的值通过#[derive(PartialEq)]
属性派生的PartialEq
实例的所有字段(对于枚举:活动变体的所有字段)都具有结构相等性,则它也具有结构相等性。 - 如果裸指针被定义为常量整数(然后进行转换/转换),则它具有结构相等性。
- 如果浮点值不是
NaN
,则它具有结构相等性。 - 其他的都不具有结构相等性。
特别需要指出的是,在构建模式时(即单态化之前),必须知道 C
的值。
这意味着当涉及泛型参数时,其中的关联常量不能用作模式。
在确保所有条件都满足之后,常量值被转换为模式,现在它的行为就像直接写入了该模式一样。
特别需要指出的是,它还完全参与了详尽性检查。
(对于原始指针,常量是编写此类模式的唯一方式。对于这些类型,只有 _
被认为是详尽的。)
Or-patterns
or模式
_or模式_是能匹配两个或多个并列子模式(例如:A | B | C
)中的一个的模式。
此模式可以任意嵌套。
除了 let
绑定和函数参数(包括闭包参数)中的模式(此时句法上使用 _PatternNoTopAlt_产生式),or模式在句法上允许在任何其他模式出现的地方出现(这些模式句法上使用 _Pattern_产生式)。
Static semantics
静态语义
-
假定在某个代码深度上给定任意模式
p
和q
,现假定它们组成模式p | q
,则以下情况会导致这种组成的非法:- 从
p
推断出的类型和从q
推断出的类型不一致,或 p
和q
引入的绑定标识符不一样,或p
和q
中引入的同名绑定标识符的类型和绑定模式中的类型不一致。
前面提到的所有实例中的类型都必须是精确的,隐式的类型强转在这里不适用。
- 从
-
当对表达式
match e_s { a_1 => e_1, ... a_n => e_n }
做类型检查时,假定在e_s
内部深度为d
的地方存一个表达式片段,那对于此片段,每一个匹配臂a_i
都包含了一个p_i | q_i
来与此段内容进行匹配,但如果表达式片段的类型与p_i | q_i
的类型不一致,则该模式p_i | q_i
被认为是格式错误的。 -
为了遵从匹配模式的穷尽性检查,模式
p | q
被认为同时覆盖了p
和q
。对于某些构造器c(x, ..)
来说,此时应用分配律能使c(p | q, ..rest)
与c(p, ..rest) | c(q, ..rest)
覆盖相同的一组匹配值。这个规律可以递归地应用,直到不再有形式为p | q
的嵌套模式。
注意这里的*“构造器”*这个用词,我们并没有特定提到它是元组结构模式,因为它本意是指任何能够生成类型的模式。这包括枚举变量、元组结构、具有命名字段的结构、数组、元组和切片。
Dynamic semantics
动态语义
- 检查对象表达式(scrutinee expression)
e_s
与深度为d
的模式c(p | q, ..rest)
(这里c
是某种构造器,p
和q
是任意的模式,rest
是c
构造器的任意的可选因子)进行匹配的动态语义与此表达式与c(p, ..rest) | c(q, ..rest)
进行匹配的语法定义相同。
Precedence with other undelimited patterns
无分解符模式的优先级
如本章其他部分所示,有几种类型的模式在语法上没有定义分界符,它们包括标识符模式、引用模式和 or模式。它们组合在一起时,or模式的优先级总是最低的。这允许我们为将来可能的类型特性保留语法空间,同时也可以减少歧义。例如,x @ A(..) | B(..)
将导致一个错误,即 x
不是在所有模式中都存在绑定关系; &A(x) | B(x)
将导致不同子模式中的 x
之的类型不匹配。
请仔细参研匹配表达式中的 MatchExpression产生式,搞清楚匹配臂(MatchArm)的位置。
文字叙述有些晦涩,译者举个例子:假如 if let &Some(y) = &&&Some(3) {
,此时会首先剥掉等号两边的第一层 &
号,然后是 Some(y)
和 &&Some(3)
匹配,此时发现是非引用模式和引用匹配上了,就再对 &&Some(3)
做重复解引用,解出 Some(3)
,然后从外部转向内部,见到最后的变量 y
和检验对象 3
,就更新 y
的默认绑定方式为 ref
,所以 y
就匹配为 &3
;如果我们这个例子的变量 y
改为 ref y
,不影响 y
的绑定效果;极端的情况 if let &Some(y) = &&&Some(x) {
,如果 x
是可变的,那么此时 y
的绑定方式就是 ref mut
,再进一步极端 if let &Some(ref y) = &&&Some(x) {
,此时 y
的绑定方式仍是 ref
。
Type system
类型系统
type-system.md
commit: 4a2bdf896cd2df370a91d14cb8ba04e326cd21db
本章译文最后维护日期:2020-10-28
Types
类型
types.md
commit: b5596cb97e4c94237b54639579ad1853d64fe930
本章译文最后维护日期:2022-08-21
Rust 程序中的每个变量、程序项和值都有一个类型。值的类型定义了该如何解释用于保存它的内存数据,以及定义了可以对该值执行的操作。
内置的类型以非平凡的方式(in nontrivial ways)紧密地集成到语言中,这种方式是不可能在用户定义的类型中模拟的。用户定义的类型功能有限。
内置类型列表:
- 原生类型(primitive types):
- 布尔型(Boolean) —
bool
- 数字类(Numeric) — 整型(integer) 和 浮点型(float)
- 文本类(Textual) — 字符型(
char
) 和 字符串切片(str
) - never类型 —
!
— 没有值的类型
- 布尔型(Boolean) —
- 序列类型(sequence types):
- 用户自定义类型(user-defined types):
- 函数类型(function types):
- 指针类型(pointer types):
- trait类型(Trait types):
Type expressions
类型表达式
句法
Type :
TypeNoBounds
| ImplTraitType
| TraitObjectTypeTypeNoBounds :
ParenthesizedType
| ImplTraitTypeOneBound
| TraitObjectTypeOneBound
| TypePath
| TupleType
| NeverType
| RawPointerType
| ReferenceType
| ArrayType
| SliceType
| InferredType
| QualifiedPathInType
| BareFunctionType
| MacroInvocation
上表中的 Type 文法规则中定义的各种类型表达式都是某个具体类型的句法产生式。它们可以覆盖指代:
- 序列类型(tuple, array, slice)。
- 类型路径(type paths),这些包括:
- 指针类型(引用, 裸指针, 函数指针)。
- 自动推断型类型(inferred type),就是请求编译器确定类型的类型。
- 用来消除歧义的圆括号。
- Trait类型:trait对象(trait object) 和 实现trait(impl trait).
- never型(
!
)。 - 展开成类型表达式的宏。
Parenthesized types
圆括号组合类型
ParenthesizedType :
(
Type)
在某些情况下,类型组合在一起时可能会产生二义性。此时可以在类型周围使用元括号来避免歧义。例如,引用类型的类型约束列表中的 +
运算符搞不清楚其左值的边界位置在哪里,因此需要使用圆括号来明确其边界。这里需要的消歧文法就是使用 TypeNoBounds 句法规则替代 Type 句法规则。
#![allow(unused)] fn main() { use std::any::Any; type T<'a> = &'a (dyn Any + Send); }
Recursive types
递归类型
标称类型(nominal types) — 结构体(struct
)、枚举(enum
)和联合体(union
) — 可以是递归的。也就是说,每个枚举(enum
)变体或结构体(struct
)或联合体(union
)的字段可以直接或间接地指向此枚举(enum
)或结构体(struct
)类型本身。这种递归有一些限制:
- 递归类型必须在递归中包含一个标称类型(不能仅是类型别名或其他结构化的类型,如数组或元组)。因此不允许使用
type Rec = &'static [Rec]
。 - 递归类型的内存宽度必须是有限的;也就是说,类型的递归字段必须是指针类型。
递归类型及使用示例:
#![allow(unused)] fn main() { enum List<T> { Nil, Cons(T, Box<List<T>>) } let a: List<i32> = List::Cons(7, Box::new(List::Cons(13, Box::new(List::Nil)))); }
Boolean type
布尔型
boolean.md
commit: d3a66eb69892b25794b2d82a1249ec01d8ead9f1
本章译文最后维护日期:2023-11-05
#![allow(unused)] fn main() { let b: bool = true; }
布尔型或布尔数是一种可以为*真(true
)或假(false
)*的原语数据类型。
这种类型的值可以使用字面量表达式创建,使用关键字 true
和 false
来表达对应名称的值。
该类型是此语言的预导入包的一部分,使用名称 bool
来表示。
布尔型的对象内存宽度和对齐量均为1。false 的位模式为 0x00
, true 的位模式为 0x01
。其他的任何其他位模式的布尔型的象都是未定义的行为。
布尔型是多种表达式的操作数的类型:
注意:布尔型的行为类似于枚举类型,但它确实不是枚举类型。在实践中,这主要意味着构造函数不与类型相关联(例如没有
bool::true
这种写法)。
和其他所有的原语类型一样,布尔型实现了 Clone
、Copy
、Sized
、Send
和 Sync
这些 traits。
注意: 参见标准库文档中的相关操作运算。
Operations on boolean values
布尔运算
当使用带有布尔型的操作数的特定操作符表达式时,它们使用[布尔逻辑规则][boolean logic]进行计算。Logical not
逻辑非
b | !b |
---|---|
true | false |
false | true |
Logical or
逻辑或
a | b | a | b |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
Logical and
逻辑与
a | b | a & b |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
Logical xor
逻辑异或
a | b | a ^ b |
---|---|---|
true | true | false |
true | false | true |
false | true | true |
false | false | false |
Comparisons
比较
a | b | a == b |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | true |
a | b | a > b |
---|---|---|
true | true | false |
true | false | true |
false | true | false |
false | false | false |
a != b
等同于!(a == b)
a >= b
等同于a == b | a > b
a < b
等同于!(a >= b)
a <= b
等同于a == b | a < b
Bit validity
位有效性
Rust会保证 bool
类型的单个字节被初始化(换句话说,transmute::<bool, u8>(...)
总是健全(sound)的——但由于一些位模式是无效的 bool
,因此相反的操作并不总是健全的)。
Numeric types
数字型
mumeric.md
commit: 80ec1463a515858cbb49ac4c9f601a75992893b2
本章译文最后维护日期:2023-11-05
Integer types
整型/整数类型
无符号整数类型:
类型 | 最小值 | 最大值 |
---|---|---|
u8 | 0 | 28-1 |
u16 | 0 | 216-1 |
u32 | 0 | 232-1 |
u64 | 0 | 264-1 |
u128 | 0 | 2128-1 |
有符号二进制补码整型包括:
类型 | 最小值 | 最大值 |
---|---|---|
i8 | -(27) | 27-1 |
i16 | -(215) | 215-1 |
i32 | -(231) | 231-1 |
i64 | -(263) | 263-1 |
i128 | -(2127) | 2127-1 |
Floating-point types
浮点型
Rust 对应 IEEE 754-2008 的“binary32”和“binary64”浮点类型分别是 f32
和 f64
。
Machine-dependent integer types
和计算平台相关的整型
usize
类型是一种无符号整型,其宽度与平台的指针类型的宽度相同。它可以表示进程中的每个内存地址。
isize
类型是一种有符号整型,其宽度与平台的指针类型的宽度相同。(Rust 规定)对象的内存宽度和数组的长度的理论上限是 isize
的最大值。这确保了 isize
可以用来计算指向对象或数组内部的指针之间的差异,并且可以寻址对象中的每个字节以及末尾之后的那个字节。
usize
和 isize
的宽度至少是 16-bits。
注意:许多 Rust 代码可能会假设指针、
usize
和isize
是 32-bit 或 64-bit 的。因此,16-bit 指针的支持是有限的,这部分支持可能需要来自库的明确关注和确认。
Bit validity
位有效性
对于每种数字类型 T
,T
的位有效性等同于 [u8; size_of::<T>()]
的位有效性。未初始化的字节不是有效的 u8
。
Textual types
文本类类型
textual.md
commit: 192178f936f0057932e117183f774f0e2cce832c
本章译文最后维护日期:2023-11-05
类型 char
和 str
用于保存文本数据。
字符型(char
)的值是 Unicode 标量(scalar)值(即不是代理项(surrogate)的代码点),可以表示为 0x0000~0xD7FF 或 0xE000~0x10FFFF 范围内的 32-bit 无符号字符。创建超出此范围的字符直接触发未定义行为(Undefined Behavior)。一个 [char]
实际上是长度为1的 UCS-4 / UTF-32 字符串。
str
类型的值的表示方法与 [u8]
相同,也一个 8-bit 无符号字节类型的切片。但是,Rust 标准库对 str
做了额外的假定:str
上的方法会假定并确保其中的数据是有效的 UTF-8。调用 str
的方法来处理非UTF-8 缓冲区上的数据可能或早或晚地出现未定义行为。
由于 str
是一个动态内存宽度类型,所以它只能通过指针类型实例化,比如 &str
。
Layout and bit validity
内存布局和位有效性
char
在所有平台上都保证具有与 u32
相同的尺寸和对齐方式。
char
的每个字节都保证会被初始化(换句话说,transmute::<char, [u8; size_of::<char>()]>(...)
总是正确的——但由于某些位模式是无效的 char
,因此相反的操作并不总是正确的)。
Never type
never类型
never.md
commit: 6eacdb0d8551dcdb9e9782e4abad60ec17ddb214
本章译文最后维护日期:2023-07-21
句法
NeverType :!
never类型(!
)是一个没有值的类型,表示永远不会完成计算的结果。!
的类型表达式可以强转为任何其他类型。
类型!
目前只能出现在函数返回类型中,这表明它是一个从不真正返回的发散函数。
#![allow(unused)] fn main() { fn foo() -> ! { panic!("This call never returns."); } }
#![allow(unused)] fn main() { extern "C" { pub fn no_return_extern_func() -> !; } }
Tuple types
元组类型
tuple.md
commit: 80f4867bcc2baaa9bcdb650c43276cd98883389c
本章译文最后维护日期:2021-07-24
元组类型是由其他类型的异构列表组合成的一类结构化类型1。
元组类型的语法规则为一对圆括号封闭的逗号分割的类型列表。 为和圆括号类型区分开来,一元元组的元素类型后面需要有一个逗号。
元组类型的字段数量等同于其封闭的异构类型列表的长度。
字段的数量决定元组的元数(arity)。
有 n
个字段的元组叫做 n元元组(n-ary tuple)。
例如,有两个字段的元组就是二元元组。
元组的字段用它在列表中的位置数字来索引。
第一个字段索引为 0
。
第二个字段索引为 1
。
然后以此类推。
每个字段的类型都是元组类型列表中相同位置的类型。
出于方便和历史原因,不带元素(()
)的元组类型通常被称为单元(unit)或单元类型(unit type)。
它的值也被称为单元或单元值。
元组类型的示例:
()
(单元)(f64, f64)
(String, i32)
(i32, String)
(跟前一个示例类型不一样)(i32, f64, Vec<String>, Option<bool>)
这种类型的值是使用元组表达式来构造的。 此外,如果没有其他有意义的值可供求得/返回,很多种表达式都将生成单元值。 元组字段可以通过元组索引表达式或模式匹配来访问。
结构化类型的特点就是其内部对等位置的类型如果是相等的,那么这些结构化类型就是相等的。有关元组的标称类型版本,请参见元组结构体。
Array types
数组类型
array.md
commit: 2f459e22ec30a94bafafe417da4e95044578df73
本章译文最后维护日期:2020-11-14
句法
ArrayType :
[
Type;
Expression]
数组是 N
个类型为 T
的元素组成的固定长度(fixed-size)的序列,数组类型写为 [T; N]
。长度(size)是一个计算结果为 usize
的常量表达式。
示例:
#![allow(unused)] fn main() { // 一个栈分配的数组 let array: [i32; 3] = [1, 2, 3]; // 一个堆分配的数组,被自动强转成切片 let boxed_array: Box<[i32]> = Box::new([1, 2, 3]); }
数组的所有元素总是初始化过的,使用 Rust 中的安全(safe)方法或操作符来访问数组时总是会先做越界检查。
注意:标准库类型
Vec<T>
提供了堆分配方案的可调整大小的数组类型。
Slice types
切片类型
slice.md
commit: 5ae4a38e36135e02e01933ba81d206b1c2e9ce70
本章译文最后维护日期:2021-07-31
句法
SliceType :
[
Type]
切片是一种动态内存宽度类型(dynamically sized type),它代表类型为 T
的元素组成的数据序列的一个“视图(view)”。切片类型写为 [T]
。
切片类型通常都是通过指针类型来使用,例如:
&[T]
,共享切片('shared slice'),常被直接称为切片(slice
)。它不拥有它指向的数据,只是借用。&mut [T]
,可变切片('mutable slice')。它可变借用它指向的数据。Box<[T]>
, 装箱的切片('boxed slice')。
示例:
#![allow(unused)] fn main() { // 一个堆分配的数组,被自动强转成切片 let boxed_array: Box<[i32]> = Box::new([1, 2, 3]); // 数组上的(共享)切片 let slice: &[i32] = &boxed_array[..]; }
切片的所有元素总是初始化过的,使用 Rust 中的安全(safe)方法或操作符来访问切片时总是会做越界检查。
Struct types
结构体类型
struct.md
commit: 8a98437835db5f3cf57044aedcd1e00bd0d889f9
本章译文最后维护日期:2021-1-17
结构体(struct
)类型是其由他类型异构产生的类型,这些其他类型被称为结构体类型的字段。1
结构体(struct
)的实例可以用结构体表达式来构造。
默认情况下,结构体(struct
)的内存布局是未定义的(默认允许进行一些编译器优化,比如字段重排),但也可以使用repr
属性来使其布局在定义时就固定下来。在这两种情况下,字段在相应的结构体表达式中都可以以任何顺序给出;(但在相同的编译目标中,在)同一布局规则下生成的结构体(struct
)值将始终具有相同的内存布局。
结构体(struct
)的字段可以由可见性修饰符(visibility modifiers)限定,以允许从模块之外来访问结构体中的数据。
*元组结构体(uple struct)*类型与结构体类型类似,只是字段是匿名的。
*单元结构体(unit-like struct)*类型类似于结构体类型,只是它没有字段。由初始结构体表达式构造的值是驻留在此类类型中惟一的值。
struct
类型类似于 C 中的 struct
类型、ML家族的 record 类型或 Lisp 家族的 struct 类型。
Enumerated types
枚举类型
enum.md
commit: d8cbe4eedb77bae3db9eff87b1238e7e23f6ae92
本章译文最后维护日期:2021-2-21
枚举类型是一种标称型(nominal)的、异构的、不相交的类型联合起来组成的类型,它直接用枚举(enum
)程序项的名称来表示。1
枚举(enum
)程序项同时声明了类型和它的各种变体(variants),其中每个变体都独立命名,可使用定义结构体、元组结构体或单元结构体(unit-like struct)的句法来定义它们。
枚举(enum
)的实例可以通过结构体体表达式来构造。
任何枚举值消耗的内存和其同类型的其他变体都是相同的,具体都为其枚举(enum
)类型的最大变体所需的内存再加上存储其判别值(discriminant)所需的内存。
枚举类型不能在结构上表示为类型,必须通过对枚举程序项的具名引用(named reference)来表示。2
enum
类型类似于 ML 中的数据(data
)构造函数声明,或 Limbo 中的 pick ADT。
译者理解这句话的意思是:枚举不同于普通结构化的类型,所有的枚举类型都是对枚举程序项的引用;这里引用分两种,一种是类C枚举,就是对程序项的直接具名引用;另一种是带字段的枚举变体,这种其实是类似于 Box
、Rc
这样的具名引用,它通过封装其他类型来指导数据的存储和限定其上可用的操作。
Union types
联合体类型
union.md
commit: 969e45e09c68a167ecf456901f490cd6a21a70a2
本章译文最后维护日期:2022-08-21
联合体类型是一种标称型(nominal)的、异构的、类似C语言里的 union 的类型,具体的类型名称由联合体(union
)程序项的名称表示。
联合体没有“活跃字段(active field)”的概念。相反,每次对联合体的访问都会将联合体的部分存储内容转化为被访问字段的类型。
由于这种转化可能会导致意外或未定义行为,所以读取联合体字段需要用到 unsafe
,联合体字段的类型也被限制为一些类型子集,这确保了它们永远不需要销毁。有关详细信息,请参阅程序项文档。
默认情况下,联合体(union
)的内存布局是未定义的(特别是它的字段可以不从相对的0地址开始),但是可以使用 #[repr(...)]
属性来固定为某一类型布局。
Function item types
函数项类型
function-item.md
commit: b0e0ad6490d6517c19546b1023948986578fc378
本章译文最后维护日期:2020-11-14
当被引用函数项、元组结构体的构造函数或枚举变体的构造函数时,会产生它们的*函数项类型(function item type)*的零内存宽度的值。这种类型(也就是此值)显式地标识了该函数——标识出的内容包括程序项定义时的名字、类型参数,及其定义时的生存期参数(不是后期绑定的生存期参数,后期绑定的生存期参数只在函数被调用时才被赋与)——所以该值不需要包含一个实际的函数指针,当此函数被调用时也不需要一个间接的寻址操作去查找此函数。
没有直接引用函数项类型的句法,但是编译器会在错误消息中会显示类似于 fn(u32) -> i32 {fn_name}
这样的“类型”。
因为函数项类型显式地标识了其函数,所以如果它们所指的程序项不同(包括不同的程序项,或相同的程序项但泛型参数的实际类型不同),则此函数项类型也不同,混合使用它们将导致类型错误:
#![allow(unused)] fn main() { fn foo<T>() { } let x = &mut foo::<i32>; *x = foo::<u32>; //~ 错误:类型不匹配 }
但确实存在从函数项类型到具有相同签名的函数指针的自动强转(coercion),这种自动强转一般发生在使用函数项类型却直接预期函数指针时;另一种发生情况是 if
表达式或匹配(match
)表达式的不同分支返回使用了具有相同签名却使用了不同函数项类型的情况:
#![allow(unused)] fn main() { let want_i32 = false; fn foo<T>() { } // 这里 `foo_ptr_1` 标注使用了 `fn()` 这个函数指针类型。 let foo_ptr_1: fn() = foo::<i32>; // ... `foo_ptr_2` 也行 - 这次是通过类型检查(type-checks)做到的。 let foo_ptr_2 = if want_i32 { foo::<i32> } else { foo::<u32> }; }
所有的函数项类型都实现了 Fn
、FnMut
、FnOnce
、Copy
、Clone
、Send
和 Sync
。
Closure types
闭包类型
closure.md
commit: e4833e4076305e4554baf480f7443eb82f09a28c
本章译文最后维护日期:2021-07-31
闭包表达式生成的闭包值具有唯一性和无法写出的匿名性。闭包类型近似相当于包含捕获变量的结构体。比如以下闭包示例:
#![allow(unused)] fn main() { fn f<F : FnOnce() -> String> (g: F) { println!("{}", g()); } let mut s = String::from("foo"); let t = String::from("bar"); f(|| { s += &t; s }); // 打印 "foobar". }
生成大致如下所示的闭包类型:
struct Closure<'a> {
s : String,
t : &'a String,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
fn call_once(self) -> String {
self.s += &*self.t;
self.s
}
}
所以调用 f
相当于:
f(Closure{s: s, t: &t});
Capture modes
捕获方式
编译器倾向于优先通过不可变借用(immutable borrow)来捕获闭合变量(closed-over variable),其次是通过唯一不可变借用(unique immutable borrow)(见下文),再其次可变借用(mutable borrow),最后使用移动语义(move)来捕获。编译器将选择与捕获的变量在闭包体中的使用方式兼容的第一种捕获方式。编译器不会考虑周围的代码,比如所涉及的变量的生存期,或者闭包本身的生存期。
如果使用了关键字 move
,那么所有捕获都是通过移动(move)语义进行的(当然对于 Copy
类型,则是通过拷贝语义进行的),而不管借用是否可用。关键字 move
通常用于允许闭包比其捕获的值活得更久,例如返回闭包或用于生成新线程。
复合类型(如结构体、元组和枚举)始终是全部捕获的,而不是各个字段分开捕获的。如果真要捕获单个字段,那可能需要先借用该字段到本地局部变量中:
#![allow(unused)] fn main() { use std::collections::HashSet; struct SetVec { set: HashSet<u32>, vec: Vec<u32> } impl SetVec { fn populate(&mut self) { let vec = &mut self.vec; self.set.iter().for_each(|&n| { vec.push(n); }) } } }
相反,如果闭包直接使用了 self.vec
,那么它将尝试通过可变引用捕获 self
。但是因为 self.set
已经被借出用来迭代了,所以代码将无法编译。
Unique immutable borrows in captures
捕获中的唯一不可变借用
捕获方式中有一种被称为唯一不可变借用的特殊类型的借用捕获,这种借用不能在语言的其他任何地方使用,也不能显式地写出。唯一不可变借用发生在修改可变引用的引用对象(referent)时,如下面的示例所示:
#![allow(unused)] fn main() { let mut b = false; let x = &mut b; { let mut c = || { *x = true; }; // 下行代码不正确 // let y = &x; c(); } let z = &x; }
在这种情况下,不能去可变借用 x
,因为 x
没有标注 mut
。但与此同时,如果不可变借用 x
,那对其赋值又会非法,因为 & &mut
引用可能不是唯一的,因此此引用不能安全地用于修改值。所以这里闭包使用了唯一不可变借用:它采用了不可变的方式借用了 x
,但是又像可变借用一样,当然前提是此借用必须是唯一的。在上面的例子中,解开 y
那行上的注释将产生错误,因为这将违反闭包对 x
的借用的唯一性;z
的声明是有效的,因为闭包的生存期在块结束时已过期,从而已经释放了对 x
的借用。
Call traits and coercions
调用trait 和自动强转
闭包类型都实现了 FnOnce
,这表明它们可以通过消耗掉闭包的所有权来调用执行它一次。此外,一些闭包实现了更具体的调用trait:
-
对没采用移动语义来捕获任何变量(译者注:变量的原始所有者仍拥有该变量的所有权)的闭包都实现了
FnMut
,这表明该闭包可以通过可变引用来调用。 -
对捕获的变量没移出其值,也没去修改其值的闭包都实现了
Fn
,这表明该闭包可以通过共享引用来调用。
注意:
move
闭包可能仍然实现Fn
或FnMut
,即使它们通过移动(move)语义来捕获变量。这是因为闭包类型实现什么样的 trait 是由闭包对捕获的变量值做了什么来决定的,而不是由闭包如何捕获它们来决定的。1
*非捕获闭包(Non-capturing closures)*是指不捕获环境中的任何变量的闭包。它们可以通过匹配签名的方式被自动强转成函数指针(例如 fn()
)。
#![allow(unused)] fn main() { let add = |x, y| x + y; let mut x = add(5,7); type Binop = fn(i32, i32) -> i32; let bo: Binop = add; x = bo(5,7); }
Other traits
其他 trait
所有闭包类型都实现了 Sized
。此外,闭包捕获的变量的类型如果实现了如下 trait,此闭包类型也会自动实现这些 trait:
闭包类型实现 Send
和 Sync
的规则与普通结构体类型实现这俩 trait 的规则一样,而 Clone
和 Copy
就像它们在 derived
属性中表现的一样。对于 Clone
,捕获变量的克隆顺序目前还没正式的规范出来。
由于捕获通常是通过引用进行的,因此会出现以下一般规则:
- 如果所有的捕获变量都实现了
Sync
,则此闭包就也实现了Sync
。 - 如果所有非唯一不可变引用捕获的变量都实现了
Sync
,并且所有由唯一不可变、可变引用、复制或移动语义捕获的值都实现了Send
,则此闭包就也实现了Send
。 - 如果一个闭包没有通过唯一不可变引用或可变引用捕获任何值,并且它通过复制或移动语义捕获的所有值都分别实现了
Clone
或Copy
,则此闭包就也实现了Clone
或Copy
。
译者的实验代码:
#![allow(unused)] fn main() { fn f1<F : Fn() -> i32> (g: F) { println!("{}", g());} fn f2<F : FnMut() -> i32> (mut g: F) { println!("{}", g());} let t = 8; f1(move || { t }); f2(move || { t }); }
Pointer types
指针类型
pointer.md
commit: 13b5af85c57df0898f808651f5126c7fe7568349
本章译文最后维护日期:2023-11-05
Rust 中所有的指针都是显式的一等(first-class)值。 它们可以被移动或复制,存储到其他数据结构中,或从函数中返回。
References (&
and &mut
)
引用(&
和 &mut
)
句法
ReferenceType :
&
Lifetime?mut
? TypeNoBounds
Shared references (&
)
共享引用(&
)
共享引用指向由其他值拥有的内存。
当创建了对值的共享引用后,它可以阻止对该值的直接更改。
但在某些特定情况下,内部可变性又提供了这种情况的一种例外。
顾名思义,对一个值的共享引用的次数没有限制。共享引用类型被写为 &type
;当需要指定显式的生存期时可写为 &'a type
。
拷贝一个引用是一个“浅拷贝(shallow)”操作:它只涉及复制指针本身,也就是指针实现了 Copy
trait 的意义所在。
释放引用对共享引用所指向的值没有影响,但是对临时值的引用的存在将使此临时值在此引用的作用域内保持存活状态。
Mutable references (&mut
)
可变引用(&mut
)
可变引用指向其他值所拥有的内存。
可变引用类型被写为 &mut type
或 &'a mut type
。
可变引用(其还未被借出1)是访问它所指向的值的唯一方法,所以可变引用没有实现 Copy
trait。
Raw pointers (*const
and *mut
)
裸指针(*const
和 *mut
)
句法
RawPointerType :
*
(mut
|const
) TypeNoBounds
裸指针是没有安全性或可用性(liveness)保证的指针。
裸指针写为 *const T
或 *mut T
,例如,*const i32
表示指向 32-bit 有符号整数的裸指针。
拷贝或销毁(dropping )裸指针对任何其他值的生命周期(lifecycle)都没有影响。对裸指针的解引用是非安全(unsafe
)操作,可以通过重新借用裸指针(&*
或 &mut *
)将其转换为引用。
通常不鼓励使用裸指针;
它们的存在是为了提升与外部代码的互操作性,以及编写对性能要求很高的函数或很底层的函数。
在比较裸指针时,比较的是它们的地址,而不是它们指向的数据。 当比较裸指针和动态内存宽度类型时,还会比较它们指针上的附加/元数据。
可以直接使用 core::ptr::addr_of!
创建 *const
类型的裸指针,通过 core::ptr::addr_of_mut!
创建 *mut
类型的裸指针。
Smart Pointers
智能指针
标准库包含了一些额外的“智能指针”类型,它们提供了在引用和裸指针这类低级指针之外的更多的功能。
Bit validity
位有效性
Despite pointers and references being similar to usize
s in the machine code emitted on most platforms,
the semantics of transmuting a reference or pointer type to a non-pointer type is currently undecided.
Thus, it may not be valid to transmute a pointer or reference type, P
, to a [u8; size_of::<P>()]
.
尽管指针和引用类似于在大多数平台上发出的机器码中的 usize
,将引用或指针类型转换为非指针类型的语义目前尚未确定。
因此,将指针或引用类型 P
转换为 [u8; size_of::<P>()]
可能是无效的。
对于瘦裸指针(比如对于P = *const T
或 P = *mut T
,其中 T: Sized
),逆向转换(从整数或整数数组强转为 P
)总是有效的。
然而,通过这种转换产生的指针可能不能被解引用(即使 T
的尺寸为零也不行)。
译者理解这里是指 &mut type
如果被借出,就成了 &&mut type
,这样就又成了不可变借用了。
Function pointer types
函数指针类型
function-pointer.md
commit: f24b8d4ca7aeb5340db673c7364b19e056b4de48
本章译文最后维护日期:2022-10-23
句法
BareFunctionType :
ForLifetimes? FunctionTypeQualifiersfn
(
FunctionParametersMaybeNamedVariadic?)
BareFunctionReturnType?FunctionTypeQualifiers:
unsafe
? (extern
Abi?)?BareFunctionReturnType:
->
TypeNoBoundsFunctionParametersMaybeNamedVariadic :
MaybeNamedFunctionParameters | MaybeNamedFunctionParametersVariadicMaybeNamedFunctionParameters :
MaybeNamedParam (,
MaybeNamedParam )*,
?MaybeNamedParam :
OuterAttribute* ( ( IDENTIFIER |_
):
)? TypeMaybeNamedFunctionParametersVariadic :
( MaybeNamedParam,
)* MaybeNamedParam,
OuterAttribute*...
函数指针类型(使用关键字 fn
写出)指向那些在编译时不必知道函数标识符的函数。它们也可以由函数项类型或非捕获(non-capturing)闭包经过一次自动强转(coercion)来创建。
非安全(unsafe
)限定符表示类型的值是一个非安全函数,而外部(extern
)限定符表示它是一个外部函数。
可变参数只能通过使用 "C"
或 "cdecl"
的 ABI调用约定的 extern
函数类型来指定。
下面示例中 Binop
被定义为函数指针类型:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { x + y } let mut x = add(5,7); type Binop = fn(i32, i32) -> i32; let bo: Binop = add; x = bo(5,7); }
Attributes on function pointer parameters
函数指针参数上的属性
函数指针参数上的属性遵循与常规函数参数相同的规则和限制。
Trait objects
trait对象
trait-object.md
commit: 364066f09c801acd45c656a345301f8cdb1e5870
本章译文最后维护日期:2022-08-21
句法
TraitObjectType :
dyn
? TypeParamBoundsTraitObjectTypeOneBound :
dyn
? TraitBound
*trait对象1*是另一种类型的不透明值(opaque value),它实现了一组 trait。2 这组 trait 是由一个对象安全的基础trait(base trait) 加上任意数量的自动trait(auto traits)组成。
trait对象实现了基础trait、它的自动trait 以及其基础trait 的任何超类trait(supertraits)。
trait对象被写为关键字 dyn
后跟一组 trait约束,这些 trait约束有如此限制:除了第一个 trait 外,其他所有 trait 都必须是自动trait;生存期不能超过一个;不允许选择退出约束(opt-out bounds)(例如 ?Sized
)。此外,trait 的路径可以用圆括号括起来。
例如,给定一个trait Trait
,下面所有的形式都是 trait对象:
dyn Trait
dyn Trait + Send
dyn Trait + Send + Sync
dyn Trait + 'static
dyn Trait + Send + 'static
dyn Trait +
dyn 'static + Trait
.dyn (Trait)
版次差异: 在2021之前的版次中, 关键字
dyn
可以省略。注意: 除非你的代码库需要在 1.26 或更低版本的 Rust 下编译,否则为了表意清晰,强烈建议总是给 trait对象加上
dyn
关键字。
版次差异:在 2015 版里,如果 trait对象的第一个约束是以
::
开头的路径,那么dyn
会被视为路径的一部分。可以把第一条路径放在圆括号中来绕过这个问题。因此,如果希望 trait对象具有::your_module::Trait
路径,那么应该将其写为dyn (::your_module::Trait)
。从2018版开始,
dyn
是一个真正的关键字了,不允许在路径中使用,(不存在二义性了,)因此括号就没必要了。
如果基础trait 互为别名,并且自动trait 相同,生存期约束也相同,则这两种 trait对象类型互为别名。例如,dyn Trait + Send + UnwindSafe
和 dyn Trait + UnwindSafe + Send
是等价的。
由于值的具体类型是不透明的,trait对象是动态内存宽度类型。像所有的 DST 一样,trait对象常被用在某种类型的指针后面;例如 &dyn SomeTrait
或 Box<dyn SomeTrait>
。每个指向 trait对象的指针实例包括:
- 一个指向实现
SomeTrait
的(那个真实的不透明的)类型T
的实例的指针 - 一个指向*虚拟方法表(virtual method table_)*的指针。虚拟方法表也通常被称为 虚函数表(vtable),它包含了
T
实现的SomeTrait
的所有方法,T
实现的SomeTrait
的超类trait 的每个方法,还有指向T
的实现的指针(即函数指针)。
trait对象的目的是允许方法的“延迟绑定(late binding)”。在 trait对象上调用一个方法会导致运行时的虚拟分发(virtual dispatch):也就是说,一个函数指针从 trait对象的虚函数表(vtable)中被加载进来,并被间接调用。每个虚函数表实体的实际实现可能因对象的不同而不同。
一个 trait对象的例子:
trait Printable { fn stringify(&self) -> String; } impl Printable for i32 { fn stringify(&self) -> String { self.to_string() } } fn print(a: Box<dyn Printable>) { println!("{}", a.stringify()); } fn main() { print(Box::new(10) as Box<dyn Printable>); }
在本例中,trait Printable
作为 trait对象出现在 以 print
为类型签名的函数的参数中 和 main
中的类型转换表达式(cast expression)中。
Trait Object Lifetime Bounds
trait对象的生存期约束
因为 trait对象可以包含引用,所以这些引用的生存期需要表示为 trait对象的一部分。这种生存期被写为 Trait + 'a
。默认情况下,可以通过合理的选择来推断此生存期。
本书行文中,使用“trait对象”这个词时,没区分 trait对象的值和类型本身,但一般都指类型。
译者认为这样翻译可能更容易理解:trait对象是丢失了/隐藏了具体真实类型的 trait实现的类型,此类型本身一般会实现了一组 trait。
Impl trait
impl-object.md
commit: d82c9ac4054136efa6e9cc908ca3c589ed862d5a
本章译文最后维护日期:2023-11-05
句法
ImplTraitType :impl
TypeParamBoundsImplTraitTypeOneBound :
impl
TraitBound
impl Trait
提供了指定实现某trait 的具体的但匿名的类型的方法。
它可以出现在两种位置上:参数位置(在这里它可以充当函数的匿名类型参数)和返回位置(在这里它可以充当抽象返回类型)。
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} // 参数位置:匿名类型参数 fn foo(arg: impl Trait) { } // 返回位置:抽象返回类型 fn bar() -> impl Trait { } }
Anonymous type parameters
匿名类型参数
注意:匿名类型参数通常被称为“参数位置上的 trait约束(impl Trait in argument position)”。 (术语“参数”在这里更正式和正确,但 “impl Trait in argument position” 是在开发该特性时就使用的措辞,所以它仍保留在部分实现中。)
函数可以使用 impl
后跟一组 trait约束,将参数声明为具有某匿名类型。
调用者必须提供一个满足匿名类型参数声明的约束的类型,并且在函数内只能使用该匿名参数类型的 trait约束内部声明的方法。
例如,下面两种形式几乎等价:
#![allow(unused)] fn main() { trait Trait {} // 泛型类型参数 fn with_generic_type<T: Trait>(arg: T) { } // 参数位置上的trait fn with_impl_trait(arg: impl Trait) { } }
也就是说,参数位置上的 impl Trait
是泛型类型参数(如 <T: Trait>
)的语法糖,只是该类型是匿名的,并且不出现在 GenericParams 列表中。
注意: 对于函数参数,泛型类型参数和
impl Trait
并不完全等价。 使用诸如<T:Trait>
之类的泛型参数,调用者可以在调用点使用 GenericArgs(例如,foo::<usize>(1)
)形式来显式指定T
的泛型参数。 如果impl Trait
是任意函数参数的类型,则调用者在调用该函数时不能提供任何泛型参数。这同样适用于返回类型或任何常量泛型的泛型参数。因此,这样将函数签名从一个变更为另一个,对函数的调用者来说仍可能是一种破坏性的变更。
Abstract return types
抽象返回类型
注意: 这通常被称为“返回位置上的 Trait约束”(impl Trait in return position)。
函数可以使用 impl Trait
返回抽象返回类型。
这些类型代表另一个具体类型,调用者只能使用指定的 Trait
内声明的方法。
函数的每个可能的返回分支返回的返回值都必须解析为相同的具体类型。
返回位置上的 impl Trait
允许函数返回不用装箱的抽象类型。
这对于闭包和迭代器特别有用。
例如,闭包具有唯一的、不可写的类型。
以前,从函数返回闭包的唯一方法是使用trait对象:
#![allow(unused)] fn main() { fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } }
这不得不承受因堆分配和动态调度而带来的性能损失。
并且者无法完全指定闭包的类型,只能使用 Fn
trait。
这意味着此时 trait对象是必要的。
但是,使用 impl Trait
,可以像如下这样来更简单地编写代码:
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
这也避免了对 trait对象进行装箱的缺陷。
类似地,在迭代器的使用坏境中,如果将此步操作前的所有迭代器操作合并到一个执行链中,迭代器里的具体类型可能变得非常复杂。此时返回 impl Iterator
意味着一个函数只需将 Iterator
trait 作为其返回类型的约束来公开,而不须显式指定链内所有其他涉及的迭代器类型。
Return-position impl Trait
in traits and trait implementations
trait 和 trait实现 的返回位置上的impl Trait
trait中的函数也可以使用 impl Trait
作为匿名关联类型的句法。
trait中关联函数的返回类型中的每个 impl Trait
都被脱糖为匿名关联类型。出现在实现中的函数签名中的返回类型用于确定关联类型的确定类型。
Differences between generics and impl Trait
in return position
泛型与 impl Trait
在返回位置上的差异
在参数位置,impl Trait
在语义上与泛型类型参数非常相似。
然而,两者在返回位置上存在显著差异。
与泛型类型参数不同,使用 impl Trait
时,函数选择返回类型,调用者不能选择返回类型。
泛型函数:
#![allow(unused)] fn main() { trait Trait {} fn foo<T: Trait>() -> T { // ... panic!() } }
允许调用者来指定返回类型T
,然后函数返回该类型。
impl Trait
函数:
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} fn foo() -> impl Trait { // ... } }
不允许调用者指定返回类型。
相反,函数自身选择返回类型,但只承诺返回类型将实现 Trait
。
Limitations
限制
impl Trait
只能作为 非extern
函数的参数类型或返回类型出现。
它不能 let
绑定、成员字段的类型,也不能出现在类型别名中。
Type parameters
类型参数
parameters.md
commit: eb02dd5194a747277bfa46b0185d1f5c248f177b
本章译文最后维护日期:2020-11-14
在带有类型参数声明的程序项的代码体内,这些类型参数的名称可以直接当做类型使用:
#![allow(unused)] fn main() { fn to_vec<A: Clone>(xs: &[A]) -> Vec<A> { if xs.is_empty() { return vec![]; } let first: A = xs[0].clone(); let mut rest: Vec<A> = to_vec(&xs[1..]); rest.insert(0, first); rest } }
这里,first
的类型为 A
,援引的是 to_vec
的类型参数 A
;rest
的类型为 Vec<A>
,它是一个元素类型为 A
向量(vector)。
Inferred type
自动推断型类型
parameters.md
commit: 43dc1a42f19026f580e34a095e91804c3d6da186
本章译文最后维护日期:2020-11-14
句法
InferredType :_
自动推断型类型要求编译器尽可能根据周围可用的信息推断出实际使用类型。它不能用于程序项的签名中。它经常用于泛型参数中:
#![allow(unused)] fn main() { let x: Vec<_> = (0..10).collect(); }
Dynamically Sized Types
动态内存宽度类型
dynamically-sized-types.md
commit: 83f725f1b9dda6166589d7b715b75b7f54143b8e
本章译文最后维护日期:2021-07-31
大多数的类型都有一个在编译时就已知的固定内存宽度,并实现了 trait Sized
。只有在运行时才知道内存宽度的类型称为动态内存宽度类型(dynamically sized type)(DST),或者非正式地称为非固定内存宽度类型(unsized type)。切片和 trait对象是 DSTs 的两个例子。此类类型只能在某些情况下使用:
- 指向 DST 的指针类型的内存宽度是固定的(sized),但是是指向固定内存宽度类型的指针的内存宽度的两倍
- 指向切片的指针也存储了切片的元素的数量。
- 指向 trait对象的指针也存储了一个指向虚函数表(vtable)的指针地址
- DST 可以作为类型实参( type arguments)来传给有
?Sized
约束的泛型参数。 当关联类型的声明有?size
约束时,它们也可以被用于关联类型定义。 默认情况下,任何类型参数或关联类型都有Sized
约束,除非它们使用了Sized
来放宽其约束。 - 可以为 DSTs 实现 trait。
与泛型类型参数中的默认设置不同,在 trait定义中,
Self: ?Sized
约束是默认情况。 - 结构体可以包含一个 DST 作为最后一个字段;这使得该结构体也成为是一个 DST。
Type Layout
类型布局
type-layout.md
commit: a432cf4afdb6f0f452de19c4d123fae81a840d50
本章译文最后维护日期:2024-05-02
类型的布局描述类型的内存宽度(size)、对齐量(alignment)和字段(fields)的相对偏移量(relative offsets)。对于枚举,其判别值(discriminant)的布局和解释也是类型布局的一部分。
每次编译都有可能更改类型布局。这里我们只阐述当前编译器所保证的内容,而没试图去阐述编译器对此做了什么。
Size and Alignment
内存宽度和对齐量
所有值都有一个对齐量和内存宽度。
值的对齐量指定了哪些地址可以有效地存储该值。对齐量为 n
的值只能存储地址为 n 的倍数的内存地址上。例如,对齐量为 2 的值必须存储在偶数地址上,而对齐量为 1 的值可以存储在任何地址上。对齐量是用字节数来度量的,必须至少是 1,并且总是 2 的幂次。值的对齐量可以通过函数 align_of_val
来检测。
值的内存宽度是同类型的值组成的数组中连续两个元素之间的字节偏移量,此偏移量包括了为保持程序项数据类型内部对齐而对此类型做的对齐填充。值的内存宽度总是其对齐量的整数倍数。注意,某些类型的内存宽度为 0;0 被认为是任意对齐量的整数倍(例如:在一些平台上,类型[u16; 0]
的对齐量为 2,内存宽度为 0)。值的内存宽度可以通过函数 size_of_val
来检测。
如果某类型的所有的值都具有相同的内存宽度和对齐量,并且两者在编译时都是已知的,并且实现了 Sized
trait,则可以使用函数 size_of
和 align_of
对此类型进行检测。没有实现 Sized
trait 的类型被称为动态内存宽度类型。由于实现了 Sized
trait 的某一类型的所有值共享相同的内存宽度和对齐量,所以我们分别将这俩共享值称为该类型的内存宽度和该类型的对齐量。
Primitive Data Layout
原生类型的布局
下表给出了大多数原生类型(primitives)的内存宽度。
类型 | size_of::<Type>() |
---|---|
bool | 1 |
u8 / i8 | 1 |
u16 / i16 | 2 |
u32 / i32 | 4 |
u64 / i64 | 8 |
u128 / i128 | 16 |
usize / isize | 见下方 |
f32 | 4 |
f64 | 8 |
char | 4 |
usize
和 isize
的内存宽度足以包含目标平台上的每个内存地址。例如,在 32-bit 目标上,它们是 4 个字节,而在 64-bit 目标上,它们是 8 个字节。
原生类型的对齐量是特定于目标平台的。
大多数情况下,原生类型的对齐量通常与它们的内存宽度保持一致,但也可以小于。
特别是,i128
和 u128
通常对齐到 4 或 8 个字节,即使它们的尺寸是 16,并且在许多 32位平台上,i64
、u64
和 f64
仅对齐到4个字节,而不是 8 个字节。
Pointers and References Layout
指针和引用的布局
指针和引用具有相同的布局。指针或引用的可变性不会影响其布局。
指向固定内存宽度类型(sized type)的值的指针具有和 usize
相同的内存宽度和对齐量。
指向非固定内存宽度类型(unsized types)的值的指针是固定内存宽度的。其内存宽度和对齐量至少等于一个指针的内存宽度和对齐量
注意:虽然不应该依赖于此,但是目前所有指向 DST 的指针都是
usize
的两倍内存宽度,并且具有相同的对齐量。
Array Layout
数组的布局
数组 [T; N]
的内存宽度为 size_of::<T>() * N
,对齐量和 T
的对齐量相同。所以数组的布局使得数组的第 n 个(nth
)元素(其中索引 n 是从0开始的)为从数组开始的位置向后偏移 n * size_of::<T>()
个字节数。
Slice Layout
切片的布局
切片的布局与它们所切的那部分数组片段相同。
注意:这是关于原生的
[T]
类型,而不是指向切片的指针(&[T]
、Box<[T]>
等)。
str
Layout
字符串切片(str
)的布局
字符串切片是一种 UTF-8 表型(representation)的字符序列,它们与 [u8]
类型的切片拥有相同的类型布局。
Tuple Layout
元组的布局
元组根据Rust
表形(representation)来布局内存存储格式。
一个例外情况是单元结构体(unit tuple)(()
)类型,它被保证为内存宽度为 0,对齐量为 1。
Trait Object Layout
trait对象的布局
trait对象的布局与 trait对象的值相同。
注意:这是关于原生 trait对象类型(raw trait object type)的,而不是指向 trait对象的指针(
&dyn Trait
,Box<dyn Trait>
等)。
Closure Layout
闭包的布局
闭包的布局没有保证。
Representations
表形/表示形式
所有用户定义的复合类型(结构体(struct
)、枚举(enum
)和联合体(union
))都有一个*表形(representation)*属性,该属性用于指定该类型的布局。类型的可能表形有:
类型的表形可以通过对其应用 repr
属性来更改。下面的示例展示了一个 C
表形的结构体。
#![allow(unused)] fn main() { #[repr(C)] struct ThreeInts { first: i16, second: i8, third: i32 } }
可以分别使用 align
和 packed
修饰符增大或缩小对齐量。它们可以更改属性中指定表形的对齐量。如果未指定表形,则更改默认表形的。
#![allow(unused)] fn main() { // 默认表形,把对齐量缩小到2。 #[repr(packed(2))] struct PackedStruct { first: i16, second: i8, third: i32 } // C表形,把对齐量增大到8 #[repr(C, align(8))] struct AlignedStruct { first: i16, second: i8, third: i32 } }
注意:由于表形是程序项的属性,因此表形不依赖于泛型参数。具有相同名称的任何两种类型都具有相同的表形。例如,
Foo<Bar>
和Foo<Baz>
都有相同的表形。
类型的表形可以更改字段之间的填充,但不会更改字段本身的布局。例如一个使用 C
表形的结构体,如果它包含一个默认表形的字段 Inner
,那么它不会改变 Inner
的布局。
The Rust
Representation
Rust
表型
Rust
表型是没有 repr
属性修饰的标称类型的默认表型。使用 repr
属性显式指定此表型,Rust编译器会保证与完全省略该属性相同。
此表形所提供的唯一数据布局保证是健全性(soundness)所需的数据布局形式。 这些形式包括:
- 成员字段被适当的对齐摆放。
- 成员字段之间没有重叠。
- 对齐量至少是其最宽成员字段的对齐量。
形式上来说,第一个保证意味着任何成员字段的偏移量都可以被该成员字段的对齐量整除。第二个保证意味着可以对成员字段进行排序,使得偏移量加上任何成员字段的内存宽度小于或等于排序中下一个成员字段的偏移量。摆放顺序不必与类型声明中指定成员字段的顺序相同。
请注意,第二个保证并不意味着成员字段具有不同的内存地址:0内存宽度的类型可能与同一结构中的其他成员字段具有相同的内存地址。
此表形对数据布局没有其他保证。
The C
Representation
C
表形
C
表形被设计用于双重目的:一个目的是创建可以与 C语言互操作的类型;第二个目的是创建可以正确执行依赖于数据布局的操作的类型,比如将值重新解释为其他类型。
因为这种双重目的存在,可以只利用其中的一个目的,如只创建有固定布局的类型,而放弃与 C语言的互操作。
这种表型可以应用于结构体(structs)、联合体(unions)和枚举(enums)。一个例外是零变体枚举(zero-variant enums),它的 C
表形是错误的。
#[repr(C)]
Structs
#[repr(C)]
结构体
结构体的对齐量是其*最大对齐量的字段(most-aligned field)*的对齐量。
字段的内存宽度和偏移量则由以下算法确定:
-
把当前偏移量设为从 0 字节开始。
-
对于结构体中的每个字段,按其声明的先后顺序,首先确定其内存宽度和对齐量;如果当前偏移量不是对其齐量的整倍数,则向当前偏移量添加填充字节,直至其对齐量的倍数1;至此,当前字段的偏移量就是当前偏移量;下一步再根据当前字段的内存宽度增加当前偏移量。
-
最后,整个结构体的内存宽度就是当前偏移量向上取整到结构体对齐量的最小整数倍数。
下面用伪代码描述这个算法:
/// 返回偏移(`offset`)之后需要的填充量,以确保接下来的地址将被安排到可对齐的地址。
fn padding_needed_for(offset: usize, alignment: usize) -> usize {
let misalignment = offset % alignment;
if misalignment > 0 {
// 向上取整到对齐量(`alignment`)的下一个倍数
alignment - misalignment
} else {
// 已经是对齐量(`alignment`)的倍数了
0
}
}
struct.alignment = struct.fields().map(|field| field.alignment).max();
let current_offset = 0;
for field in struct.fields_in_declaration_order() {
// 增加当前字的偏移量段(`current_offset`),使其成为该字段对齐量的倍数。
// 对于第一个字段,此值始终为零。
// 跳过的字节称为填充字节。
current_offset += padding_needed_for(current_offset, field.alignment);
struct[field].offset = current_offset;
current_offset += field.size;
}
struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);
警告:这个伪代码使用了一个简单粗暴的算法,是为了清晰起见,它忽略了溢出问题。要在实际代码中执行内存布局计算,请使用 Layout
。
注意:此算法可以生成零内存宽度的结构体。在 C 语言中,像
struct Foo { }
这样的空结构体声明是非法的。然而,gcc 和 clang 都支持启用此类结构体的选项,并将其内存宽度指定为零。跟 Rust 不同的是 C++ 给空结构体指定的内存宽度为 1,并且除非它们是继承的,否则它们是具有[[no_unique_address]]
属性的字段(在这种情况下,它们不会增大结构体的整体内存宽度)。
#[repr(C)]
Unions
#[repr(C)]
联合体
使用 #[repr(C)]
声明的联合体将与相同目标平台上的 C语言中的 C联合体声明具有相同的内存宽度和对齐量。联合体的对齐量等同于其所有字段的最大对齐量,内存宽度将为其所有字段的最大内存宽度,再对其向上取整到对齐量的最小整数倍。这些最大值可能来自不同的字段。
#![allow(unused)] fn main() { #[repr(C)] union Union { f1: u16, f2: [u8; 4], } assert_eq!(std::mem::size_of::<Union>(), 4); // 来自于 f2 assert_eq!(std::mem::align_of::<Union>(), 2); // 来自于 f1 #[repr(C)] union SizeRoundedUp { a: u32, b: [u16; 5], } assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // 来自于 a assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 12); // 首先来自于b的内存宽度10,然后向上取整到最近的4的整数倍12。 }
#[repr(C)]
Field-less Enums
#[repr(C)]
无字段枚举
对于无字段枚举(field-less enums),C
表形的内存宽度和对齐量与目标平台的 C ABI 的默认枚举内存宽度和对齐量相同。
注意:C中的枚举的表形是由枚举的相应实现定义的,所以在 Rust 中,给无字段枚举应用 C表形得到的表型很可能是一个“最佳猜测”。特别是,当使用某些特定命令行参数来编译特定的 C代码时,这可能是不正确的。
警告:C语言中的枚举与 Rust 中的那些应用了 #[repr(C)]
表型的无字段枚举之间有着重要的区别。C语言中的枚举主要是 typedef
加上一些具名常量;换句话说,C枚举(enum
)类型的对象可以包含任何整数值。例如,C枚举通常被用做标志位。相比之下,Rust的无字段枚举只能合法地2保存判别式的值,其他的都是未定义行为。因此,在 FFI 中使用无字段枚举来建模 C语言中的枚举(enum
)通常是错误的。
#[repr(C)]
Enums With Fields
#[repr(C)]
带字段枚举
带字段的 repr(C)
枚举的表形其实等效于一个带两个字段的 repr(C)
结构体(这种在 C语言中也被称为“标签联合(tagged union)”),这两个字段:
- 一个为
repr(C)
表形的枚举(在这个等效结构体内,它也被叫做标签(the tag)字段),它就是原枚举所有的判别值组合成的新枚举,也就是它的变体是原枚举变体移除了它们自身所带的所有字段。 - 一个为
repr(C)
表形的联合体(在这个等效结构体内,它也被叫做载荷(the payload)字段),它的各个字段就是原枚举的各个变体把自己下面的字段重新组合成的repr(C)
表形的结构体。
注意:由于等效出的结构体和联合体是
repr(C)
表形的,因此如果原来某一变体只有单个字段,则直接将该字段放入等效出的联合体中,或将其包装进一个次级结构体后再放入联合体中是没有区别的;因此,任何希望操作此类枚举表形的系统都可以选择使用这两种形式里对它们来说更方便或更一致的形式。
#![allow(unused)] fn main() { // 这个枚举的表形等效于 ... #[repr(C)] enum MyEnum { A(u32), B(f32, u64), C { x: u32, y: u8 }, D, } // ... 这个结构体 #[repr(C)] struct MyEnumRepr { tag: MyEnumDiscriminant, payload: MyEnumFields, } // 这是原判别值组成的新枚举类型. #[repr(C)] enum MyEnumDiscriminant { A, B, C, D } // 这是原变体的字段组成的联合体. #[repr(C)] union MyEnumFields { A: MyAFields, // 译者注:因为原枚举变体A只有一个字段,所以此处的类型标注也可以直接替换为 u32,以省略 MyAFields这层封装 B: MyBFields, C: MyCFields, D: MyDFields, } #[repr(C)] #[derive(Copy, Clone)] struct MyAFields(u32); #[repr(C)] #[derive(Copy, Clone)] struct MyBFields(f32, u64); #[repr(C)] #[derive(Copy, Clone)] struct MyCFields { x: u32, y: u8 } // 这个结构体可以被省略(它是一个零内存宽度类型),但它必须出现在 C/C++ 头文件中 #[repr(C)] #[derive(Copy, Clone)] struct MyDFields; }
注意: 联合体(
union
)可带有未实现Copy
的字段的功能还没有纳入稳定版,具体参见 55149。
Primitive representations
原语表形
原语表形是与原生整型具有相同名称的表形。也就是:u8
,u16
,u32
,u64
,u128
,usize
,i8
,i16
,i32
,i64
,i128
和 isize
。
原语表形只能应用于枚举,此时枚举有没有字段会给原语表形带来不同的表现。给零变体枚举应用原始表形是错误的。将两个原语表形组合在一起也是错误的
Primitive Representation of Field-less Enums
无字段枚举的原语表形
对于无字段枚举,原语表形将其内存宽度和对齐量设置成与给定表形同名的原生类型的表形的值。例如,一个 u8
表形的无字段枚举只能有0和255之间的判别值。
Primitive Representation of Enums With Fields
带字段枚举的原语表形
带字段枚举的原语表形是一个 repr(C)
表形的联合体,此联合体的每个字段对应一个和原枚举变体对应的 repr(C)
表形的结构体。这些结构体的第一个字段是原枚举的变体移除了它们所有的字段组成的原语表形版的无字段枚举(“the tag”),那这些结构体的其余字段是原变体移走的字段。
注意:如果在联合体中,直接把标签的成员赋予给标签(“the tag”),那么这种表形结构仍不变的,并且这样操作对您来说可能会更清晰(尽管遵循 c++ 的标准,标签也应该被包装在结构体中)。
#![allow(unused)] fn main() { // 这个枚举的表形效同于 ... #[repr(u8)] enum MyEnum { A(u32), B(f32, u64), C { x: u32, y: u8 }, D, } // ... 这个联合体. #[repr(C)] union MyEnumRepr { A: MyVariantA, //译者注:此字段类型也可直接用 u32 直接替代 B: MyVariantB, //译者注:此字段类型也可直接用 (f32, u64) 直接替代 C: MyVariantC, D: MyVariantD, } // 这是原判别值组合成的新枚举。 #[repr(u8)] #[derive(Copy, Clone)] enum MyEnumDiscriminant { A, B, C, D } #[repr(C)] #[derive(Clone, Copy)] struct MyVariantA(MyEnumDiscriminant, u32); #[repr(C)] #[derive(Clone, Copy)] struct MyVariantB(MyEnumDiscriminant, f32, u64); #[repr(C)] #[derive(Clone, Copy)] struct MyVariantC { tag: MyEnumDiscriminant, x: u32, y: u8 } #[repr(C)] #[derive(Clone, Copy)] struct MyVariantD(MyEnumDiscriminant); }
注意: 联合体(
union
)带有未实现Copy
trait 的字段的功能还没有纳入稳定版,具体参见 55149。
Combining primitive representations of enums with fields and #[repr(C)]
带字段枚举的原语表形与#[repr(C)]
表形的组合使用
对于带字段枚举,还可以将 repr(C)
和原语表形(例如,repr(C, u8)
)结合起来使用。这是通过将判别值组成的枚举的表形改为原语表形来实现的。因此,如果选择组合 u8
表形,那么组合出的判别值枚举的内存宽度和对齐量将为 1 个字节。
那么这个判别值枚举就从前面示例中的样子变成:
#![allow(unused)] fn main() { #[repr(C, u8)] // 这里加上了 `u8` enum MyEnum { A(u32), B(f32, u64), C { x: u32, y: u8 }, D, } // ... #[repr(u8)] // 所以这里就用 `u8` 替代了 `C` enum MyEnumDiscriminant { A, B, C, D } // ... }
例如,对于有 repr(C, u8)
属性的枚举,不可能有257个唯一的判别值(“tags”),而同一个枚举,如果只有单一 repr(C)
表形属性,那在编译时就不会出任何问题。
在 repr(C)
附加原语表形可以改变 repr(C)
表形的枚举的内存宽度:
#![allow(unused)] fn main() { #[repr(C)] enum EnumC { Variant0(u8), Variant1, } #[repr(C, u8)] enum Enum8 { Variant0(u8), Variant1, } #[repr(C, u16)] enum Enum16 { Variant0(u8), Variant1, } // C表形的内存宽度依赖于平台 assert_eq!(std::mem::size_of::<EnumC>(), 8); // 一个字节用于判别值,一个字节用于 Enum8::Variant0 中的值 assert_eq!(std::mem::size_of::<Enum8>(), 2); // 两个字节用于判别值,一个字节用于Enum16::Variant0中的值,加上一个字节的填充 assert_eq!(std::mem::size_of::<Enum16>(), 4); }
The alignment modifiers
对齐量的修饰符
align
和 packed
修饰符可分别用于增大和减小结构体的和联合体的对齐量。packed
也可以改变字段之间的填充 (虽然它不会改变任何字段内部的填充)。
align
和 packed
本身并不提供关于结构布局或枚举变体布局中成员字段顺序的保证,尽管它们可以与提供这种保证的表型(如 C
)组合使用。
对齐量被指定为整型参数,形式为 #[repr(align(x))]
或 #[repr(packed(x))]
。对齐量的值必须是从1到229之间的2的次幂数。对于 packed
,如果没有给出任何值,如 #[repr(packed)]
,则对齐量的值为1。
对于 align
,如果类型指定的对齐量比其不带 align
修饰符时的对齐量小,则该指定的对齐量无效。
对于 packed
,如果类型指定的对齐量比其不带 packed
修饰符时的对齐量大,则该指定的对齐量和布局无效。为了定位字段,每个字段的对齐量是指定的对齐量和字段的类型的对齐量中较小的那个对齐量。
字段间的填充保证为每个字段(可能更改)的对齐量填充所需的最小值(但请注意,packed
本身并不能保证字段顺序)。这些规则的一个重要后果是:#[repr(packed(1))]
(或#[repr(packed)]
) 的类型不会有字段间填充。
align
和 packed
修饰符不能应用于同一类型,且 packed
修饰的类型不能直接或间接地包含另一个 align
修饰的类型。align
和 packed
修饰符只能应用于Rust
表形和 C
表形中。
align
修饰符也可以应用在枚举上。如果这样做了,其对枚举对齐量的影响与将此枚举包装在一个新的使用了相同的 align
修饰符的结构体中的效果相同。
注意:不能引用未对齐的成员字段,因为这是一种未定义行为。 当成员字段由于特定对齐修饰符而未能对齐时,请考虑以下做法来使用引用和解引用:
#![allow(unused)] fn main() { #[repr(packed)] struct Packed { f1: u8, f2: u16, } let mut e = Packed { f1: 1, f2: 2 }; // 不要建对字段的引用,而是将值复制到局部变量中。 let x = e.f2; // 或者在类似 `println!` 的情况下使用大括号将其值先做一次复制,再引用。 println!("{}", {e.f2}); // 或者,如果你真需要用指针,请使用那些不要求对齐的方法进行读取和写入,而不是直接对指针做解引用。 let ptr: *const u16 = std::ptr::addr_of!(e.f2); let value = unsafe { ptr.read_unaligned() }; let mut_ptr: *mut u16 = std::ptr::addr_of_mut!(e.f2); unsafe { mut_ptr.write_unaligned(3) } }
The transparent
Representation
透明(transparent
)表形
透明(transparent
)表型只能在只有一个字段的结构体(struct
)上或只有一个变体的枚举(enum
)上使用,这里只有一个字段/变体的意思是:
- 只能有一个非零内存宽度的字段/变体,和
- 任意数量的内存宽度为零对齐量为1的字段(例如:
PhantomData<T>
)
使用这种表形的结构体和枚举与只有那个非零内存宽度的字段具有相同的布局和 ABI。
这与 C
表形不同,因为带有 C
表形的结构体将始终拥有 C结构体(C
struct
)的ABI,例如,那些只有一个原生类型字段的结构体如果应用了透明表形(transparent
),将具有此原生类型字段的ABI。
因为此表形将类型布局委托给另一种类型,所以它不能与任何其他表形一起使用。
至此,上一个字段就填充完成,开始计算本字段了。也就是说每一个字段的偏移量是其字段的段首位置;那第一个字段的偏移量就始终为 0。
这里合法的意思是变体的判别值受 repr(u8)
这样的表形属性约束,像这个例子中,变体的判别值就只能位于 0~255 之间。
Interior Mutability
内部可变性
interior-mutability.md
commit: ece3e184c0beeadba97c78eed9005533c3874e43
本章译文最后维护日期:2022-08-21
有时一个类型需要在存在多个别名时进行更改。在 Rust 中,这是通过一种叫做内部可变性的模式实现的。如果一个类型的内部状态可以通过对它的共享引用来进行更改,那么就说这个类型就具有内部可变性。这违背了共享引用所指向的值不能被更改的通常要求。
std::cell::UnsafeCell<T>
类型是 Rust 中唯一可以合法失效此要求的方法。当 UnsafeCell<T>
存在其他不变性的别名1时,仍然可以安全地对它包含的 T
进行更改或获得 T
的一个可变引用。与所有其他类型一样,拥有多个 &mut UnsafeCell<T>
别名是未定义行为。
通过使用 UnsafeCell<T>
作为字段,可以创建具有内部可变性的其他类型。标准库提供了几种这样的类型,这些类型都提供了安全的内部变更的 API。例如,std::cell::RefCell<T>
使用运行时借用检查来确保多个引用存在时的常规规则的执行。std::sync::atomic
模块包含了一些类型,这些类型包装了一个只能通过原子操作访问的值,以允许在线程之间共享和修改该值。
当变量和指针表示的内存区域有重叠时,它们互为对方的别名。
Subtyping and Variance
子类型化和型变
subtyping.md
commit: 36a66c0ccf9f8a5b941b0df9923ece80d8714c83
本章译文最后维护日期:2022-08-21
子类型化是隐式的,可以出现在类型检查或类型推断的任何阶段。 子类型化仅限于两种情况:引入生存期(lifetimes)带来的类型型变(variance),以及引入高阶生存期带来的类型型变之间。如果我们从类型中擦除了生存期,那么子类型化只能是类型相等(type equality)了。
考虑下面的例子:字符串字面量总是拥有 'static
生存期。不过,我们还是可以把 s
赋值给 t
:
#![allow(unused)] fn main() { fn bar<'a>() { let s: &'static str = "hi"; let t: &'a str = s; } }
因为 'static
比生存期参数 'a
的寿命长,所以 &'static str
是 &'a str
的子类型。
高阶函数指针和trait对象可以形成另一种父子类型的关系。 它们是那些通过替换高阶生存期而得出的类型的子类型。举些例子:
#![allow(unused)] fn main() { // 这里 'a 被替换成了 'static let subtype: &(for<'a> fn(&'a i32) -> &'a i32) = &((|x| x) as fn(&_) -> &_); let supertype: &(fn(&'static i32) -> &'static i32) = subtype; // 这对于 trait对象也是类似的 let subtype: &(dyn for<'a> Fn(&'a i32) -> &'a i32) = &|x| x; let supertype: &(dyn Fn(&'static i32) -> &'static i32) = subtype; // 我们也可以用一个高阶生存期来代替另一个 let subtype: &(for<'a, 'b> fn(&'a i32, &'b i32))= &((|x, y| {}) as fn(&_, &_)); let supertype: &for<'c> fn(&'c i32, &'c i32) = subtype; }
Variance
型变
型变是泛型类型相对其参数具有的属性。 泛型类型在它的某个参数上的型变是描述该参数的子类型化去如何影响此泛型类型的子类型化。
- 如果
T
是U
的一个子类型意味着F<T>
是F<U>
的一个子类型(即子类型化“通过(passes through)”),则F<T>
在T
上是协变的(covariant)。 - 如果
T
是U
的一个子类型意味着F<U>
是F<T>
的一个子类型,则F<T>
在T
上是逆变的(contravariant)。 - 其他情况下(即不能由参数类型的子类型化关系推导出此泛型的型变关系),
F<T>
在T
上是的不变的(invariant)。
类型的型变关系由下表中的规则自动确定:
Type | 在 'a 上的型变 | 在 T 上的型变 |
---|---|---|
&'a T | 协变的 | 协变的 |
&'a mut T | 协变的 | 不变的 |
*const T | 协变的 | |
*mut T | 不变的 | |
[T] 和 [T; n] | 协变的 | |
fn() -> T | 协变的 | |
fn(T) -> () | 逆变的 | |
std::cell::UnsafeCell<T> | 不变的 | |
std::marker::PhantomData<T> | 协变的 | |
dyn Trait<T> + 'a | 协变的 | 不变的 |
结构体(struct
)、枚举(enum
)和联合体(union
)类型上的型变关系是通过查看其字段类型的型变关系来决定的。
如果参数用在了多处且具有不同型变关系的位置上,则该类型在该参数上是不变的。
例如,下面示例的结构体在 'a
和 T
上是协变的,在 'b
、'c
和 U
上是不变的。
#![allow(unused)] fn main() { use std::cell::UnsafeCell; struct Variance<'a, 'b, 'c, T, U: 'a> { x: &'a U, // 这让 `Variance` 在 'a 上是协变的, 也让在 U 上是协变的, 但是后面也使用了 U y: *const T, // 在 T 上是协变的 z: UnsafeCell<&'b f64>, // 在 'b 上是不变的 w: *mut U, // 在 U 上是不变的, 所以让整个结构体在 U 上是不变的 f: fn(&'c ()) -> &'c () // 无论是协变还是逆变,都会导致此结构体在 'c 上不变。 } }
当在结构体(struct
)、枚举(enum
)和联合体(union
)之外使用时,会在每个位置分别检查参数的型变。
#![allow(unused)] fn main() { use std::cell::UnsafeCell; fn generic_tuple<'short, 'long: 'short>( // 'long 同时应用在一个元组里的协变和不变的位置上 x: (&'long u32, UnsafeCell<&'long u32>), ) { // 由于这些位置的型变是单独计算的,我们可以在协变位置自由地收缩 'long。 let _: (&'short u32, UnsafeCell<&'long u32>) = x; } fn takes_fn_ptr<'short, 'middle: 'short>( // 'middle 被用在了一个既是协变又是逆变的位置上。 f: fn(&'middle ()) -> &'middle (), ) { // 由于这些位置的型变是单独计算的,我们可以在协变位置自由地收缩 'middle,并在逆变位置拉伸它。 let _: fn(&'static ()) -> &'short () = f; } }
Trait and lifetime bounds
trait约束和生存期约束
trait-bounds.md
commit: 82517788e162791dad3305a8c8d8d20d49510ad6
本章译文最后维护日期:2024-06-15
句法
TypeParamBounds :
TypeParamBound (+
TypeParamBound )*+
?TypeParamBound :
Lifetime | TraitBoundTraitBound :
?
? ForLifetimes? TypePath
|(
?
? ForLifetimes? TypePath)
LifetimeBounds :
( Lifetime+
)* Lifetime?Lifetime :
LIFETIME_OR_LABEL
|'static
|'_
trait约束和生存期约束为泛型程序项提供了一种方法来限制将哪些类型和生存期可被用作它们的参数。通过 where子句可以为任何泛型提供约束。对于某些常见的情况,也可以使用如下简写形式:
- 跟在泛型参数声明之后的约束:
fn f<A: Copy>() {}
与fn f<A>() where A: Copy {}
效果等价。 - 在 trait声明中作为指定超类trait(supertraits) 约束时:
trait Circle : Shape {}
等同于trait Circle where Self : Shape {}
。 - 在 trait声明中作为指定关联类型上的约束时:
trait A { type B: Copy; }
等同于trait A where Self::B: Copy { type B; }
。
在程序项上应用了约束就要求在使用该程序项时使用者必须满足这些约束。当对泛型程序项进行类型检查和借用检查时,约束可用来确认当前准备用来单态化此泛型的实例类型是否实现了约束给出的 trait。例如,给定 Ty: Trait
:
- 在泛型函数体中,
Trait
中的方法可以被Ty
类型的值调用。同样,Trait
上的相关常数也可以被使用。 Trait
上的关联类型可以被使用。- 带有
T: Trait
约束的泛型函数或类型可以在使用T
的地方替换使用Ty
。
#![allow(unused)] fn main() { type Surface = i32; trait Shape { fn draw(&self, surface: Surface); fn name() -> &'static str; } fn draw_twice<T: Shape>(surface: Surface, sh: T) { sh.draw(surface); // 能调用此方法上因为 T: Shape sh.draw(surface); } fn copy_and_draw_twice<T: Copy>(surface: Surface, sh: T) where T: Shape { let shape_copy = sh; // sh 没有被使用移动语义移走,是因为 T: Copy draw_twice(surface, sh); // 能使用泛型函数 draw_twice 是因为 T: Shape } struct Figure<S: Shape>(S, S); fn name_figure<U: Shape>( figure: Figure<U>, // 这里类型 Figure<U> 的格式正确是因为 U: Shape ) { println!( "Figure of two {}", U::name(), // 可以使用关联函数 ); } }
trait 和生存期约束也被用来命名 trait对象。
?Sized
?
仅用于放宽类型参数或关联类型 上的Sized
trait。
目前 ?Sized
还不能用作其他类型的约束。
Lifetime bounds
生存期约束
生存期约束可以应用于类型或其他生存期。
约束 'a: 'b
通常被读做 'a
比 'b
存活的时间久(outlives)。
'a: 'b
意味着 'a
持续的时间至少和 'b
一样长,所以只要 &'a ()
有效,则 &'b ()
必定有效。1
#![allow(unused)] fn main() { fn f<'a, 'b>(x: &'a i32, mut y: &'b i32) where 'a: 'b { y = x; // 因为 'a: 'b,所以&'a i32 是 &'b i32 的子类型 let r: &'b &'a i32 = &&0; // &'b &'a i32 格式合法是因为 'a: 'b } }
T: 'a
意味着 T
的所有生存期参数都比 'a
存活得时间长。
例如,如果 'a
是一个任意的(unconstrained)生存期参数,那么 i32: 'static
和 &'static str: 'a
都合法,但 Vec<&'a ()>: 'static
不合法。
Higher-ranked trait bounds
高阶trait约束
ForLifetimes :
for
GenericParams
可以在生存期上再进行更高阶的类型约束。这些高阶约束指定了一个对所有生存期都必须保证为真的约束。例如,像 for<'a> &'a T: PartialEq<i32>
这样的约束需要一个如下的实现
#![allow(unused)] fn main() { struct T; impl<'a> PartialEq<i32> for &'a T { // ... fn eq(&self, other: &i32) -> bool {true} } }
这样就可以拿任意生存期的 &'a T
和 i32
做比较啦。
下面这类场景只能使用高阶trait约束,因为引用的生命周期比函数的生命周期参数短:2
#![allow(unused)] fn main() { fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) { let zero = 0; f(&zero);} }
译者注:译者下面举例代码可以和上面原文的代码对比着看。下面代码中,因为
F
没约束'a
,导致参数f
引用了未经扩展生存期的zero
#![allow(unused)] fn main() { fn call_on_ref_zero<'a, F>(f: F) where F: Fn(&'a i32) { let zero = 0; f(&zero); } }
高阶生存期也可以在 trait 前面指定:唯一的区别是生存期参数的作用域 ,像下面这样 'a
的作用域只扩展到后面跟的 trait 的末尾,而不是整个约束3。下面这个函数和上一个等价。
#![allow(unused)] fn main() { fn call_on_ref_zero<F>(f: F) where F: for<'a> Fn(&'a i32) { let zero = 0; f(&zero); } }
Implied bounds
隐式约束
生存期约束有效需要以类型定义格式合法(well-formed)为前提,有时甚至需要一些编译器推断。
#![allow(unused)] fn main() { fn requires_t_outlives_a<'a, T>(x: &'a T) {} }
类型参数T
需要比类型&'a T
的'a
生存时间长才能形成格式合法(well-formed)。
之所以推断出这一点,是因为函数签名包含类型&'a T
,该类型仅在 T: 'a
成立时才有效。
Rust 会为函数的所有参数和输出添加隐式约束。在 requires_t_outlives_a
内部,即使没有明确指定,也可以假定 T: 'a
得到了保持:
#![allow(unused)] fn main() { fn requires_t_outlives_a_not_implied<'a, T: 'a>() {} fn requires_t_outlives_a<'a, T>(x: &'a T) { // 这能够编译是因为 `T: 'a` 是由引用类型 `&'a T` 隐式约束的。 requires_t_outlives_a_not_implied::<'a, T>(); } }
#![allow(unused)] fn main() { fn requires_t_outlives_a_not_implied<'a, T: 'a>() {} fn not_implied<'a, T>() { // 这里报错是因为函数签名并没有 `T: 'a` 的隐式约束。 requires_t_outlives_a_not_implied::<'a, T>(); } }
只有生存期约束是隐式约束,特征约束仍然必须显式添加。 因此,以下示例会导致错误:
#![allow(unused)] fn main() { use std::fmt::Debug; struct IsDebug<T: Debug>(T); // error[E0277]: `T` 没有实现 `Debug` fn doesnt_specify_t_debug<T>(x: IsDebug<T>) {} }
还推断出任何类型的类型定义和impl块的生存期界限: 在类型定义和类型的 impl块中,生存期约束的推断逻辑仍会被编译器执行:
#![allow(unused)] fn main() { struct Struct<'a, T> { // 这里要求 `T: 'a`, 这点是编译器推断出的 field: &'a T, } enum Enum<'a, T> { // 这里要求 `T: 'a`, 这点是编译器推断出的 // // 注意,即便是只使用 `Enum::OtherVariant`,仍需要保持 `T: 'a` SomeVariant(&'a T), OtherVariant, } trait Trait<'a, T: 'a> {} // 这里会报错是因为任何类型的 impl头中都没有隐含 T: 'a` 的逻辑 // impl<'a, T> Trait<'a, T> for () {} // 这里能通过编译是因为 self类型 `&'a T` 中隐含了 `T: 'a`的逻辑 impl<'a, T> Trait<'a, T> for &'a T {} }
译者理解:理解这种关系时,可以把生存期 'a
和 'b
理解成去引用对象时需传入的参数,给定 'a: 'b
和类型 T
,如果 'a T
有效,那此时再传入 'b
就去引用 T
必定有效。
译者理解:高阶 trait约束就是对带生存期的类型重新进行约束。像这句中的例子就是对 &'a T
加上了 PartialEq<i32>
的约束,其中 for<'a>
可以理解为:对于 'a
的所有可能选择。更多信息请参见:https://doc.rust-lang.org/std/cmp/trait.PartialEq.html 和 https://doc.rust-lang.org/nightly/nomicon/hrtb.html
译者理解此例中的代码 for<'a> F: Fn(&'a i32)
为:F
对于 'a
的所有可能选择都受 Fn(&'a i32)
的约束。
译者理解这句的意思是:如果 F
的约束有多个 trait,那这种方式里, 'a
的作用域只是扩展它后面紧跟的那个 trait 的方法,即 Fn(&'a i32)
里。
Type coercions
类型自动强转
type-coercions.md
commit: 1b00b703825387edc02ddd2030d478cb5d78fa9f
本章译文最后维护日期:2023-12-30
类型自动强转是改变值的类型的隐式操作。它们在特定的位置自动发生,但实际自动强转的类型也受到很多限制。
任何允许自动强转的转换都可以由类型强制转换操作符 as
来显式执行。
自动强转最初是在 RFC 401 中定义的,并在 RFC 1558 中进行了扩展。
Coercion sites
自动强转点
自动强转只能发生在程序中的某些自动强转点(coercion sites)上;典型的位置是那些所需的类型是显式给出了的地方,或者是那些可以从给出的显式类型传播推导(be derived by propagation)出所需的类型(注意这里不是类型推断)的地方。可能的强转点有:
-
let
语句中显式给出了类型。例如,下面例子中
&mut 42
自动强转成&i8
类型:#![allow(unused)] fn main() { let _: &i8 = &mut 42; // 译者注释:`&i8` 是显示给出的所需类型 }
-
静态(
static
)项和常量(const
)项声明(类似于let
语句)。 -
函数调用的参数
被强制的值是实参(actual parameter),它的类型被自动强转为形参(formal parameter)的类型。
例如,下面例子中
&mut 42
自动强转成&i8
类型:fn bar(_: &i8) { } fn main() { bar(&mut 42); }
对于方法调用,接受者(
self
参数)的类型的强转方式不同,有关详细信息,请参阅方法调用表达式的文档。 -
实例化结构体、联合体或枚举变体的字段。
例如,下面例子中
&mut 42
自动强转成&i8
类型:struct Foo<'a> { x: &'a i8 } fn main() { Foo { x: &mut 42 }; }
-
函数结果—块中的最终表达式或者
return
语句中的任何表达式。例如,下面例子中
x
将自动强转成&dyn Display
类型:#![allow(unused)] fn main() { use std::fmt::Display; fn foo(x: &u32) -> &dyn Display { x } }
如果一在自动强转点中的表达式是自动强转传播型表达式(coercion-propagating expression),那么该表达式中的对应子表达式也是自动强转点。传播从这些新的自动强转点开始递归。传播表达式(propagating expressions)及其相关子表达式有:
-
数组字面量,其数组的类型为
[U; n]
。数组字面量中的每个子表达式都是自动强转到类型U
的自动强转点。 -
重复句法声明的数组字面量,其数组的类型为
[U; n]
。重复子表达式是用于自动强转到类型U
的自动强转点。 -
元组,其中如果元组是自动强转到类型
(U_0, U_1, ..., U_n)
的强转点,则每个子表达式都是相应类型的自动强转点,比如第0个子表达式是到类型U_0
的 自动强转点。 -
圆括号括起来的子表达式(
(e)
):如果整个括号表达式的类型为U
,则子表达式e
是自动强转到类型U
的自动强转点。 -
块:如果块的类型是
U
,那么块中的最后一个表达式(如果它不是以分号结尾的)就是一个自动强转到类型U
的自动强转点。这里的块包括作为控制流语句的一部分的条件分支代码块,比如if
/else
,当然前提是这些块的返回需要有一个已知的类型。
Coercion types
自动强转类型
自动强转允许发生在下列类型之间:
-
T
到U
如果T
是U
的一个子类型 (反射性场景(reflexive case)) -
T_1
到T_3
当T_1
可自动强转到T_2
同时T_2
又能自动强转到T_3
(传递性场景(transitive case))注意这个还没有得到完全支持。
-
&mut T
到&T
-
*mut T
到*const T
-
&T
到*const T
-
&mut T
到*mut T
-
&T
或&mut T
到&U
如果T
实现了Deref<Target = U>
。例如:use std::ops::Deref; struct CharContainer { value: char, } impl Deref for CharContainer { type Target = char; fn deref<'a>(&'a self) -> &'a char { &self.value } } fn foo(arg: &char) {} fn main() { let x = &mut CharContainer { value: 'y' }; foo(x); //&mut CharContainer 自动强转成 &char. }
-
&mut T
到&mut U
如果T
实现了DerefMut<Target = U>
. -
TyCtor(
T
) 到 TyCtor(U
),其中 TyCtor(T
) 是下列之一1&T
&mut T
*const T
*mut T
Box<T>
并且
U
能够通过非固定内存宽度类型自动强转得到。 -
函数项到函数指针(
fn
pointers) -
非捕获闭包(Non capturing closures)到函数指针(
fn
pointers) -
!
到任意T
Unsized Coercions
非固定内存宽度类型自动强转
下列自动强转被称为非固定内存宽度类型自动强转(unsized coercions
),因为它们与将固定内存宽度类型(sized types)转换为非固定内存宽度类型(unsized types)有关,并且在一些其他自动强转不允许的情况(也就是上面罗列的情况之外的情况)下允许使用。也就是说它们可以发生在任何自动强转发生的地方。
Unsize
和 CoerceUnsized
这两个 trait 被用来协助这种转换的发生,并公开给标准库来使用。以下自动强转方式是内置的,并且,如果 T
可以用其中任一方式自动强转成 U
,那么就会为 T
提供一个 Unsize<U>
的内置实现:
-
[T; n]
到[T]
. -
T
到dyn U
, 当T
实现U + Sized
, 并且U
是对象安全的时。 -
Foo<..., T, ...>
到Foo<..., U, ...>
, 当:Foo
是一个结构体。T
实现了Unsize<U>
。Foo
的最后一个字段是和T
相关的类型。- 如果这最后一个字段是类型
Bar<T>
,那么Bar<T>
实现了Unsized<Bar<U>>
。 T
不是任何其他字段的类型的一部分。
此外,当 T
实现了 Unsize<U>
或 CoerceUnsized<Foo<U>>
时,类型 Foo<T>
可以实现 CoerceUnsized<Foo<U>>
。这就允许 Foo<T>
提供一个到 Foo<U>
的非固定内存宽度类型自动强转。
注:虽然非固定内存宽度类型自动强转的定义及其实现已经稳定下来,但
Unsize
和CoerceUnsized
这两个 trait 本身还没稳定下来,因此还不能直接用于稳定版的 Rust。
Least upper bound coercions
最小上界自动强转
在某些上下文中,编译器必须将多个类型强制在一起,以尝试找到最通用的类型。这被称为“最小上界(Least Upper Bound,简称 LUB)”自动强转。LUB自动强转只在以下情况中使用:
- 为一系列的 if分支查找共同的类型。
- 为一系列的匹配臂查找共同的类型。
- 为数组元素查找共同的类型。
- 为带有多个返回项语句的闭包的返回类型查找共同的类型。
- 检查带有多个返回语句的函数的返回类型。
在这每种情况下,都有一组类型 T0..Tn
被共同自动强转到某个未知的目标类型 T_t
,注意开始时 T_t
是未知的。LUB 自动强转的计算过程是不断迭代的。首先把目标类型 T_t
定为从类型 T0
开始。对于每一种新类型 Ti
,考虑如下步骤是否成立:
- 如果
Ti
可以自动强转为当前目标类型T_t
,则不做任何更改。 - 否则,检查
T_t
是否可以被自动强转为Ti
;如果是这样,T_t
就改为Ti
。(此检查还取决于到目前为止所考虑的所有源表达式是否带有隐式自动强转。) - 如果不是,尝试计算一个
T_t
和Ti
的共同的超类型(supertype),此超类型将成为新的目标类型。
Examples:
示例:
#![allow(unused)] fn main() { let (a, b, c) = (0, 1, 2); // if分支的情况 let bar = if true { a } else if false { b } else { c }; // 匹配臂的情况 let baw = match 42 { 0 => a, 1 => b, _ => c, }; // 数组元素的情况 let bax = [a, b, c]; // 多个返回项语句的闭包的情况 let clo = || { if true { a } else if false { b } else { c } }; let baz = clo(); // 检查带有多个返回语句的函数的情况 fn foo() -> i32 { let (a, b, c) = (0, 1, 2); match 42 { 0 => a, 1 => b, _ => c, } } }
在这些例子中,ba*
的类型可以通过 LUB自动强转找到。编译器检查 LUB自动强转在处理函数 foo
时,是否把 a
,b
,c
的结果转为了 i32
。
Caveat
警告
我们这种描述显然是非正式的,但目前使文字描述更精确的工作正作为精细化 Rust 类型检查器的一般性工作的一部分正紧锣密鼓的进行中。
TyCtor为类型构造器 type constructor 的简写。
Destructors
析构器
destructors.md
commit: https://doc.rust-lang.org/
本章译文最后维护日期:2023-11-05
当一个初始化了的变量或临时变量超出作用域时,其析构器(destructor)将运行,或者说它将被销毁(dropped)。此外,赋值操作也会运行其左操作数的析构器(如果它已经初始化了)。如果变量只部分初始化了,则只销毁其已初始化的字段。
类型T
的析构器由以下内容组成:
- 如果其有约束
T: Drop
, 则调用<T as std::ops::Drop>::drop
- 递归运行其所有字段的析构器。
如果析构器必须手动运行,比如在实现自定义的智能指针时,可以使用标准库函数 std::ptr::drop_in_place
。
举些(析构器的)例子:
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("{}", self.0); } } let mut overwritten = PrintOnDrop("当覆写时执行销毁"); overwritten = PrintOnDrop("当作用域结束时执行销毁"); let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second")); let moved; // 没有析构器在赋值时运行 moved = PrintOnDrop("Drops when moved"); // 这里执行销毁,但随后变量进入未初始化状态 moved; // 未初始化不会被销毁 let uninitialized: PrintOnDrop; // 在部分移动之后,后续销毁动作只销毁剩余字段。 let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten")); // 执行部分移出,只留下 `partial_move.0` 处于初始化状态 core::mem::forget(partial_move.1); // 当 partial_move 的作用域结束时, 这里就只有第一个字段被销毁。 }
Drop scopes
存续作用域
每个变量或临时变量都与一个存续作用域(drop scope)1相关联。当控制流离开一个存续作用域时,与该作用域关联的所有变量都将按照其声明(对变量而言)或创建(对临时变量而言)的相反顺序销毁。
存续作用域是在将 for
、if let
和 while let
这些表达式替换为等效的 match
表达式之后确定的。在确定存续作用域的边界时,不会对重载操作符与内置的操作符做区分2,模式的变量绑定方式(binding modes)也不会影响存续作用域的确定。
给定一个函数或闭包,存在以下的存续作用域:
存续作用域相互嵌套有如下规则。当同时离开多个作用域时,比如从函数返回时,变量会从内层向外层依次销毁。
- 整个函数作用域是最外层的作用域。
- 函数体块包含在整个函数作用域内。
- 表达式的父作用域是该表达式所在的语句自己构成的作用域。
let
语句的初始化器(initializer)的父作用域是let
语句构成的作用域。- 语句作用域的父作用域是包含该语句的块作用域。
- 匹配守卫(
match
guard)表达式的父作用域是该守卫所在的匹配臂构成的作用域。 - 匹配表达式(
match
expression)中=>
之后的表达式e 的父作用域是此表达式e 所在的匹配臂构成的作用域。 - 匹配臂的作用域的父作用域是此臂所在的匹配表达式(
match
expression)构成的作用域。 - 所有其他作用域的父作用域都是直接封闭该表达式的作用域。
Scopes of function parameters
函数参数的作用域
函数参数在整个函数体的作用域内有效,因此在对函数求值时,它们是最后被销毁的。实参会在形参引入的模式绑定变量销毁之后销毁。
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } // 先销毁 `y`,然后是第二个(元组)参数, 接下来是 `x`, 最后是第一个(元组)参数 fn patterns_in_parameters( (x, _): (PrintOnDrop, PrintOnDrop), (_, y): (PrintOnDrop, PrintOnDrop), ) {} // 销毁顺序是 3 2 0 1 patterns_in_parameters( (PrintOnDrop("0"), PrintOnDrop("1")), (PrintOnDrop("2"), PrintOnDrop("3")), ); }
Scopes of local variables
本地变量的作用域
在 let
语句中声明的局部变量的作用域与包含此 let
语句的块作用域相关。在匹配(match
)表达式中声明的局部变量与声明它们的匹配(match
)臂构成的作用域相关。
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } let declared_first = PrintOnDrop("在外层作用域内最后销毁"); { let declared_in_block = PrintOnDrop("在内层作用域内销毁"); } let declared_last = PrintOnDrop("在外层作用域内最先销毁"); }
如果在一个匹配(match
)表达式的同一个匹配臂中使用了多个模式,则毁顺序不确定。(译者注:这里译者不确定后半句翻译是否准确,这里给出原文:then an unspecified pattern will be used to determine the drop order.)
Temporary scopes
临时作用域
表达式的临时作用域(那个)用于保存此表达式在位置上下文上求出的结果的临时变量的作用域。注意如果此变量被提升了,那此时临时作用域就不存在了。
除了生存期扩展之外,表达式的临时作用域是包含该表达式的最小作用域,临时作用域是以下情况之一:
- 整个函数体。
- 一条语句。
if
表达式、while
表达式 或loop
表达式这三种表达式的代码体。if
表达式的else
块。if
表达式的条件表达式,while
表达式的条件表达式,或匹配表达式中的匹配(match
)守卫。- 匹配臂上的主体表达式。
- 惰性求值的布尔表达式的第二操作数。
注意:
在函数体的最终表达式(final expression)中创建的临时变量会在任何具名变量销毁之后销毁。 此时整个函数的销毁作用域就是它的销毁作用域,因为这里没有更小的封闭它的临时作用域。
匹配(
match
)表达式的检验对象本身不是一个临时作用域(,但它内部可以包含临时作用域),因此可以在销毁匹配(match
)表达式的作用域之后再来销毁检验对象表达式中的临时作用域。例如,match 1 { ref mut z => z };
中的1
所在的临时变量一直存活到此语句结束。
一些示例:
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } let local_var = PrintOnDrop("local var"); // 在条件表达式执行后立即销毁 if PrintOnDrop("If condition").0 == "If condition" { // 在此块的末尾处销毁 PrintOnDrop("If body").0 } else { unreachable!() }; // 在此条语句的末尾处销毁 (PrintOnDrop("first operand").0 == "" // 在 ) 处销毁 || PrintOnDrop("second operand").0 == "") // 在此表达式的末尾处销毁 || PrintOnDrop("third operand").0 == ""; // 在函数末尾处,局部变量之后销毁之后销毁 // 将下面这段更改为一个包含返回(return)表达式的语句将使临时变量在本地变量之前被删除。 // 如果把此临时变量绑定到一个变量,然后返回这个变量,也会先删除这个临时变量 match PrintOnDrop("Matched value in final expression") { // 在条件表达式执行后立即销毁 _ if PrintOnDrop("guard condition").0 == "" => (), _ => (), } }
Operands
操作数
在同一表达式中,在对其他操作数求值时,也会创建临时变量(/作用域)来将已求值的操作数的结果保存起来。临时变量与该操作数所属的表达式的作用域相关。操作数的临时变量一般不用销毁,因为一旦表达式求值,临时变量就被移走了,所以销毁它们没有任何效果和意义,除非整个表达式的某一操作数出现异常,或返回了,或触发了 panic。
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } loop { // 元组表达式未结束求值就提前返回了,所以其操作数按声明的反序销毁 ( PrintOnDrop("Outer tuple first"), PrintOnDrop("Outer tuple second"), ( PrintOnDrop("Inner tuple first"), PrintOnDrop("Inner tuple second"), break, ), PrintOnDrop("Never created"), ); } }
Constant promotion
常量提升
当表达式可以被写入到一个常量中,并被被借用时,就会将此值表达式提升到 'static
插槽('static
slot)状态,此时还可以通过此借用的逆操作(解引用)来解出最初写入的表达式,并且也不会改变运行时行为。也就是说,可以在编译时对此提升了的表达式进行求值,这求得的值不具备内部可变性或不包含析构器(这些特性在可能的情况下根据具体的值确定,例如 &None
的类型总是 &'static Option<_>
,因为 &None
的值是唯一确定的)。
Temporary lifetime extension
临时生存期扩展
注意:临时生存期扩展的确切规则可能还会改变。这里只描述了当前的行为表现。
let
语句中表达式的临时作用域有时会扩展到包含此 let
语句的块作用域内。根据某些句法规则,当通常的临时作用域太小时,就会这样做。例如:
#![allow(unused)] fn main() { let x = &mut 0; // 通常上面存储0的临时变量(或者临时位置)到这里就会被丢弃,但这里是一直存在到块的末尾。 println!("{}", x); }
如果一个借用、解引用、字段或元组索引表达式有一个扩展的临时作用域,那么它们的操作数也会跟着扩展。如果索引表达式有扩展的临时作用域,那么被它索引的表达式也会一并扩展作用域。
Extending based on patterns
基于模式的作用域扩展
能扩展临时作用域的*扩展性模式(extending pattern)*是下面任一:
- 通过引用或可变引用来实现变量绑定的标识符模式。
- 结构体(
struct
)模式、元组模式、元组结构体模式或切片模式,其中它们至少有一个直接子模式是扩展性模式。
所以 ref x
、V(ref x)
和 [ref x, y]
都是(能扩展临时作用域的)扩展性模式,但是 x
、&ref x
和 &(ref x,)
不是。
如果 let
语句中的模式是扩展性模式,那么其初始化器表达式中的临时作用域会被扩展。
Extending based on expressions
基于表达式的作用域扩展
对于带有初始化器的 let语句来说,能扩展临时作用域的*扩展性表达式(extending expression)*是以下表达式之一:
- 初始化表达式(initializer expression)。
- 扩展性的借用表达式的操作数。
- 扩展性的数组表达式、强制转换(cast)表达式、花括号括起来的结构体表达式或元组表达式的操作数。
- 任何扩展性的块表达式的最终表达式(final expression);
因此,在 &mut 0
、(&1, &mut 2)
和 Some { 0: &mut 3 }
中的借用表达式都是能扩展临时作用域的扩展性表达式。在 &0 + &1
和一些 Some(&mut 0)
中的借用不是:它们在句法上是函数调用表达式。
任何扩展性借用表达式的操作数的临时作用域都会随此表达式的临时作用域的扩展而扩展。
Examples
示例
下面是一些扩展了临时作用域的表达式的示例:
#![allow(unused)] fn main() { fn temp() {} trait Use { fn use_temp(&self) -> &Self { self } } impl Use for () {} // 在如下情况下,存储了 `temp()` 的结果的临时变量与 x 在同一个作用域中。 let x = &temp(); let x = &temp() as &dyn Send; let x = (&*&temp(),); let x = { [Some { 0: &temp(), }] }; let ref x = temp(); let ref x = *&temp(); x; }
下面是一些表达式没有扩展临时作用域的示例:
#![allow(unused)] fn main() { fn temp() {} trait Use { fn use_temp(&self) -> &Self { self } } impl Use for () {} // 在如下情况下,存储了 `temp()` 的结果的临时变量只存活到 let语句结束。 let x = Some(&temp()); // ERROR let x = (&temp()).use_temp(); // ERROR x; }
Not running destructors
阻断执行析构操作
std::mem::forget
被用来阻断变量的的析构操作,std::mem::ManuallyDrop
提供了一个包装器(wrapper)来防止变量或字段被自动销毁。
注意:通过 [
std::mem::forget
] 或其他方式阻止一个变量的析构操作是安全的,即使此变量的类型不是'static
。 除了本文档定义的能确保析构器正常运行的地方之外,类型的可靠性却依赖于析构器的正常执行,那此类型将不那么保险。
后文有时也直接简称为作用域。
这里说这句是因为操作符的操作数也涉及到销毁作用域范围的确定。
对这句话,这里译者按自己的理解再翻译一遍:一个表达式在位置表达式上使用时会被求值,如果此时没有具名变量和此值绑定,那就会先被保存进一个临时变量里,临时作用域就是伴随这此临时变量而生成。此作用域通常在此表达式所在的语句结束时结束,但如果求出的值被通过借用绑定给具名变量,此作用域会扩展到此具名变量的作用域(后面生存期扩展会讲到)。如果求出的值通过引用赋给了常量,这个作用域还会被进一步提升到全局作用域,这就是所谓的常量提升。
Lifetime elision
生存期(类型参数)省略
lifetime-elision.md
commit: 8c6c7cdc2b3188246747adbb9aa91ef39fd23fcf
本章译文最后维护日期:2024-03-09
Rust 拥有一套允许在多种位置省略生存期的规则,但前提是编译器在这些位置上能推断出合理的默认生存期。
Lifetime elision in functions
函数上的生存期(类型参数)省略
为了使常用模式使用起来更方便,可以在函数项、函数指针和闭包trait1的签名中省略生存期类型参数。以下规则用于推断出被省略的生存期类型参数。省略不能被推断出的生存期类型参数是错误的。占位符形式的生存期,'_
,也可以用这一套规则来推断出。对于路径中的生存期,首选使用 '_
。trait对象的生存期类型参数遵循不同的规则,具体这里讨论。
- 参数中省略的每个生存期类型参数都会(被推断)成为一个独立的生存期类型参数。
- 如果参数中只使用了一个生存期(省略或不省略都行),则将该生存期作为所有省略的输出生存期类型参数。
在方法签名中有另一条规则
- 如果接受者(receiver)类型为
&Self
或&mut Self
,那么对Self
的引用的生存期会被作为所有省略的输出生存期类型参数。
示例:
#![allow(unused)] fn main() { trait T {} trait ToCStr {} struct Thing<'a> {f: &'a i32} struct Command; trait Example { fn print1(s: &str); // 省略 fn print2(s: &'_ str); // 也省略 fn print3<'a>(s: &'a str); // 未省略 fn debug1(lvl: usize, s: &str); // 省略 fn debug2<'a>(lvl: usize, s: &'a str); // 未省略 fn substr1(s: &str, until: usize) -> &str; // 省略 fn substr2<'a>(s: &'a str, until: usize) -> &'a str; // 未省略 fn get_mut1(&mut self) -> &mut dyn T; // 省略 fn get_mut2<'a>(&'a mut self) -> &'a mut dyn T; // 未省略 fn args1<T: ToCStr>(&mut self, args: &[T]) -> &mut Command; // 省略 fn args2<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // 未省略 fn new1(buf: &mut [u8]) -> Thing<'_>; // 省略 - 首选的 fn new2(buf: &mut [u8]) -> Thing; // 省略 fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>; // 未省略 } type FunPtr1 = fn(&str) -> &str; // 省略 type FunPtr2 = for<'a> fn(&'a str) -> &'a str; // 未省略 type FunTrait1 = dyn Fn(&str) -> &str; // 省略 type FunTrait2 = dyn for<'a> Fn(&'a str) -> &'a str; // 未省略 }
#![allow(unused)] fn main() { // 下面的示例展示了不允许省略生存期类型参数的情况。 trait Example { // 无法推断,因为没有可以推断的起始参数。 fn get_str() -> &str; // 非法 // 无法推断,这里无法确认输出的生存期类型参数该遵从从第一个还是第二个参数的。 fn frob(s: &str, t: &str) -> &str; // 非法 } }
Default trait object lifetimes
默认的 trait对象的生存期
假定存在于(代表) trait对象(的那个胖指针)上的生存期(assumed lifetime)类型参数称为此 trait对象的默认对象生存期约束(default object lifetime bound)。这些在 RFC 599 中定义,在 RFC 1156 中修定增补。
当 trait对象的生存期约束被完全省略时,会使用默认对象生存期约束来替代上面定义的生存期类型参数省略规则。但如果使用 '_
作为生存期约束,则该约束仍遵循上面通常的省略规则。
如果将 trait对象用作泛型类型的类型参数,则首先使用此容器泛型来尝试为此 trait对象推断一个约束(来替代那个假定的生存期)。
- 如果存在来自此容器泛型的唯一约束,则该约束就为此 trait对象的默认约束
- 如果此容器泛型有多个约束,则必须指定一个约显式束为此 trait对象的默认约束
如果这两个规则都不适用,则使用该 trait对象的声明时的 trait约束:
- 如果原 trait 声明为单生命周期约束,则此 trait对象使用该约束作为默认约束。
- 如果
'static
被用做原 trait声明的任一一个生存期约束,则此 trait对象使用'static
作为默认约束。 - 如果原 trait声明没有生存期约束,那么此 trait对象的生存期会在表达式中根据上下文被推断出来,在表达式之外直接用
'static
。
#![allow(unused)] fn main() { // 对下面的 trait 来说,... trait Foo { } // 这两个是等价的,因为 `Box<T>` 对 `T` 没有生存期约束 type T1 = Box<dyn Foo>; //译者注:此处的 `T1` 和 上面备注中提到的 `Box<T>` 都是本节规则中所说的泛型类型,即容器泛型 type T2 = Box<dyn Foo + 'static>; // ...这也是等价的 impl dyn Foo {} impl dyn Foo + 'static {} // ...这也是等价的, 因为 `&'a T` 需要 `T: 'a` type T3<'a> = &'a dyn Foo; type T4<'a> = &'a (dyn Foo + 'a); // `std::cell::Ref<'a, T>` 也需要 `T: 'a`, 所以这俩也是等价的 type T5<'a> = std::cell::Ref<'a, dyn Foo>; type T6<'a> = std::cell::Ref<'a, dyn Foo + 'a>; }
#![allow(unused)] fn main() { // 这是一个反面示例: trait Foo { } struct TwoBounds<'a, 'b, T: ?Sized + 'a + 'b> { f1: &'a i32, f2: &'b i32, f3: T, } type T7<'a, 'b> = TwoBounds<'a, 'b, dyn Foo>; // ^^^^^^^ // 错误: 不能从上下文推导出此对象类型的生存期约束 }
注意,像 &'a Box<dyn Foo>
这样多层包装的,只需要看最内层包装 dyn Foo
的那层,所以扩展后仍然为 &'a Box<dyn Foo + 'static>
#![allow(unused)] fn main() { // 对下面的 trait 来说,... trait Bar<'a>: 'a { } // ...这两个是等价的: type T1<'a> = Box<dyn Bar<'a>>; type T2<'a> = Box<dyn Bar<'a> + 'a>; // ...这俩也是等价的: impl<'a> dyn Bar<'a> {} impl<'a> dyn Bar<'a> + 'a {} }
'static
lifetime elision
静态('static
)生存期省略
除非指定了显式的生存期,引用类型的常量项声明和静态项声明都具有隐式的静态('static
)生存期。因此,有 'static
生存期的常量项声明在编写时可以略去其生存期。
#![allow(unused)] fn main() { // STRING: &'static str const STRING: &str = "bitstring"; struct BitsNStrings<'a> { mybits: [u32; 2], mystring: &'a str, } // BITS_N_STRINGS: BitsNStrings<'static> const BITS_N_STRINGS: BitsNStrings<'_> = BitsNStrings { mybits: [1, 2], mystring: STRING, }; }
注意,如果静态项(static
)或常量项(const
)包含对函数或闭包的引用,而这些函数或闭包本身也包含引用,此时编译器将首先尝试使用标准的省略规则来推断生存期类型参数。如果它不能通过通常的生存期省略规则来推断出生存期类型参数,那么它将报错。举个例子:
#![allow(unused)] fn main() { struct Foo; struct Bar; struct Baz; fn somefunc(a: &Foo, b: &Bar, c: &Baz) -> usize {42} // 解析为 `for<'a> fn(&'a str) -> &'a str`。 const RESOLVED_SINGLE: fn(&str) -> &str = |x| x; // 解析为 `for<'a, 'b, 'c> Fn(&'a Foo, &'b Bar, &'c Baz) -> usize`。 const RESOLVED_MULTIPLE: &dyn Fn(&Foo, &Bar, &Baz) -> usize = &somefunc; }
#![allow(unused)] fn main() { struct Foo; struct Bar; struct Baz; fn somefunc<'a,'b>(a: &'a Foo, b: &'b Bar) -> &'a Baz {unimplemented!()} // 没有足够的信息将返回值的生存期与参数的生命周期绑定起来,因此这是一个错误 const RESOLVED_STATIC: &dyn Fn(&Foo, &Bar) -> &Baz = &somefunc; // ^ // 这个函数的返回类型包含一个借用来的值,但是签名没有说明它是从参数1还是从参数2借用来的 }
指 Fn、FnMute 和 FnOnce 这三个 trait。
Special types and traits
特殊类型和 trait
special-types-and-traits.md
commit: 330ef9569444a7414633ba08cf5090da312f1f18
本章译文最后维护日期:2024-05-02
标准库中的某些类型和 trait 在 Rust 编译器中也直接能用。本章就阐述了这些类型和 trait 的特殊特性。
Box<T>
Box<T>
有一些特殊的特性,Rust 目前还不允许用户定义类型时使用。
Box<T>
的[解引用操作符]会产生一个可从中移出值的内存位置1。这(种特殊性)意味着应用在Box<T>
上的*
运算符和Box<T>
的析构函数都是语言内置的。- 方法可以使用
Box<Self>
作为接受者。 Box<T>
可以绕过孤儿规则(orphan rules),与T
在同一 crate 中实现同一 trait,其他泛型类型无法绕过。
Rc<T>
Arc<T>
Pin<P>
UnsafeCell<T>
std::cell::UnsafeCell<T>
用于内部可变性。它确保编译器不会对此类类型执行不正确的优化。它还能确保具有内部可变类型的静态(static
)项不会被放在标记为只读的内存中。
PhantomData<T>
std::marker::PhantomData<T>
是一个零内存宽度零的、最小对齐量的、被认为拥有(own) T
的类型,这个类型存在目的是应用在确定型变关系、销毁检查和自动trait 中的。
Operator Traits
运算符/操作符trait
std::ops
和 std::cmp
中的 trait 看用于重载 Rust 的运算符/操作符、索引表达式和调用表达式。
Deref
and DerefMut
除了重载一元 *
运算符外,Deref
和 DerefMut
也用于方法解析(method resolution)和利用 Deref
达成自动强转。
Drop
Drop
trait 提供了一个析构函数,每当要销毁此类型的值时就会运行它。
Copy
Copy
trait 改变实现它的类型的语义。其类型实现了 Copy
的值在赋值时将被复制而不是移动。
只能为未实现 Drop
trait 且字段都是 Copy
的类型实现 Copy
。2
对于枚举,这意味着所有变体的所有字段都必须是 Copy
的。
对于联合体,这意味着所有的变体都必须是 Copy
的。
Copy
已由编译器实现给了下述类型:
Clone
Clone
trait 是 Copy
的超类trait,所以它也需要编译器生成实现。它被编译器实现给了以下类型:
Send
Send
trait 表明这种类型的值可以安全地从一个线程发送给另一个线程。
Sync
Sync
trait 表示在多个线程之间共享这种类型的值是安全的。必须为不可变静态(static
)项中使用的所有类型实现此 trait。
Termination
Termination
trait 表示 main函数或 test函数 那些可被接受的返回类型。
Auto traits
Send
、Sync
、Unpin
、UnwindSafe
和 RefUnwindSafe
trait 都是自动trait(auto traits)。自动trait 具有特殊的属性。
对于给定类型,如果没有为其显式实现或否定实现(negative implementation)某自动trait,那么编译器就会根据以下规则去自动此为类型实现该自动trait:
- 如果
T
实现了某自动trait,那&T
、&mut T
、*const T
、*mut T
、[T; n]
和[T]
也会实现此自动trait。 - 函数项类型(function item types)和函数指针会自动实现这些自动trait。
- 如果结构体、枚举、联合体和元组它们的所有字段都实现了这些自动trait,则它们本身也会自动实现这些自动trait。
- 如果闭包捕获的所有变量的类型都实现了这些自动trait,那么闭包会自动实现这些自动trait。一个闭包通过共享引用捕获了一个
T
,同时通过传值的方式捕获了一个U
,那么该闭包会自动实现&T
和U
所共同实现的那些自动trait。
对于泛型类型(上面的这些内置类型也算是建立在 T
上的泛型),如果泛型实现在当前已够用,则编译器不会再为其实现其他的自动trait,除非它们不满足必需的 trait约束。例如,标准库在 T
是 Sync
的地方都为 &T
实现了 Send
;这意味着如果 T
是 Send
,而不是 Sync
,编译器就不会为 &T
自动实现 Send
。
自动trait 也可以有否定实现,在标准库文档中表示为 impl !AutoTrait for T
,它覆盖了自动实现。例如,*mut T
有一个关于 Send
的否定实现,所以 *mut T
不是 Send
的,即使 T
是。目前还没有稳下来的方法来指定其他类型的否定实现;目前否定实现只能在标准库里可以稳定使用。3
自动trait 可以作为额外的约束添加到任何 trait对象上,尽管通常我们见到的 trait对象的类型名上一般只显示一个 trait。例如,Box<dyn Debug + Send + UnwindSafe>
就是一个有效的类型。
Sized
Sized
trait表明这种类型的内存宽度在编译时是已知的;也就是说,它不是一个动态内存宽度类型。
类型参数 (除了 trait 中的Self
)和关联类型默认是 Sized
的。
Sized
总是由编译器自动实现,而不是由实现(implementation items)主动实现的。
这些隐式的 Sized
约束可以通过指定 ?Sized
约束来放宽约束。
这里是相对普通的借用/引用来说,普通的借用/引用对指向的内存位置不拥有所有权,所以无法从中移出值。
实现 Drop
trait 的类型只能是使用移动语义(move)的类型。
标准库外使用需要打开特性 #![feature(negative_impls)]
。
Names
名称
keywords.md
commit: 77ab06c34e50e9cce04acf979a4402fa01ef48e9 本章译文最后维护日期:2024-06-15
*实体(entity)*是一种语言结构,在源程序中可以以某种方式被引用,通常是通过[路径(path)][paths]。实体包括类型、程序项、泛型参数、变量绑定、循环标签、生存期、字段、属性和各种lints。\
声明(declaration)是一种句法结构,它可以引入名称来引用实体。实体的名称在相关作用域(scope)内有效。作用域是指可以引用该名称的源码区域。
有些实体是在源码中显式声明的,有些则隐式声明为语言或编译器扩展的一部分。
路径用于引用可能在其他的模块或类型内的实体。生存期和循环标签使用一个带有前导单引号的专用语法来表达。
名称被分隔成不同的命名空间,这样允许不同名称空间中的实体拥有相同的名称,且不会发生冲突。
名称解析是将路径、标识符和标签绑定到实体声明的编译时过程。
对某些名称的访问可能会受到此名称的可见性的限制。
Explicitly declared entities
显式声明的实体
在源码中显式引入名称的实体有:
- 程序项:
- 表达式:
- 泛型参数
- 高阶trait约束
let
语句中的模式绑定macro_use
属性可以从其他 crate 里引入宏名称。macro_export
属性可以为当前宏引入一个在当前 crate 的根模块下生效的别名
此外,宏调用和属性可以通过扩展源代码到上述程序项之一来引入名称。
Implicitly declared entities
隐式声明的实体
以下实体由语言隐式定义,或由编译器选项和编译器扩展引入:
- 语言预导入包:
- 布尔型 —
bool
- 文本型 —
char
andstr
- 整型 —
i8
,i16
,i32
,i64
,i128
,u8
,u16
,u32
,u64
,u128
- 和机器平台相关的整型 —
usize
andisize
- 浮点型 —
f32
andf64
- 布尔型 —
- 内置属性
- 标准库预导入包里的程序项、属性和宏
- 在根模块下的标准库里的crate
- 通过编译器链接进的外部crate
- 工具类属性
- Lints 和 工具类lint属性
- 派生辅助属性无需显示引入,就在其程序项内有效
'static
生存期标签
此外,crate 的根模块没有名称,但可以使用某些路径限定符或别名来引用。
Namespaces
命名空间
use-declarations.md
commit: aa9c70bda63b3ab73b15746609831dafb96f56ff
本章译文最后维护日期:2023-05-03
命名空间是已声明的名称的逻辑分组。根据名称所指的实体类型,名称被分隔到不同的命名空间中。 名称空间允许一个名称空间中出现的名称与另一个名称空间中的相同,且不会导致冲突。
在命名空间中,名称被组织在不同的层次结构中,层次结构的每一层都有自己的命名实体集合。
程序有几个不同的命名空间,每个名称空间包含不同种类的实体。使用名称时将根据上下文来在不同的命名空间中去查找该名称的声明,名称解析一章有讲到这些。
下面是一系列命名空间及其对应实体的列表:
- 类型命名空间
- 值命名空间
- 宏命名空间
- 生存期命名空间
- 标签命名空间
如何清晰地使用不同命名空间中的同名名称的示例:
#![allow(unused)] fn main() { // Foo 在类型命名空间中引入了一个类型,在值命名空间中引入了一个构造函数 struct Foo(u32); // 宏`Foo`在宏命名空间中声明 macro_rules! Foo { () => {}; } // 参数`f` 的类型中的 `Foo` 指向类型命名空间中的 `Foo` // `'Foo` 引入一个生存期命名空间里的新的生存期 fn example<'Foo>(f: Foo) { // `Foo` 引用值命名空间里的 `Foo`构造器。 let ctor = Foo; // `Foo` 引用宏命名空间里的 `Foo`宏。 Foo!{} // `'Foo` 引入一个标签命名空间里的标签。 'Foo: loop { // `'Foo` 引用 `'Foo`生存期参数, `Foo` 引用类型命名空间中的类型。 let x: &'Foo Foo; // `'Foo` 引用了 `'Foo`标签. break 'Foo; } } }
Named entities without a namespace
无命名空间的命名实体
下面的实体有显式的名称,但是这些名称不属于任何特定的命名空间。
Fields
字段
即使结构体、枚举和联合体的字段被命名,但这些命名字段并不存在于任何显式的命名空间中。它们只能通过字段表达式访问,该表达式只检测被访问的特定类型的字段名。
Use declarations
use声明
use声明命名了导入到当前作用域中的实体,但 use
项本身不属于任何特定的命名空间。相反,它可以在多个名称空间中引入别名,这取决于所导入的程序项类型。
Sub-namespaces
子命名空间
宏的命名空间分为两个子命名空间:一个子命名空间用于[那种带感叹号(!)的宏][bang style macros],另一个子命名空间用于属性。 解析属性时,此属性所影响的作用域中的任何带感叹号(!)的宏都将被忽略。 反之,解析带感叹号(!)的宏将忽略作用域中的任何属性宏。 这样可以防止一种形式的宏遮挡另一种形式的。
例如,cfg
属性和cfg
宏是宏命名空间中具有相同名称的两个不同实体,但它们仍然可以在各自的上下文中使用。
使用 use
导入来对另一个宏进行遮蔽处理仍然是错误的,不管它们的子命名空间是什么
Scopes
作用域
use-declarations.md
commit: fe9eec6b3eb3dc4e42d83d40adc2c9a2660ea874
本章译文最后维护日期:2024-06-15
作用域是源文件中的区域,在这个区域中命名的实体可以用该名称来引用。 下面的部分提供了关于作用域规则和行为的详细信息,这些规则和行为取决于实体的类型及其声明的位置。 名称解析一章描述了如何将名称解析为实体的过程。 关于“删除作用域”的更多信息,可以在析构函数一章中找到。
Item scopes
程序项作用域
在模块中直接声明的程序项的名称具有从模块开始到模块结束的作用域。这些程序项也是模块的成员,可以用以它们的模块名为前导的路径来引用。
声明为语句的程序项的名称具有从该项语句所在的块的开始直到块的结束的作用域。
在同一模块(或块)内的同一命名空间中不可引入重复名称。
::*
式的导入 在处理重复名称和遮蔽方面有特殊的行为,请参阅链接章节来了解更多详细信息。
模块中的程序项可能会遮蔽掉预导入包中的程序项。
外部模块中的程序项名称在内部模块中不起作用。 路径可以用来指向另一个模块中的一个程序项。
Associated item scopes
关联程序项的作用域
关联项没有作用域,只能通过使用从它们关联的类型或 trait 名开始的路径来引用。 方法也可以通过调用表达式引用。
与模块或块中的程序项类似,如果在 trait 或实现中引入一个程序项,而该项名与同一命名空间中的trait或impl中的另一个项名重复,则会出现错误。
Pattern binding scopes
模式绑定的(符号)作用域
局部变量的模式绑定的作用域取决于使用它的位置:
- [
let
语句]这种绑定的作用域从let
语句之后一直到声明它的块的末尾。 - 函数参数这种绑定的作用域在函数体中。
- 闭包参数这种绑定的作用域在闭包体中。
for
和 [whilet
] 这种绑定在循环体中。- [
if-let
] 这种绑定在后继块中。 - [
match
arms] 这种绑定在匹配守卫和匹配臂表达式中。
局部变量的作用域不会扩展到程序项的声明中。
Pattern binding shadowing
模式绑定的(符号) 遮蔽效应
允许模式绑定遮蔽作用域中的任何名称,但遮蔽以下程序项是错误的:
下面的示例说明了本地绑定如何遮蔽已声明的程序项:
#![allow(unused)] fn main() { fn shadow_example() { // 由于作用域中还没有局部变量,因此将解析为函数。 foo(); // prints `function` let foo = || println!("closure"); fn foo() { println!("function"); } // 这解析为本地闭包,因为它遮蔽前面声明的程序项。 foo(); // prints `closure` } }
Generic parameter scopes
泛型参数的作用域
泛型参数在GenericParams列表中声明。 泛型参数的作用域在包含在声明它的程序项中。
所有参数都能在泛型参数列表中的作用域内有效,无论它们的声明顺序如何。 下面展示了一些参数在声明之前可以引用的示例:
#![allow(unused)] fn main() { // 'b在被定义之前就可以被使用 fn params_scope<'a: 'b, 'b>() {} trait SomeTrait<const Z: usize> {} // 在声明常量N之前,N就能在 trait约束中使用 fn f<T: SomeTrait<N>, const N: usize>() {} }
泛型参数在类型约束和 where子句的作用域内也算是域内有效的,例如:
#![allow(unused)] fn main() { trait SomeTrait<'a, T> {} // `SomeTrait` 的 <'a, U> 可以使用 `bounds_scope` 的泛型参数 'a 和 U。 fn bounds_scope<'a, T: SomeTrait<'a, U>, U>() {} fn where_scope<'a, T, U>() where T: SomeTrait<'a, U> {} }
函数内声明的程序项不能从其外部作用域里引用泛型参数。
#![allow(unused)] fn main() { fn example<T>() { fn inner(x: T) {} // ERROR: can't use generic parameters from outer function } }
Generic parameter shadowing
泛型参数的遮蔽效应
不能遮蔽泛型参数,但允许在函数中声明的程序项去遮蔽该函数的泛型形参名称。
#![allow(unused)] fn main() { fn example<'a, T, const N: usize>() { // 允许在函数中的声明的程序项在作用域中遮蔽上层泛型参数。 fn inner_lifetime<'a>() {} // OK fn inner_type<T>() {} // OK fn inner_const<const N: usize>() {} // OK } }
#![allow(unused)] fn main() { trait SomeTrait<'a, T, const N: usize> { fn example_lifetime<'a>() {} // ERROR: 'a is already in use fn example_type<T>() {} // ERROR: T is already in use fn example_const<const N: usize>() {} // ERROR: N is already in use fn example_mixed<const T: usize>() {} // ERROR: T is already in use } }
Lifetime scopes
生存期的作用域
生存期参数在 GenericParams列表和高阶 trait约束中声明
'static
和占位符生存期 '_
拥有特殊的意义,不能被声明为参数。
Lifetime generic parameter scopes
泛型生存期的作用域
常量项和静态项和常量上下文只允许 'static
生存期引用,因此它们中不能有其他生存期。
关联常量允许引用在其所在的trait或实现中声明的生存期。
Higher-ranked trait bound scopes
高阶 trait约束的作用域
声明为高阶 trait约束的生存期参数的作用域取决于使用它的场景。
- 作为 TypeBoundWhereClauseItem 所声明的生存期参数在类型和类型约束的作用域内有效。
- 作为 TraitBound 所声明的生存期参数在此类型约束路径所形成的作用域内有效。
- 作为 BareFunctionType 所声明的生存期参数在函数参数和返回类型的作用域内有效。
#![allow(unused)] fn main() { trait Trait<'a>{} fn where_clause<T>() // 'a 在类型和类型约束内都有效 where for <'a> &'a T: Trait<'a> {} fn bound<T>() // 'a 只在约束内有效 where T: for <'a> Trait<'a> {} struct Example<'a> { field: &'a u32 } // 'a 在函数参数和返回类型的作用域内有效 type FnExample = for<'a> fn(x: Example<'a>) -> Example<'a>; }
Impl trait restrictions
Impl trait 时的限制
Impl trait类型只能引用在函数或实现上声明的生存期。
#![allow(unused)] fn main() { trait Trait1 { type Item; } trait Trait2<'a> {} struct Example; impl Trait1 for Example { type Item = Element; } struct Element; impl<'a> Trait2<'a> for Element {} // 此处的 `impl Trait2` 不允许引用 'b,但允许引用 'a。 fn foo<'a>() -> impl for<'b> Trait1<Item = impl Trait2<'a>> { // ... Example } }
Loop label scopes
循环标签的作用域
循环标签可以由循环表达式声明。
循环标签的作用域是从声明它的地方到循环表达式的末尾。
循环标签的作用域不会扩展到程序项、闭包、异步块、常量实参、常量上下文和定义 [for循环][for
loop]的迭代器表达式。
#![allow(unused)] fn main() { 'a: for n in 0..3 { if n % 2 == 0 { break 'a; } fn inner() { // 在这使用 'a 会报错。 // break 'a; } } // The label is in scope for the expression of `while` loops. 'a: while break 'a {} // Loop does not run. 'a: while let _ = break 'a {} // Loop does not run. // The label is not in scope in the defining `for` loop: 'a: for outer in 0..5 { // 这将中断外循环,跳过内循环并停止外循环。T 'a: for inner in { break 'a; 0..1 } { println!("{}", inner); // 不会执行到这里 } println!("{}", outer); // 也不会执行到这里 } }
循环标签可以遮蔽外部作用域中的同名的标签。 引用循环标签时引用的是最近定义的那个。
#![allow(unused)] fn main() { // 循环标签遮蔽效果示例。 'a: for outer in 0..5 { 'a: for inner in 0..5 { // 这将终止内部循环,但外部循环将继续运行。 break 'a; } } }
Prelude scopes
预导入包的作用域
预导入包将实体引入到模块的作用域内。 实体不是当前模块的成员,但在名称解析期间隐式引入。 预导入包内实体名称可以被模块中的实体声明所遮蔽。
预导入包是分层的,因此如果它们中包含了同名的实体,则后相互遮蔽。 预导入包遮蔽其他预导入包的顺序如下,其中较早导入的程序项可以遮蔽较晚导入的:
macro_rules
scopes
声明宏的作用域
macro_rules
宏的作用域在macros By Example一章中详述。
声明宏的行为取决于 macro_use
和 [macror_export
]属性的使用。
Derive macro helper attributes
派生宏辅助属性
派生宏辅助属性在应用了相应derive
属性的程序项的作用域内有效。
这种作用域从 derive
属性之后扩展到该程序项的末尾。<!--注:严格来说不完全正确,具体请参阅https://github.com/rust-lang/rust/issues/79202 -->
辅助对象属性在作用域内会遮蔽同名的其他属性。
Self
scope
Self
作用域
尽管 Self
是一个具有特殊含义的关键字,但它与名称解析的交互方式类似于普通名称。
struct、enum、union、trait或implementation的定义中的隐式 Self
类型被用类似于[泛型参数](#generic parameter scopes)的方式来处理,并以与泛型类型参数相同的方式来生效作用域。
implementation中的隐式Self
值构造函数在实现的主体代码(实现的关联项)的作用域内有效。
#![allow(unused)] fn main() { // Self type within struct definition. struct Recursive { f1: Option<Box<Self>> } // Self type within generic parameters. struct SelfGeneric<T: Into<Self>>(T); // Self value constructor within an implementation. struct ImplExample(); impl ImplExample { fn example() -> Self { // Self type Self() // Self value constructor } } }
Preludes
预导入包
use-declarations.md
commit: 0181f237f508a8109d99db76a4a5ab48d6132b93
本章译文最后维护日期:2022-03-14
预导入包是一组名称的集合,它会自动把这些名称导入到 crate 中的每个模块的作用域中。
预导入包中的那些名称不是当前模块本身的一部分:它们在名称解析期间被隐式导入。例如,即使像 Box
这样在每个模块的作用域中到处使用的名称,你也不能通过 self::Box
来引用它,因为它不是当前模块的成员。
有几个不同的预导入包:
Standard library prelude
标准库预导入包
每个 crate 都有一个标准库预导入包,此包是一个标准库模块。
具体使用的模块取决于 crate 的版次,以及 no_std
属性是否应用于此 crate:
版次 | 非 no_std 环境 | no_std 环境 |
---|---|---|
2015 | std::prelude::rust_2015 | core::prelude::rust_2015 |
2018 | std::prelude::rust_2018 | core::prelude::rust_2018 |
2021 | std::prelude::rust_2021 | core::prelude::rust_2021 |
注意:
std::prelude::rust_2015
和std::prelude::rust_2018
与std::prelude::v1
的内容一致。
core::prelude::rust_2015
andcore::prelude::rust_2018
与core::prelude::v1
的内容一致。
Extern prelude
外部预导入包
在根模块中使用 extern crate
导入的外部crate 或直接给编译器提供的的外部crate(也就是在 rustc
命令下使用 --extern
命令行参数选项)会被添加到外部预导入包中。如果使用 extern crate orig_name as new_name
这类别名导入,则符号 new_name
将被添加到此预导入包。
core
crate 总是会被添加到外部预导入包中。只要 no_std
属性没有在 crate根模块中指定,那么std
crate 就会被添加进来
版次差异:在 2015 版中,在外部预导入包中的 crate 不能通过 use声明来直接引用,因此通常标准做法是用
extern crate
将那它们纳入到当前作用域。从 2018 版开始, use声明可以直接引用外部预导入包里的 crate,所以再在代码里使用
extern crate
就会被认为是不规范的。
注意: 随
rustc
一起引入的 crate,如alloc
和test
,在使用 Cargo 时不会自动被包含在--extern
命令行参数选项中。即使在 2018 版中,也必须通过外部crate(extern crate
)声明来把它们引入到当前作用域内。#![allow(unused)] fn main() { extern crate alloc; use alloc::rc::Rc; }
Cargo却会将
proc_macro
带入到编译类型为 proc-macro 的 crate 的外部预导入包中
The no_std
attribute
no_std
属性
默认情况下,标准库自动包含在 crate根模块中。在 std
crate 被添加到根模块中的同时,还会隐式生效一个 macro_use
属性,它将所有从 std
中导出的宏放入到macro_use
预导入包中。默认情况下,core
和 std
都被添加到外部预导入包中。
*no_std
属性*可以应用在 crate 级别上,用来防止 std
crate 被自动添加到相关作用域内。此属性作了如下三件事:
- 阻止
std
crate 被添加进外部预导入包。 - 影响标准库预导入包(前面描述过)具体由哪一个模块组成。
- 使用
core
crate 替代std
crate 来注入到当前 crate 的根模块中,同时把core
crate下的所有宏导入到macro_use
预导入包中。
注意:当 crate 的目标平台不支持标准库或者故意不使用标准库的功能时,使用核心预导入包而不是标准预导入包是很有用的。此时没有导入的标准库的那些功能主要是动态内存分配(例如:
Box
和'Vec
)和文件,以及网络功能(例如:std::fs
和std::io
)。
警告:使用 no_std
并不会阻止标准库被链接进来。使用 extern crate std;
将 std
crate 导入仍然有效,相关的依赖项也可以被正常链接进来。
Language prelude
语言预导入包
语言预导入包包括语言内置的类型名称和属性名称。语言预导入包总是在当前作用域内有效的。它包括以下内容:
- 类型命名空间
- 布尔型 —
bool
- 文本型 —
char
和str
- 整型 —
i8
,i16
,i32
,i64
,i128
,u8
,u16
,u32
,u64
,u128
- 和机器平台相关的整型 —
usize
和isize
- 浮点型 —
f32
和f64
- 布尔型 —
- 宏命名空间
macro_use
prelude
macro_use
预导入包
macro_use
预导入包包含了外部crate 中的宏,这些宏是通过在当前文档源码内部的 extern crate
声明语句上应用 macro_use
属性来导入此声明中的 crate 内部的宏。
Tool prelude
工具类预导入包
工具类预导入包包含了在类型命名空间中声明的外部工具的工具名称。请参阅工具类属性一节,以了解更多细节。
The no_implicit_prelude
attribute
no_implicit_prelude
属性
*no_implicit_prelude
属性*可以应用在 crate级别或模块上,用以指示它不应该自动将标准库预导入包、外部预导入包或工具类预导入包引入到当前模块或其任何子模块的作用域中。
此属性不影响语言预导入包。
版次差异: 在 2015版中,
no_implicit_prelude
属性不会影响macro_use
预导入包,从标准库导出的所有宏仍然包含在macro_use
预导入包中。从 2018版开始,它也会禁止macro_use
预导入包生效。
Paths
路径
paths.md
commit: 6c77f499eaf64bd89c29a8932e63a9343ee73663
本章译文最后维护日期:2024-04-06
路径是一个或多个由命名空间限定符(::
)逻辑分隔的路径段(path segments)组成的序列(译者注:如果只有一个段的话,::
不是必须的)。如果路径仅由一个路径段组成,则它引用局部控制域(control scope)内的程序项或变量。如果路径包含多个路径段,则总是引用程序项。
仅由标识符段组成的简单路径(simple paths)的两个示例:
x;
x::y::z;net
Types of paths
路径分类
Simple Paths
简单路径
句法
SimplePath :
::
? SimplePathSegment (::
SimplePathSegment)*SimplePathSegment :
IDENTIFIER |super
|self
|crate
|$crate
简单路径可用于可见性标记、属性、宏和 use
程序项中。示例:
#![allow(unused)] fn main() { use std::io::{self, Write}; mod m { #[clippy::cyclomatic_complexity = "0"] pub (in super) fn f1() {} } }
Paths in expressions
表达式中的路径
句法
PathInExpression :\net::
? PathExprSegment (::
PathExprSegment)*PathExprSegment :
PathIdentSegment (::
GenericArgs)?PathIdentSegment :
IDENTIFIER |super
|self
|Self
|crate
|$crate
GenericArgs :
<
>
|<
( GenericArg,
)* GenericArg,
?>
GenericArg :\net Lifetime | Type | GenericArgsConst | GenericArgsBinding | GenericArgsBounds
GenericArgsConst :
BlockExpression
| LiteralExpression
|-
LiteralExpression
| SimplePathSegmentGenericArgsBinding :
IDENTIFIER GenericArgs?=
TypeGenericArgsBounds :
IDENTIFIER GenericArgs?:
TypeParamBounds
表达式中的路径允许指定带有泛型参数的路径。它们在各种表达式和模式中都有使用。
token ::
必须在泛型参数的左尖括号(<
)的前面,以避免和小于号操作符产生混淆。这就是俗称的“涡轮鱼(turbofish)”句法。
#![allow(unused)] fn main() { (0..10).collect::<Vec<_>>();net Vec::<u8>::with_capacity(1024); }
泛型参数的顺序被限制为生存期参数,然后是类型参数,然后是常量参数,再后是相应的约束。
常量实参必须用花括号括起来,除非它们是字面量或单段路径。
impl Trait
这类的合成类型参数是隐式的,不能显式指定。
Qualified paths
限定性路径
句法
QualifiedPathInExpression :
QualifiedPathType (::
PathExprSegment)+QualifiedPathType :
<
Type (as
TypePath)?>
QualifiedPathInType :
QualifiedPathType (::
TypePathSegment)+
完全限定性路径可以用来为 trait实现(trait implementations)消除路径歧义,也可以用来指定规范路径。用在指定具体类型时,它支持使用如下所示的类型句法:
#![allow(unused)] fn main() { struct S; impl S { fn f() { println!("S"); } } trait T1 { fn f() { println!("T1 f"); } } impl T1 for S {} trait T2 { fn f() { println!("T2 f"); } } impl T2 for S {} S::f(); // 调用固有实现 <S as T1>::f(); // 调用T1的 trait函数 <S as T2>::f(); // 调用T2的 trait函数 }
Paths in types
类型中的路径
句法
TypePath :
::
? TypePathSegment (::
TypePathSegment)*TypePathSegment :
PathIdentSegment (::
? (GenericArgs | TypePathFn))?TypePathFn :
(
TypePathFnInputs?)
(->
TypeNoBounds)?
类型路径用于类型定义、trait约束(trait bound)、类型参数约束,以及限定性路径。
尽管在泛型参数之前允许使用 token ::
,但它不是必需的,因为在类型路径中不存在像 PathInExpression 句法中那样的歧义。
#![allow(unused)] fn main() { mod ops { pub struct Range<T> {f1: T} pub trait Index<T> {} pub struct Example<'a> {f1: &'a i32} } struct S; impl ops::Index<ops::Range<usize>> for S { /*...*/ } fn i<'a>() -> impl Iterator<Item = ops::Example<'a>> { // ... const EXAMPLE: Vec<ops::Example<'static>> = Vec::new(); EXAMPLE.into_iter() } type G = std::boxed::Box<dyn std::ops::FnOnce(isize) -> isize>; }
Path qualifiers
路径限定符
路径可以被各种能改变其解析方式的前导限定符限定。
::
以 ::
开头的路径被认为是全局路径,其中的路径首段在不同的版次中的解析方式有所不同。但路径中的每个标识符都必须解析为一个程序项。
版次差异: 在2015版中,标识符解析从“create 根模块(crate root)”(2018版中表示为
crate::
)开始,“create 根模块(crate root)”中包含了一系列不同的程序项,包括外部crate、默认create(如std
或core
),以及 crate下的各种顶层程序项(包括use
导入)。从 2018 版开始,以
::
开头的路径被解析为外部预导入包中的一个 crate。也就是说,其后必须跟一个 crate的名称。
#![allow(unused)] fn main() { pub fn foo() { // 在 2018版中,这种访问 `std` 的方法是通过外部预导入包的形式达到的 // 在 2015版中, 这种访问 `std` 的方法是通过crate根的形式达到的 let now = ::std::time::Instant::now(); println!("{:?}", now); } }
// 2015 Edition mod a { pub fn foo() {} } mod b { pub fn foo() { crate::a::foo(); // 调用 `a`'的 foo 函数 // 在 Rust 2018 中, `::a` 会被解析为 crate `a`. } } fn main() {}
self
self
表示路径是相对于当前模块的路径。self
仅可以用作路径的首段,不能有前置 ::
。
在方法体中,由 self
段组成的路径被解析为此方法的 self参数。
fn foo() {} fn bar() { self::foo(); } struct S(bool); impl S { fn baz(self) { self.0; } } fn main() {}
Self
Self
(首字母大写)用于指代 trait 和实现中的类型。
Self
仅可以用作路径的首段,不能有前置 ::
。
#![allow(unused)] fn main() { trait T { type Item; const C: i32; // `Self` 将是实现 `T` 的任何类型。 fn new() -> Self; // `Self::Item` 将是实现中指定的类型的别名。 fn f(&self) -> Self::Item; } struct S; impl T for S { type Item = i32; const C: i32 = 9; fn new() -> Self { // `Self` 是类型 `S`. S } fn f(&self) -> Self::Item { // `Self::Item` 是类型 `i32`. Self::C // `Self::C` 是常量值 `9`. } } }
super
super
在路径中被解析为父模块。它只能用于路径的前导段,可以置于 self
路径段之后。
mod a { pub fn foo() {} } mod b { pub fn foo() { super::a::foo(); // 调用 a'的 foo 函数 } } fn main() {}
super
可以在第一个 super
或 self
之后重复多次,以引用祖先模块。
mod a { fn foo() {} mod b { mod c { fn foo() { super::super::foo(); // 调用 a'的 foo 函数 self::super::super::foo(); // 调用 a'的 foo 函数 } } } } fn main() {}
crate
crate
解析相对于当前 crate 的路径。crate 仅能用作路径的首段,不能有前置 ::
。
fn foo() {} mod a { fn bar() { crate::foo(); } } fn main() {}
$crate
$crate 仅用在宏转码器(macro transcriber)中,且仅能用作路径首段,不能有前置 ::
。$crate
将被扩展为从定义宏的 crate 的顶层访问该 crate 中的各程序项的路径,而不用去考虑宏调用发生时所在的现场 crate。
pub fn increment(x: u32) -> u32 { x + 1 } #[macro_export] macro_rules! inc { ($x:expr) => ( $crate::increment($x) ) } fn main() { }
Canonical paths
规范路径
定义在模块或者实现中的程序项都有一个规范路径,该路径对应于其在其 crate 中定义的位置。在该路径之外,所有指向这些程序项的路径都是别名(路径)。规范路径被定义为一个路径前缀后跟一个代表程序项本身的那段路径段。
尽管实现所定义/实现的程序项有规范路径,但实现作为一个整体,自身没有规范路径。use声明也没有规范路径。块表达式中定义的程序项也没有规范路径。在没有规范路径的模块中定义的程序项也没有规范路径。在实现中定义的关联项(associated items)如果它指向没有规范路径的程序项——例如,实现类型(implementing type)、被实现的 trait、类型参数或类型参数上的约束——那它也没有规范路径。
模块的路径前缀就是该模块的规范路径。对于裸实现(bare implementations)来说,它里面的程序项的路径前缀是:它当前实现的程序项的规范路径,再用尖括号(<>
)括把此路径括起来。对于 trait实现来说,它内部的程序项的路径前缀是:当前实现的程序项的规范路径后跟一个 as
,再后跟 trait 本身的规范路径,然后整个使用尖括号(<>
)括起来。
规范路径只在给定的 crate 中有意义。在 crate 之间没有全局命名空间,所以程序项的规范路径只在其 crate 中可标识。
// 注释解释了程序项的规范路径 mod a { // crate::a pub struct Struct; // crate::a::Struct pub trait Trait { // crate::a::Trait fn f(&self); // crate::a::Trait::f } impl Trait for Struct { fn f(&self) {} // <crate::a::Struct as crate::a::Trait>::f } impl Struct { // 译者注:这是一个裸实现 fn g(&self) {} // <crate::a::Struct>::g } } mod without { // crate::without fn canonicals() { // crate::without::canonicals struct OtherStruct; // None trait OtherTrait { // None fn g(&self); // None } impl OtherTrait for OtherStruct { fn g(&self) {} // None } impl OtherTrait for crate::a::Struct { fn g(&self) {} // None } impl crate::a::Trait for OtherStruct { fn f(&self) {} // None } } } fn main() {}
Name resolution
名称解析
use-declarations.md
commit: eabdf09207bf3563ae96db9d576de0758c413d5d
本章译文最后维护日期:2021-1-24
注意:这是未来本章内容扩展的占位符。
Visibility and Privacy
可见性与隐私权
visibility-and-privacy.md
commit: ece3e184c0beeadba97c78eed9005533c3874e43
本章译文最后维护日期:2022-08-21
句法
Visibility :
pub
|pub
(
crate
)
|pub
(
self
)
|pub
(
super
)
|pub
(
in
SimplePath)
这两个术语经常互换使用,它们试图表达的是对“这个地方能使用这个程序项吗?”这个问题的回答。
Rust 的名称解析是在命名空间的层级结构的全局层(即最顶层)上运行的。此层级结构中的每个级别都可以看作是某个程序项。这些程序项就是我们前面提到的那些程序项之一(注意这也包括外部crate)。声明或定义一个新模块可以被认为是在定义的位置向此层次结构中插入一个新的(层级)树。
为了控制接口是否可以被跨模块使用,Rust 会检查每个程序项的使用,来看看它是否被允许使用。此处就是生成隐私告警的地方,或者说是提示:“you used a private item of another module and weren't allowed to.”的地方。(译者注:这句提示可以翻译为:您使用了另一个模块的私有程序项,但不允许这样做。)
默认情况下,Rust 中的所有内容都是私有的,但有两个例外:pub
trait 中的关联程序项默认为公有的;pub
枚举中的枚举变体也默认为公有的。当一个程序项被声明为 pub
时,它可以被认为是外部世界能以访问的。例如:
fn main() {} // 声明一个私有结构体 struct Foo; // 声明一个带有私有字段的公有结构体 pub struct Bar { field: i32, } // 声明一个带有两个公有变体的公有枚举 pub enum State { PubliclyAccessibleState, PubliclyAccessibleState2, }
依据程序项是公有的还是私有的,Rust 分两种情况来访问数据:
-
如果某个程序项是公有的,那么如果可以从外部的某一模块
m
访问到该程序项的所有祖先模块,则一定可以从这个模块m
中访问到该程序项。甚至还可以通过重导出来命名该程序项。具体见后文。 -
如果某个程序项是私有的,则当前模块及当前模块的后代模块都可以访问它。
这两种情况在创建能对外暴露公共API 同时又隐藏内部实现细节的模块层次结构时非常好用。为了帮助理解,以下是一些常见案例和它们需要做的事情:
-
库开发人员需要将一些功能暴露给链接了其库的 crate。作为第一种情况的结果,这意味着任何那些在外部可用的程序项自身以及其路径层级结构中的每一层都必须是公有的(
pub
)的。并且此层级链中的任何私有程序项都不允许被外部访问。 -
crate 需要一个全局可用的“辅助模块(helper module)”,但又不想将辅助模块公开为公共API。为了实现这一点,可以在整个 crate 的根模块(路径层级结构中的最顶层)下建一个私有模块,该模块在内部是“公共API”。因为整个 crate 都是根模块的后代,所以整个本地 crate 里都可以通过第二种情况访问这个私有模块。
-
在为模块编写单元测试时,通常的习惯做法是给要测试的模块加一个命名为
mod test
的直接子模块。这个模块可以通过第二种情况访问父模块的任何程序项,这意味着内部实现细节也可以从这个子模块里进行无缝地测试。
在第二种情况下,我们提到了当前模块及其后代“可以访问”私有项,但是访问一个程序项的确切含义取决于该项是什么。例如,访问一个模块意味着要查看它的内部(以导入更多的程序项);访问一个函数意味着它被调用了。此外,路径表达式和导入语句也被视为访问一个程序项,但只有当访问目标位于当前可见的作用域内时,它们才算是有效的数据访问。
下面是一段示例程序,它例证了上述三种情况:
// 这个模块是私有的,这意味着没有外部crate 可以访问这个模块。 // 但是,由于它在当前 crate 的根模块下, // 因此当前 crate 中的任何模块都可以访问该模块中任何公有可见性程序项。 mod crate_helper_module { // 这个函数可以被当前 crate 中的任何东西使用 pub fn crate_helper() {} // 此函数*不能*被用于 crate 中的任何其他模块中。它在 `crate_helper_module` 之外不可见, // 因此只有当前模块及其后代可以访问它。 fn implementation_detail() {} } // 此函数“对根模块是公有”的,这意味着它可被链接了此crate 的其他crate 使用。 pub fn public_api() {} // 与 'public_api' 类似,此模块是公有的,因此其他的crate 是能够看到此模块内部的。 pub mod submodule { use crate::crate_helper_module; pub fn my_method() { // 本地crate 中的任何程序项都可以通过上述两个规则的组合来调用辅助模块里的公共接口。 crate_helper_module::crate_helper(); } // 此函数对任何不是 `submodule` 的后代的模块都是隐藏的 fn my_implementation() {} #[cfg(test)] mod test { #[test] fn test_my_implementation() { // 因为此模块是 `submodule` 的后代,因此允许它访问 `submodule` 内部的私有项,而不会侵犯隐私权。 super::my_implementation(); } } } fn main() {}
对于一个 Rust 程序要通过隐私检查,所有的路径都必须满足上述两个访问规则。这里说的路径包括所有的 use语句、表达式、类型等。
pub(in path)
, pub(crate)
, pub(super)
, and pub(self)
除了公有和私有之外,Rust 还允许用户(用关键字 pub
)声明仅在给定作用域内可见的程序项。声明形式的限制规则如下:
pub(in path)
使一个程序项在提供的path
中可见。path
必须是声明其可见性的程序项的祖先模块。pub(crate)
使一个程序项在当前 crate 中可见。pub(super)
使一个程序项对父模块可见。这相当于pub(in super)
。pub(self)
使一个程序项对当前模块可见。这相当于pub(in self)
或者根本不使用pub
。
版次差异: 从 2018版开始,
pub(in path)
的路径必须以crate
、self
或super
开头。2015版还可以使用以::
开头的路径,或以根模块下的模块名的开头的路径。
这里是一些示例:
pub mod outer_mod { pub mod inner_mod { // 此函数在 `outer_mod` 内部可见 pub(in crate::outer_mod) fn outer_mod_visible_fn() {} // 同上,但这只能在2015版中有效 pub(in outer_mod) fn outer_mod_visible_fn_2015() {} // 此函数对整个 crate 都可见 pub(crate) fn crate_visible_fn() {} // 此函数在 `outer_mod` 下可见 pub(super) fn super_mod_visible_fn() { // 此函数之所以可用,是因为我们在同一个模块下 inner_mod_visible_fn(); } // 这个函数只在 `inner_mod` 中可见,这与它保持私有的效果是一样的。 pub(self) fn inner_mod_visible_fn() {} } pub fn foo() { inner_mod::outer_mod_visible_fn(); inner_mod::crate_visible_fn(); inner_mod::super_mod_visible_fn(); // 此函数不再可见,因为我们在 `inner_mod` 之外 // 错误! `inner_mod_visible_fn` 是私有的 //inner_mod::inner_mod_visible_fn(); } } fn bar() { // 此函数仍可见,因为我们在同一个 crate 里 outer_mod::inner_mod::crate_visible_fn(); // 此函数不再可见,因为我们在`outer_mod`之外 // 错误! `super_mod_visible_fn` 是私有的 //outer_mod::inner_mod::super_mod_visible_fn(); // 此函数不再可见,因为我们在`outer_mod`之外 // 错误! `outer_mod_visible_fn` 是私有的 //outer_mod::inner_mod::outer_mod_visible_fn(); outer_mod::foo(); } fn main() { bar() }
注意: 此句法仅对程序项的可见性添加了另一个限制。它不能保证该程序项在指定作用域的所有部分都可见。要访问一个程序项,当前作用域内它的所有父项还是必须仍然可见。
Re-exporting and Visibility
重导出和可见性
Rust 允许使用指令 pub use
公开重导出程序项。因为这是一个公有指令,所以允许通过上面的规则验证后在当前模块中使用该程序项。重导出本质上允许使用公有方式访问重导出的程序项的内部。例如,下面程序是有效的:
pub use self::implementation::api; mod implementation { pub mod api { pub fn f() {} } } fn main() {}
这意味着任何外部 crate,只要引用 implementation::api::f
都将收到违反隐私的错误报告,而使用路径 api::f
则被允许。
当重导出私有程序项时,可以认为它允许通过重导出短路了“隐私链(privacy chain)”,而不是像通常那样通过命名空间层次结构来传递“隐私链”。
Memory model
内存模型
memory-model.md
commit: df97fc9e6d5d2745104074c9964181933dd3dea7
本章译文最后维护日期:2022-08-21
Rust 还没有明确的内存模型。许多学者和行业专业人士正在研究各种提案,但就目前而言,这在该语言中仍是一个未明确定义的地方。
Memory allocation and lifetime
内存分配和生存期
memory-allocation-and-lifetime.md
commit: af1cf6d3ca3b7a8c434c142148742aa912e37c34
本章译文最后维护日期:2020-11-16
程序的程序项是那些函数、模块和类型,它们的值在编译时被计算出来,并且唯一地存储在 Rust 进程的内存映像中。程序项既不是动态分配的,也不是动态释放的。
堆是描述 box类型(译者注:box是一种堆分配形式,该堆分配返回一个指向该堆分配的内存地址的指针,后文称这个指针为 box指针或 box引用)的通用术语。堆分配的生存期取决于指向它的 box指针的生存期。由于 box指针本身可能被传入或传出栈帧,或者存储在堆中,因此堆分配可能比初始分配它们的栈帧存活的时间长。在堆分配的整个生存期内,该堆分配被保证驻留在堆中的单一位置 — 它永远不会因移动 box指针而重新分配内存地址。
Variables
变量
variables.md
commit: 79fcc6e4453919977b8b3bdf5aee71146c89217d
本章译文最后维护日期:2021-08-21
变量是栈帧里的一个组件,可以是具名函数参数、匿名的临时变量或具名局部变量。
局部变量(或本地栈(stack-local)分配)直接持有一个值,该值在栈内存中分配。该值是栈帧的一部分。
局部变量是不可变的,除非特别声明。例如:let mut x = ...
。
函数参数是不可变的,除非用 mut
声明。关键字 mut
只应用于紧跟着它的那个参数。例如:|mut x, y|
和 fn f(mut x: Box<i32>, y: Box<i32>)
声明了一个可变变量 x
和一个不可变变量 y
。
分配时不会初始化局部变量。此处一反常态的是在帧建立时,以未初始化状态分配整个帧值的局部变量。函数中的后续语句可以初始化局部变量,也可以不初始化局部变量。局部变量只有在通过所有可到达的控制流路径初始化后才能使用
在下面示例中,init_after_if
是在 if
表达式执行后被初始化的,而 uninit_after_if
不是,因为它没有在 else
分支里被初始化。
#![allow(unused)] fn main() { fn random_bool() -> bool { true } fn initialization_example() { let init_after_if: (); let uninit_after_if: (); if random_bool() { init_after_if = (); uninit_after_if = (); } else { init_after_if = (); } init_after_if; // ok // uninit_after_if; // 错误:使用可能未初始化的 `uninit_after_if` } }
Linkage
链接
linkage.md
commit: 99ed3f87f0c9f1bdb232878d9e360042e43bef4e
本章译文最后维护日期:2023-03-04
注意:本节更多的是从编译器的角度来描述的,而不是语言。
Rust 编译器支持多种将 crate 链接起来使用的方法,这些链接方法可以是静态的,也可以是动态的。本节将聚焦探索这些链接方法,关于本地库的更多信息请参阅 The Book 中的 FFI 相关章节。
在一个编译会话中,编译器可以通过使用命令行参数或内部 crate_type
属性来生成多个构件(artifacts)。如果指定了一个或多个命令行参数,则将忽略(源码内部指定的)所有 crate_type
属性,以便只构建由命令行指定的构件。
-
--crate-type=bin
或#![crate_type = "bin"]
- 将生成一个可执行文件。这就要求在 crate 中有一个main
函数,它将在程序开始执行时运行。这将链接所有 Rust 和本地依赖,生成一个单独的可分发的二进制文件。此类型为默认的 crate 类型。 -
--crate-type=lib
或#![crate_type = "lib"]
- 将生成一个 Rust库(library)。但最终会确切输出/生成什么类型的库在未生成之前还不好清晰确定,因为库有多种表现形式。使用lib
这个通用选项的目的是生成“编译器推荐”的类型的库。像种指定输出库类型的选项在 rustc 里始终可用,但是每次实际输出的库的类型可能会随着实际情况的不同而不同。其它的输出(库的)类型选项都指定了不同风格的库类型,而lib
可以看作是那些类型中的某个类型的别名(具体实际的输出的类型是编译器决定的)。 -
--crate-type=dylib
或#![crate_type = "dylib"]
- 将生成一个动态 Rust库。这与lib
选项的输出类型不同,因为这个选项会强制生成动态库。生成的动态库可以用作其他库和/或可执行文件的依赖。这种输出类型将创建依赖于具体平台的库(Linux 上为*.so
,macOS 上为*.dylib
、Windows 上为*.dll
)。 -
--crate-type=staticlib
或#![crate_type = "staticlib"]
- 将生成一个静态系统库。这个选项与其他选项的库输出的不同之处在于——当前编译器永远不会尝试去链接此staticlib
输出1。此选项的目的是创建一个包含所有本地 crate 的代码以及所有上游依赖的静态库。此输出类型将在 Linux、macOS 和 Windows(MinGW) 平台上创建*.a
归档文件(archive),或者在 Windows(MSVC) 平台上创建*.lib
库文件。在这些情况下,例如将 Rust代码链接到现有的非 Rust应用程序中,推荐使用这种类型,因为它不会动态依赖于其他 Rust 代码。 -
--crate-type=cdylib
或#![crate_type = "cdylib"]
- 将生成一个动态系统库。如果编译输出的动态库要被另一种语言加载使用,请使用这种编译选项。这种选项的输出将在 Linux 上创建*.so
文件,在 macOS 上创建*.dylib
文件,在 Windows 上创建*.dll
文件。 -
--crate-type=rlib
或#![crate_type = "rlib"]
- 将生成一个“Rust库”。它被用作一个中间构件,可以被认为是一个“静态 Rust库”。与staticlib
类型的库文件不同,这些rlib
类型的库文件以后会作为其他 Rust代码文件的上游依赖,未来对那些 Rust代码文件进行编译时,那时的编译器会链并解释此rlib
文件。这本质上意味着(那时的)rustc
将在(此)rlib
文件中查找元数据(metadata),就像在动态库中查找元数据一样。跟staticlib
输出类型类似,这种类型的输出常配合用于生成静态链接的可执行文件(statically linked executable)。 -
--crate-type=proc-macro
或#![crate_type = "proc-macro"]
- 生成的输出类型没有被指定,但是如果通过-L
提供了路径参数,编译器将把输出构件识别为宏,输出的宏可以被其他 Rust 程序加载使用。使用此 crate 类型编译的 crate 只能导出过程宏。编译器将自动设置proc_macro
属性配置选项。编译 crate 的目标平台(target)总是和当前编译器所在平台一致。例如,如果在x86_64
CPU 的 Linux 平台上执行编译,那么目标将是x86_64-unknown-linux-gnu
,即使该 crate 是另一个不同编译目标的 crate 的依赖。
请注意,这些选项是可堆叠使用的,如果同时使用了多个选项,那么编译器将生成所有这些选项关联的的输出类型,而不必反复多次编译。但是,命令行和内部 crate_type
属性配置不能同时起效。如果只使用了不带属性值的 crate_type
属性配置,则将生成所有类型的输出,但如果同时指定了一个或多个 --crate-type
命令行参数,则只生成这些指定的输出。
对于所有这些不同类型的输出,如果 crate A 依赖于 crate B,那么整个系统中很可能有多种不同形式的 B,但是,编译器只会查找 rlib
类型的和动态库类型的。有了依赖库的这两个选项,编译器在某些时候还是必须在这两种类型之间做出选择。考虑到这一点,编译器在决定使用哪种依赖关系类型时将遵循以下规则:
-
如果当前生成静态库,则需要所有上游依赖都以
rlib
类型可用。这个需求源于不能将动态库转换为静态类型的原因。注意,不可能将本地动态依赖链接到静态库,在这种情况下,将打印有关所有未链接的本地动态依赖的警告。
-
如果当前生成
rlib
文件,则对上游依赖的可用类型没有任何限制,仅需要求所有这些文件都可以从其中读出元数据。原因是
rlib
文件不包含它们的任何上游依赖。但如果所有的rlib
文件都包含一份libstd.rlib
的副本,那编译效率和执行效率将大幅降低。 -
如果当前生成可执行文件,并且没有指定
-C prefer-dynamic
参数,则首先尝试以rlib
类型查找依赖。如果某些依赖在 rlib 类型文件中不可用,则尝试动态链接(见下文)。 -
如果当前生成动态链接的动态库或可执行文件,则编译器将尝试协调从 rlib 或 dylib 类型的文件里获取可用依赖关系,以创建最终产品。
编译器的主要目标是确保任何一个库不会在任何构件中出现多次。例如,如果动态库 B 和 C 都静态地去链接了库 A,那么当前 crate 就不能同时链接到 B 和 C,因为 A 有两个副本。编译器允许混合使用 rlib 和 dylib 类型,但这一限制必须被满足。
编译器目前没有实现任何方法来提示库应该链接到哪种类型的库。当选择动态链接时,编译器将尝试最大化动态依赖,同时仍然允许通过 rlib 类型链接某些依赖。
对于大多数情况,如果所有的可用库都是 dylib 类型的动态库,则推荐选择动态链接。对于其他情况,如果编译器无法确定一个库到底应该去链接它的哪种类型的版本,则会发布警告。
通常,--crate-type=bin
或 --crate-type=lib
应该足以满足所有的编译需求,只有在需要对 crate 的输出类型进行更细粒度的控制时,才需要使用其他选项。
Static and dynamic C runtimes
静态C运行时和动态C运行时
一般来说,标准库会同时尽力支持编译目标的静态链接型C运行时和动态链接型C运行时。例如,目标 x86_64-pc-windows-msvc
和 x86_64-unknown-linux-musl
通常都带有C运行时,用户可以按自己的偏好去选择静态链接或动态链接到此运行时。编译器中所有的编译目标都有一个链接到 C运行时的默认模式。默认情况下,常见的编译目标都默认是选择动态链接的,但也存在默认情况下是静态链接的情况,例如:
arm-unknown-linux-musleabi
arm-unknown-linux-musleabihf
armv7-unknown-linux-musleabihf
i686-unknown-linux-musl
x86_64-unknown-linux-musl
C运行时的链接类型被配置为通过 crt-static
目标特性值来开启。这些目标特性通常是从命令行通过命令行参数传递给编译器来设置的。例如,要启用静态运行时,应该执行:
rustc -C target-feature=+crt-static foo.rs
如果想动态链接到C运行时,应该执行:
rustc -C target-feature=-crt-static foo.rs
不支持在到 C运行时的链接类型之间切换的编译目标将忽略这个标志。建议检查生成的二进制文件,以确保在编译成功之后,如预期的那样链接了 C运行时。
crate 本身也可以检测如何链接 C运行时。例如,MSVC平台上的代码需要根据链接运行时的方式进行差异性的编译(例如选择使用 /MT
或 /MD
)。目前可通过 cfg
属性的 target_feature
选项导出检测结果:
#![allow(unused)] fn main() { #[cfg(target_feature = "crt-static")] fn foo() { println!("C运行时应该被静态链接"); } #[cfg(not(target_feature = "crt-static"))] fn foo() { println!("C运行时应该被动态链接"); } }
还请注意,Cargo构建脚本可以通过环境变量来检测此特性。在构建脚本中,您可以通过如下代码检测链接类型:
use std::env; fn main() { let linkage = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new()); if linkage.contains("crt-static") { println!("C运行时应该被静态链接"); } else { println!("C运行时应该被动态链接"); } }
要在本地使用此特性,通常需要使用 RUSTFLAGS
环境变量通过 Cargo 来为编译器指定参数。例如,要在 MSVC 平台上编译静态链接的二进制文件,需要执行:
RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-pc-windows-msvc
译者理解此编译选项是将本地 Rust crate 和其上游依赖资源编译输出成一个库,供其他应用程序使用的。所以对当前编译而言,不能依赖还不存在的资源;又因为其他 Rust crate 的编译无法将静态库作为编译时依赖的上游资源,所以编译其他 Rust crate 的编译器也不会来链接此 staticlib
输出。
Inline assembly
内联汇编
behavior-considered-undefined.md
commit: 8eda943339b7033205604386472d3e6e1dfa28ed
本章译文最后维护日期:2024-02-03
Rust 通过 asm!
和 global_asm!
这两个宏来提供了对内联汇编的支持。
它可用于在编译器生成的汇编程序输出中嵌入手写的汇编程序。
目前对内联汇编的支持在以下目标架构上是稳定的:
- x86 和 x86-64
- ARM
- AArch64
- RISC-V
- LoongArch
如果在不支持的目标架构上使用 asm!
,编译器会报错。
Example
示例
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; // 使用以为和相加运算来实现 x 乘6的效果 let mut x: u64 = 4; unsafe { asm!( "mov {tmp}, {x}", "shl {tmp}, 1", "shl {x}, 2", "add {x}, {tmp}", x = inout(reg) x, tmp = out(reg) _, ); } assert_eq!(x, 4 * 6); } }
Syntax
句法
下面的 ABNF语法规范指定了通用的内联汇编语法:
format_string := STRING_LITERAL / RAW_STRING_LITERAL
dir_spec := "in" / "out" / "lateout" / "inout" / "inlateout"
reg_spec := <register class> / "\"" <explicit register> "\""
operand_expr := expr / "_" / expr "=>" expr / expr "=>" "_"
reg_operand := [ident "="] dir_spec "(" reg_spec ")" operand_expr
clobber_abi := "clobber_abi(" <abi> *("," <abi>) [","] ")"
option := "pure" / "nomem" / "readonly" / "preserves_flags" / "noreturn" / "nostack" / "att_syntax" / "raw"
options := "options(" option *("," option) [","] ")"
operand := reg_operand / clobber_abi / options
asm := "asm!(" format_string *("," format_string) *("," operand) [","] ")"
global_asm := "global_asm!(" format_string *("," format_string) *("," operand) [","] ")"
Scope
作用域
内联汇编可以以下面两种方式来使用。
通过 asm!
宏,汇编代码在函数作用域中被发射,且最终被集成到编译器生成的函数的汇编代码中。
此汇编代码必须遵守 严格规则以避免未定义行为。
注意,在某些情况下,编译器可能选择将汇编代码作为一个独立的函数发射,并生成对它的调用汇编。
通过 global_asm!
宏,汇编代码在全局作用域中被发射。
这可以用来使用汇编代码编写完整的函数,并且通常提供更多的自由来使用任意寄存器和汇编指令。
Template string arguments
模板字符串参数
汇编器模板使用与格式字符串相同的语法(即占位符是由一对花括号来标定)。 相应的参数通过顺序、索引或名称来获取。 但是不支持隐式的命名参数(RFC #2795引入)。
asm!
宏调用可以有一个或多个模板字符串参数;带有多个模板字符串参数的 asm!
的所有字符串参数都会被视为用 \n
连接在了一起的大的字符串。
预期的用法是每个模板字符串参数对应一行汇编代码。
所有的模板字符串参数必须放在其他类型的参数之前。
和格式字符串一样,命名参数必须在位置参数之后出现。 显式的寄存器操作必须出现在操作(operand)列表的末尾(译者注:不懂操作列表的,请回头看看上面内联汇编句法中 asm 的参数表达式),如果有命名参数,还要在命名参数之后。
在模板字符串中,占位符不能使用(带有)显式寄存器的操作。 所有其他命名操作和位置操作必须在模板字符串中至少出现一次,否则将生成编译器错误。
确切的汇编代码语法是特定于目标架构的,对于编译器来说是不透明的,当然除了编译器可以将各种操作类型去替换模板字符串以形成能传递给汇编器的汇编代码。
目前,所有支持的目标架构都遵循 LLVM 内部汇编器使用的汇编代码语法,这通常对应于 GNU汇编器(GAS)。
x86目标架构上,默认使用 GAS的 .intel_syntax noprefix
模式。
ARM目标架构上,使用 .syntax unified
模式。
这些目标架构对汇编代码做了一些额外的限制:任何汇编器状态(例如,可以使用 .section
更改的当前节)必须在 asm字符串末尾恢复为其原始值。
不符合 GAS语法的汇编代码将导致特定于汇编器的行为。
对内联汇编使用的指令的进一步的约束在本章后面的指令支持章节有详细说明。
Operand type
操作类型
目前支持以下几种操作:
in(<reg>) <expr>
<reg>
可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。- 在这段 asm代码的开头,此已分配的寄存器将包含
<expr>
的值。 - 在这段 asm代码的结尾,该寄存器内的值必须恢复如初(除了另有
lateout
操作也被分配使用了此寄存器)。
out(<reg>) <expr>
<reg>
可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。- 在这段 asm代码的开头,此已分配的寄存器将包含一个未定义的值。
<expr>
必须是一个(可能未被初始化的)位置表达式,在这段 asm代码的结尾,此寄存器的内容会被写到此位置表达式里。- 可以使用下划线(
_
)替代这个位置表达式,这将导致在这段 asm代码的结尾,此寄存器的内容被丢弃(等效于一个 clobber寄存器(译者注:clobber寄存器会被该asm语句中的汇编代码隐性修改,也因此,编译器在为输入操作数和输出操作数挑选寄存器时,就不会使用这类寄存器,这样就避免了发生数据覆盖等逻辑错误)。
lateout(<reg>) <expr>
- 除了寄存器分配器可以重用分配给
in
的寄存器外,其他的同out
。 - 应该只在读取所有输入后才写入此寄存器,否则可能会毁坏真实的输入。
- 除了寄存器分配器可以重用分配给
inout(<reg>) <expr>
<reg>
可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。- 在这段 asm代码的开头,此已分配的寄存器将包含
<expr>
的值。 <expr>
必须是一个可变的已初始化的位置表达式,在这段 asm代码的结尾,此已分配的寄存器内的内容将会被写入此表达式。
inout(<reg>) <in expr> => <out expr>
- 除了从
<in expr>
里取值来初始化此寄存器外,其他的同inout
。 <out expr>
必须是一个(可能未被初始化的)位置表达式,在这段 asm代码的结尾,此寄存器的内容会被写到此位置表达式里。- 可以使用下划线(
_
)替代这个<out expr>
表达式,这将导致在这段 asm代码的结尾,此寄存器的内容被丢弃(等效于一个 clobber寄存器)。 <in expr>
和<out expr>
可以有不同的类型。
- 除了从
inlateout(<reg>) <expr>
/inlateout(<reg>) <in expr> => <out expr>
- 除了寄存器分配器可以重用分配给
in
的寄存器外(如果编译器知道in
与inlateout
具有相同的初始值,则可能发生这种情况),其他的同inout
。 - 应该只在读取所有输入后才写入此寄存器,否则可能会毁坏真实的输入。
- 除了寄存器分配器可以重用分配给
sym <path>
<path>
必须指向一个fn
程序项 或static
程序项。- 引用该程序项的混淆符号名(mangled symbol)称被替换为 asm模板字符串。
- 替换的字符串不包含任何修饰符(例如GOT、PLT、重定位等)。
<path>
允许指向#[thread_local]
静态项,在这种情况下,asm代码可以将符号与重定位(例如@plt
、@TPOFF
)结合起来,来从线程内的本地变量中读取数据。
操作表达式从左到右求值,就像函数调用参数一样。
在 asm!
执行后,会按从左到右的顺序写出输出。
这一点很重要,如果两个输出指向同一个位置:该位置(表达式)将包含最右侧输出的值。
因为 global_asm!
存在于函数外部,它只能使用 sym
操作。
Register operands
寄存器操作
输入和输出操作可以被用来指定一个显式寄存器或某一类寄存器(可以被寄存器分配器选择和分配的一类寄存器,每次可以从中选择和分配其中的一个)。
显式寄存器是被字符串文本(例如 "eax"
)指定的,而寄存器类是通过标识符(例如 reg
)来指定的。
请注意,显式寄存器将寄存器别名(例如ARM上的 r14
和 lr
)和寄存器的较小视图(例如 eax
和 rax
)视为与其基寄存器(base register)等效。
对两个输入操作或两个输出操作使用相同的显式寄存器会在编译期报错。
此外,在输入操作或输出操作中发生寄存器重叠(如ARM VFP)也会在编译期报错。
仅允许以下类型的值作为的内联汇编操作:
- 整型 (有符号的和无符号的)
- 浮点型数值
- 指针 (仅廋指针)
- 函数指针
- SIMD向量 (使用
#[repr(simd)]
定义的并且实现了Copy
的结构体)。 这包括在std::arch
中定义的特定于体系架构的向量类型,如__m128
(x86) 或int8x16_t
(ARM)。
以下是当前支持的寄存器类列表:
体系架构 | 寄存器类 | 寄存器 | LLVM约束代码 |
---|---|---|---|
x86 | reg | ax , bx , cx , dx , si , di , bp , r[8-15] (仅 x86-64) | r |
x86 | reg_abcd | ax , bx , cx , dx | Q |
x86-32 | reg_byte | al , bl , cl , dl , ah , bh , ch , dh | q |
x86-64 | reg_byte * | al , bl , cl , dl , sil , dil , bpl , r[8-15]b | q |
x86 | xmm_reg | xmm[0-7] (x86) xmm[0-15] (x86-64) | x |
x86 | ymm_reg | ymm[0-7] (x86) ymm[0-15] (x86-64) | x |
x86 | zmm_reg | zmm[0-7] (x86) zmm[0-31] (x86-64) | v |
x86 | kreg | k[1-7] | Yk |
x86 | kreg0 | k0 | 仅 clobbers |
x86 | x87_reg | st([0-7]) | 仅 clobbers |
x86 | mmx_reg | mm[0-7] | 仅 clobbers |
x86-64 | tmm_reg | tmm[0-7] | 仅 clobbers |
AArch64 | reg | x[0-30] | r |
AArch64 | vreg | v[0-31] | w |
AArch64 | vreg_low16 | v[0-15] | x |
AArch64 | preg | p[0-15] , ffr | 仅 clobbers |
ARM (ARM/Thumb2) | reg | r[0-12] , r14 | r |
ARM (Thumb1) | reg | r[0-7] | r |
ARM | sreg | s[0-31] | t |
ARM | sreg_low16 | s[0-15] | x |
ARM | dreg | d[0-31] | w |
ARM | dreg_low16 | d[0-15] | t |
ARM | dreg_low8 | d[0-8] | x |
ARM | qreg | q[0-15] | w |
ARM | qreg_low8 | q[0-7] | t |
ARM | qreg_low4 | q[0-3] | x |
RISC-V | reg | x1 , x[5-7] , x[9-15] , x[16-31] (non-RV32E) | r |
RISC-V | freg | f[0-31] | f |
RISC-V | vreg | v[0-31] | 仅 clobbers |
LoongArch | reg | $r1 , $r[4-20] , $r[23,30] | r |
LoongArch | freg | $f[0-31] | f |
注意:
在x86上,我们将
reg_byte
与 reg区别对待,因为编译器可以为
reg_byte分别分配低位的 'al' 和高位的 'ah',而
reg` 却只能持有整个寄存器。在x86-64上,
reg_byte
寄存器类中没有高位寄存器(例如没有ah
)。一些寄存器类被标记为 "仅 clobbers" 意味着此类中的寄存器不能被用于输入或输出,只能用于
out(<explicit register>) _
或lateout(<explicit register>) _
这样的表达形式的。
每个寄存器类都有可以与之一起使用的值类型的约束。
这是必要的,因为值被加载到寄存器的方式取决于它的类型。
例如,在大端机系统上,将 i32x4
和 i8x16
加载到 SIMD寄存器可能会导致不同的寄存器内容,即便这两个值的字节内存表示形式相同。
特定寄存器类支持的类型的可用性可能取决于当前启用的目标特性。
体系架构 | 寄存器类 | 目标特性 | 允许的类型 |
---|---|---|---|
x86-32 | reg | None | i16 , i32 , f32 |
x86-64 | reg | None | i16 , i32 , f32 , i64 , f64 |
x86 | reg_byte | None | i8 |
x86 | xmm_reg | sse | i32 , f32 , i64 , f64 , i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
x86 | ymm_reg | avx | i32 , f32 , i64 , f64 , i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 i8x32 , i16x16 , i32x8 , i64x4 , f32x8 , f64x4 |
x86 | zmm_reg | avx512f | i32 , f32 , i64 , f64 , i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 i8x32 , i16x16 , i32x8 , i64x4 , f32x8 , f64x4 i8x64 , i16x32 , i32x16 , i64x8 , f32x16 , f64x8 |
x86 | kreg | avx512f | i8 , i16 |
x86 | kreg | avx512bw | i32 , i64 |
x86 | mmx_reg | N/A | 仅 clobbers |
x86 | x87_reg | N/A | 仅 clobbers |
x86 | tmm_reg | N/A | 仅 clobbers |
AArch64 | reg | None | i8 , i16 , i32 , f32 , i64 , f64 |
AArch64 | vreg | neon | i8 , i16 , i32 , f32 , i64 , f64 , i8x8 , i16x4 , i32x2 , i64x1 , f32x2 , f64x1 , i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
AArch64 | preg | N/A | 仅 clobbers |
ARM | reg | None | i8 , i16 , i32 , f32 |
ARM | sreg | vfp2 | i32 , f32 |
ARM | dreg | vfp2 | i64 , f64 , i8x8 , i16x4 , i32x2 , i64x1 , f32x2 |
ARM | qreg | neon | i8x16 , i16x8 , i32x4 , i64x2 , f32x4 |
RISC-V32 | reg | None | i8 , i16 , i32 , f32 |
RISC-V64 | reg | None | i8 , i16 , i32 , f32 , i64 , f64 |
RISC-V | freg | f | f32 |
RISC-V | freg | d | f64 |
RISC-V | vreg | N/A | 仅 clobbers |
LoongArch64 | reg | None | i8 , i16 , i32 , i64 , f32 , f64 |
LoongArch64 | freg | None | f32 , f64 |
注意: 对于上述表里,指针、函数指针和
isize
/usize
被视为等效的整数类型(i16
/i32
/i64
具体取决于目标架构)。
如果某个值的内存宽度小于分配给它的寄存器,则在输入时该寄存器的高位将有一个未定义的输入值,输出时将忽略该寄存器的高位值。
唯一的例外是 RISC-V 上的 freg
寄存器类,其中 f32
值按照 RISC-V 体系架构的要求被以 NaN-boxed的形式表达为 f64
。
当为 inout
操作分别指定了输入和输出表达式时,这两个表达式必须具有相同的数据类型。
唯一的例外是两个操作(所操作的表达式)都是指针或整数时,它们只需要具有相同的类型内存宽度。
存在此约束是因为 LLVM 和 GCC 中的寄存器分配器有时无法让既定的操作(operand)和不同的数据类型进行有效绑定。
Register names
寄存器名称
一些寄存器有多个名称。 编译器会将它们视为与其基寄存器名称相同。 以下是所有受支持的寄存器别名的列表:
体系架构 | 基寄存器 | 别名 |
---|---|---|
x86 | ax | eax , rax |
x86 | bx | ebx , rbx |
x86 | cx | ecx , rcx |
x86 | dx | edx , rdx |
x86 | si | esi , rsi |
x86 | di | edi , rdi |
x86 | bp | bpl , ebp , rbp |
x86 | sp | spl , esp , rsp |
x86 | ip | eip , rip |
x86 | st(0) | st |
x86 | r[8-15] | r[8-15]b , r[8-15]w , r[8-15]d |
x86 | xmm[0-31] | ymm[0-31] , zmm[0-31] |
AArch64 | x[0-30] | w[0-30] |
AArch64 | x29 | fp |
AArch64 | x30 | lr |
AArch64 | sp | wsp |
AArch64 | xzr | wzr |
AArch64 | v[0-31] | b[0-31] , h[0-31] , s[0-31] , d[0-31] , q[0-31] |
ARM | r[0-3] | a[1-4] |
ARM | r[4-9] | v[1-6] |
ARM | r9 | rfp |
ARM | r10 | sl |
ARM | r11 | fp |
ARM | r12 | ip |
ARM | r13 | sp |
ARM | r14 | lr |
ARM | r15 | pc |
RISC-V | x0 | zero |
RISC-V | x1 | ra |
RISC-V | x2 | sp |
RISC-V | x3 | gp |
RISC-V | x4 | tp |
RISC-V | x[5-7] | t[0-2] |
RISC-V | x8 | fp , s0 |
RISC-V | x9 | s1 |
RISC-V | x[10-17] | a[0-7] |
RISC-V | x[18-27] | s[2-11] |
RISC-V | x[28-31] | t[3-6] |
RISC-V | f[0-7] | ft[0-7] |
RISC-V | f[8-9] | fs[0-1] |
RISC-V | f[10-17] | fa[0-7] |
RISC-V | f[18-27] | fs[2-11] |
RISC-V | f[28-31] | ft[8-11] |
LoongArch | $r0 | $zero |
LoongArch | $r1 | $ra |
LoongArch | $r2 | $tp |
LoongArch | $r3 | $sp |
LoongArch | $r[4-11] | $a[0-7] |
LoongArch | $r[12-20] | $t[0-8] |
LoongArch | $r21 | |
LoongArch | $r22 | $fp , $s9 |
LoongArch | $r[23-31] | $s[0-8] |
LoongArch | $f[0-7] | $fa[0-7] |
LoongArch | $f[8-23] | $ft[0-15] |
LoongArch | $f[24-31] | $fs[0-7] |
某些寄存器不能用于输入或输出操作:
体系架构 | 不支持的寄存器 | 原因 |
---|---|---|
All | sp | 栈指针必须在 asm代码块末尾恢复为其原始值。 |
All | bp (x86), x29 (AArch64), x8 (RISC-V), $fp (LoongArch) | 帧指针不能用作输入或输出。 |
ARM | r7 or r11 | 在 ARM 上,帧指针可以是 r7 或 r11 ,具体取决于目标架构。帧指针不能用作输入或输出。 |
All | si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64), x9 (RISC-V), $s8 (LoongArch) | LLVM 在内部将其用作具有复杂栈帧的函数的“基指针”。 |
x86 | ip | 这是程序计数器,不是真正的寄存器。 |
AArch64 | xzr | 这是一个不能修改的常量零寄存器。 |
AArch64 | x18 | 这是一些 AArch64目标架构上的操作系统预留寄存器。 |
ARM | pc | 这是程序计数器,不是真正的寄存器。 |
ARM | r9 | 这是一些 ARM目标架构上的操作系统预留寄存器。 |
RISC-V | x0 | 这是一个不能修改的常量零寄存器。 |
RISC-V | gp , tp | 这些寄存器是预留的,不能用作输入或输出。 |
LoongArch | $r0 or $zero | 这是一个不能修改的常量零寄存器。 |
LoongArch | $r2 or $tp | 这是为TLS保留的。 |
LoongArch | $r21 | 这是 ABI 所保留的。 |
帧指针和基指针寄存器(base pointer register)预留供 LLVM 内部使用。而 asm!
语句不能显式去指定使用预留寄存器,但在某些情况下,LLVM 将为 reg
操作分配其中一个预留寄存器。使用预留寄存器的汇编代码应该小心,因为 reg
操作可能在使用相同的寄存器。
Template modifiers
模板修饰符
占位符可以通过在大括号中的 :
之后指定的修饰符进行扩充。
这些修饰符不影响寄存器分配,但会更改插入模板字符串时操作(operand)的格式化方式。
每个模板占位符只允许一个修饰符。
支持的修饰符是 LLVM(和 GCC)asm模板参数修饰符的子集,但不使用相同的字母代码。
体系架构 | 寄存器类 | 修饰符 | 输出示例 | LLVM修饰符 |
---|---|---|---|---|
x86-32 | reg | None | eax | k |
x86-64 | reg | None | rax | q |
x86-32 | reg_abcd | l | al | b |
x86-64 | reg | l | al | b |
x86 | reg_abcd | h | ah | h |
x86 | reg | x | ax | w |
x86 | reg | e | eax | k |
x86-64 | reg | r | rax | q |
x86 | reg_byte | None | al / ah | None |
x86 | xmm_reg | None | xmm0 | x |
x86 | ymm_reg | None | ymm0 | t |
x86 | zmm_reg | None | zmm0 | g |
x86 | *mm_reg | x | xmm0 | x |
x86 | *mm_reg | y | ymm0 | t |
x86 | *mm_reg | z | zmm0 | g |
x86 | kreg | None | k1 | None |
AArch64 | reg | None | x0 | x |
AArch64 | reg | w | w0 | w |
AArch64 | reg | x | x0 | x |
AArch64 | vreg | None | v0 | None |
AArch64 | vreg | v | v0 | None |
AArch64 | vreg | b | b0 | b |
AArch64 | vreg | h | h0 | h |
AArch64 | vreg | s | s0 | s |
AArch64 | vreg | d | d0 | d |
AArch64 | vreg | q | q0 | q |
ARM | reg | None | r0 | None |
ARM | sreg | None | s0 | None |
ARM | dreg | None | d0 | P |
ARM | qreg | None | q0 | q |
ARM | qreg | e / f | d0 / d1 | e / f |
RISC-V | reg | None | x1 | None |
RISC-V | freg | None | f0 | None |
LoongArch | reg | None | $r1 | None |
LoongArch | freg | None | $f0 | None |
注意:
- 对于 ARM架构
e
/f
: 这将打印出 NEON quad(128位)寄存器的低双字或高双字寄存器名称。- 对于 x86架构: 对于没有修饰符的
reg
,Rust 编译器与 GCC 的编译行为不同。 GCC 将根据操作值的类型推断修饰符,而 Rust 默认为完整寄存器宽度。- 对于 x86架构的
xmm_reg
: LLVM修饰符x
、t
和g
目前还未在 LLVM 中真正实现(目前它们仅被 GCC 支持),但这应该将只是一个小变动。
如前一节开头所述,传递小于寄存器宽度的输入值将导致寄存器的高位包含未定义的值。
如果内联asm 仅访问寄存器的低位,则这不是问题,这可以通过使用模板修饰符在 asm代码中使用子寄存器名称(例如,使用 ax
替代 rax
)来实现。
由于这是一个容易犯的错误,编译器将建议在适当的情况下使用模板修饰符来给出输入值的类型。
但如果某个操作的所有引用都已包含了修饰符,则对该操作的警告将会被抑制。
ABI clobbers
关键字clobber_abi
可用于将与之对应的一组默认的 clobber寄存器应用于 asm!
块内。
这在调用具有特定调用约定的函数时,会根据需要自动插入必要的 clobber约束:如果调用约定没有在调用过程中完整保持寄存器的值,则会将 lateout("...") _
隐式添加到操作列表中 (其中 ...
要用具体的寄存器名字替换掉)。
clobber_abi
可以指定任意多次。它将为所有指定了调用约定的寄存器集合中的所有单一寄存器都分配一个 clobber寄存器。
当使用 clobber_abi
时,不允许把通用寄存器类作为输出寄存器类来指定:此时所有输出寄存器必须显式指定。
有 clobber_abi
时,把显式输出寄存器标记为输出寄存器的工作优先于给寄存器隐式插入 clobber标志:此时只有当该寄存器明确未用作输出时,才会为寄存器插入 clobber标记。
以下的 ABI 可与 clobber_abi
一起使用:
体系架构 | ABI名称 | Clobbered寄存器 |
---|---|---|
x86-32 | "C" , "system" , "efiapi" , "cdecl" , "stdcall" , "fastcall" | ax , cx , dx , xmm[0-7] , mm[0-7] , k[1-7] , st([0-7]) |
x86-64 | "C" , "system" (Windows平台), "efiapi" , "win64" | ax , cx , dx , r[8-11] , xmm[0-31] , mm[0-7] , k[1-7] , st([0-7]) , tmm[0-7] |
x86-64 | "C" , "system" (在非Windows平台上), "sysv64" | ax , cx , dx , si , di , r[8-11] , xmm[0-31] , mm[0-7] , k[1-7] , st([0-7]) , tmm[0-7] |
AArch64 | "C" , "system" , "efiapi" | x[0-17] , x18 *, x30 , v[0-31] , p[0-15] , ffr |
ARM | "C" , "system" , "efiapi" , "aapcs" | r[0-3] , r12 , r14 , s[0-15] , d[0-7] , d[16-31] |
RISC-V | "C" , "system" , "efiapi" | x1 , x[5-7] , x[10-17] , x[28-31] , f[0-7] , f[10-17] , f[28-31] , v[0-31] |
LoongArch | "C" , "system" , "efiapi" | $r1 , $r[4-20] , $f[0-23] |
注意:
- 在 AArch64架构上,如果
x18
不是目标架构上的预留寄存器,则它仅被包括在 clobber寄存器列表中。
随着体系架构中新的寄存器的加入,在rustc中,每个 ABI 对应的 clobbered寄存器列表也会一并更新:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm!
里的 clobber寄存器的正确性也能得到保持。
Options
可选项
我们使用标志(Flag)技术来进一步影响内联汇编块的行为。 目前定义了以下可选项(标志):
pure
:此asm!
块没有副作用,且最终必须返回,其输出仅取决于其直接输入(比如,输入的值本身,而不是它们指向的对象)或从内存读取的值(除非还设置了nomem
可选项)。 这允许编译器执行此asm!
块的次数少于程序中指定的次数(例如,通过将其从循环中提出来),或者如果asm!
块没有输出的情况下,编译器甚至可能完全消除此段 asm代码。pure
可选项必须与nomem
或readonly
可选项组合使用,否则会导致编译期错误。nomem
:此asm!
块不读取或写入任何内存。 这允许编译器将修改过的全局变量的值缓存在跨当前asm!
块的寄存器中,因为它知道当前asm!
不会读取或写入它们。 编译器还假定此asm!
块不执行与其他线程的任何类型的同步,例如通过 fence。readonly
:此asm!
块不写入任何内存。 这允许编译器将未修改的全局变量的值缓存在跨当前asm!
块的寄存器中,因为它知道它们不是由当前asm!
写入的。 编译器还假定此asm!
块不执行与其他线程的任何类型的同步,例如通过 fence。preserves_flags
:此asm!
块不修改标志寄存器(在后面章节的规则表中有定义)。 这可以避免编译器在此asm!
块之后重新计算条件标志。noreturn
:此asm!
块没有具体返回值,其返回类型被定义为!
(never)。 如果执行超过 asm代码的末尾,则为未定义行为。(译者注:asm代码末尾应该有跳转,否则执行程序会顺序执行二进制指令,从而执行超出 asm代码块) 有noreturn
可选项的 asm块的行为就像一个不返回的函数;需要注意的是,其作用域中的局部变量在被调用之前不会被销毁。nostack
:此asm!
块不会将数据推送到栈上,也不会写入栈的红色区域(red-zone)(有些目标架构会支持red-zone)。 如果此可选项未使用,则保证栈指针为函数调用会(根据目标ABI)适当对齐。att_syntax
:此可选项仅在 x86架构上有效,并导致汇编器使用 GNU汇编器的.att_syntax prefix
模式。 寄存器操作在被替换时会自动加上前导%
。raw
:这将导致模板字符串被解析为裸汇编字符串,对{
和}
也不做特殊处理。 这在使用include_str!
从外部文件中包含裸汇编代码时非常有用。
编译器对可选项执行一些附加检查:
nomem
和readonly
是互斥的:同时指定这两个选项会导致编译期错误。- 在没有输出或只有丢弃的输出(
_
)的 asm块上指定pure
会导致编译期错误。 - 在带有输出的 asm块上指定
noreturn
会导致编译期错误。
global_asm!
仅支持 att_syntax
和 raw
可选项。
其余可选项对于全局作用域类型的内联汇编没有意义。
Rules for inline assembly
内联汇编规则
为了避免未定义行为,在使用函数作用域类型的内联汇编(asm!
)时必须遵循以下规则:
- 任何未指定为输入的寄存器在进入 asm块时都会包含未定义的值。
- 内联汇编上下文中的“未定义值”意味着寄存器可以(非确定性地)具有体系架构允许的任何一个可能值。
值得注意的是,它与 LLVM 的
undef
不同,LLVM 的undef
每次读取时都会有不同的值(因为在汇编代码中不存在这样的概念)。
- 内联汇编上下文中的“未定义值”意味着寄存器可以(非确定性地)具有体系架构允许的任何一个可能值。
值得注意的是,它与 LLVM 的
- 任何未指定为输出的寄存器在退出 asm块时必须具有与进入时相同的值,否则为未定义行为。
- 这仅适用于可指定为输入或输出的寄存器。 其他寄存器遵循特定于目标架构的规则。
- 请注意,
lateout
可以分配给与in
相同的寄存器,在这种情况下,此规则不适用。 然而,代码不应该依赖于此,因为它依赖于寄存器分配的结果。
- 如果在 asm块中执行展开(unwind),则为未定义行为。
- 如果汇编代码调用一个函数,然后该函数展开,则这也适用此规则。
- 汇编代码允许读取和写入的内存位置集与 FFI函数允许读取和写入的内存位置集相同。
- 有关确切规则,请参阅不安全代码指南。
- 如果设置了
readonly
可选项,则只允许内存读取。 - 如果设置了
nomem
可选项,则不允许对内存进行读取或写入。 - 这些规则不适用于 asm代码私有的内存空间,例如 asm块内分配的堆栈空间。
- 编译器不能假定 asm块中的指令是最终实际执行的指令。
- 这实际上意味着编译器必须将
asm!
作为一个黑盒对待,只考虑接口规范,而不考虑 asm块内的指令本身。 - 允许通过特定于目标架构的机制来执行运行时代码补丁。
- 这实际上意味着编译器必须将
- 除非设置了
nostack
可选项,否则可以允许 asm代码使用栈指针下方的栈空间。- 当进入 asm块后,为保证栈指针为函数调用,此指针会(根据目标ABI)适当对齐。
- 你有责任确保不会溢出堆栈(例如,使用堆栈探测以确保命中保护页)。
- 根据目标ABI的要求分配堆栈内存时,应调整堆栈指针。
- 在离开 asm块之前,必须将堆栈指针还原为其原始值。
- 如果设置了
noreturn
可选项,则如果执行超过 asm块的末尾,则此行为未定义。 - 如果设置了
pure
可选项,那么如果asm!
代码除了有直接输出外,还有其他副作用,则此行为未定义。 如果两次执行asm!
代码,它们具有相同输入,但输出不同,此行为也未定义。- 当与
nomem
可选项一起使用时,所谓的“输入”只能是asm!
的直接输入。 - 当与
readonly
可选项一起使用时,所谓的“输入”包含asm!
的直接输入,还包含允许asm!
块来读取的全部内存。
- 当与
- 如果设置了
preserves_flags
可选项,则在退出 asm块时必须恢复这些标志寄存器:- x86
EFLAGS
(CF, PF, AF, ZF, SF, OF)中的状态标志寄存器。- 浮点状态字 (全部)。
MXCSR
(PE, UE, OE, ZE, DE, IE)中的浮点异常标志寄存器。
- ARM
CPSR
(N, Z, C, V)中的条件标志寄存器。CPSR
(Q)中的饱和标志寄存器。CPSR
(GE)中的大于或等于标志寄存器。FPSCR
(N, Z, C, V)中的条件标志寄存器。FPSCR
(QC)中的饱和标志寄存器。FPSCR
(IDC, IXC, UFC, OFC, DZC, IOC)中的浮点异常标志寄存器。
- AArch64
- 条件标志寄存器(
NZCV
)。 - 浮点状态寄存器(
FPSR
).
- 条件标志寄存器(
- RISC-V
fcsr
(fflags
)中的浮点异常标志寄存器。- 向量扩展状态寄存器(
vtype
,vl
,vcsr
)。
- LoongArch
$fcc[0-7]
中的浮点条件标志寄存器。
- x86
- 在 x86 上,方向标志寄存器(
EFLAGS
中的 DF)在进入 asm块时清除,在退出时也必须清除。- 如果在退出 asm块时设置了方向标志,则此行为未定义。
- 在 x86 上,x87浮点寄存器栈必须保持不变,除非所有的
st([0-7])
寄存器都被用out("st(0)") _, out("st(1)") _, ...
标记标记为 clobber寄存器。- 如果所有 x87寄存器都被标记为 clobber寄存器,则在进入
asm
块时,x87寄存器栈被保证为空。并且汇编代码必须确保退出 asm块时 x87寄存器栈也为空。
- 如果所有 x87寄存器都被标记为 clobber寄存器,则在进入
- 将堆栈指针和非输出寄存器恢复为其原始值的要求仅在退出
asm!
块时适用。- 这意味着永不返回的
asm!
块(即使未标记为noreturn
)不需要恢复这些寄存器。 - 当返回到其他的
asm!
块(例如上下文切换)时,这些寄存器必须包含它们在进入当前(你正在退出)的asm!
块的值。- 你不能退出尚未进入的
asm!
块。 你也不能退出已退出的asm!
块(也可以理解为不能未进入而退出)。 - 你负责切换任何特定于目标架构的状态(例如线程级的本地存储和堆栈边界)。
- 你不能从一个
asm!
块中的地址跳转块到另一个asm!
块中的地址,即使在同一个函数或块中也不行。应将两个asm!
块的上下文视为潜在不同,跳转时会进行上下文切换。 你不能假设这些上下文中的任何特定值(例如当前堆栈指针或堆栈指针下的临时值)在两个asm!
块之间保持不变。 - 你可以访问的内存位置集是你进入的
asm!
块所允许的内存位置和你刚退出的asm!
块所允许的内存位置的交集。
- 你不能退出尚未进入的
- 这意味着永不返回的
- 即使两个
asm!
块在源代码中相邻,并且它们之间没有任何其他代码,你仍不能假设它们也将在二进制中的地址连续,不能保证它们之间没有任何其他指令。 - 你不能假设
asm!
块在输出的二进制文件中只出现一次。 允许编译器实例化asm!
块的多个副本块,例如当包含它的函数在多个位置内联时。 - 在 x86 上,内联汇编不能以指令前缀(如
LOCK
)结尾(这些指令前缀被用于编译器生成的指令)。- 由于内联汇编的编译方式,编译器当前还无法检测到此问题,但将来可能会捕获并拒绝此问题。
注意:作为一个一般性原则,
preserves_flags
包含的标志是在执行函数调用时未保留的标志。
Correctness and Validity
正确性和有效性
除了前面的所有规则外,asm!
的字符串参数(在计算完所有其他参数后,执行格式化并转换其操作数)必须最终转换成为(对于目标体系结构而言)语法正确且语义有效的汇编代码。
其中,格式化规则允许编译器生成具有正确语法的汇编代码。
有关操作数的规则允许将 Rust操作数有效转换为asm!
,以及从 asm!
转换出。
为使最终汇编代码既正确又有效,遵守这些规则是必要的,但还不够。例如:
- 格式化后,参数可能被放置在语法不正确的位置
- 一条指令可能被正确写入,但该操作数在给定的体系结构上却是无效的
- 对应体系结构未支持的指令可以汇编成不确定的代码
- 一组指令,每一条都正确有效,如果连续放置,仍有可能会导致未定义的行为
因此,这些规则是 非穷尽 的。编译器不需要检查初始字符串的正确性和有效性,也不需要检查生成的最终汇编代码。
汇编器可以检查这些代码的正确性和有效性,但不需要这样做。
使用 asm!
时,键入错误就足以使程序变得不健壮。完全排除这类错误太难了,汇编规则那是包含在数千页的体系结构参考手册中的呀。
程序员应该谨慎行事,调用这种 unsafe
的功能需要承担不违反编译器或体系结构规则的责任。
Directives Support
伪指令支持
内联汇编支持 GNU AS 和 LLVM 的内部汇编器支持的伪指令集的一个子集,具体如下所示。 也有部分伪指令的效果是特定于汇编器的(可能会导致错误,或者可能会被接受)。
如果内联汇编包含任何修改后续汇编程序的“有状态(stateful)”伪指令,则块必须在内联汇编结束之前撤消任何此类伪指令的执行效果。
下面这些伪指令被汇编器确保支持:
.2byte
.4byte
.8byte
.align
.alt_entry
.ascii
.asciz
.balign
.balignl
.balignw
.bss
.byte
.comm
.data
.def
.double
.endef
.equ
.equiv
.eqv
.fill
.float
.global
.globl
.inst
.lcomm
.long
.octa
.option
.p2align
.popsection
.private_extern
.pushsection
.quad
.scl
.section
.set
.short
.size
.skip
.sleb128
.space
.string
.text
.type
.uleb128
.word
Target Specific Directive Support
基于特定目标规范的指令支持
Dwarf Unwinding
Dwarf展开
支持 DWARF展开信息的 ELF目标平台支持以下指令:
.cfi_adjust_cfa_offset
.cfi_def_cfa
.cfi_def_cfa_offset
.cfi_def_cfa_register
.cfi_endproc
.cfi_escape
.cfi_lsda
.cfi_offset
.cfi_personality
.cfi_register
.cfi_rel_offset
.cfi_remember_state
.cfi_restore
.cfi_restore_state
.cfi_return_column
.cfi_same_value
.cfi_sections
.cfi_signal_frame
.cfi_startproc
.cfi_undefined
.cfi_window_save
Structured Exception Handling
结构化异常处理
在带有结构化异常处理机制的目标平台上,可以确保下面的这些附加指令得到支持:
.seh_endproc
.seh_endprologue
.seh_proc
.seh_pushreg
.seh_savereg
.seh_setframe
.seh_stackalloc
x86 (32-bit and 64-bit)
x86 (32位 and 64位)
无论是 32位还是 64位 x86目标平台,可以确保下面的这些附加指令得到支持:
.nops
.code16
.code32
.code64
只有在退出汇编块之前将状态重置为默认状态时,才支持使用 .code16
、.code32
和 .code64
这些指令。
默认情况下,32位 x86平台使用 .code32
,64位 x86平台使用 .code64
。
ARM (32-bit)
ARM (32位)
ARM目标平台下,可以确保下面的这些附加指令得到支持:
.even
.fnstart
.fnend
.save
.movsp
.code
.thumb
.thumb_func
Unsafety
非安全性
unsafety.md
commit: b0e0ad6490d6517c19546b1023948986578fc378
本章译文最后维护日期:2020-11-2
非安全操作(Unsafe operations)是那些可能潜在地违反 Rust 静态语义里的和内存安全保障相关的操作。
以下语言级别的特性不能在 Rust 的安全(safe)子集中使用:
- 读取或写入可变静态变量;读取或写入或外部静态变量。
- 访问[联合体(
union
)]的字段,注意不是给它的字段赋值。 - 调用一个非安全(unsafe)函数(包括外部函数和和内部函数(intrinsic))。
- 实现非安全(unsafe) trait.
The unsafe
keyword
unsafe
关键字
unsafe-keyword.md
commit: 451a8e4542cb6500a7aca9de5902075e6c8ec188
本章译文最后维护日期:2023-07-21
unsafe
关键字可以出现在几个不同的上下文中:
unsafe函数(unsafe fn
)、unsafe块(unsafe {}
)、unsafe traits(unsafe trait
),以及 unsafe实现(unsafe impl
)中。
根据它的使用位置以及是否启用了 unsafe_op_in_unsafe_fn
lint,它扮演着几种不同的角色:
- 它用于标记定义额外安全条款要求(
unsafe fn
、unsafe trait
)的代码 - 它用于标记需要满足额外安全条款(satisfy)要求的代码(
unsafe {}
、unsafe impl
、不带unsafe_op_in_unsafe_fn
的unsafe fn
)
接下来会讨论这里的每种情况。 参见关键字相关文档,那里有一些直观的例子。
Unsafe functions (unsafe fn
)
unsafe函数(unsafe fn
)
unsafe函数是指在所有上下文和/或所有可能的输入中可能不安全的函数。
我们说函数有额外的安全条款要求,这是所有调用者必须遵守的要求,并且编译器不对其进行检查。
例如,get_unchecked
具有额外的安全条款要求,即索引必须在边界内。
unsafe函数应随附文档列明这些额外安全条款要求。
unsafe函数必须以 unsafe
关键字为前缀,并且只能在 unsafe
块内部去调用,或者在不带unsafe_op_in_unsafe_fn
lint的 unsafe fn
内部去调用。
Unsafe blocks (unsafe {}
)
一块代码可以以 unsafe
关键字为前缀,以允许调用 unsafe
函数或解引用裸指针。
默认情况下,unsafe函数的主体也被视为 unsafe块;这可以通过启用 unsafe_op_in_unsafe_fn
lint来改变这个默认。
通过将操作放入一个 unsafe块中,程序员声明确认他们已经注意到了满足该块中所有操作的额外安全条款要求。
unsafe块是 unsafe函数的逻辑对偶:
在 unsafe函数定义调用方必须秉承的证明义务(proof obligation)的情况下,unsafe块则声明在本块内调用的函数或操作的所有相关的证明义务都已解除(编译器认为相关的证明义务已经被程序员完成)。
履行举证义务有多种方式;例如,可以有运行时检查或通过结构化的不变式(invariant)来保证特定属性肯定为真,或者让 unsafe块放在 unsafe fn
内,这样,块就可以使用该函数的证明义务来解除(编译器对)从块内产生的(让程序员)履行证明义务的要求。
unsafe块用于包装外部库、直接使用硬件或实现当前语言中不直接存在的特性(features)。 例如,Rust 提供了在语言中实现内存安全并发所需的语言特性,但背后的事实是:标准库中线程和消息传递功能都是通过使用了 unsafe块来实现的。
Rust的类型系统是动态安全需求的保守近似实现,所以在某些情况下,使用安全代码会带来较大的性能开销。
例如,双链表不是树结构,只能用安全代码中的引用计数指针表示。
通过使用 unsafe
块将反向链接表示为裸指针,它可以在不进行引用计数的情况下实现。
(请参阅"Learn Rust With Entirely Too Many Linked Lists"来对这个特定示例进行更深入的探索。)
Unsafe traits (unsafe trait
)
unsafe trait 是一种带有额外安全条款要求的 trait,该trait 的实现必须要满足实现这些安全条款。 unsafe trait 应随附解释这些额外安全条款要求的文档。
这种 trait 必须以 unsafe
关键字为前缀,并且只能由 unsafe impl
块来实现。
Unsafe trait implementations (unsafe impl
)
当实现一个 unsafe trait 时,实现需要以 unsafe
关键字作为前缀。
通过书写 unsafe impl
,程序员表示他们已经注意到并满足了此trait 所需的额外安全条款要求。
unsafe trait实现是 unsafe trait 的逻辑对偶:在 unsafe trait 里定义了要实现本 trait 的实现必须秉承的证明义务,unsafe实现则申明了所有相关的证明义务(程序员)都已完成了(,编译器可以不用管这里的证明义务了)。
Behavior considered undefined
未定义的行为
behavior-considered-undefined.md
commit: 142b2ed77d33f37a9973772bd95e6144ed9dce43
本章译文最后维护日期:2023-11-05
如果 Rust 代码出现了下面列表中的任何行为,则此代码被认为不正确。这包括非安全(unsafe
)块和非安全函数里的代码。非安全只意味着避免出现未定义行为(undefined behavior)的责任在程序员;它没有改变任何关于 Rust 程序必须确保不能写出导致未定义行为的代码的事实。
在编写非安全代码时,确保任何与非安全代码交互的安全代码不会触发下述未定义行为是程序员的责任。对于任何使用非安全代码的安全客户端(safe client),如果当前条件满足了此非安全代码对于安全条件的要求,那此此非安全代码对于此安全客户端就是健壮的(sound);如果非安全(unsafe
)代码可以被安全代码滥用以致出现未定义行为,那么此非安全(unsafe
)代码对这些安全代码来说就是不健壮的(unsound)。
在编写非安全代码之前,请阅读 Rustonomicon。
-
数据竞争。
-
存取基于悬垂或[未对齐的指针][based on a misaligned pointer]上的地址。
-
违反界内指针算术偏移要求的地址映射操作。
-
破坏指针别名规则。
Box<T>
、&mut T
和&T
遵循 LLVM 的作用域无别名(noalias)模型(scoped noalias model),除非&T
包含一个UnsafeCell<U>
类型。活动的引用和 box类型的智能指针不可为悬垂dangling状态。活动持续时间并未明确指定,但存在一些限制条件:- 对于引用来说,活动持续时间由借用检查器指定的句法生存期上限来限制;它不能存活得比那个生存期更长。
- 每次将引用或 box类型的智能指针传递给函数或从函数返回时,它都被视为是活动的。
- 当引用(注意不是 box类型的智能指针!)传递给函数时,它存活的至少与该函数调用一样长,这次同样排除了
&T
包含了UnsafeCell<U>
的情况。
当这些类型的值(
Box<T>
、&mut T
和&T
类型的值)被传递给复合类型的(内嵌)成员字段时,所有的这些规则都适用,但注意传递给间接寻址的指针不适用。 -
修改不可变的字节数据。常量(
const
)项内的所有字节都是不可变的。 不可变变量所拥有的字节数据是不可变的,除非这些字节是UnsafeCell<U>
的一部分。此外,共享引用指向的字节数据是不可变的,包括通过其他引用(共享的和可变的)和
Box
方式传递过来的;这里传递过来的(也就是传递性)包括那些存储在复合类型成员字段中的各种引用。修改是指与相关字节位上超过0字节的任何写入(即使该写入不会更改内存内容)。
-
通过编译器内部函数(compiler intrinsics)调用未定义行为。[^译注1]
-
执行基于当前平台不支持的平台特性编译的代码(参见
target_feature
),除非此平台特别申明执行带有此特性的代码安全。 -
用错误的 ABI约定来调用函数,或使用错误的 ABI展开约定来从某函数里发起展开(unwinding)。
-
产生非法值(invalid value),即使在私有字段和本地变量中也是如此。“产生”值发生在这些时候:把值赋给位置表达式、从位置表达式里读取值、传递值给函数/基本运算(primitive operation)或从函数/基本运算中返回值。 以下值非法值(相对于它们各自的类型来说):
-
布尔型
bool
中除false
(0
) 或true
(1
) 之外的值。 -
不包括在该枚举(
enum
)类型定义中的判别值。 -
指向为空(null)的函数指针(
fn
pointer)。 -
代理码点(Surrogate)或码点大于
char::MAX
的字符(char
)值。 -
!
类型的值(任何此类型的值都是非法的)。 -
从未初始化的内存中,或从字符串切片(
str
)的未初始化部分获取的整数(i*/u*)、浮点值(f*)或裸指针。 -
引用或
Box<T>
(代表的指针)指向了悬垂(dangling)、未对齐或指向非法值。 -
宽(wide)引用、
Box<T>
或原始指针中带有非法元数据(metadata):- 如果 trait对象(
dyn Trait
)的元数据不是指向Trait
的虚函数表(vtable)(该虚函数表与该指针或引用所指向的实际动态 trait 相匹配)的指针,则dyn Trait
元数据非法。 - 如果切片的长度不是有效的
usize
,则该切片的元数据是非法的(也就是说,不能从它未初始化的内存中读取它)。
- 如果 trait对象(
-
带有非法值的自定义类型的值非法。在标准库中,这条促成了
NonNull<T>
和NonZero*
的出现。注意:
rustc
是使用还未稳定下来的属性rustc_layout_scalar_valid_range_*
来验证这条规则的。
-
-
错误的使用内联汇编,具体细节,参见使用内联汇编编写代码时的相关规则。
-
在常量上下文中: 将指向某些以分配对象的指针(引用、原始指针或函数指针)转换或以其他方式重新解释为非指针类型(如整数)。 “重新解释”是指在不进行强制转换的情况下以整数类型加载指针值,例如通过执行原始指针强制转换或使用联合体(union)。
注意: 未初始化的内存对于任何具有有限有效值集的类型来说也隐式非法。也就是说,允许读取未初始化内存的情况只发生在联合体(union
)内部和“对齐填充区(padding)”里(类型的字段/元素之间的间隙)。
注意:未定义行为影响整个程序。例如,在 C 中调用一个 C函数已经出现了未定义行为,这意味着包含此调用的整个程序都包含了未定义行为。如果 Rust 再通过 FFI 来调用这段 C程序/代码,那这段 Rust 代码也包含了未定义行为。反之亦然。因此 Rust 中的未定义行为会对任何其他通过 FFI 过来调用的代码造成不利影响。
Pointed-to bytes
指向字节数据
指针或引用“指向”的字节数据是由指针值和指针对象类型的尺寸(使用size_of_val
)来确定的。
Places based on misaligned pointers
基于未对齐指针的地址[based on a misaligned pointer]: #places-based-on-misaligned-pointers
如果地址计算过程中的最后一个*
操作是在未按其类型对齐的指针上执行的,则称地址“基于未对齐的指针”。(如果位置表达式中没有*
操作,则是访问局部变量的成员字段,rustc将确保正确对齐。如果有多个*
,则每个 *
操作都会导致指针从内存中被解引用出来,并且每个*
操作都受对齐约束。请注意,由于自动解引用的存在,在 Rust语法中可以省略一些 *
操作;我们在这里考虑的是全展开形式的位置表达式。)
例如,如果 ptr
的类型为 *const S
,其中 S
的对齐方式为 8,则 ptr
必须是 8位对齐的,否则 (*ptr).f
就是“基于未对齐的指针”。
即使字段 f
的类型是 u8
(诸如此类对齐量为1的类型),这个要求也是必须的。换句话说,对齐要求是源于被解引用的指针的类型,而不是正在访问的字段的类型。
请注意,只有在加载或存储到基于未对齐指针的地址时才会导致未定义行为。在基于未对齐指针执行 addr_of
/addr_of_mut!
是允许的。在一个地址上执行 &
/&mut
操作需要此地址按变量的字段类型进行对齐(否则程序将“产生非法值”),这通常是一个比基于对齐指针的限制更少的要求。如果字段类型可能比包含它的类型(例如 repr(packed
修饰的变量的字段)更需要对齐,则将导致编译器报错。这意味着基于对齐的指针总是足以确保在其上出现的新引用总是对齐的,虽然这并不总是必要的。
Dangling pointers
悬垂指针
如果引用/指针为空,或者它指向的所有字节都不是同一个实时分配(live allocation,也因此它们都必须是某次分配)的一部分,那么它就是“悬垂(dangling)”的。
如果类型的内存宽度为0,则该指针必定 要么指向某个初始化内存的内部(包括刚好指向分配的最后一个字节之后),要么直接从非零整型字面量来构造而来。
请注意,动态内存宽度类型(如切片和字符串)指向其底层的整个数据范围,因此他们的代表长度的元数据永远不要太大,这一点很重要。特别需要注意的是 Rust里,值的动态内存宽度(由size_of_val
来确定)不能超过 isize::MAX
。
Behavior not considered unsafe
不被认为是非安全的行为
behavior-not-considered-unsafe.md
commit: a7473287cc6e2fb972165dc5a7ffd26dad1fc907
本章译文最后维护日期:2021-1-16
虽然程序员可能(应该)发现下列行为是不良的、意外的或错误的,但 Rust 编译器并不认为这些行为是非安全的(unsafe)。
死锁(Deadlocks)
内存和其他资源的泄漏(Leaks of memory and other resources)
退出而不调用析构函数(Exiting without calling destructors)
通过指针泄漏暴露随机基地址(Exposing randomized base addresses through pointer leaks)
整数溢出(Integer overflow)
如果程序包含算术溢出(arithmetic overflow),则说明程序员犯了错误。在下面的讨论中,我们将区分算术溢出和包装算法(wrapping arithmetic)。前者是错误的,而后者是有意为之的。
当程序员启用了 debug_assert!
断言(例如,通过启用非优化的构建方式),相应的实现就必须插进来以便在溢出时触发 panic
。而其他类型的构建形式有可能也在溢出时触发 panics
,也有可能仅仅隐式包装一下溢出值,对溢出过程做静音处理。也就是说具体怎么对待溢出由插进来的编译实现决定。
在隐式包装溢出的情况下,(编译器实现的包装算法)实现必须通过使用二进制补码溢出(two's complement overflow)约定来提供定义良好的(即使仍然被认为是错误的)溢出包装结果。
整型提供了一些固有方法(inherent methods),允许程序员显式地执行包装算法。例如,i32::wrapping_add
提供了使用二进制补码溢出约定算法的加法,即包装类加法(wrapping addition)。
标准库还提供了一个 Wrapping<T>
的新类型,该类型确保 T
的所有标准算术操作都具有包装语义。
请参阅 RFC 560 以了解错误条件、基本原理以及有关整数溢出的更多详细信息。
逻辑错误(Logic errors)
安全代码可能会被添加一些既不能在编译时也不能在运行时检查到的逻辑限制。如果程序打破了这样的限制,其表现可能是未指定的(unspecified),但不会导致未定义行为(undefined behavior)。这些表现可能包括 panics、错误的结果、程序中止(aborts)和程序无法终止(non-termination)。并且这些表现在运行期、构建期或各种构建期之间的的具体表现也可能有所不同。
例如,同时实现 Hash
和 Eq
就要求被认为相等的值具有相等的散列。另一个例子是像 BinaryHeap
、BTreeMap
、BTreeSet
、HashMap
和 HashSet
这样的数据结构,它们描述了在数据结构中修改键的约束。违反这些约束并不被认为是非安全的,但程序(在逻辑上却)被认为是错误的,其行为是不可预测的。
Constant evaluation
常量求值
const_eval.md
commit: 01c8196e0120f0577f6aa05ada9d962f0019a86c
本章译文最后维护日期:2024-05-26
常量求值是在编译过程中计算[表达式][expressions]结果的过程。(不是所有表达式都可以在编译时求值,也就是说)只有全部表达式的某个子集可以在编译时求值。
Constant expressions
常量表达式
某些形式的表达式(被称为常量表达式)可以在编译时求值。在常量(const)上下文中,常量表达式是唯一允许的表达式,并且总是在编译时求值。在其他地方,比如 let语句,常量表达式可以在编译时求值,但不能保证总能在此时求值。如果值必须在编译时求得(例如在常量上下文中),则像数组索引越界或溢出这样的行为都是编译错误。如果不是必须在编译时求值,则这些行为在编译时只是警告,但它们在运行时可能会触发 panic。
下列表达式中,只要它们的所有操作数都是常量表达式,并且求值/计算不会引起任何 Drop::drop
函数的运行,那这些表达式就是常量表达式。
- 字面量。
- 常量参数。
- 指向函数项和常量项的路径。不允许递归地定义常量项。
- 指向静态项的路径。这种路径只允许出现在静态项的初始化器中。
- 元组表达式。
- 数组表达式。
- 结构体表达式。
- 块表达式,包括
unsafe
块和const
块。 - 字段表达式。
- 索引表达式,长度为
usize
的数组索引或切片。 - 区间表达式。
- 未从环境捕获变量的闭包。
- 在整型、浮点型、布尔型(
bool
)和字符型(char
)上做的各种内置运算,包括:取反、算术、逻辑、比较 或 惰性布尔运算。 - 排除借用类型为内部可变借用的共享借用表达式。
- 排除解引用裸指针的解引用操作。8425f5bad3ac40e807e3f75f13b989944da28b62
- 指针到地址的强制转换,
- 函数指针到地址的强制转换,和
- 到 trait对象的非固定内存宽度类型强换(unsizing casts)。
- 调用常量函数和常量方法。
- loop, while 和
while let
表达式。 - if,
if let
和 [匹配(match)] 表达式。
Const context
常量上下文
下述位置是常量上下文:
- 数组类型的长度表达式
- 分号分隔的数组创建形式中的长度表达式
- 下述表达式的初始化器(initializer):
- 常量型泛型实参
- 常量块
Const Functions
常量函数
*常量函数(const fn)*可以在常量上下文中调用。给一个函数加一个常量(const
)标志对该函数的任何现有的使用都没有影响,它只限制参数和返回可以使用的类型,并防止在这两个位置上使用不被允许的表达式类型。程序员可以自由地用常量函数去做任何用常规函数能做的事情。
当从常量上下文中调用这类函数时,编译器会在编译时解释该函数。这种解释发生在编译目标环境中,而不是在当前主机环境中。因此,如果是针对一个 32
位目标系统进行编译,那么 usize
就是 32
位,这与在一个 64
位还是在一个 32
位主机环境中进行编译动作无关。
常量函数有各种限制以确保其可以在编译时可被求值。因此,例如,不可以将随机数生成器编写为常量函数。在编译时调用常量函数将始终产生与运行时调用它相同的结果,即使多次调用也是如此。这个规则有一个例外:如果在极端情况下执行复杂的浮点运算,那么可能得到(非常轻微)不同的结果。建议不要让数组长度和枚举判别值/式依赖于浮点计算。
常量上下文有,但常量函数不具备的比较著名的特性包括:
- 浮点运算
- (在常量函数中,)处理浮点值就像处理只有
Copy
trait约束的泛型参数一样,不能用它们做任何事,只能复制/移动它们。
- (在常量函数中,)处理浮点值就像处理只有
相反,以下情况在常量函数中是有可能的,但在常量上下文中则不可能:
- 使用泛型类型和生存期参数。
- 常量上下文允许有限地使用常量型泛型形参。
Application Binary Interface (ABI)
应用程序二进制接口(ABI)
abi.md.md
commit: e773318a837092d7b5276bbeaf9fda06cca61cee
本章译文最后维护日期:2021-1-16
本节介绍影响 crate 编译输出的 ABI 的各种特性。
有关为导出函数(exporting functions)指定 ABI 的信息,请参阅外部函数。参阅[外部块]]external blocks了解关于指定 ABI 来链接外部库的信息。
The used
attribute
used
属性
used
属性只能用在静态(static
)项上。此属性强制编译器将该变量保留在输出对象文件中(.o、.rlib 等,不包括最终的二进制文件),即使该变量没有被 crate 中的任何其他项使用或引用。注意,链接器(linker)仍有权移除此类变量。
下面的示例显示了编译器在什么条件下在输出对象文件中保留静态(static
)项。
#![allow(unused)] fn main() { // foo.rs // 将保留,因为 `#[used]`: #[used] static FOO: u32 = 0; // 可移除,因为没实际使用: #[allow(dead_code)] static BAR: u32 = 0; // 将保留,因为这个是公有的: pub static BAZ: u32 = 0; // 将保留,因为这个被可达公有函数引用: static QUUX: u32 = 0; pub fn quux() -> &'static u32 { &QUUX } // 可移除,因为被私有且未被使用的函数引用: static CORGE: u32 = 0; #[allow(dead_code)] fn corge() -> &'static u32 { &CORGE } }
$ rustc -O --emit=obj --crate-type=rlib foo.rs
$ nm -C foo.o
0000000000000000 R foo::BAZ
0000000000000000 r foo::FOO
0000000000000000 R foo::QUUX
0000000000000000 T foo::quux
The no_mangle
attribute
no_mangle
属性
可以在任何程序项上使用 no_mangle
属性来禁用标准名称符号名混淆(standard symbol name mangling)。禁用此功能后,此程序项的导出符号(symbol)名将直接是此程序项的原来的名称标识符。
此外,就跟used
属性一样,此属性修饰的程序项也将从生成的库或对象文件中公开导出。
The link_section
attribute
link_section
属性
link_section
属性指定了输出对象文件中函数或静态项的内容将被放置到的节点位置。它使用 MetaNameValueStr元项属性句法指定节点名称。
#![allow(unused)] fn main() { #[no_mangle] #[link_section = ".example_section"] pub static VAR1: u32 = 1; }
The export_name
attribute
export_name
属性
export_name
属性指定将在函数或静态项上导出的符号的名称。它使用 MetaNameValueStr元项属性句法指定符号名。
#![allow(unused)] fn main() { #[export_name = "exported_symbol_name"] pub fn name_in_rust() { } }
The Rust runtime
Rust运行时
abi.md.md
commit: 32fc50e5d211a6a02f874d7e6ae29b4a344f3bdb
本章译文最后维护日期:2022-07-17
本节介绍 Rust运行时的某些方面的特性。
The panic_handler
attribute
panic_handler
属性
panic_handler
属性只能应用于签名为 fn(&PanicInfo) -> !
的函数。有此属性标记的函数定义了 panic 的行为。核心库内的结构体 PanicInfo
可以收集 panic 发生点的一些信息。在二进制、dylib 或 cdylib 类型的 crate 的依赖关系图(dependency graph)中必须有一个panic_handler
函数。
下面展示了一个 panic_handler
函数,它记录(log) panic 消息,然后终止(halts)线程。
#![no_std]
use core::fmt::{self, Write};
use core::panic::PanicInfo;
struct Sink {
// ..
_0: (),
}
impl Sink {
fn new() -> Sink { Sink { _0: () }}
}
impl fmt::Write for Sink {
fn write_str(&mut self, _: &str) -> fmt::Result { Ok(()) }
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let mut sink = Sink::new();
// logs "panicked at '$reason', src/main.rs:27:4" to some `sink`
let _ = writeln!(sink, "{}", info);
loop {}
}
Standard behavior
标准行为
标准库提供了 panic_handler
的一个实现,它的默认设置是展开堆栈,但也可以更改为中止(abort)进程。标准库的 panic 行为可以使用 set_hook 函数在运行时里去修改。
The global_allocator
attribute
global_allocator
属性
在实现 GlobalAlloc
trait 的静态项上使用 global_allocator
属性来设置全局分配器。
The windows_subsystem
attribute
windows_subsystem
属性
当为一个 Windows 编译目标配置链接属性时,windows_subsystem
属性可以用来在 crate 级别上配置子系统(subsystem)类别。它使用 MetaNameValueStr元项属性句法用 console
或 windows
两个可行值指定子系统。对于非windows 编译目标和非二进制的 crate类型,该属性将被忽略。
其中,默认指定为 console
子系统。如果控制台进程是从现有控制台运行的,那么它将连接到该控制台,否则将创建一个新的控制台窗口。
console
子系统通常由不希望在启动时显示控制台窗口的 GUI应用程序使用。它将独立于任何现有控制台运行。
#![allow(unused)] #![windows_subsystem = "windows"] fn main() { }
Appendices
附录
Appendix:Macro Follow-Set Ambiguity Formal Specification
附录:关于宏随集的二义性的形式化规范
macro-ambiguity.md
commit: 6ee9fc95ac3b7df8a01079e13280a283a7a24612
本章译文最后维护日期:2021-07-11
本文介绍了下述声明宏规则的正式规范。它们最初是在 RFC 550 中指定的(本文的大部分内容都是从其中复制过来的),并在后续的 RFC 中进行了进一步的展开论述。
Definitions & Conventions
定义和约定
macro
:宏,源代码中任何可调用的类似foo!(...)
的东西。MBE
:macro-by-example,声明宏,由macro_rules
定义的宏。matcher
:匹配器,macro_rules
调用中一条规则的左侧部分,或其子部分(subportion)。(译者注:子部分的意思是匹配器可嵌套,可相互包含)macro parser
:宏解释器,Rust 解析器中的一段程序,这段程序使用从所有命中的匹配器中推导出的文法规则来解析宏输入。fragment
:匹配段,给定匹配器将接受(或“匹配”)的 Rust 句法对象。repetition
:重复元,遵循正则重复模式的匹配段。NT
:non-terminal,非终结符,可以出现在匹配器中的各种“元变量”或重复元匹配器,在声明宏(MBE)句法中用前导字符$
标明。simple NT
:简单NT,“元变量”类型的非终结符(下面会进一步讨论)。complex NT
:复杂NT,重复元类型的非终结符,通过重复元操作符(*
,+
,?
)指定重复次数。token
:匹配器中不可再细分的元素;例如,标识符、操作符、开/闭定界符和简单NT(simple NT)。token tree
:token树,token树由 token(叶)、复杂NT 和子token树(token树的有限序列)组成的树形数据结构。delimiter token
:定界符,一种用于划分一个匹配段的结束和下一个匹配段的开始的 token。separator token
:分隔符,复杂NT 中的可选定界符,用在重复元里以分隔元素。separated complex NT
:带分隔符的复杂NT,分隔符是重复元的一部分的复杂NT。delimited sequence
:有界序列,在序列的开始和结束处使用了适当的开闭定界符的 token树。empty fragment
:空匹配段,一种不可见的 Rust 句法对象,它分割各种 token,例如空白符(whitespace)或者(在某些词法上下文中的)空标记序列。fragment specifier
:匹配段选择器,简单NT 中的后段标识符部分,指定 NT 接受哪种类型的匹配段。language
:与上下文无关的语言。
示例:
#![allow(unused)] fn main() { macro_rules! i_am_an_mbe { (start $foo:expr $($i:ident),* end) => ($foo) } }
(start $foo:expr $($i:ident),* end)
是一个匹配器(matcher)。整个匹配器是一段有界字符序列(使用开闭定界符 (
和 )
界定),$foo
和 $i
是简单NT(simple NT), expr
和 ident
是它们各自的匹配段选择器(fragment specifiers)。
$(i:ident),*
也是一个 NT;它是一个复杂NT,匹配那些被逗号分隔成的标识符类型的重复元。,
是这个复杂NT 的分隔符;它出现在匹配段的每对元素(如果有的话)之间。
复杂NT 的另一个例子是 $(hi $e:expr ;)+
,它匹配 hi <expr>; hi <expr>; ...
这种格式的代码,其中 hi <expr>;
至少出现一次。注意,这种复杂NT 没有专用的分隔符。
(请注意,Rust 解析器会确保这类有界字符序列始终具有正确的 token树的嵌套结构以及开/闭定界符的正确匹配。)
下面,我们将用变量“M”表示匹配器,变量“t”和“u”表示任意单一 token,变量“tt”和“uu”表示任意 token树。(使用“tt”确实存在潜在的歧义,因为它的额外角色是一个匹配段选择器;但不用太担心,因为从上下文中,可以很清楚地看出哪个解释更符合语义)
用“SEP”代表分隔符,“OP”代表重复元运算符 *
, +
, 和 ?
“OPEN”/“CLOSE”代表包围定界字符序列的 token对(例如 [
和 ]
)。
用希腊字母 "α" "β" "γ" "δ" 代表潜在的空token树序列。(注意没有使用希腊字母 "ε","ε"(epsilon)在此表示形式中代表一类特殊的角色,不代表 token树序列。)
- 这种希腊字母约定通常只是在需要展现一段字符序列的技术细节时才被引入;特别是,当我们希望强调我们操作的是一个 token树序列时,我们将对该序列使用表义符 "tt ...",而不是一个希腊字母。
请注意,匹配器仅仅是一个 token树。如前所述,“简单NT”是一个元变量类型的 NT;因此,这是一个非重复元。例如,$foo:ty
是一个简单NT,而 $($foo:ty)+
是一个复杂NT。
还请注意,在这种形式体系的上下文中,术语“token”通常包括简单NT。
最后,读者要记住,根据这种形式体系的定义,简单NT 不会匹配空匹配段,因此也没有 token 会匹配 Rust句法的空匹配段。(因此,能够匹配空匹配段的 NT 唯有复杂NT。)但这还不是全部事实,因为 vis
匹配器可以匹配空匹配段。因此,为了达到这种形式体系自洽统一的目的,我们将把 $v:vis
看作是 $($v:vis)?
,来让匹配器匹配一个空匹配段。
The Matcher Invariants
匹配器的不变式
为了有效,匹配器必须满足以下三个不变式。注意其中 FIRST 和 FOLLOW 的定义将在后面进行描述。
- 对于匹配器
M
中的任意两个连续的 token树序列(即M = ... tt uu ...
),并且uu ...
非空,必有 FOLLOW(... tt
) ∪ {ε} ⊇ FIRST(uu ...
)。 - 对于匹配器中任何带分隔符的复杂NT,
M = ... $(tt ...) SEP OP ...
,必有SEP
∈ FOLLOW(tt ...
) - 对于匹配器中不带分隔符的复杂NT,
M = ... $(tt ...) OP ...
,如果 OP =*
或+
,必有 FOLLOW(tt ...
) ⊇ FIRST(tt ...
)。
第一个不变式表示,无论匹配器后出现什么 token(如果有的话),它都必须出现在先决随集(predetermined follow set)中的某个地方。这将确保合法的宏定义将继续对 ... tt
的结束和 uu ...
的开始执行相同的判定(determination),即使将来语言中添加了新的句法形式。
The first invariant says that whatever actual token that comes after a matcher, if any, must be somewhere in the predetermined follow set. This ensures that a legal macro definition will continue to assign the same determination as to where ... tt
ends and uu ...
begins, even as new syntactic forms are added to the language.
第二个不变式表示一个带分隔符的复杂NT 必须使用一个分隔符,它是 NT 的内部内容的先决随集的一部分。这将确保合法的宏定义将继续将输入匹配段解析成相同的定界字符序列 tt ...
,即使在将来语言中添加了新的语法形式。
The second invariant says that a separated complex NT must use a separator token that is part of the predetermined follow set for the internal contents of the NT. This ensures that a legal macro definition will continue to parse an input fragment into the same delimited sequence of tt ...
's, even as new syntactic forms are added to the language.
第三个不变式说的是,当我们有一个复杂NT,它可以匹配同一字符序列的两个或多个副本,并且两者之间没有分隔符,那么根据第一个不变式,它们必须可以放在一起。这个不变式还要求它们是非空的,这消除了可能出现的歧义。 The third invariant says that when we have a complex NT that can match two or more copies of the same thing with no separation in between, it must be permissible for them to be placed next to each other as per the first invariant. This invariant also requires they be nonempty, which eliminates a possible ambiguity.
注意:由于历史疏忽和对行为的严重依赖,第三个不变式目前没有被执行。目前还没有决定下一步该怎么做。不遵循这个不变式的宏可能会在未来的 Rust版本中失效。参见跟踪问题 NOTE:The third invariant is currently unenforced due to historical oversight and significant reliance on the behaviour. It is currently undecided what to do about this going forward. Macros that do not respect the behaviour may become invalid in a future edition of Rust. See the tracking issue.
FIRST and FOLLOW, informally
非正式的 FIRST集合和 FOLLOW集合定义
给定匹配器 M 映射到三个集合:FIRST(M),LAST(M) 和 FOLLOW(M)。 A given matcher M maps to three sets:FIRST(M), LAST(M) and FOLLOW(M).
这三个集合中的每一个都是由一组 token 组成的。FIRST(M) 和 LAST(M) 也可能包含一个可区分的非token元素 ε ("epsilon"),这表示 M 可以匹配空匹配段。(但是 FOLLOW(M) 始终只是一组 token。) Each of the three sets is made up of tokens. FIRST(M) and LAST(M) may also contain a distinguished non-token element ε ("epsilon"), which indicates that M can match the empty fragment. (But FOLLOW(M) is always just a set of tokens.)
非正式定义(Informally):
-
FIRST(M):收集匹配段与 M 匹配时可能首先使用的 token。collects the tokens potentially used first when matching a fragment to M.
-
LAST(M):收集匹配段与 M 匹配时可能最后使用的 token。collects the tokens potentially used last when matching a fragment to M.
-
FOLLOW(M):允许紧跟在由 M 匹配的某个匹配段之后的 token集合。the set of tokens allowed to follow immediately after some fragment matched by M.
换言之:t ∈ FOLLOW(M) 当且仅当存在(可能为空的)token序列 α、β、γ、δ,其中:
- M 匹配 β,
- t 与 γ 匹配,并且
- 连结 α β γ δ 是一段可解析的 Rust程序。 In other words:t ∈ FOLLOW(M) if and only if there exists (potentially empty) token sequences α, β, γ, δ where:
- M matches β,
- t matches γ, and
- The concatenation α β γ δ is a parseable Rust program.
我们使用简写的 ANYTOKEN 来表示所有 token(包括简单NT)的集合。例如,如果任何 token 在匹配器 M 之后都是合法的,那么 FOLLOW(M) = ANYTOKEN。 We use the shorthand ANYTOKEN to denote the set of all tokens (including simple NTs). For example, if any token is legal after a matcher M, then FOLLOW(M) = ANYTOKEN.
(为了加深对上述非正式定义描述的理解,读者在阅读正式定义之前,可以先在这里读一遍后面 [关于 FIRST 和 LAST 的示例](#examples-of FIRST -and- LAST)。) (To review one's understanding of the above informal descriptions, the reader at this point may want to jump ahead to the examples of FIRST/LAST before reading their formal definitions.)
FIRST, LAST
下面是对 FIRST 和 LAST 的正式归纳定义(formal inductive definitions)。
“A∪B”表示集合并集,“A∩B”表示集合交集,“A\B”表示集合差集(即存在于A中,且不存在于B中的所有元素的集合)。
FIRST
FIRST(M) 是通过对序列 M 及其第一个 token树(如果有的话)的结构进行案例分析来定义的: FIRST(M) is defined by case analysis on the sequence M and the structure of its first token-tree (if any):
-
如果 M 为空序列,则 FIRST(M) = { ε },if M is the empty sequence, then FIRST(M) = { ε },
-
如果 M 以 token t 开始,则 FIRST(M) = { t },if M starts with a token t, then FIRST(M) = { t },
(注意:这涵盖了这样一种情况:M 以一个定界的token树序列开始,
M = OPEN tt ... CLOSE ...
,此时t = OPEN
,因此 FIRST(M) = {OPEN
}。) (Note:this covers the case where M starts with a delimited token-tree sequence,M = OPEN tt ... CLOSE ...
, in which caset = OPEN
and thus FIRST(M) = {OPEN
}.)(注意:这主要依赖于没有简单NT与空匹配段匹配这一特性。) (Note:this critically relies on the property that no simple NT matches the empty fragment.)
-
否则,M 是一个以复杂NT开始的token树序列:
M = $( tt ... ) OP α
,或M = $( tt ... ) SEP OP α
,(其中α
是匹配器其余部分的token树序列(可能是空的))。Otherwise, M is a token-tree sequence starting with a complex NT:M = $( tt ... ) OP α
, orM = $( tt ... ) SEP OP α
, (whereα
is the (potentially empty) sequence of token trees for the rest of the matcher).- Let SEP_SET(M) = { SEP } 如果存在 SEP 且 ε ∈ FIRST(
tt ...
);否则 SEP_SET(M) = {}。
- Let SEP_SET(M) = { SEP } 如果存在 SEP 且 ε ∈ FIRST(
-
Let ALPHA_SET(M) = FIRST(
α
) if OP =*
or?
and ALPHA_SET(M) = {} if OP =+
. -
FIRST(M) = (FIRST(
tt ...
) \ {ε}) ∪ SEP_SET(M) ∪ ALPHA_SET(M).
复杂NT 的定义值得商榷。SEP_SET(M) 定义了分隔符可能是 M 的第一个有效token的可能性,当定义了分隔符且重复匹配段可能为空时,就会发生这种情况。ALPHA_SET(M)定义了复杂NT可能为空的可能性,这意味着 M 的第一个有效token集合是后继token树序列 α
。当使用了操作符 *
或 ?
时,这种情况下可能没有重复元。理论上,如果 +
与一个可能为空的重复匹配段一起使用,也会出现这种情况,但是第三个不变式禁止这样做。
The definition for complex NTs deserves some justification. SEP_SET(M) defines the possibility that the separator could be a valid first token for M, which happens when there is a separator defined and the repeated fragment could be empty. ALPHA_SET(M) defines the possibility that the complex NT could be empty, meaning that M's valid first tokens are those of the following token-tree sequences α
. This occurs when either *
or ?
is used, in which case there could be zero repetitions. In theory, this could also occur if +
was used with a potentially-empty repeating fragment, but this is forbidden by the third invariant.
From there, clearly FIRST(M) can include any token from SEP_SET(M) or ALPHA_SET(M), and if the complex NT match is nonempty, then any token starting FIRST(tt ...
) could work too. The last piece to consider is ε. SEP_SET(M) and FIRST(tt ...
) \ {ε} cannot contain ε, but ALPHA_SET(M) could. Hence, this definition allows M to accept ε if and only if ε ∈ ALPHA_SET(M) does. This is correct because for M to accept ε in the complex NT case, both the complex NT and α must accept it. If OP = +
, meaning that the complex NT cannot be empty, then by definition ε ∉ ALPHA_SET(M). Otherwise, the complex NT can accept zero repetitions, and then ALPHA_SET(M) = FOLLOW(α
). So this definition is correct with respect to \varepsilon as well.
LAST
LAST(M), defined by case analysis on M itself (a sequence of token-trees):
-
if M is the empty sequence, then LAST(M) = { ε }
-
if M is a singleton token t, then LAST(M) = { t }
-
if M is the singleton complex NT repeating zero or more times,
M = $( tt ... ) *
, orM = $( tt ... ) SEP *
-
Let sep_set = { SEP } if SEP present; otherwise sep_set = {}.
-
if ε ∈ LAST(
tt ...
) then LAST(M) = LAST(tt ...
) ∪ sep_set -
otherwise, the sequence
tt ...
must be non-empty; LAST(M) = LAST(tt ...
) ∪ {ε}.
-
-
if M is the singleton complex NT repeating one or more times,
M = $( tt ... ) +
, orM = $( tt ... ) SEP +
-
Let sep_set = { SEP } if SEP present; otherwise sep_set = {}.
-
if ε ∈ LAST(
tt ...
) then LAST(M) = LAST(tt ...
) ∪ sep_set -
otherwise, the sequence
tt ...
must be non-empty; LAST(M) = LAST(tt ...
)
-
-
if M is the singleton complex NT repeating zero or one time,
M = $( tt ...) ?
, then LAST(M) = LAST(tt ...
) ∪ {ε}. -
if M is a delimited token-tree sequence
OPEN tt ... CLOSE
, then LAST(M) = {CLOSE
}. -
if M is a non-empty sequence of token-trees
tt uu ...
,-
If ε ∈ LAST(
uu ...
), then LAST(M) = LAST(tt
) ∪ (LAST(uu ...
) \ { ε }). -
Otherwise, the sequence
uu ...
must be non-empty; then LAST(M) = LAST(uu ...
).
-
Examples of FIRST and LAST
关于 FIRST 和 LAST 的示例
下面是一些关于 FIRST 和 LAST 的例子。(请特别注意,特殊元素 ε 是如何根据输入匹配段之间的相互作用来引入和消除的。) Below are some examples of FIRST and LAST. (Note in particular how the special ε element is introduced and eliminated based on the interaction between the pieces of the input.)
我们的第一个例子以树状结构呈现,以详细说明匹配器的分析是如何组成的。(一些较简单的子树已被删除。) Our first example is presented in a tree structure to elaborate on how the analysis of the matcher composes. (Some of the simpler subtrees have been elided.)
INPUT: $( $d:ident $e:expr );* $( $( h )* );* $( f ; )+ g
~~~~~~~~ ~~~~~~~ ~
| | |
FIRST: { $d:ident } { $e:expr } { h }
INPUT: $( $d:ident $e:expr );* $( $( h )* );* $( f ; )+
~~~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~
| | |
FIRST: { $d:ident } { h, ε } { f }
INPUT: $( $d:ident $e:expr );* $( $( h )* );* $( f ; )+ g
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~ ~~~~~~~~~ ~
| | | |
FIRST: { $d:ident, ε } { h, ε, ; } { f } { g }
INPUT: $( $d:ident $e:expr );* $( $( h )* );* $( f ; )+ g
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
FIRST: { $d:ident, h, ;, f }
Thus:
- FIRST(
$($d:ident $e:expr );* $( $(h)* );* $( f ;)+ g
) = {$d:ident
,h
,;
,f
}
Note however that:
- FIRST(
$($d:ident $e:expr );* $( $(h)* );* $($( f ;)+ g)*
) = {$d:ident
,h
,;
,f
, ε }
Here are similar examples but now for LAST.
- LAST(
$d:ident $e:expr
) = {$e:expr
} - LAST(
$( $d:ident $e:expr );*
) = {$e:expr
, ε } - LAST(
$( $d:ident $e:expr );* $(h)*
) = {$e:expr
, ε,h
} - LAST(
$( $d:ident $e:expr );* $(h)* $( f ;)+
) = {;
} - LAST(
$( $d:ident $e:expr );* $(h)* $( f ;)+ g
) = {g
}
FOLLOW(M)
Finally, the definition for FOLLOW(M) is built up as follows. pat, expr, etc. represent simple nonterminals with the given fragment specifier.
-
FOLLOW(pat) = {
=>
,,
,=
,|
,if
,in
}`. -
FOLLOW(expr) = FOLLOW(stmt) = {
=>
,,
,;
}`. -
FOLLOW(ty) = FOLLOW(path) = {
{
,[
,,
,=>
,:
,=
,>
,>>
,;
,|
,as
,where
, block nonterminals}. -
FOLLOW(vis) = {
,
l any keyword or identifier except a non-rawpriv
; any token that can begin a type; ident, ty, and path nonterminals}. -
FOLLOW(t) = ANYTOKEN for any other simple token, including block, ident, tt, item, lifetime, literal and meta simple nonterminals, and all terminals.
-
FOLLOW(M), for any other M, is defined as the intersection, as t ranges over (LAST(M) \ {ε}), of FOLLOW(t).
The tokens that can begin a type are, as of this writing, {(
, [
, !
, *
,
&
, &&
, ?
, lifetimes, >
, >>
, ::
, any non-keyword identifier, super
,
self
, Self
, extern
, crate
, $crate
, _
, for
, impl
, fn
, unsafe
,
typeof
, dyn
}, although this list may not be complete because people won't
always remember to update the appendix when new ones are added.
Examples of FOLLOW for complex M:
- FOLLOW(
$( $d:ident $e:expr )*
) = FOLLOW($e:expr
) - FOLLOW(
$( $d:ident $e:expr )* $(;)*
) = FOLLOW($e:expr
) ∩ ANYTOKEN = FOLLOW($e:expr
) - FOLLOW(
$( $d:ident $e:expr )* $(;)* $( f |)+
) = ANYTOKEN
Examples of valid and invalid matchers
With the above specification in hand, we can present arguments for why particular matchers are legal and others are not.
-
($ty:ty < foo ,)
:illegal, because FIRST(< foo ,
) = {<
} ⊈ FOLLOW(ty
) -
($ty:ty , foo <)
:legal, because FIRST(, foo <
) = {,
} is ⊆ FOLLOW(ty
). -
($pa:pat $pb:pat $ty:ty ,)
:illegal, because FIRST($pb:pat $ty:ty ,
) = {$pb:pat
} ⊈ FOLLOW(pat
), and also FIRST($ty:ty ,
) = {$ty:ty
} ⊈ FOLLOW(pat
). -
( $($a:tt $b:tt)* ; )
:legal, because FIRST($b:tt
) = {$b:tt
} is ⊆ FOLLOW(tt
) = ANYTOKEN, as is FIRST(;
) = {;
}. -
( $($t:tt),* , $(t:tt),* )
:legal, (though any attempt to actually use this macro will signal a local ambiguity error during expansion). -
($ty:ty $(; not sep)* -)
:illegal, because FIRST($(; not sep)* -
) = {;
,-
} is not in FOLLOW(ty
). -
($($ty:ty)-+)
:illegal, because separator-
is not in FOLLOW(ty
). -
($($e:expr)*)
:illegal, because expr NTs are not in FOLLOW(expr NT).
Influences
影响来源
influences.md
commit: dc3808468e37ff4c1f663d26c491a3549a42c201
本章译文最后维护日期:2020-11-3
Rust 并不是一种极端原创的语言,它的设计元素来源广泛。下面列出了其中一些(包括已经删除的):
- SML,OCaml:代数数据类型、模式匹配、类型推断、分号语句分隔
- C++:引用,RAII,智能指针,移动语义,单态化(monomorphization),内存模型
- ML Kit, Cyclone:基于区域的内存管理(region based memory management)
- Haskell (GHC):类型属性(typeclasses), 类型簇(type families)
- Newsqueak, Alef, Limbo:通道,并发
- Erlang:消息传递,线程失败,
链接线程失败,轻量级并发 - Swift:可选绑定(optional bindings)
- Scheme:卫生宏(hygienic macros)
- C#:属性
- Ruby:闭包句法,
块句法 - NIL, Hermes:
typestate - Unicode Annex #31:标识符和模式句法
Glossary
术语表
glossary.md
commit: dc064e08f202177dd0e9f14b6a782761df325b93
本章译文最后维护日期:2022-03-14
Abstract syntax tree
抽象句法树
“抽象句法树”,或“AST”,是编译器编译程序时,程序结构的中间表示形式。
An ‘abstract syntax tree’, or ‘AST’, is an intermediate representation of the structure of the program when the compiler is compiling it.
Alignment
对齐量
值的对齐量指定值的首选起始存储地址。对齐量总是2的幂次。值的引用必须是对齐的。更多。
The alignment of a value specifies what addresses values are preferred to start at. Always a power of two. References to a value must be aligned. More.
Arity
元数
元数是指函数或运算符接受的参数个数。例如,f(2, 3)
和 g(4, 6)
的元数为2,而 h(8, 2, 6)
的元数为3。 !
运算符的元数为1。
Arity refers to the number of arguments a function or operator takes. For some examples, f(2, 3)
and g(4, 6)
have arity 2, while h(8, 2, 6)
has arity 3. The !
operator has arity 1.
Array
数组
数组,有时也称为固定大小数组或内联数组,是描述关于元素集合的值,每个元素都可由程序在运行时计算的索引选择。内存模型上,数组占用连续的内存区域。
An array, sometimes also called a fixed-size array or an inline array, is a value describing a collection of elements, each selected by an index that can be computed at run time by the program. It occupies a contiguous region of memory.
Associated item
关联程序项/关联项
关联程序项是与另一个程序项关联的程序项。关联程序项在 trait 中声明,在实现中定义。只有函数、常量和类型别名可以作为关联程序项。它与自由程序项形成对比。
An associated item is an item that is associated with another item. Associated items are defined in implementations and declared in traits. Only functions, constants, and type aliases can be associated. Contrast to a free item.
Blanket implementation
包覆实现
指为无覆盖类型实现的任何实现。impl<T> Foo for T
、impl<T> Bar<T> for T
、impl<T> Bar<Vec<T>> for T
、 和 impl<T> Bar<T> for Vec<T>
被认为是 包覆实现。但是,impl<T> Bar<Vec<T>> for Vec<T>
不被认为是,因为这个 impl
中所有的 T
的实例都被 Vec
覆盖。
Any implementation where a type appears uncovered. impl<T> Foo for T
, impl<T> Bar<T> for T
, impl<T> Bar<Vec<T>> for T
, and impl<T> Bar<T> for Vec<T>
are considered blanket impls. However, impl<T> Bar<Vec<T>> for Vec<T>
is not a blanket impl, as all instances of T
which appear in this impl
are covered by Vec
.
Bound
约束
约束是对类型或 trait 的限制。例如,如果在函数的形数上设置了约束,则传递给该函数的实参的类型必须遵守该约束。
Bounds are constraints on a type or trait. For example, if a bound is placed on the argument a function takes, types passed to that function must abide by that constraint.
Combinator
组合子
组合子是高阶函数,它的参数全是函数或之前定义的组合子。组合子利用这些函数或组合子返回的结果作为入参进行进一步的逻辑计算和输出。组合子可用于以模块化的方式管理控制流。
Combinators are higher-order functions that apply only functions and earlier defined combinators to provide a result from its arguments. They can be used to manage control flow in a modular fashion.
Crate
crate 是编译和链接的最小单元。crate的类型有多种,如常见的库或可执行文件。crate 可以链接和引用其他被称为外部crate 的库crate。crate是一个自包含的模块树,此树从一个未命名模块(此模块一般称为此crate的根模块)开始。crate内的[程序项]可以通过在根模块中将其标记为公有(pub)来让其对其他 crate 可见(注意在标记公有的过程中也要让此程序项的完整路径公有)。
查看更多。
A crate is the unit of compilation and linking. There are different types of crates, such as libraries or executables. Crates may link and refer to other library crates, called external crates. A crate has a self-contained tree of modules, starting from an unnamed root module called the crate root. Items may be made visible to other crates by marking them as public in the crate root, including through paths of public modules.
More.
Dispatch
分发
分发是一种机制,用于确定涉及到多态性时实际运行的是哪个版本的代码。分发的两种主要形式是静态分发和动态分发。虽然 Rust 支持静态分发,但它也通过一种称为 trait对象的机制支持动态分发。
Dispatch is the mechanism to determine which specific version of code is actually run when it involves polymorphism. Two major forms of dispatch are static dispatch and dynamic dispatch. While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’.
Dynamically sized type
动态内存宽度类型
动态内存宽度类型(DST)是一种没有静态已知内存宽度或对齐量的类型。
A dynamically sized type (DST) is a type without a statically known size or alignment.
Entity
实体
实体(entity)是一种语言结构,在源程序中可以以某种方式被引用,通常是通过路径(path)。实体包括类型、程序项、泛型参数、变量绑定、循环标签、生存期、字段、属性和lints。
An entity is a language construct that can be referred to in some way within the source program, usually via a path. Entities include types, items, generic parameters, variable bindings, loop labels, lifetimes, fields, attributes, and lints.
Expression
表达式
表达式是值、常量、变量、运算符/操作符和函数的组合,计算/求值结果为单个值,有或没有副作用都有可能。
比如,2 + (3 * 4)
是一个返回值为14的表达式。
An expression is a combination of values, constants, variables, operators and functions that evaluate to a single value, with or without side-effects.
For example, 2 + (3 * 4)
is an expression that returns the value 14.
Free item
自由程序项
不是任何实现item的成员的[程序项],如自由函数或自由常量。自由程序项是与关联程序项相对的概念。
An item that is not a member of an implementation, such as a free function or a free const. Contrast to an associated item.
Fundamental traits
基础性trait
基础性trait 就是如果为现有的类型实现它,就会为该类型带来突破性改变的 trait。比如 Fn
和 Sized
就是基础性trait。
A fundamental trait is one where adding an impl of it for an existing type is a breaking change. The Fn
traits and Sized
are fundamental.
Fundamental type constructors
基本类型构造器
基本类型构造器是这样一种类型,在它之上实现一个 包覆实现是一个突破性的改变。&
、&mut
、Box
、和 Pin
是基本类型构造器。
如果任何时候 T
都被认为是本地类型,那 &T
、&mut T
、Box<T>
、和 Pin<T>
也被认为是本地类型。基本类型构造器不能覆盖其他类型。任何时候使用术语“有覆盖类型”时,都默认把&T
、&mut T
、Box<T>
、和Pin<T>
排除在外。
A fundamental type constructor is a type where implementing a blanket implementation over it is a breaking change. &
, &mut
, Box
, and Pin
are fundamental.
Any time a type T
is considered local, &T
, &mut T
, Box<T>
, and Pin<T>
are also considered local. Fundamental type constructors cannot cover other types. Any time the term "covered type" is used, the T
in &T
, &mut T
, Box<T>
, and Pin<T>
is not considered covered.
Inhabited
如果类型具有构造函数,因此可以实例化,则该类型是 inhabited。inhabited 类型不是“空的”,因为可以有类型对应的值。与之相对的是 Uninhabited。
A type is inhabited if it has constructors and therefore can be instantiated. An inhabited type is not "empty" in the sense that there can be values of the type. Opposite of Uninhabited.
Inherent implementation
固有实现
单独标称类型上的实现 ,注意关键字 impl
后直接是标称类型,而非 trait-标称类型对(trait-type pair)上的实现。更多。
An implementation that applies to a nominal type, not to a trait-type pair. More.
Inherent method
固有方法
在固有实现中而不是在 trait实现中定义的方法。
A method defined in an inherent implementation, not in a trait implementation.
Initialized
初始化
如果一个变量已经被分配了一个值,并且此值还没有被移动走,那此变量就被初始化了。对此变量而言,它会假设它之外的所有其他内存位置都未初始化。只有非安全Rust 可以在未初始化的情况下开辟内存区域。
A variable is initialized if it has been assigned a value and hasn't since been moved from. All other memory locations are assumed to be uninitialized. Only unsafe Rust can create a memory location without initializing it.
Local trait
本地 trait
本地 trait 是在当前 crate 中定义的 trait
。它可以在模块局部定义,也可以是依附于其他类型参数而定义。给定 trait Foo<T, U>
,Foo
总是本地的,不管替代 T
和 U
的类型是什么。
A trait
which was defined in the current crate. A trait definition is local or not independent of applied type arguments. Given trait Foo<T, U>
, Foo
is always local, regardless of the types substituted for T
and U
.
Turbofish
表达式中带有泛型参数的路径必须在左尖括号前加上一个 ::
。
这种为表达泛型而结合起来形式(::<>
)看起来有些像一条鱼。
因此,在口头上就被称为 turbofish句法。
Paths with generic parameters in expressions must prefix the opening brackets with a ::
.
Combined with the angular brackets for generics, this looks like a fish ::<>
.
As such, this syntax is colloquially referred to as turbofish syntax.
例如:
#![allow(unused)] fn main() { let ok_num = Ok::<_, ()>(5); let vec = [1, 2, 3].iter().map(|n| n * 2).collect::<Vec<_>>(); }
这里必须使用 ::
前缀,以便在逗号分隔的列表中进行多次比较时消除泛型路径可能的歧义。
参见 the bastion of the turbofish 中因为没有此前缀的而引起歧义的示例。
This ::
prefix is required to disambiguate generic paths with multiple comparisons in a comma-separate list.
See the bastion of the turbofish for an example where not having the prefix would be ambiguous.
Local type
本地类型
指在当前 crate 中定义的 struct
、enum
、或 union
。本地类型不会受到类型参数的影响。struct Foo
被认为是本地的,但 Vec<Foo>
不是。LocalType<ForeignType>
是本地的。类型别名不影响本地性。
A struct
, enum
, or union
which was defined in the current crate. This is not affected by applied type arguments. struct Foo
is considered local, but Vec<Foo>
is not. LocalType<ForeignType>
is local. Type aliases do not affect locality.
Module
模块
模块是容纳零个或多个程序项的容器。模块以树的形式组织,从一个被称为当前crate的根或根模块的未命名的模块开始。在模块内,可使用路径来引用其他模块的程序项,但这些程序项的可见性要受到可见性规则的限定。
查看更多。
A module is a container for zero or more items. Modules are organized in a tree, starting from an unnamed module at the root called the crate root or the root module. Paths may be used to refer to items from other modules, which may be restricted by visibility rules.
More
Name
名称
名称是一个指向实体的标识符或生存期或循环标签。*名称绑定(name binding)*是指实体声明时引入了与该实体相关联的标识符或标签。路径、标识符和标签用于引用实体。
A name is an identifier or lifetime or loop label that refers to an entity. A name binding is when an entity declaration introduces an identifier or label associated with that entity. Paths, identifiers, and labels are used to refer to an entity.
Name resolution
名称解析
名称解析是将路径、标识符、标签和实体(entity)声明绑定在一起的的编译过程。
Name resolution is the compile-time process of tying paths, identifiers, and labels to entity declarations.
Namespace
命名空间
命名空间是基于名称所引用的实体类型的声明名称的逻辑分组。命名空间让在一个命名空间中出现的名称不会与另一个命名空间中的相同名称冲突。
A namespace is a logical grouping of declared names based on the kind of entity the name refers to. Namespaces allow the occurrence of a name in one namespace to not conflict with the same name in another namespace.
在命名空间中,名称统一组织在一个层次结构中,层次结构的每一层都有自己的命名实体集合。
Within a namespace, names are organized in a hierarchy, where each level of the hierarchy has its own collection of named entities.
Nominal types
标称类型
可用路径直接引用的类型。具体来说就是枚举(enum
)、结构体(struct
)、联合体(union
)和 trait对象。
Types that can be referred to by a path directly. Specifically enums, structs, unions, and trait objects.
Object safe traits
对象安全trait
可以用作 [trait对象]的 trait。只有遵循特定规则的 trait 才是对象安全的。
Traits that can be used as trait objects. Only traits that follow specific rules are object safe.
Path
路径
路径是一个或多个路径段组成的序列,用于引用当前作用域或某命名空间层次结构中的实体。
A path is a sequence of one or more path segments used to refer to an entity in the current scope or other levels of a namespace hierarchy.
Prelude
预加载模块集/预导入包
预加载模块集,或者 Rust 预加载模块集,是一个会被导入到每个 crate 中的每个模块的小型程序项集合(其中大部分是 trait)。trait 在预加载模块集中很普遍。
Prelude, or The Rust Prelude, is a small collection of items - mostly traits - that are imported into every module of every crate. The traits in the prelude are pervasive.
Scope
作用域
作用域是源文本的一个区域,在该区域中可以直接使用其名称来引用在其下命名的命名实体。
A scope is the region of source text where a named entity may be referenced with that name.
Scrutinee
检验对象\检验对象表达式
检验对象是在匹配(match
)表达式和类似的模式匹配结构上匹配的表达式。例如,在 match x { A => 1, B => 2 }
中,表达式 x
是 scrutinee。
A scrutinee is the expression that is matched on in match
expressions and similar pattern matching constructs. For example, in match x { A => 1, B => 2 }
, the expression x
is the scrutinee.
Size
类型内存宽度/内存宽度
值的内存宽度有两个定义。
第一个是必须分配多少内存来存储这个值。
第二个是它是在具有该项类型的数组中连续元素之间的字节偏移量。
它是对齐量的整数倍数,包括零倍。内存宽度会根据编译器版本(进行新的优化时)和目标平台(类似于 usize
在不同平台上的变化)而变化。
查看更多.
The size of a value has two definitions.
The first is that it is how much memory must be allocated to store that value.
The second is that it is the offset in bytes between successive elements in an array with that item type.
It is a multiple of the alignment, including zero. The size can change depending on compiler version (as new optimizations are made) and target platform (similar to how usize
varies per-platform).\
Slice
切片
切片是一段连续的内存序列上的具有动态内存宽度视图功能的类型,写为 [T]
。
它经常以借用的形式出现,可变借用和共享借用都有可能。共享借用切片类型是 &[T]
,可变借用切片类型是 &mut [T]
,其中 T
表示元素类型。
A slice is dynamically-sized view into a contiguous sequence, written as [T]
.
It is often seen in its borrowed forms, either mutable or shared. The shared slice type is &[T]
, while the mutable slice type is &mut [T]
, where T
represents the element type.
Statement
语句
语句是编程语言中最小的独立元素,它命令计算机执行一个动作。
A statement is the smallest standalone element of a programming language that commands a computer to perform an action.
String literal
字符串字面量
字符串字面量是直接存储在最终二进制文件中的字符串,因此在 'static
生存期内是有效的。
它的类型是借用形式的生存期 'static
的字符串切片,即:&'static str
。
A string literal is a string stored directly in the final binary, and so will be valid for the 'static
duration.
Its type is 'static
duration borrowed string slice, &'static str
.
String slice
字符串切片(str
)
字符串切片是 Rust 中最基础的字符串类型,写为 str
。它经常以借用的形式出现,可变借用和共享借用都有可能。共享借用的字符串切片类型是 &str
,可变借用的字符串切片类型是 &mut str
。
字符串切片总是有效的 UTF-8。
A string slice is the most primitive string type in Rust, written as str
. It is often seen in its borrowed forms, either mutable or shared. The shared string slice type is &str
, while the mutable string slice type is &mut str
.
Strings slices are always valid UTF-8.
Trait
trait 是一种程序项,用于描述类型必须提供的功能。它允许类型对其行为做出某些承诺。
泛型函数和泛型结构体可以使用 trait 来限制或约束它们所接受的类型。
A trait is a language item that is used for describing the functionalities a type must provide. It allows a type to make certain promises about its behavior.
Generic functions and generic structs can use traits to constrain, or bound, the types they accept.
Uncovered type
无覆盖类型
不作为其他类型的参数出现的类型。例如,T
就是无覆盖的,但 Vec<T>
中的 T
就是有覆盖的。这(种说法)只与类型参数相关。
A type which does not appear as an argument to another type. For example, T
is uncovered, but the T
in Vec<T>
is covered. This is only relevant for type arguments.
Undefined behavior
未定义行为
非指定的编译时或运行时行为。这可能导致,但不限于:进程终止或崩溃;不正确的、不正确的或非预定计算;或特定于平台的结果。查看更多
Compile-time or run-time behavior that is not specified. This may result in, but is not limited to: process termination or corruption; improper, incorrect, or unintended computation; or platform-specific results. More.
Uninhabited
如果类型没有构造函数,因此永远不能实例化,则该类型是 Uninhabited。一个 Uninhabited 类型是“空的”,意思是该类型没有值。Uninhabited 类型的典型例子是 never type !
,或不带变体的 enum Never { }
。与之相对的是 Inhabited。
A type is uninhabited if it has no constructors and therefore can never be instantiated. An uninhabited type is "empty" in the sense that there are no values of the type. The canonical example of an uninhabited type is the never type !
, or an enum with no variants enum Never { }
. Opposite of Inhabited.
- alignment:对齐量、对齐值
- annotated:标注
- artifact:部件
- associated constant/function:关联常量/函数
- attribute macro:属性宏
- the attributed function:此(属性限定的)函数
- bare attribute:裸属性。-指没有属性值或属性列表的属性
- block:块
- body: 代码体
- boxed: 装箱的、box化的
- byte order mark: 字节序标记
- checked:安全检查通过的
- unchecked:不受安全检查的
- coercion:自动强转、强制转换
- combinator:组合器
- canonical:规范
- constructor:构造器、构造函数
- delimiter: 定界符
- destructor:析构行为、析构函数
- desugar:脱糖
- discriminant: 判别值、判别式
- doc comment: 文档型注解
- dyn trait:trait对象、动态分发
- emit:发布、扩展出(对宏展开专用)
- endianness: 字节序
- evaluation:求值、计算
- feature:特性
- field: 字段
- final expression: 最终表达式
- follow-set: 随集
- fragment specifier:片段分类符
- grammar:语法、文法
- hygiene:卫生性
- implementing type:实现类型
- inherent implementation: 固有实现
- initializer: 程序初始化器、初始化器
- initializer expression: 初始化表达式
- irrefutable: 不可反驳型
- item: 程序项,
- associated item:关联程序项
- constant item:常量项
- module item:模块项
- static item:静态项
- trait item:trait项
- key: 键
- lexer: 词法
- lifecycle:生命周期
- lifetime: 生存期
- macro attribute:宏属性、属性宏(定义的)属性
- mark、marker:标记、标记符
- match arm: 匹配臂
- matcher: 匹配器
- meta item attribute:元项属性
- monomorphization:单态化
- namespace: 命名空间
- nominal:标称、标称型;-就是可以使用路径来指向的
- nominal type: 标称类型;-就是可以实现trait的普通类型
- notation: 符号、表义符
- never type: never类型、无返回类型
- operator: 操作符、运算符
- binary operators: 双目运算符、二元运算符
- unary operators: 单目运算符、一元运算符
- parameter pattern: 参数模式
- predicate: 谓词
- prelude:预导入包
- primitive type:原生类型
- public: 公有、公有的
- rang:区间(表示数据类型时用)
- raw pointer: 裸指针
- re-exported:重新导出的
- re-exporting: 重导出
- refer to: 指代、引用
- referent:所指对象、引用对象
- representation: 表示形式、表形
- primitive representation: 原语表形
- safety:安全条款
- separator: 分隔符
- scrutinee: 检验对象表达式、检验对象
- sign-extend:符号扩展
- signal an error/warning:触发编译器报错/告警
- size: 内存宽度
- sized:固定内存宽度
- unsized:非固定内存宽度
- trait object:trait对象
- str:字符串切片
-
- stringified: 字符串化
- subtrait:子trait
- supertrait: 超类trait
- symbol: 签名
- syntax: 句法
- terminate:结束
- the lint check attributes: lint检查类属性
- trait bounds: trait约束
- transcriber: 转码器
- type annotation:类型标注
- unit-like struct:单元结构体
- unit struct:单元结构体
- unit tuple: 单元结构体
- unreachable code :执行不到的代码
- unstable: 暂未稳定的
- unwind: 展开
- variant: 变体。指枚举或元组的变体
- whitespace: 空白符
- wrap: 包装
- unwrap:解包
- zero-extend:零扩展
- zero-sized: 零内存宽度
- zero-variant enums: 零变体枚举