译者序
本书是 《The Rust Reference》 的中文翻译,译者仿照其他语言惯例译作《Rust 参考手册》。《The Rust Reference》编写的是 Rust 这门计算机语言的规则、规范,记录了权威、完整、可查阅的 Rust 语言细节,包含语言语法、语义、特性等详实的内容,是深入了解 Rust 语言不可或缺的学习资料。
在翻译过程中,译者首先通过 DeepSeek 对全书进行初翻,随后人工通读全篇并对照上游代码的改动进行校对。翻译力求严谨、忠实原文,尽可能保持原文档的结构、语气与信息密度,不擅自增删文字细节,只在英文长句或被动语态影响中文阅读流畅性时,进行适度意译的调整,不改变原意。在专有名词、概念等术语上,译者参照了 《Rust 程序设计语言 简体中文版》 等社区中高质量的中文翻译内容,以期诸位读者在读到本书译文时,不会对某些概念或专有名词的翻译感到困惑。同时,代码与格式力求规范,每次提交均根据《Rust 参考手册开发者指南》中的运行测试板块进行格式与链接检查,力争保证输出内容的质量。
本书翻译的是《The Rust Reference》的 nightly 版本,这可能与您在其他地方看到的 stable 版本 的内容有些许不同。
本书的翻译通过同步上游仓库代码、翻译内容变化部分的方式保持所有翻译内容为最新。当前翻译的英文原文提交哈希为 5d5be50f,提交时间为 2026 年 5 月 21 日。
您可以通过以下方式阅读本书:
- 在线阅读:《Rust 参考手册》
- PDF 下载:从仓库的 Release 页面 下载
- 本地构建:参考 CONTRIBUTING.md 的构建与测试板块,下载项目后构建 HTML 文件,进一步本地生成 PDF
本书专业性强,译者能力有限,翻译难免会有疏漏和欠妥之处,恳请各位读者批评指正。如果您在阅读过程中发现任何问题,欢迎到项目仓库提交 Issues 或 Pull Requests 参与贡献翻译内容。
引言
本书是 Rust 编程语言的主要参考手册。
Note
关于本书中已知的错误和遗漏,请参阅我们的 GitHub issues。如果你发现编译器行为与此处文本不一致的情况,请提交 issue,以便我们判断哪一方是正确的。
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/。
《Rust 参考手册》不是什么
本书不作为语言入门教程。 假定读者已经具备该语言的背景知识。 有一本配套书籍可用来帮助获得这种背景知识。
本书也不作为语言发行版中包含的标准库的参考。 这些库的文档是通过从其源代码中提取文档属性来单独生成的。 许多人们可能认为是语言特性的东西实际上是 Rust 中的库特性,所以你要找的内容可能在那里,而不是这里。
同样,本书通常不记录 rustc 作为工具或 Cargo 的具体细节。
rustc 有自己的手册。
Cargo 有一本手册,其中包含一个参考。
但仍有少数页面(例如链接)描述了 rustc 的工作方式。
本书也仅作为稳定版 Rust 中可用内容的参考。 关于正在开发中的不稳定特性,请参阅不稳定特性手册。
Rust 编译器(包括 rustc)会执行优化。
本参考手册不指定哪些优化是允许或不允许的。
相反,可以把编译后的程序看作一个黑盒。
你只能通过运行它、给它输入并观察其输出来探测它。
通过这种方式发生的一切都必须符合本参考手册的规定。
如何使用本书
本书并不假设你是按顺序阅读的。 每章通常可以独立阅读,但会交叉链接到其他章节,以引用它们涉及但未讨论的语言层面。
阅读本文档主要有两种方式。
第一种是回答特定问题。
如果你知道哪个章节回答了该问题,可以直接跳到目录中的该章。
否则,你可以按 s 键或点击顶部栏的放大镜图标,搜索与问题相关的关键词。
例如,你想知道 let 语句中创建的临时值何时被丢弃。
如果你还不知道临时值的生命周期是在表达式章节中定义的,你可以搜索 “temporary let”,第一个搜索结果就会带你到那一节。
第二种是全面提升你对语言某个层面的了解。 在这种情况下,只需浏览目录,直到看到你想进一步了解的内容,然后开始阅读。 如果某个链接看起来有趣,点击它并阅读该节。
话虽如此,阅读本书没有错误的方式。以你觉得最有帮助的方式阅读即可。
约定
与所有技术书籍一样,本书在信息展示方面有一些约定。 这些约定记录在此处。
-
定义术语的语句将该术语以斜体显示。 每当该术语在该章之外使用时,通常是一个指向包含此定义的小节的链接。
示例术语就是一个正在被定义的术语的示例。
-
正文描述最新的稳定版本。与先前版本的差异在版本块中单独说明:
2018 Edition differences
在 2018 版本之前,行为是这样的。从 2018 版本开始,行为是那样的。
-
包含关于本书状态的有用信息或指出有用但大多超出范围的信息的注释,放在注释块中。
Note
这是一个示例注释。
-
示例块展示演示某个规则或指出某些有趣方面的示例。有些示例可能有隐藏行,可以通过将鼠标悬停或点击示例时出现的眼睛图标来查看。
Example
这是一个代码示例。
#![allow(unused)] fn main() { println!("hello world"); } -
展示语言中的不安全行为或可能令人困惑的语言特性交互的警告,放在特殊的警告框中。
Warning
这是一个示例警告。
-
文本中的内联代码片段放在
<code>标签内。较长的代码示例放在语法高亮的框中,右上角有复制、执行和显示隐藏行的控件。
// 这是隐藏行。 fn main() { println!("This is a code example"); }除非另有说明,所有示例都针对最新版本编写。
-
语法和词法产生式在记法章节中描述。
-
规则标识符出现在每个语言规则之前,用方括号括起来。这些标识符提供了一种引用和链接到语言中特定规则的方式(例如)。规则标识符使用句点将各部分从最通用到最具体地分隔开(例如 destructors.scope.nesting.function-body)。在窄屏幕上,规则名称会折叠显示为
[*]。点击规则名称可以链接到该规则。
Warning
规则的组织方式目前尚在调整中。目前,这些标识符名称在各版本之间并不稳定,如果发生变化,指向这些规则的链接可能会失效。我们计划在组织方式稳定后,使得指向规则名称的链接在版本之间不会断开。
-
有关联测试的规则会在其下方包含一个
Tests链接(在窄屏幕上,链接为[T])。点击该链接会弹出一个测试列表,可以点击查看测试。例如,参见 input.encoding.utf8。将规则链接到测试是一项持续进行的工作。概览请参见测试摘要章节。
贡献
我们欢迎各种形式的贡献。
你可以通过向 the Rust Reference repository 提交 issue 或 pull request 来为本书做出贡献。
如果本书没有回答你的问题,并且你认为答案属于本书的范围,请随时提交 issue或在 Zulip 上的 t-lang/doc 频道中询问。
了解人们最常将本书用于什么目的,有助于我们将注意力集中在使这些章节尽可能完善上。
当然,如果你看到任何错误或非规范性但未明确标明的内容,也请提交 issue。
记法
语法
以下记法约定用于词法分析器和语法片段:
| 记法 | 示例 | 含义 |
|---|---|---|
| CAPITAL | KW_IF, INTEGER_LITERAL | 词法分析器产生的 token |
| ItalicCamelCase | LetStatement, Item | 语法产生式 |
string | x, while, * | 精确的字符 |
| x? | pub? | 可选项 |
| x* | OuterAttribute* | 0 个或多个 x |
| x+ | MacroMatch+ | 1 个或多个 x |
| xa..b | HEX_DIGIT1..6 | x 重复 a 到 b 次,不含 b |
| xa..=b | HEX_DIGIT1..=5 | x 重复 a 到 b 次,含 b |
| xn:a..=b | #n:1..=255 | x 重复 a 到 b 次(含 b),计数绑定到名称 n |
| xn | #n | x 按前一个带标签的重复所绑定的次数 n 重复 |
| Rule1 Rule2 | fn Name Parameters | 按顺序排列的规则序列 |
| | | u8 | u16, Block | Item | 二者之一 |
| ! | !COMMENT | 若其后没有该表达式则匹配,且不消耗输入 |
| [ ] | [b B] | 所列字符中的任意一个 |
| [ - ] | [a-z] | 该范围内的任意字符 |
| ~[ ] | ~[b B] | 除所列字符外的任意字符 |
~string | ~\n, ~*/ | 除该序列外的任意字符 |
| ( ) | (, Parameter)? | 将项分组 |
| ^ | b' ^ ASCII_FOR_CHAR | 序列剩余部分必须匹配,否则解析将无条件失败(硬切割运算符) |
| U+xxxx..xxxxxx | U+0060 | 单个 Unicode 字符 |
| <text> | <any ASCII char except CR> | 对应匹配内容的英文描述 |
| Rule suffix | IDENTIFIER_OR_KEYWORD except crate | 对前一条规则的修改 |
| // Comment. | // 单行注释。 | 延伸到行尾的注释。 |
序列的优先级高于 | 交替。
硬切割运算符
该语法使用有序交替:解析器从左到右尝试各分支,采纳第一个匹配的分支。如果某个分支在序列中途失败,解析器通常回溯并尝试下一个分支。切割运算符(^)可以阻止这种情况。一旦序列中 ^ 左侧的所有表达式都已匹配,序列剩余部分必须匹配,否则解析将无条件失败。
Mizushima 等人将切割运算符引入了解析表达式文法。在 PEG 文献中,软切割仅阻止在最内层有序选择的范围内回溯——外层选择仍可恢复。硬切割则阻止切割点之后的所有回溯;失败是确定性的。本语法中使用的 ^ 是硬切割。
硬切割运算符是必要的,因为 Rust 中的某些 token 以本身也是合法 token 的前缀开头。例如,c" 开始一个 C 字符串字面量,但单独的 c 是一个合法的标识符。如果没有切割,当 c"\0" 无法被词法分析为 C 字符串字面量时(因为 C 字符串不允许空字节),解析器可能回溯并将其词法分析为两个 token:标识符 c 和字符串字面量 "\0"。c" 后的切割可以防止这种情况——一旦识别出起始定界符,解析器就不能回退。同样的原理适用于字节字面量、字节字符串字面量、原始字符串字面量以及其他以前缀开头的字面量,这些前缀本身也是合法的 token。
字符串表产生式
语法中的某些规则——特别是一元运算符、二元运算符和关键字——以简化形式给出:作为可打印字符串的列表。这些情况构成了关于 token 规则的一个子集,并假定是向解析器提供输入的词法分析阶段的结果,该阶段由 DFA 驱动,操作于所有此类字符串表条目的析取之上。
当语法中出现 monospace 字体的字符串时,它是对此类字符串表产生式中单个成员的隐式引用。详见 token 词法单元。
语法可视化
每个语法块下方有一个按钮,可以切换显示语法图。方形元素是非终结规则,圆角矩形是终结符。
词法结构
输入格式
Lexer
CHAR → [U+0000-U+D7FF U+E000-U+10FFFF] // a Unicode scalar value
ASCII → [U+0000-U+007F]
NUL → U+0000
本章描述源文件如何被解释为 token 序列。
关于程序如何组织为文件的描述,请参阅 crate 和源文件。
源文件编码
每个源文件被解释为以 UTF-8 编码的 Unicode 字符序列。
如果文件不是有效的 UTF-8,则视为错误。
字节顺序标记移除
如果序列中的第一个字符是 U+FEFF(BYTE ORDER MARK),则将其移除。
CRLF 规范化
每对字符 U+000D(CR)后紧跟 U+000A(LF)将被替换为单个 U+000A(LF)。此过程仅执行一次,而非重复执行,因此规范化之后,输入中仍可能存在紧跟 U+000A(LF)的 U+000D(CR)(例如,原始输入包含 “CR CR LF LF”)。
其他位置的字符 U+000D(CR)将保留(它们被视为空白字符)。
Shebang 移除
如果存在 shebang,则将其从输入序列中移除(因此被忽略)。
Token 化
然后将生成的字符序列转换为 token,具体描述见本章其余部分。
Note
标准库的
include!宏会对其读取的文件应用以下转换:
- 字节顺序标记移除。
- CRLF 规范化。
- 在程序项上下文中(而非表达式或语句上下文)调用时的 shebang 移除。
include_str!和include_bytes!宏不应用这些转换。
Shebang
Shebang 是一个可选行,通常用于类 Unix 系统中,用来指定执行该文件的解释器。
Example
#!/usr/bin/env rustx fn main() { println!("Hello!"); }
Lexer
SHEBANG →
#! !( ( WHITESPACE | LINE_COMMENT | BLOCK_COMMENT )* [ )
~LF* ( LF | EOF )
Shebang 以字符 #! 开头,并延伸到第一个 U+000A(LF)处,如果没有 LF 则延伸到 EOF。如果 #! 字符后面跟随 [(忽略中间的注释或空白字符),则该行不被视为 shebang(以避免与内部属性产生歧义)。
Shebang 可以紧接在文件开头出现,也可以出现在可选的字节顺序标记之后。
关键字
Rust 将关键字分为三类:
严格关键字
这些关键字只能在其正确的上下文中使用。它们不能用作以下名称:
以下关键字在所有版本中均存在:
_asasyncawaitbreakconstcontinuecratedynelseenumexternfalsefnforifimplinletloopmatchmodmovemutpubrefreturnselfSelfstaticstructsupertraittruetypeunsafeusewherewhile
2018 Edition differences
以下关键字在 2018 版本中添加:
asyncawaitdyn
保留关键字
这些关键字尚未使用,但为将来使用而保留。它们与严格关键字具有相同的限制。这样做的原因是禁止程序使用这些关键字,从而使当前程序与未来的 Rust 版本保持向前兼容。
abstractbecomeboxdofinalgenmacrooverrideprivtrytypeofunsizedvirtualyield
2018 Edition differences
try关键字在 2018 版本中作为保留关键字添加。
2024 Edition differences
gen关键字在 2024 版本中作为保留关键字添加。
弱关键字
这些关键字仅在特定上下文中具有特殊含义。例如,可以用 union 这个名称声明变量或方法。
'staticmacro_rulesrawsafeunion
macro_rules用于创建自定义宏。
union用于声明联合体,且仅在联合体声明中才是关键字。
-
'static用于静态生命周期,不能用作泛型生命周期参数或循环标签// 错误[E0262]: 无效的生存期参数名称: `'static` fn invalid_lifetime_parameter<'static>(s: &'static str) -> &'static str { s }
safe用于函数和静态项,在外部块中具有含义。
raw用于原始借用运算符,且仅在匹配原始借用运算符形式(如&raw const expr或&raw mut expr)时才是关键字。
2018 Edition differences
在 2015 版本中,
dyn是用在类型位置并且后跟不以::或<开头的路径、生命周期、问号、for关键字或左括号时的关键字。从 2018 版本开始,
dyn被提升为严格关键字。
标识符
Lexer
IDENTIFIER_OR_KEYWORD → ( XID_Start | _ ) XID_Continue*
XID_Start → <XID_Start defined by Unicode>
XID_Continue → <XID_Continue defined by Unicode>
RAW_IDENTIFIER → r# IDENTIFIER_OR_KEYWORD
NON_KEYWORD_IDENTIFIER → IDENTIFIER_OR_KEYWORDexcept a strict or reserved keyword
IDENTIFIER → NON_KEYWORD_IDENTIFIER | RAW_IDENTIFIER
RESERVED_RAW_IDENTIFIER →
r# ( _ | crate | self | Self | super ) !XID_Continue
标识符遵循 Unicode 标准附录 #31 中 Unicode 版本 17.0 的规范,并包含下文所述的补充。以下是一些标识符的示例:
foo_identifierr#trueМосква東京
UAX #31 使用的配置为:
- Start :=
XID_Start,加上下划线字符(U+005F) - Continue :=
XID_Continue - Medial := 空
Note
以下划线开头的标识符通常用于表示有意未使用的标识符,并且会消除
rustc中“未使用“的警告。
标识符不能是严格或保留关键字,除非使用下文原始标识符中描述的 r# 前缀。
零宽度非连接符(ZWNJ U+200C)和零宽度连接符(ZWJ U+200D)字符不允许出现在标识符中。
在以下情况下,标识符仅限于 XID_Start 和 XID_Continue 的 ASCII 子集:
extern crate声明(除 AsClause 标识符外)- 在路径中引用的外部 crate 名称
- 从文件系统加载且不带
path属性的模块名称 - 带有
no_mangle属性的程序项 - 外部块中的程序项名称
规范化
标识符使用 Unicode 标准附录 #15 中定义的规范化形式 C(NFC)进行规范化。如果两个标识符的 NFC 形式相等,则它们相等。
原始标识符
原始标识符类似于普通标识符,但带有 r# 前缀。(注意,r# 前缀不算作实际标识符的一部分。)
与普通标识符不同,原始标识符可以使用任何严格或保留关键字,但 RAW_IDENTIFIER 中以上列出的除外。
使用 RESERVED_RAW_IDENTIFIER token 是错误的。
注释
Lexer
COMMENT →
LINE_COMMENT
| INNER_LINE_DOC
| OUTER_LINE_DOC
| INNER_BLOCK_DOC
| OUTER_BLOCK_DOC
| BLOCK_COMMENT
LINE_COMMENT →
// ( ~[/ ! LF] | // ) ~LF*
| // EOF
| //immediately followed by LF
BLOCK_COMMENT →
/* ^
( BLOCK_COMMENT_OR_DOC | ( !*/ CHAR ) )*
*/
INNER_LINE_DOC →
//! ^ LINE_DOC_COMMENT_CONTENT ( LF | EOF )
LINE_DOC_COMMENT_CONTENT → ( !CR ~LF )*
INNER_BLOCK_DOC →
/*! ^ ( BLOCK_COMMENT_OR_DOC | BLOCK_CHAR )* */
OUTER_LINE_DOC →
/// ^ LINE_DOC_COMMENT_CONTENT ( LF | EOF )
OUTER_BLOCK_DOC →
/** ![* /]
^
( ~* | BLOCK_COMMENT_OR_DOC )
( BLOCK_COMMENT_OR_DOC | BLOCK_CHAR )*
*/
BLOCK_CHAR → ( !( */ | CR ) CHAR )
BLOCK_COMMENT_OR_DOC →
INNER_BLOCK_DOC
| OUTER_BLOCK_DOC
| BLOCK_COMMENT
非文档注释
注释遵循通用的 C++ 风格,支持行(//)和块(/* ... */)注释形式。支持嵌套块注释。
非文档注释被解释为一种空白字符。
文档注释
以恰好三条斜杠(///)开头的行文档注释,以及块文档注释(/** ... */),都是外部文档注释,会被解释为 doc 属性 的特殊语法。
也就是说,它们等价于在注释主体周围书写 #[doc="..."],即 /// Foo 转变为 #[doc=" Foo"],/** Bar */ 转变为 #[doc=" Bar "]。它们因此必须出现在接受外部属性的项之前。
以 //! 开头的行注释和块注释 /*! ... */ 是应用于注释父项的文档注释,而不是应用于其后跟随的项。
也就是说,它们等价于在注释主体周围书写 #![doc="..."]。//! 注释通常用于为占据一个源文件的模块编写文档。
字符 U+000D(CR)不允许出现在文档注释中。
Note
按照惯例,文档注释会包含 Markdown,这是
rustdoc所期望的。但是,注释语法本身并不遵循任何 Markdown 规则。/** `glob = "*/*.rs";` */会在第一个*/处终止注释,剩余代码将导致语法错误。与行文档注释相比,这稍微限制了块文档注释的内容。
Note
U+000D(CR)后紧跟U+000A(LF)的序列在之前已被转换为单个U+000A(LF)。
示例
#![allow(unused)]
fn main() {
//! 应用于此 crate 的隐式匿名模块的文档注释
pub mod outer_module {
//! - 内部行文档注释
//!! - 仍然是内部行文档注释(但开头有一个感叹号)
/*! - 内部块文档注释 */
/*!! - 仍然是内部块文档注释(但开头有一个感叹号) */
// - 仅是一个注释
/// - 外部行文档注释(恰好 3 条斜杠)
//// - 仅是一个注释
/* - 仅是一个注释 */
/** - 外部块文档注释(恰好 2 个星号) */
/*** - 仅是一个注释 */
pub mod inner_module {}
pub mod nested_comments {
/* Rust 中 /* 我们可以 /* 嵌套注释 */ */ */
// 所有三种块注释都可以包含或被嵌套在任何其他类型的注释中:
/* /* */ /** */ /*! */ */
/*! /* */ /** */ /*! */ */
/** /* */ /** */ /*! */ */
pub mod dummy_item {}
}
pub mod degenerate_cases {
// 空内部行文档注释
//!
// 空内部块文档注释
/*!*/
// 空行注释
//
// 空外部行文档注释
///
// 空块注释
/**/
pub mod dummy_item {}
// 空 2 星号块不是文档块,而是块注释
/***/
}
/* 下一个是不允许的,因为外层文档注释需要一个接收该文档的项 */
/// 我的项在哪里?
mod boo {}
}
}
空白字符
Lexer
WHITESPACE →
U+0009 // Horizontal tab, '\t'
| U+000A // Line feed, '\n'
| U+000B // Vertical tab
| U+000C // Form feed
| U+000D // Carriage return, '\r'
| U+0020 // Space, ' '
| U+0085 // Next line
| U+200E // Left-to-right mark
| U+200F // Right-to-left mark
| U+2028 // Line separator
| U+2029 // Paragraph separator
TAB → U+0009 // Horizontal tab, '\t'
LF → U+000A // Line feed, '\n'
CR → U+000D // Carriage return, '\r'
空白字符是指任何非空字符串,其中仅包含具有 Pattern_White_Space Unicode 属性的字符。
Rust 是一种“自由格式“语言,这意味着所有形式的空白字符仅用于分隔语法中的 token,没有语义意义。
如果将一个 Rust 程序中的每个空白字符元素替换为任意其他合法的空白字符元素(如单个空格字符),程序将具有相同的含义。
Token
Lexer
Token →
RESERVED_TOKEN
| RAW_IDENTIFIER
| CHAR_LITERAL
| STRING_LITERAL
| RAW_STRING_LITERAL
| BYTE_LITERAL
| BYTE_STRING_LITERAL
| RAW_BYTE_STRING_LITERAL
| C_STRING_LITERAL
| RAW_C_STRING_LITERAL
| FLOAT_LITERAL
| INTEGER_LITERAL
| LIFETIME_TOKEN
| PUNCTUATION
| IDENTIFIER_OR_KEYWORD
Token 是语法中由正则(非递归)语言定义的基本产生式。Rust 源代码可以分解为以下几种 token:
在本文档的语法中,“简单” token 以字符串表产生式的形式给出,并以 monospace 字体呈现。
字面量
字面量是用于字面量表达式的 token。
示例
字符与字符串
| 示例 | # 组数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 | 引号 与 字节 与 Unicode |
| 原始 C 字符串 | cr#"hello"# | <256 | 所有 Unicode | N/A |
ASCII 转义
| 名称 | |
|---|---|
\x41 | 7 位字符码(恰好 2 个十六进制数字,最大 0x7F) |
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\\ | 反斜杠 |
\0 | 空 |
字节转义
| 名称 | |
|---|---|
\x7F | 8 位字符码(恰好 2 个十六进制数字) |
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\\ | 反斜杠 |
\0 | 空 |
Unicode 转义
| 名称 | |
|---|---|
\u{7FFF} | 24 位 Unicode 字符码(最多 6 个十六进制数字) |
引号转义
| 名称 | |
|---|---|
\' | 单引号 |
\" | 双引号 |
数字
后缀
后缀是一串紧跟在字面量主体部分之后(中间没有空白字符)的字符,其形式与非原始标识符或关键字相同。
Lexer
SUFFIX →
_ ^ XID_Continue+
| XID_Start XID_Continue*
任何类型的字面量(字符串、整数等)带有任意后缀作为 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 的后缀是受限的。非数字字面量 token 上的任何后缀都会被拒绝,而数字字面量 token 仅接受下表中的后缀。
| 整数 | 浮点数 |
|---|---|
u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize | f32, f64 |
字符与字符串字面量
字符字面量
Lexer
CHAR_LITERAL →
'
( ~[' \ LF CR TAB] | 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..=6valid hex char value }3
字符字面量是包含在两个 U+0027(单引号)字符之间的单个 Unicode 字符,但 U+0027 本身除外,它必须由前置的 U+005C 字符(\)进行转义。
字符串字面量
Lexer
STRING_LITERAL →
" (
~[" \ CR]
| QUOTE_ESCAPE
| ASCII_ESCAPE
| UNICODE_ESCAPE
| STRING_CONTINUE
)* " SUFFIX?
STRING_CONTINUE → \ LF
字符串字面量是包含在两个 U+0022(双引号)字符之间的任意 Unicode 字符序列,但 U+0022 本身除外,它必须由前置的 U+005C 字符(\)进行转义。
字符串字面量中允许换行,由字符 U+000A(LF)表示。字符 U+000D(CR)不能出现在字符串字面量中。当未转义的 U+005C 字符(\)紧接在换行之前出现时,该换行不会出现在 token 所表示的字符串中。详见字符串续行转义。
字符转义
在字符字面量或非原始字符串字面量中,还可以使用一些额外的转义。转义以 U+005C(\)开头,并以下列形式之一继续:
- 7 位码点转义以
U+0078(x)开头,后跟恰好两位十六进制数字,值最大为0x7F。它表示与提供的十六进制值相等的 ASCII 字符。不允许更高的值,因为无法确定它表示的是 Unicode 码点还是字节值。
- 24 位码点转义以
U+0075(u)开头,后跟最多六位十六进制数字,并用花括号U+007B({)和U+007D(})包围。它表示与提供的十六进制值相等的 Unicode 码点。该值必须是合法的 Unicode 标量值。
- 空白转义是字符
U+006E(n)、U+0072(r)或U+0074(t)之一,分别表示 Unicode 值U+000A(LF)、U+000D(CR)或U+0009(HT)。
- 空转义是字符
U+0030(0),表示 Unicode 值U+0000(NUL)。
- 反斜杠转义是字符
U+005C(\),必须进行转义才能表示其自身。
原始字符串字面量
Lexer
RAW_STRING_LITERAL →
r " ^ RAW_STRING_CONTENT " SUFFIX?
| r #n:1..=255 ^ " RAW_STRING_CONTENT_HASHED " #n SUFFIX?
RAW_STRING_CONTENT → ( !" ~CR )*
RAW_STRING_CONTENT_HASHED → ( !( " #n ) ~CR )*
原始字符串字面量不处理任何转义。它们以字符 U+0072(r)开头,后跟少于 256 个的字符 U+0023(#)和一个 U+0022(双引号)字符。
原始字符串主体可以包含除 U+000D(CR)外的任意 Unicode 字符序列。它仅由另一个 U+0022(双引号)字符后跟与起始 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
}
字节与字节字符串字面量
字节字面量
Lexer
BYTE_LITERAL →
b' ^ ( ASCII_FOR_CHAR | BYTE_ESCAPE ) ' SUFFIX?
ASCII_FOR_CHAR → ![' \ LF CR TAB] ASCII
BYTE_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 位整数数字字面量。
字节字符串字面量
Lexer
BYTE_STRING_LITERAL →
b" ^ ( ASCII_FOR_STRING | BYTE_ESCAPE | STRING_CONTINUE )* " SUFFIX?
ASCII_FOR_STRING → ![" \ CR] ASCII
非原始字节字符串字面量是 ASCII 字符和转义的序列,前置字符 U+0062(b)和 U+0022(双引号),后跟字符 U+0022。如果字面量中出现字符 U+0022,必须由前置的 U+005C(\)字符进行转义。或者,字节字符串字面量也可以是原始字节字符串字面量,定义如下。
字节字符串字面量中允许换行,由字符 U+000A(LF)表示。字符 U+000D(CR)不能出现在字节字符串字面量中。当未转义的 U+005C 字符(\)紧接在换行之前出现时,该换行不会出现在 token 所表示的字符串中。详见字符串续行转义。
在字节字面量或非原始字节字符串字面量中,还可以使用一些额外的转义。转义以 U+005C(\)开头,并以下列形式之一继续:
- 字节转义转义以
U+0078(x)开头,后跟恰好两位十六进制数字。它表示与提供的十六进制值相等的字节。
- 空白转义是字符
U+006E(n)、U+0072(r)或U+0074(t)之一,分别表示字节值0x0A(ASCII LF)、0x0D(ASCII CR)或0x09(ASCII HT)。
- 空转义是字符
U+0030(0),表示字节值0x00(ASCII NUL)。
- 反斜杠转义是字符
U+005C(\),必须进行转义才能表示其 ASCII 编码0x5C。
原始字节字符串字面量
Lexer
RAW_BYTE_STRING_LITERAL →
br " ^ RAW_BYTE_STRING_CONTENT " SUFFIX?
| br #n:1..=255 ^ " RAW_BYTE_STRING_CONTENT_HASHED " #n SUFFIX?
RAW_BYTE_STRING_CONTENT → ( !" ASCII_FOR_RAW )*
RAW_BYTE_STRING_CONTENT_HASHED → ( !( " #n ) ASCII_FOR_RAW )*
ASCII_FOR_RAW → !CR ASCII
原始字节字符串字面量不处理任何转义。它们以字符 U+0062(b)开头,后跟 U+0072(r),再后跟少于 256 个的字符 U+0023(#)和一个 U+0022(双引号)字符。
原始字符串主体可以包含除 U+000D(CR)外的任意 ASCII 字符序列。它仅由另一个 U+0022(双引号)字符后跟与起始 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 字符串与原始 C 字符串字面量
C 字符串字面量
Lexer
C_STRING_LITERAL →
c" ^ (
~[" \ CR NUL]
| BYTE_ESCAPEexcept \0 or \x00
| UNICODE_ESCAPEexcept \u{0}, \u{00}, …, \u{000000}
| STRING_CONTINUE
)* " SUFFIX?
C 字符串字面量是 Unicode 字符和转义的序列,前置字符 U+0063(c)和 U+0022(双引号),后跟字符 U+0022。如果字面量中出现字符 U+0022,必须由前置的 U+005C(\)字符进行转义。或者,C 字符串字面量也可以是原始 C 字符串字面量,定义如下。
C 字符串隐式地以字节 0x00 终止,因此 C 字符串字面量 c"" 等价于从字节字符串字面量 b"\x00" 手动构造一个 &CStr。除了隐式终止符外,C 字符串内不允许出现字节 0x00。
C 字符串字面量中允许换行,由字符 U+000A(LF)表示。字符 U+000D(CR)不能出现在 C 字符串字面量中。当未转义的 U+005C 字符(\)紧接在换行之前出现时,该换行不会出现在 token 所表示的字符串中。详见字符串续行转义。
在非原始 C 字符串字面量中,还可以使用一些额外的转义。转义以 U+005C(\)开头,并以下列形式之一继续:
- 字节转义转义以
U+0078(x)开头,后跟恰好两位十六进制数字。它表示与提供的十六进制值相等的字节。
- 24 位码点转义以
U+0075(u)开头,后跟最多六位十六进制数字,并用花括号U+007B({)和U+007D(})包围。它表示与提供的十六进制值相等的 Unicode 码点,以 UTF-8 编码。
- 空白转义是字符
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 Edition differences
C 字符串字面量在 2021 版本及更高版本中接受。在更早的版本中,token
c""被词法分析为c ""。
原始 C 字符串字面量
Lexer
RAW_C_STRING_LITERAL →
cr " ^ RAW_C_STRING_CONTENT " SUFFIX?
| cr #n:1..=255 ^ " RAW_C_STRING_CONTENT_HASHED " #n SUFFIX?
RAW_C_STRING_CONTENT → ( !" ~[CR NUL] )*
RAW_C_STRING_CONTENT_HASHED → ( !( " #n ) ~[CR NUL] )*
原始 C 字符串字面量不处理任何转义。它们以字符 U+0063(c)开头,后跟 U+0072(r),再后跟少于 256 个的字符 U+0023(#)和一个 U+0022(双引号)字符。
原始 C 字符串主体可以包含除 U+0000(NUL)和 U+000D(CR)外的任意 Unicode 字符序列。它仅由另一个 U+0022(双引号)字符后跟与起始 U+0022(双引号)字符之前相同数量的 U+0023(#)字符来终止。
原始 C 字符串主体中包含的所有字符都以 UTF-8 编码表示其自身。U+0022(双引号,当后跟至少与启动原始 C 字符串字面量时所用相同数量的 U+0023(#)字符时除外)或 U+005C(\)没有任何特殊含义。
2021 Edition differences
原始 C 字符串字面量在 2021 版本及更高版本中接受。在更早的版本中,token
cr""被词法分析为cr "",cr#""#被词法分析为cr #""#(不合语法)。
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
}
数字字面量
数字字面量可以是整数字面量或浮点数字面量。识别这两种字面量的语法是混合的。
整数字面量
Lexer
INTEGER_LITERAL →
( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL | DEC_LITERAL )
^ !RESERVED_FLOAT SUFFIX?
DEC_LITERAL → DEC_DIGIT ( DEC_DIGIT | _ )*
BIN_LITERAL → 0b ^ _* BIN_DIGIT ( BIN_DIGIT | _ )* ![e E 2-9]
OCT_LITERAL → 0o ^ _* OCT_DIGIT ( OCT_DIGIT | _ )* ![e E 8-9]
HEX_LITERAL → 0x ^ _* HEX_DIGIT ( HEX_DIGIT | _ )*
BIN_DIGIT → [0-1]
OCT_DIGIT → [0-7]
DEC_DIGIT → [0-9]
HEX_DIGIT → [0-9 a-f A-F]
RESERVED_FLOAT → . !( . | _ | XID_Start )
整数字面量有以下四种形式:
- 十进制字面量以十进制数字开头,后续可以是十进制数字和下划线的任意混合。
- 十六进制字面量以字符序列
U+0030U+0078(0x)开头,后续是十六进制数字和下划线的任意混合(至少一位数字)。
- 八进制字面量以字符序列
U+0030U+006F(0o)开头,后续是八进制数字和下划线的任意混合(至少一位数字)。
- 二进制字面量以字符序列
U+0030U+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;
}
}
无效的整数字面量
某些整数字面量形式是无效的。为避免歧义,分词器会拒绝它们,而不是将其拆分为多个 token。
#![allow(unused)]
fn main() {
0b0102; // 这不是 `0b010` 后跟 `2`。
0o1279; // 这不是 `0o127` 后跟 `9`。
0x80.0; // 这不是 `0x80` 后跟 `.` 和 `0`。
0b101e; // 这不是带后缀的字面量或 `0b101` 后跟 `e`。
0b; // 这不是整数字面量或 `0` 后跟 `b`。
0b_; // 这不是整数字面量或 `0` 后跟 `b_`。
2em; // 这不是带后缀的字面量或 `2` 后跟 `em`。
2.0em; // 这不是带后缀的字面量或 `2.0` 后跟 `em`。
}
不带后缀的二进制或八进制字面量后紧跟其基数范围之外的十进制数字(中间没有空白字符)是错误的。
不带后缀的二进制、八进制或十六进制字面量后紧跟一个句点字符(中间没有空白字符)是错误的(受限于与浮点数字面量中关于句点后允许内容的相同限制)。
不带后缀的二进制或八进制字面量后紧跟字符 e 或 E(中间没有空白字符)是错误的。
基数前缀之后(在任何可选的前导下划线之后)没有至少一个有效的该进制数字是错误的。
元组索引
Lexer
TUPLE_INDEX → DEC_LITERAL | BIN_LITERAL | OCT_LITERAL | HEX_LITERAL
元组索引直接与字面量 token 进行比较。元组索引以 0 开始,每个连续的索引以十进制值递增 1。因此,只有十进制值会匹配,且值不能有任何额外的前缀 0 字符。
元组索引不能包含任何后缀(如 usize)。
#![allow(unused)]
fn main() {
let example = ("dog", "cat", "horse");
let dog = example.0;
let cat = example.1;
// 以下示例是无效的。
let cat = example.01; // ERROR 没有名为 `01` 的字段
let horse = example.0b10; // ERROR 没有名为 `0b10` 的字段
let unicorn = example.0usize; // ERROR 元组索引上的后缀是无效的
let underscore = example.0_0; // ERROR 类型 `(&str, &str, &str)` 上没有字段 `0_0`
}
浮点数字面量
Lexer
FLOAT_LITERAL →
DEC_LITERAL ( . DEC_LITERAL )? FLOAT_EXPONENT SUFFIX?
| DEC_LITERAL . DEC_LITERAL SUFFIX?
| DEC_LITERAL . !( . | _ | XID_Start )
FLOAT_EXPONENT →
( e | E ) ^ ( + | - )? _* 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;
}
}
浮点数字面量的指数没有数字是错误的。
#![allow(unused)]
fn main() {
2e; // 这不是浮点数字面量或 `2` 后跟 `e`。
2.0e; // 这不是浮点数字面量或 `2.0` 后跟 `e`。
}
生命周期与循环标签
Lexer
LIFETIME_TOKEN →
RAW_LIFETIME
| ' IDENTIFIER_OR_KEYWORD !'
LIFETIME_OR_LABEL →
RAW_LIFETIME
| ' NON_KEYWORD_IDENTIFIER !'
RAW_LIFETIME →
'r# ^ IDENTIFIER_OR_KEYWORD !'
RESERVED_RAW_LIFETIME → 'r# ( _ | crate | self | Self | super ) !( ' | XID_Continue )
生命周期参数和循环标签使用 LIFETIME_OR_LABEL token。任何 LIFETIME_TOKEN 都会被词法分析器接受,例如可以用于宏中。
原始生命周期类似于普通生命周期,但其标识符带有 r# 前缀。(注意,r# 前缀不算作实际生命周期的一部分。)
与普通生命周期不同,原始生命周期可以使用任何严格或保留关键字,但上述 RAW_LIFETIME 所列的除外。
使用 RESERVED_RAW_LIFETIME token 是错误的。
2021 Edition differences
原始生命周期在 2021 版本及更高版本中接受。在更早的版本中,token
'r#lt被词法分析为'r # lt。
标点符号
标点符号 token 用作运算符、分隔符以及语法的其他组成部分。
Lexer
PUNCTUATION →
...
| ..=
| <<=
| >>=
| !=
| %=
| &&
| &=
| *=
| +=
| -=
| ->
| ..
| /=
| ::
| <-
| <<
| <=
| ==
| =>
| >=
| >>
| ^=
| |=
| ||
| !
| #
| $
| %
| &
| (
| )
| *
| +
| ,
| -
| .
| /
| :
| ;
| <
| =
| >
| ?
| @
| [
| ]
| ^
| {
| |
| }
| ~
Note
关于各标点符号字符的用法,请参阅语法索引。
定界符
括号标点用于语法的各个部分。开放括号必须始终与闭合括号配对。括号及其内部的 token 在宏中被称为“token 树“。三种括号类型如下:
| 括号 | 类型 |
|---|---|
{ } | 花括号 |
[ ] | 方括号 |
( ) | 圆括号 |
保留 token
几种 token 形式保留供将来使用或为避免混淆。源代码输入匹配这些形式之一是错误的。
Lexer
RESERVED_TOKEN →
RESERVED_GUARDED_STRING_LITERAL
| RESERVED_POUNDS
| RESERVED_RAW_IDENTIFIER
| RESERVED_RAW_LIFETIME
| RESERVED_TOKEN_DOUBLE_QUOTE
| RESERVED_TOKEN_LIFETIME
| RESERVED_TOKEN_POUND
| RESERVED_TOKEN_SINGLE_QUOTE
保留前缀
Lexer
RESERVED_TOKEN_DOUBLE_QUOTE →
IDENTIFIER_OR_KEYWORDexcept b or c or r or br or cr "
RESERVED_TOKEN_SINGLE_QUOTE →
IDENTIFIER_OR_KEYWORDexcept b '
RESERVED_TOKEN_POUND →
IDENTIFIER_OR_KEYWORDexcept r or br or cr #
RESERVED_TOKEN_LIFETIME →
' IDENTIFIER_OR_KEYWORDexcept r #
某些词法形式(称为保留前缀)保留供将来使用。
原本会被词法解释为非原始标识符(或关键字)且紧接后跟 #、' 或 " 字符(中间没有空白字符)的源代码输入将被识别为保留前缀。
注意,原始标识符、原始字符串字面量和原始字节字符串字面量可以包含 # 字符,但不会被解释为包含保留前缀。
类似地,用于原始字符串字面量、字节字面量、字节字符串字面量、原始字节字符串字面量、C 字符串字面量和原始 C 字符串字面量的 r、b、br、c 和 cr 前缀不会被解释为保留前缀。
原本会被词法解释为非原始生命周期(或关键字)且紧接后跟 # 字符(中间没有空白字符)的源代码输入将被识别为保留生命周期前缀。
2021 Edition differences
从 2021 版本开始,保留前缀会被词法分析器报告为错误(特别是,它们不能传递给宏)。
在 2021 版本之前,保留前缀会被词法分析器接受并解释为多个 token(例如,一个标识符或关键字 token 后跟一个
#token)。所有版本中接受的示例:
#![allow(unused)] fn main() { macro_rules! lexes {($($_:tt)*) => {}} lexes!{a #foo} lexes!{continue 'foo} lexes!{match "..." {}} lexes!{r#let#foo} // 三个 token: r#let # foo lexes!{'prefix #lt} }在 2021 版本之前接受但在此后被拒绝的示例:
#![allow(unused)] fn main() { macro_rules! lexes {($($_:tt)*) => {}} lexes!{a#foo} lexes!{continue'foo} lexes!{match"..." {}} lexes!{'prefix#lt} }
保留守卫
Lexer
RESERVED_GUARDED_STRING_LITERAL → #+ STRING_LITERAL
RESERVED_POUNDS → #2..
保留守卫是为将来使用而保留的语法,使用时会产生编译错误。
保留守卫字符串字面量是一个由一个或多个 U+0023(#)后紧跟 STRING_LITERAL 组成的 token。
保留井号是一个由两个或多个 U+0023(#)组成的 token。
2024 Edition differences
在 2024 版本之前,保留守卫会被词法分析器接受并解释为多个 token。例如,
#"foo"#形式被解释为三个 token。##被解释为两个 token。
宏
Rust 的功能和语法可以通过称为宏的自定义定义来扩展。它们被赋予名称,并通过一致的语法来调用:some_extension!(...)。
有两种定义新宏的方式:
宏调用
Syntax
MacroInvocation →
SimplePath ! DelimTokenTree
DelimTokenTree →
( TokenTree* )
| [ TokenTree* ]
| { TokenTree* }
TokenTree →
Tokenexcept delimiters | DelimTokenTree
MacroInvocationSemi →
SimplePath ! ( TokenTree* ) ;
| SimplePath ! [ TokenTree* ] ;
| SimplePath ! { TokenTree* }
宏调用在编译时展开宏,并用宏的结果替换调用。宏可以在以下情况下调用:
macro_rules转写器
当作为项或语句使用时,对于不使用花括号的情况,应使用 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!();
}
宏调用可以通过两种作用域来解析:
- 文本作用域
- 基于路径的作用域
声明宏
Syntax
MacroRulesDefinition →
macro_rules ! IDENTIFIER MacroRulesDef
MacroRulesDef →
( MacroRules ) ;
| [ MacroRules ] ;
| { MacroRules }
MacroRules →
MacroRule ( ; MacroRule )* ;?
MacroRule →
MacroMatcher => MacroTranscriber
MacroMatcher →
( MacroMatch* )
| [ MacroMatch* ]
| { MacroMatch* }
MacroMatch →
Tokenexcept $ and delimiters
| MacroMatcher
| $ ( IDENTIFIER_OR_KEYWORDexcept crate | RAW_IDENTIFIER ) : MacroFragSpec
| $ ( MacroMatch+ ) MacroRepSep? MacroRepOp
MacroFragSpec →
block | expr | expr_2021 | ident | item | lifetime | literal
| meta | pat | pat_param | path | stmt | tt | ty | vis
MacroRepSep → Tokenexcept delimiters and MacroRepOp
MacroRepOp → * | + | ?
macro_rules 允许用户以声明式的方式定义语法扩展。我们称此类扩展为“声明宏“或简称为“宏“。
每个声明宏都有一个名称,以及一条或多条规则。每条规则有两部分:一个匹配器,描述其匹配的语法;和一个转写器,描述将替换成功匹配调用的语法。匹配器和转写器都必须由分隔符包围。宏可以展开为表达式、语句、项(包括 trait、impl 和外部项)、类型或模式。
转写
当调用宏时,宏展开器按名称查找宏调用,并依次尝试每条宏规则。它转写第一个成功匹配的规则;如果这导致错误,则不会尝试后续的匹配。
匹配时不执行前瞻;如果编译器无法每次只用一个 token 明确地确定如何解析宏调用,则这是一个错误。在以下示例中,编译器不会前瞻标识符之后的下一个 token 是否是 ),尽管这可以让它明确地解析调用:
#![allow(unused)]
fn main() {
macro_rules! ambiguity {
($($i:ident)* $j:ident) => { };
}
ambiguity!(error); // Error: local ambiguity
}
在匹配器和转写器中,$ token 用于调用宏引擎的特殊行为(在下面的元变量和重复中描述)。不属于此类调用的 token 会被逐字匹配和转写,但有一个例外。例外是匹配器的外部分隔符将匹配任何一对分隔符。因此,例如,匹配器 (()) 将匹配 {()} 但不会匹配 {{}}。字符 $ 不能被逐字匹配或转写。
转发已匹配的片段
当将已匹配的片段转发给另一个声明宏时,第二个宏中的匹配器将看到该片段类型的不透明 AST。第二个宏不能在匹配器中使用字面 token 来匹配这些片段,只能使用相同类型的片段说明符。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 片段后直接匹配 token:
#![allow(unused)]
fn main() {
// compiles OK
macro_rules! foo {
($l:tt) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
foo!(3);
}
元变量
在匹配器中,$名称:片段说明符匹配指定类型的 Rust 语法片段,并将其绑定到元变量 $名称。
有效的片段说明符有:
block:一个块表达式expr:一个表达式expr_2021:一个表达式,但不包括下划线表达式和常量块表达式(参见 macro.decl.meta.edition2024)ident:一个 IDENTIFIER_OR_KEYWORD,不包括_、RAW_IDENTIFIER 或$crateitem:一个项lifetime:一个 LIFETIME_TOKENliteral:匹配-?字面量表达式meta:一个 Attr,即属性的内容pat:一个模式(参见 macro.decl.meta.edition2021)pat_param:一个 PatternNoTopAltpath:一个 TypePathstmt:一个语句,不含尾部分号(需要分号的项语句除外)tt:一个 TokenTree(一个单独的 token 或位于匹配的分隔符()、[]或{}中的 token)ty:一个类型vis:一个可能为空的可见性限定符
在转写器中,元变量简单地通过 $名称引用,因为片段类型在匹配器中已指定。元变量被替换为与它们匹配的语法元素。元变量可以被转写多次,或根本不转写。
关键字元变量 $crate 可用于引用当前 crate。
2021 Edition differences
从 2021 版开始,
pat片段说明符匹配顶层或模式(即接受 Pattern)。在 2021 版之前,它们匹配的片段与
pat_param完全相同(即接受 PatternNoTopAlt)。相关的版次是
macro_rules!定义所在的版次。
2024 Edition differences
在 2024 版之前,
expr片段说明符在顶层不匹配下划线表达式或常量块表达式。它们允许在子表达式中出现。
expr_2021片段说明符存在是为了与 2024 之前的版次保持向后兼容。
重复
在匹配器和转写器中,重复通过将需要重复的 token 放入 $(…) 中,后跟一个重复运算符,中间可选地有一个分隔符 token 来表示。
分隔符 token 可以是除分隔符或重复运算符之外的任何 token,但 ; 和 , 是最常见的。例如,$( $i:ident ),* 表示任意数量的由逗号分隔的标识符。允许嵌套重复。
重复运算符有:
*— 表示任意次数的重复。+— 表示至少一次重复。?— 表示一个可选的片段,零次或一次出现。
由于 ? 表示至多一次出现,因此不能与分隔符一起使用。
重复片段会匹配并转写为指定数量的片段,由分隔符 token 分隔。元变量会匹配其对应片段的每一次重复。例如,上面的 $( $i:ident ),* 示例将 $i 匹配到列表中的所有标识符。
在转写期间,对重复施加了额外的限制,以便编译器知道如何正确地展开它们:
- 元变量在转写器中必须以与匹配器中完全相同的数量、种类和重复嵌套顺序出现。因此,对于匹配器
$( $i:ident ),*,转写器=> { $i }、=> { $( $( $i )* )* }和=> { $( $i )+ }都是不合法的,但=> { $( $i );* }是正确的,并用分号分隔的列表替换逗号分隔的标识符列表。 - 转写器中的每个重复必须包含至少一个元变量,以决定展开多少次。如果多个元变量出现在同一个重复中,它们必须绑定到相同数量的片段。例如,
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))必须将相同数量的$i片段和$j片段绑定。这意味着用(a, b, c; d, e, f)调用宏是合法的,并展开为((a,d), (b,e), (c,f)),但(a, b, c; d, e)是不合法的,因为它没有相同的数量。此要求适用于嵌套重复的每一层。
作用域、导出和导入
出于历史原因,声明宏的作用域不完全像项那样工作。宏有两种作用域形式:文本作用域和基于路径的作用域。文本作用域基于事物在源文件中出现的顺序,甚至跨越多个文件,也是默认的作用域。下面将进一步解释。基于路径的作用域与项作用域的工作方式完全相同。宏的作用域、导出和导入主要受属性控制。
当通过非限定标识符(不是多段路径的一部分)调用宏时,首先在文本作用域中查找。如果没有结果,则在基于路径的作用域中查找。如果宏的名称是通过路径限定的,则仅在基于路径的作用域中查找。
use lazy_static::lazy_static; // 基于路径的导入。
macro_rules! lazy_static { // 文本定义。
(lazy) => {};
}
lazy_static!{lazy} // 文本查找首先找到我们的宏。
self::lazy_static!{} // 基于路径的查找忽略我们的宏,找到导入的那个。
文本作用域
文本作用域很大程度上基于事物在源文件中出现的顺序,其工作方式类似于用 let 声明的局部变量的作用域,只是它也适用于模块级别。当使用 macro_rules! 定义宏时,宏在定义之后进入作用域(注意,它仍然可以递归使用,因为名称是从调用位置查找的),直到其外围作用域(通常是一个模块)关闭。这可以进入子模块,甚至跨越多个文件:
//// src/lib.rs
mod has_macro {
// m!{} // 错误:m 不在作用域中。
macro_rules! m {
() => {};
}
m!{} // OK:在声明 m 之后出现。
mod uses_macro;
}
// m!{} // 错误:m 不在作用域中。
//// src/has_macro/uses_macro.rs
m!{} // OK:在 src/lib.rs 中声明 m 之后出现
多次定义宏不是错误;最近的声明将遮蔽先前的声明,除非它已经离开作用域。
#![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!(); // 错误:m 不在作用域中。
}
文本作用域的名称绑定对宏的基于路径作用域绑定具有遮蔽效果。
#![allow(unused)]
fn main() {
macro_rules! m2 {
() => {
println!("m2");
};
}
// 解析到下面 use 声明带来的基于路径的候选项。
m!(); // 打印 "m2\n"
// 用文本作用域引入第二个 `m` 候选项。
//
// 从这之后,这会遮蔽下面那个基于路径的候选项。
macro_rules! m {
() => {
println!("m");
};
}
// 将 `m2` 宏作为基于路径的候选项引入。
//
// 这项在整个示例中都在作用域中,不仅仅是在 use 声明之后。
use m2 as m;
// 解析到 use 声明上方的文本宏候选项。
m!(); // 打印 "m\n"
}
Note
关于不允许遮蔽的区域,参见名称解析歧义。
基于路径的作用域
默认情况下,宏没有基于路径的作用域。宏可以通过两种方式获得基于路径的作用域:
宏可以被重导出,以使其从 crate 根以外的模块获得基于路径的作用域。
#![allow(unused)]
fn main() {
mac::m!(); // OK:基于路径的查找在 mac 模块中找到 `m`。
mod mac {
// 用文本作用域引入宏 `m`。
macro_rules! m {
() => {};
}
// 从 `m` 的文本作用域内进行基于路径作用域的重导出。
pub(crate) use m;
}
}
宏具有隐式的 pub(crate) 可见性。#[macro_export] 将隐式可见性更改为 pub。
#![allow(unused)]
fn main() {
// 隐式可见性是 `pub(crate)`。
macro_rules! private_m {
() => {};
}
// 隐式可见性是 `pub`。
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK。
pub use pub_m as pub_macro; // OK。
}
#![allow(unused)]
fn main() {
// 隐式可见性是 `pub(crate)`。
macro_rules! private_m {
() => {};
}
// 隐式可见性是 `pub`。
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK。
pub use pub_m as pub_macro; // OK。
pub use private_m; // 错误:`private_m` 仅在该 crate 内公开,
// 不能在外部重导出。
}
macro_use 属性
macro_use 属性 有两个用途:它可以用于模块以扩展其中定义的宏的作用域,也可以用于 extern crate 将另一个 crate 的宏导入 macro_use 预导入中。
Example
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); }#[macro_use] extern crate log;
当用于模块时,macro_use 属性使用 MetaWord 语法。
当用于 extern crate 时,它使用 MetaWord 和 MetaListIdents 语法。关于如何使用这些语法,请参见 macro.decl.scope.macro_use.prelude。
macro_use 属性可以应用于模块或 extern crate。
Note
rustc忽略在其他位置的使用,但会发出 lint。这未来可能成为一个错误。
macro_use 属性不能用于 extern crate self。
macro_use 属性可以在一项形式体上使用任意多次。
在 MetaListIdents 语法中可以指定多个 macro_use 实例。所有指定宏的并集将被导入。
Note
在模块上,
rustc会对第一个之后的任何 MetaWordmacro_use属性发出 lint。在
extern crate上,rustc会对任何由于没有导入任何未被其他macro_use属性导入的宏而无效的macro_use属性发出 lint。如果两个或多个 MetaListIdentsmacro_use属性导入了同一个宏,则会对第一个发出 lint。如果存在任何 MetaWordmacro_use属性,则会对所有 MetaListIdentsmacro_use属性发出 lint。如果存在两个或多个 MetaWordmacro_use属性,则会对第一个之后的那些发出 lint。
当 macro_use 用于模块时,该模块的宏作用域将扩展到该模块的词法作用域之外。
Example
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); // OK }
在 crate 根中对 extern crate 声明指定 macro_use 会从该 crate 导入已导出的宏。
以这种方式导入的宏被导入到 macro_use 预导入中,而非文本方式,这意味着它们可以被任何其他名称遮蔽。通过 macro_use 导入的宏可以在导入语句之前使用。
Note
rustc当前在有冲突的情况下倾向于最后导入的宏。不要依赖这一点。这种行为是不寻常的,因为 Rust 中的导入通常是无顺序依赖的。macro_use的这种行为未来可能会改变。详情请参阅 Rust issue #148025。
当使用 MetaWord 语法时,所有导出的宏都被导入。当使用 MetaListIdents 语法时,仅导入指定的宏。
Example
#[macro_use(lazy_static)] // 或者 `#[macro_use]` 来导入所有宏。 extern crate lazy_static; lazy_static!{} // self::lazy_static!{} // 错误:lazy_static 在 `self` 中未定义。
要通过 macro_use 导入的宏必须使用 macro_export 导出。
macro_export 属性
macro_export 属性 从 crate 中导出宏,并使其在 crate 根中可用于基于路径的解析。
Example
#![allow(unused)] fn main() { self::m!(); // ^^^^ OK:基于路径的查找在当前模块中找到 `m`。 m!(); // 同上。 mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
macro_export 属性使用 MetaWord 和 MetaListIdents 语法。使用 MetaListIdents 语法时,它接受一个 local_inner_macros 值。
macro_export 属性可以应用于 macro_rules 定义。
Note
rustc忽略在其他位置的使用,但会发出 lint。这未来可能成为一个错误。
只有第一次对宏使用 macro_export 才会生效。
Note
rustc会对第一次之后的任何使用发出 lint。
默认情况下,宏只有文本作用域,不能通过路径解析。当使用 macro_export 属性时,宏在 crate 根中变得可用,可以通过其路径引用。
Example
没有
macro_export,宏只有文本作用域,因此对宏的基于路径的解析会失败。macro_rules! m { () => {}; } self::m!(); // 错误 crate::m!(); // 错误 fn main() {}使用
macro_export,基于路径的解析可以正常工作。#[macro_export] macro_rules! m { () => {}; } self::m!(); // OK crate::m!(); // OK fn main() {}
macro_export 属性使宏从 crate 根导出,以便可以在其他 crate 中通过路径引用。
Example
假设在
logcrate 中有以下定义:#![allow(unused)] fn main() { #[macro_export] macro_rules! warn { ($message:expr) => { eprintln!("WARN: {}", $message) }; } }从另一个 crate,你可以通过路径引用该宏:
fn main() { log::warn!("example warning"); }
macro_export 允许在 extern crate 上使用 macro_use 将宏导入到 macro_use 预导入中。
Example
假设在
logcrate 中有以下定义:#![allow(unused)] fn main() { #[macro_export] macro_rules! warn { ($message:expr) => { eprintln!("WARN: {}", $message) }; } }在依赖 crate 中使用
macro_use允许你从预导入使用该宏:#[macro_use] extern crate log; pub mod util { pub fn do_thing() { // 通过宏预导入解析。 warn!("example warning"); } }
在 macro_export 属性中添加 local_inner_macros 会使宏定义中所有单段宏调用隐式地带有 $crate:: 前缀。
Note
这主要是作为一种工具,用于将
$crate添加到语言之前编写的代码迁移到能与 Rust 2018 的基于路径的宏导入一起工作。不建议在新代码中使用。
Example
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // 自动转换为 $crate::helper!()。 } #[macro_export] macro_rules! helper { () => { () } } }
卫生性
声明宏具有混合位置卫生性(mixed-site hygiene)。这意味着循环标签、块标签和局部变量在宏定义位置查找,而其他符号在宏调用位置查找。例如:
#![allow(unused)]
fn main() {
let x = 1;
fn func() {
unreachable!("this is never called")
}
macro_rules! check {
() => {
assert_eq!(x, 1); // 使用定义位置的 `x`。
func(); // 使用调用位置的 `func`。
};
}
{
let x = 2;
fn func() { /* 不会 panic */ }
check!();
}
}
在宏展开中定义的标签和局部变量不在调用之间共享,因此以下代码无法编译:
#![allow(unused)]
fn main() {
macro_rules! m {
(define) => {
let x = 1;
};
(refer) => {
dbg!(x);
};
}
m!(define);
m!(refer);
}
一个特殊情况是 $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,所以在引用非宏项时,必须使用完全限定的模块路径:
#![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() {}
}
Note
在 Rust 1.30 之前,
$crate和local_inner_macros不被支持。它们是与基于路径的宏导入一起添加的,以确保辅助宏不需要由宏导出 crate 的用户手动导入。为较早版本 Rust 编写的、使用辅助宏的 crate 需要修改为使用$crate或local_inner_macros才能与基于路径的导入良好配合。
后继集合歧义限制
宏系统使用的解析器相当强大,但为了防止当前或未来版本语言中的歧义,它受到限制。
特别是,除了关于歧义展开的规则之外,由元变量匹配的非终结符之后必须跟一个已被确定为可以安全地在该类型匹配之后使用的 token。
举例来说,像 $i:expr [ , ] 这样的宏匹配器在今天的 Rust 中理论上可以被接受,因为 [,] 不可能是合法表达式的一部分,因此解析总是明确的。然而,因为 [ 可以开始尾随表达式,[ 不是一个能够安全排除在表达式之后出现的字符。如果 [,] 在 Rust 的后续版本中被接受,则该匹配器将变得歧义或解析错误,破坏正常工作的代码。然而,像 $i:expr, 或 $i:expr; 这样的匹配器是合法的,因为 , 和 ; 是合法的表达式分隔符。具体规则是:
expr和stmt之后只能跟以下之一:=>、,或;。
pat_param之后只能跟以下之一:=>、,、=、|、if或in。
pat之后只能跟以下之一:=>、,、=、if或in。
path和ty之后只能跟以下之一:=>、,、=、|、;、:、>、>>、[、{、as、where,或一个block片段说明符的宏变量。
vis之后只能跟以下之一:,,一个非原生priv之外的标识符,任何可以开始类型的 token,或一个具有ident、ty或path片段说明符的元变量。
- 所有其他片段说明符没有任何限制。
2021 Edition differences
在 2021 版之前,
pat之后也可以跟|。
当涉及重复时,规则适用于每种可能的展开次数,并考虑分隔符。这意味着:
- 如果重复包含分隔符,则该分隔符必须能够跟在重复内容之后。
- 如果重复可以重复多次(
*或+),则内容必须能够跟在自身之后。 - 重复的内容必须能够跟在其之前的任何内容之后,而其之后的任何内容必须能够跟在重复内容之后。
- 如果重复可以匹配零次(
*或?),则之后的内容必须能够跟在之前的内容之后。
更多细节请参见形式化规范。
过程宏
过程宏允许以函数执行的方式创建语法扩展。过程宏有三种类型:
过程宏允许你在编译时运行操作 Rust 语法的代码,既可以消费也可以产出 Rust 语法。你可以将过程宏看作是 AST 到另一个 AST 的函数。
过程宏必须在 crate 类型为 proc-macro 的 crate 的根部定义。这些宏不能在定义它们的 crate 中使用,只能在另一个 crate 中导入后使用。
Note
使用 Cargo 时,过程宏 crate 在清单中通过
proc-macro键定义:[lib] proc-macro = true
作为函数,它们必须返回语法、panic 或无限循环。返回的语法根据过程宏的类型来替换或添加语法。panic 会被编译器捕获并转化为编译错误。无限循环不会被编译器捕获,会导致编译器挂起。
过程宏在编译期间运行,因此拥有编译器所拥有的相同资源。例如,标准输入、错误和输出与编译器可以访问的相同。同样,文件访问也是相同的。因此,过程宏具有与 Cargo 构建脚本相同的安全考虑。
过程宏有两种报告错误的方式。第一种是 panic。第二种是发出 compile_error 宏调用。
proc_macro crate
过程宏 crate 几乎总是会链接到编译器提供的 proc_macro crate。proc_macro crate 提供了编写过程宏所需的类型以及便利设施。
此 crate 主要包含一个 TokenStream 类型。过程宏操作token 流而非 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。
过程宏的卫生性
过程宏是非卫生的。这意味着它们的行为就好像输出的 token 流被简单地内联写入到其相邻的代码中一样。这意味着它受外部项的影响,也会影响外部导入。
宏作者需要小心确保其宏在此限制下能在尽可能多的上下文中正常工作。这通常包括使用库中项的绝对路径(例如 ::std::option::Option 而不是 Option),或者确保生成的函数名称不太可能与其他函数冲突(如 __internal_foo 而不是 foo)。
proc_macro 属性
Example
此宏定义忽略其输入,并在其作用域中生成一个函数
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()); }
proc_macro 属性使用 MetaWord 语法。
proc_macro 属性只能应用于具有类型 fn(TokenStream) -> TokenStream 的 pub 函数,其中 TokenStream 来自 proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于 crate 的根部。
proc_macro 属性在一个函数上只能指定一次。
proc_macro 属性公开地在 crate 根部的宏命名空间中定义一个与该函数同名的宏。
类函数过程宏的类函数宏调用会将宏调用分隔符内的内容作为输入 TokenStream 参数传入,并用函数的输出 TokenStream 替换整个宏调用。
类函数过程宏可以在任何宏调用位置被调用,包括:
proc_macro_derive 属性
将 proc_macro_derive 属性 应用于一个函数可定义一个派生宏,该宏可以由 derive 属性调用。这些宏会接收一个 struct、enum 或 union 定义的 token 流,并且可以在这些定义之后生成新的项。它们还可以声明和使用派生宏辅助属性。
Example
此派生宏忽略其输入,并追加定义函数的 token。
#![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()); }
proc_macro_derive 属性的语法是:
Syntax
ProcMacroDeriveAttribute →
proc_macro_derive ( DeriveMacroName ( , DeriveMacroAttributes )? ,? )
DeriveMacroAttributes →
attributes ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )
派生宏的名称由 DeriveMacroName 给出。可选的 attributes 参数在 macro.proc.derive.attributes 中描述。
proc_macro_derive 属性只能应用于在 crate 根部定义的 pub 函数,该函数具有 Rust ABI,类型为 fn(TokenStream) -> TokenStream,其中 TokenStream 来自 proc_macro crate。该函数可以是 const 的,也可以使用 extern 显式指定 Rust ABI,但不能使用任何其他限定符(例如不能是 async 或 unsafe)。
proc_macro_derive 属性在一个函数上只能使用一次。
proc_macro_derive 属性在 crate 根部的宏命名空间中公开定义派生宏。
输入的 TokenStream 是应用了 derive 属性的项的 token 流。输出的 TokenStream 必须是(可能为空的)一组项。这些项被追加到同一个模块或块中的输入项之后。
派生宏辅助属性
派生宏可以声明派生宏辅助属性,这些属性可以在应用了该派生宏的项的作用域内使用。这些属性是惰性的。虽然它们的目的是供声明它们的宏使用,但任何宏都可以看到它们。
派生宏的辅助属性通过在 proc_macro_derive 属性中的 attributes 列表中添加其标识符来声明。
Example
这里声明了一个辅助属性,然后忽略它。
#![crate_type="proc-macro"] extern crate proc_macro; use proc_macro::TokenStream; #[proc_macro_derive(WithHelperAttr, attributes(helper))] pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream { TokenStream::new() }为了使用它,我们可以这样写:
#[derive(WithHelperAttr)] struct Struct { #[helper] field: (), }
当派生宏调用被应用于一个项时,该派生宏引入的辅助属性在以下情况下进入作用域:1)应用于该项且在派生宏调用之后按词法顺序出现的属性;2)应用于该项内部字段和变体的属性。
Note
rustc目前允许在引入它们的宏之前使用派生辅助属性。这种不按顺序使用的派生辅助属性可能不会遮蔽其他属性宏。此行为已被弃用,并计划移除。#[helper] // 已弃用,未来将成为硬错误。 #[derive(WithHelperAttr)] struct Struct { field: (), }更多细节请参阅 Rust issue #79202。
proc_macro_attribute 属性
proc_macro_attribute 属性 定义了一个属性宏,该宏可以作为外部属性来使用。
Example
此属性宏接受输入流并原样输出,实际上是一个无操作属性。
#![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 }
Example
这展示了编译器输出中属性宏所看到的字符串化的
TokenStreams。// 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; // Example: Basic function. #[show_streams] fn invoke1() {} // out: attr: "" // out: item: "fn invoke1() {}" // Example: Attribute with input. #[show_streams(bar)] fn invoke2() {} // out: attr: "bar" // out: item: "fn invoke2() {}" // Example: Multiple tokens in the input. #[show_streams(multiple => tokens)] fn invoke3() {} // out: attr: "multiple => tokens" // out: item: "fn invoke3() {}" // Example: Delimiters in the input. #[show_streams { delimiters }] fn invoke4() {} // out: attr: "delimiters" // out: item: "fn invoke4() {}"
proc_macro_attribute 属性使用 MetaWord 语法。
proc_macro_attribute 属性只能应用于具有类型 fn(TokenStream, TokenStream) -> TokenStream 的 pub 函数,其中 TokenStream 来自 proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于 crate 的根部。
proc_macro_attribute 属性在一个函数上只能指定一次。
proc_macro_attribute 属性在 crate 根部的宏命名空间中定义一个与该函数同名的属性。
属性宏只能用于:
第一个 TokenStream 参数是属性名称后的分隔 token 树,但不包括外部分隔符。如果应用的属性仅包含属性名称或属性名称后跟空分隔符,则 TokenStream 为空。
第二个 TokenStream 是项的其余部分,包括该项上的其他属性。
应用该属性的项将被返回的 TokenStream 中的零个或多个项替换。
声明宏 token 与过程宏 token
声明式 macro_rules 宏和过程宏使用相似但不同的 token(或 TokenTrees)定义。
macro_rules 中的 token 树(对应 tt 匹配器)定义为:
- 分隔组(
(...)、{...}等) - 语言支持的所有运算符,包括单字符和多字符(
+、+=)。- 注意,此集合不包括单引号
'。
- 注意,此集合不包括单引号
- 字面量(
"string"、1等)- 注意,取反(例如
-1)永远不是此类字面量 token 的一部分,而是一个独立的运算符 token。
- 注意,取反(例如
- 标识符,包括关键字(
ident、r#ident、fn) - 生命周期(
'ident) macro_rules中的元变量替换(例如macro_rules! mac { ($my_expr: expr) => { $my_expr } }中,在mac展开后,$my_expr将被视为一个单一的 token 树,无论传递的表达式是什么)
过程宏中的 token 树定义为:
- 分隔组(
(...)、{...}等) - 语言支持的运算符中使用的所有标点字符(
+,但不包括+=),以及单引号'字符(通常用于生命周期,关于生命周期的拆分和拼接行为请参见下文) - 字面量(
"string"、1等)- 支持将取反(例如
-1)作为整数和浮点字面量的一部分。
- 支持将取反(例如
- 标识符,包括关键字(
ident、r#ident、fn)
当 token 流传入和传出过程宏时,会处理这两种定义之间的不匹配。请注意,下面的转换可能会延迟进行,因此如果 token 没有被实际检查,转换可能不会发生。
当传入过程宏时:
- 所有多字符运算符被拆分为单字符。
- 生命周期被拆分为一个
'字符和一个标识符。 - 关键字元变量
$crate作为单个标识符传入。 - 所有其他元变量替换以其底层 token 流的形式表示。
- 当需要保持解析优先级时,此类 token 流可能被包装到具有隐式分隔符(
Delimiter::None)的分隔组(Group)中。 tt和ident替换永远不会被包装到此类组中,总是以其底层 token 树的形式表示。
- 当需要保持解析优先级时,此类 token 流可能被包装到具有隐式分隔符(
当从过程宏发出时:
- 标点字符在适用时会被粘合为多字符运算符。
- 与标识符连接的单引号
'会被粘合为生命周期。 - 负数字面量会被转换为两个 token(
-和字面量),可能被包装进具有隐式分隔符(Delimiter::None)的分隔组(Group)中,以保持解析优先级。
请注意,声明宏和过程宏都不支持文档注释 token(例如 /// Doc),因此它们在传递给宏时始终被转换为表示等效的 #[doc = r"str"] 属性的 token 流。
crate 与源文件
Syntax
Crate →
InnerAttribute*
Item*
Note
虽然 Rust 和任何其他语言一样,既可以通过解释器也可以通过编译器来实现,但目前唯一的实现是编译器,而且这门语言始终被设计为编译型。基于这些原因,本节假定使用编译器。
Rust 的语义遵循编译时和运行时之间的阶段区分。1具有静态解释的语义规则决定编译成功或失败,而具有动态解释的语义规则决定程序在运行时的行为。
编译模型以称为 crate 的工件为中心。每次编译处理一个源代码形式的 crate,如果成功,则生成一个二进制形式的 crate:要么是可执行文件,要么是某种库。2
crate 是编译和链接的单元,也是版本控制、分发和运行时加载的单元。一个 crate 包含一个由嵌套的模块作用域组成的树。这棵树的顶层是一个匿名模块(从该模块内的路径角度来看),crate 中的任何项都有一个规范模块路径来表示它在 crate 的模块树中的位置。
Rust 编译器总是以单个源文件作为输入进行调用,并且总是产生单个输出 crate。对该源文件的处理可能导致其他源文件作为模块被加载。源文件的扩展名是 .rs。
Rust 源文件描述一个模块,该模块在当前 crate 的模块树中的名称和位置是由源文件外部定义的:要么是通过引用源文件中的显式 Module 项,要么是通过 crate 本身的名称。
每个源文件都是一个模块,但并非每个模块都需要自己的源文件:模块定义可以在一个文件内嵌套。
每个源文件包含零个或多个项定义,并且可以选择以任意数量的属性开头,这些属性适用于包含该文件的模块,其中大多数影响编译器的行为。
匿名 crate 模块可以有额外的属性,这些属性适用于整个 crate。
Note
文件内容可以以 shebang 开头。
#![allow(unused)]
fn main() {
// 指定 crate 名称。
#![crate_name = "projx"]
// 指定输出工件的类型。
#![crate_type = "lib"]
// 开启一个警告。
// 这可以在任何模块中完成,不仅仅是匿名 crate 模块。
#![warn(non_camel_case_types)]
}
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;
}
Note
标准库中实现了
Termination的类型包括:
()!InfallibleExitCodeResult<T, E> where T: Termination, E: Debug
未捕获的外部展开
当“外部“展开(例如 C++ 代码抛出的异常,或使用不同 panic 处理器的 Rust 代码中的 panic!)传播到 main 函数之外时,进程将被安全终止。这可能表现为中止,在这种情况下,不能保证任何 Drop 调用会被执行,并且错误输出可能比由“原生“ Rust panic 终止运行时更少。
更多信息请参阅 panic 文档。
no_main 属性
no_main 属性 可以用在 crate 级别,以禁止为可执行二进制文件生成 main 符号。这在链接的其他对象定义了 main 时很有用。
crate_name 属性
crate_name 属性 可以用在 crate 级别,以通过 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。 ↩
条件编译
Syntax
ConfigurationPredicate →
ConfigurationOption
| ConfigurationAll
| ConfigurationAny
| ConfigurationNot
| true
| false
ConfigurationOption →
IDENTIFIER ( = ( STRING_LITERAL | RAW_STRING_LITERAL ) )?
ConfigurationAll →
all ( ConfigurationPredicateList? )
ConfigurationAny →
any ( ConfigurationPredicateList? )
ConfigurationNot →
not ( ConfigurationPredicate )
ConfigurationPredicateList →
ConfigurationPredicate ( , ConfigurationPredicate )* ,?
条件编译源代码是仅在特定条件下才被编译的源代码。
可以使用 cfg 和 cfg_attr 属性以及内置的 cfg! 和 cfg_select! 宏来实现条件编译源代码。
是否编译可以取决于被编译 crate 的目标架构、传递给编译器的任意值,以及下面进一步描述的其他事项。
每种条件编译形式都接受一个求值为 true 或 false 的配置谓词。谓词是以下之一:
- 一个配置选项。如果该选项被设置,则谓词为 true;如果未被设置,则为 false。
all()后跟一个逗号分隔的配置谓词列表。如果所有给定的谓词都为 true,或者列表为空,则为 true。
any()后跟一个逗号分隔的配置谓词列表。如果至少有一个给定的谓词为 true,则为 true。如果没有谓词,则为 false。
not()后跟一个配置谓词。如果其谓词为 false,则为 true;如果其谓词为 true,则为 false。
true或false字面量,分别始终为 true 或 false。
配置选项是名称或键值对,要么被设置,要么未被设置。
名称写作单个标识符,例如 unix。
键值对写作一个标识符、=,然后是一个字符串,例如 target_arch = "x86_64"。
Note
=周围的空白被忽略,所以foo="bar"和foo = "bar"是等价的。
键不需要唯一。例如,feature = "std" 和 feature = "serde" 可以同时被设置。
设置的配置选项
哪些配置选项被设置是在 crate 编译期间静态确定的。
有些选项是编译器设置的,基于编译的相关数据。
其他选项是任意设置的,基于从代码外部传递给编译器的输入。
不可能从正在编译的 crate 的源代码中设置配置选项。
Note
对于
rustc,任意设置的配置选项使用--cfg标志设置。指定目标的配置值可以通过rustc --print cfg --target $TARGET显示。
Note
键为
feature的配置选项是 Cargo 用于指定编译时选项和可选依赖的约定。
target_arch
键值选项,设置一次,值为目标的 CPU 架构。该值类似于平台目标三元组的第一个元素,但不完全相同。
示例值:
"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
键值选项,提供对目标更通用的描述,例如目标通常所属的操作系统或架构系列。可以设置任意数量的 target_family 键值对。
示例值:
"unix""windows""wasm"- 同时包含
"unix"和"wasm"
unix 和 windows
如果 target_family = "unix" 被设置,则 unix 被设置。
如果 target_family = "windows" 被设置,则 windows 被设置。
target_env
键值选项,设置有关目标平台的进一步消歧信息,包括关于所使用的 ABI 或 libc 的信息。出于历史原因,此值仅在确实需要消歧时才定义为非空字符串。因此,例如,在许多 GNU 平台上,此值将是空字符串。该值类似于平台目标三元组的第四个元素。一个区别是嵌入式 ABI(如 gnueabihf)将简单地将 target_env 定义为 "gnu"。
示例值:
"""gnu""msvc""musl""sgx""sim""macabi"
target_abi
键值选项,设置用于进一步消歧目标的有关目标 ABI 的信息。
出于历史原因,此值仅在确实需要消歧时才定义为非空字符串。因此,例如,在许多 GNU 平台上,此值将是空字符串。
示例值:
"""llvm""eabihf""abi64"
target_endian
键值选项,设置一次,值为 "little" 或 "big",取决于目标的 CPU 端序。
target_pointer_width
键值选项,设置一次,值为目标的指针宽度(以位为单位)。
示例值:
"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 是以 proc_macro crate 类型编译时设置。
panic
键值选项,根据 panic 策略设置。注意,未来可能会添加更多值。
示例值:
"abort""unwind"
条件编译的形式
cfg 属性
cfg 属性 根据配置谓词有条件地包含其所附着的代码形式。
Example
#![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 类 OS 编译时才包含。 #[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 属性的语法为:
Syntax
CfgAttribute → cfg ( ConfigurationPredicate )
cfg 属性可以在允许属性的任何地方使用。
cfg 属性可以在一个代码形式上使用任意多次。如果任何一个 cfg 谓词为 false,则这些属性所附着的代码形式将不会被包含,除非 cfg.attr.crate-level-attrs 中有说明。
如果谓词为 true,则该代码形式被改写为不带 cfg 属性。如果任何谓词为 false,则该代码形式从源代码中被移除。
当 crate 级别的 cfg 具有 false 谓词时,crate 本身仍然存在。任何在 cfg 之前的 crate 属性会被保留,而 cfg 之后的任何 crate 属性以及所有后续的 crate 内容都会被移除。
Example
保留前置属性的行为允许你做一些事情,例如包含
#![no_std]以避免链接std,即使#![cfg(...)]已经移除了 crate 的内容。例如:// 虽然 crate 级别的 `cfg` // 属性为 false,此 `no_std` 属性仍然保留。 #![no_std] #![cfg(false)] // 此函数不会被包含。 pub fn example() {}
cfg_attr 属性
cfg_attr 属性 根据配置谓词有条件地包含属性。
Example
以下模块将根据目标从
linux.rs或windows.rs中找到。#[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(windows, path = "windows.rs")] mod os;
cfg_attr 属性的语法为:
Syntax
CfgAttrAttribute → cfg_attr ( ConfigurationPredicate , CfgAttrs? )
cfg_attr 属性可以在允许属性的任何地方使用。
cfg_attr 属性可以在一个代码形式上使用任意多次。
crate_type 和 crate_name 属性不能与 cfg_attr 一起使用。
当配置谓词为 true 时,cfg_attr 展开为谓词之后列出的那些属性。
可以列出零个、一个或多个属性。多个属性将各自展开为独立的属性。
Example
#[cfg_attr(feature = "magic", sparkles, crackles)] fn bewitched() {} // 当 `magic` 特性标志被启用时,以上内容将展开为: #[sparkles] #[crackles] fn bewitched() {}
Note
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 宏
内置的 cfg 宏接受一个配置谓词,当谓词为 true 时求值为 true 字面量,当谓词为 false 时求值为 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);
}
cfg_select 宏
内置的 cfg_select! 宏可以在编译时根据多个配置谓词选择代码。
Example
#![allow(unused)] fn main() { cfg_select! { unix => { fn foo() { /* unix specific functionality */ } } target_pointer_width = "32" => { fn foo() { /* non-unix, 32-bit functionality */ } } _ => { fn foo() { /* fallback implementation */ } } } let is_unix_str = cfg_select! { unix => "unix", _ => "not unix", }; }
Syntax
CfgSelect → CfgSelectArms?
CfgSelectArms →
CfgSelectConfigurationPredicate =>
(
{ ^ TokenTree } ,? CfgSelectArms?
| ExpressionWithBlockNoAttrs ,? CfgSelectArms?
| ExpressionWithoutBlockNoAttrs ( , CfgSelectArms? )?
)
CfgSelectConfigurationPredicate →
ConfigurationPredicate | _
cfg_select 展开为第一个配置谓词求值为 true 的分支的有效载荷。
如果整个有效载荷被花括号包裹,则在展开时移除花括号。
配置谓词 _ 始终求值为 true。
如果没有谓词求值为 true,则产生编译错误。
每个右侧必须是宏调用位置上的语法有效展开。
程序项
Syntax
Item →
OuterAttribute* ( VisItem | MacroItem )
VisItem →
Visibility?
(
Module
| ExternCrate
| UseDeclaration
| Function
| TypeAlias
| Struct
| Enumeration
| Union
| ConstantItem
| StaticItem
| Trait
| Implementation
| ExternBlock
)
程序项是 crate 的组成部分。程序项在 crate 内通过嵌套的模块集合来组织。每个 crate 都有一个最外层的匿名模块,crate 内的所有其他程序项都在该模块树中拥有自己的路径。
程序项在编译时完全确定,通常在执行期间保持不变,并且可以驻留在只读内存中。
有以下几种程序项:
一部分程序项,称为关联程序项,可以在 traits 和实现中声明。
一部分程序项,称为外部程序项,可以在 extern 块中声明。
程序项可以以任意顺序定义,但 macro_rules 有自己的作用域行为,属于例外。
程序项名称的名称解析允许在模块或块中,在引用该程序项的位置之前或之后定义该程序项。
有关程序项的作用域规则,请参见程序项作用域。
模块
Syntax
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!();
}
}
}
模块定义在其所在模块或块的类型命名空间中。
在模块的同一命名空间中定义多个同名程序项是错误的。有关限制和遮蔽行为的更多详细信息,请参见作用域章节。
语法上允许 unsafe 关键字出现在 mod 关键字之前,但在语义层面会被拒绝。这允许宏消费该语法并利用 unsafe 关键字,然后将其从 token 流中移除。
模块源文件名
没有主体的模块从外部文件加载。当模块没有 path 属性时,该文件的路径与逻辑模块路径相对应。
祖先模块路径的各级组件是目录,模块的内容以模块名加上 .rs 扩展名命名的文件形式存在。例如,以下模块结构可以有以下对应的文件系统结构:
| 模块路径 | 文件系统路径 | 文件内容 |
|---|---|---|
crate | lib.rs | mod util; |
crate::util | util.rs | mod config; |
crate::util::config | util/config.rs |
模块文件名也可以是模块名作为目录,其内容放在该目录下一个名为 mod.rs 的文件中。上面的示例也可以将 crate::util 的内容放在文件 util/mod.rs 中。不允许同时存在 util.rs 和 util/mod.rs。
Note
在
rustc1.30 之前,使用mod.rs文件是加载带有嵌套子模块的模块的方式。鼓励使用新的命名约定,因为它更一致,并且可以避免项目中存在许多名为mod.rs的文件。
path 属性
用于加载外部文件模块的目录和文件可以通过 path 属性来影响。
对于不在内联模块块中的模块上的 path 属性,文件路径相对于源文件所在的目录。例如,以下代码片段将根据其位置使用如下所示的路径:
#[path = "foo.rs"]
mod c;
| 源文件 | c 的文件位置 | c 的模块路径 |
|---|---|---|
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 的模块。“non-mod-rs” 源文件是所有其他模块文件。在 mod-rs 文件中的内联模块块中,path 属性的路径相对于 mod-rs 文件的目录,包括作为目录的内联模块组件。对于 non-mod-rs 文件,情况相同,只是路径以 non-mod-rs 模块名称的目录开头。例如,以下代码片段将根据其位置使用如下所示的路径:
mod inline {
#[path = "other.rs"]
mod inner;
}
| 源文件 | inner 的文件位置 | inner 的模块路径 |
|---|---|---|
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 和 non-mod-rs 文件):
#[path = "thread_files"]
mod thread {
// 从 thread_files/tls.rs 加载 `local_data` 模块,相对于
// 此源文件所在的目录。
#[path = "tls.rs"]
mod local_data;
}
模块上的属性
模块和所有程序项一样,接受外部属性。它们也接受内部属性:对于有主体的模块,在 { 之后;或者对于源文件,在可选的 BOM 和 shebang 之后的文件开头。
对模块有意义的内置属性有 cfg、deprecated、doc、lint 检查属性、path 和 no_implicit_prelude。模块也接受宏属性。
extern crate 声明
Syntax
ExternCrate → extern crate CrateRef AsClause? ;
CrateRef → IDENTIFIER | self
AsClause → as ( IDENTIFIER | _ )
extern crate 声明指定对某个外部 crate 的依赖。
外部 crate 然后以给定的标识符绑定到声明所在的作用域的类型命名空间中。
此外,如果 extern crate 出现在 crate 根中,则该 crate 名称也会被添加到外部预导入中,使其自动在任何模块中可见。
as 子句可用于将导入的 crate 绑定到不同的名称。
外部 crate 在编译时解析为特定的 soname,并将对该 soname 的运行时链接要求传递给链接器,以便在运行时加载。soname 在编译时通过扫描编译器的库路径并匹配提供的可选 crate_name 与外部 crate 编译时声明的 crate_name 属性 来解析。如果没有提供 crate_name,则假定使用默认的 name 属性,该属性等于 extern crate 声明中给出的标识符。
可以导入 self crate,这会创建对当前 crate 的绑定。在这种情况下必须使用 as 子句来指定绑定名称。
三个 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 会透明地将 - 替换为 _(详见 RFC 940)。
示例如下:
// 导入 Cargo 包 hello-world
extern crate hello_world; // 连字符被替换为下划线
下划线导入
可以使用下划线形式 extern crate foo as _ 声明外部 crate 依赖而不将其名称绑定到作用域中。这对于只需要链接但从不会被引用的 crate 很有用,并且可以避免被报告为未使用。
macro_use 属性照常工作,并将宏名称导入 macro_use 预导入中。
no_link 属性
no_link 属性 可以应用于 extern crate 程序项,以阻止链接该 crate。
Note
例如,当只需要 crate 的宏时,这很有用。
Example
#[no_link] extern crate other_crate; other_crate::some_macro!();
no_link 属性使用 MetaWord 语法。
no_link 属性只能应用于 extern crate 声明。
Note
rustc会忽略在其他位置的使用但会给出 lint 警告。这将来可能变成错误。
只有在 extern crate 声明上首次使用 no_link 才会生效。
Note
rustc会对首次之后的任何使用给出 lint 警告。这将来可能变成错误。
use 声明
Syntax
UseDeclaration → use UseTree ;
UseTree →
( SimplePath? :: )? *
| ( SimplePath? :: )? { ( UseTree ( , UseTree )* ,? )? }
| SimplePath ( as ( IDENTIFIER | _ ) )?
use 声明创建一个或多个与其他路径同义的本地名称绑定。通常,use 声明用于缩短引用模块程序项所需的路径。这些声明可以出现在模块和块中,通常位于顶部。use 声明有时也称为导入,如果它是公开的,则称为重导出。
use 声明支持许多便捷的快捷方式:
- 使用花括号语法同时绑定具有公共前缀的路径列表:
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 可见性
与程序项一样,use 声明默认对包含它的模块是私有的。也与程序项一样,如果被 pub 关键字限定,use 声明可以是公开的。这样的 use 声明用于重导出一个名称。因此,公开的 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 路径
use 程序项中允许的路径遵循 SimplePath 语法,并且类似于表达式中可以使用的路径。它们可以为以下内容创建绑定:
它们不能导入关联程序项、泛型参数、局部变量、带有 Self 的路径或工具属性。更多限制如下所述。
use 将为导入实体的所有命名空间创建绑定,但 self 导入只从类型命名空间导入(如下所述)为例外。例如,以下示例展示了在两个命名空间中为同一个名称创建绑定:
#![allow(unused)]
fn main() {
mod stuff {
pub struct Foo(pub i32);
}
// 导入 `Foo` 类型和 `Foo` 构造器。
use stuff::Foo;
fn example() {
let ctor = Foo; // 使用值命名空间中的 `Foo`。
let x: Foo = ctor(123); // 使用类型命名空间中的 `Foo`。
}
}
2018 Edition differences
在 2015 版本中,
use路径相对于 crate 根。例如:mod foo { pub mod example { pub mod iter {} } pub mod baz { pub fn foobaz() {} } } mod bar { // 从 crate 根解析 `foo`。 use foo::example::iter; // `::` 前缀显式地从 crate 根 // 解析 `foo`。 use ::foo::baz::foobaz; } fn main() {}2015 版本不允许 use 声明引用外部预导入。因此,在 2015 中仍然需要
extern crate声明才能在use声明中引用外部 crate。从 2018 版本开始,use声明可以像extern crate一样指定外部 crate 依赖项。
as 重命名
as 关键字可用于更改导入实体的名称。例如:
#![allow(unused)]
fn main() {
// 为函数 `foo` 创建非公开别名 `bar`。
use inner::foo as bar;
mod inner {
pub fn foo() {}
}
}
花括号语法
花括号可以用在路径的最后一段,以从前一段导入多个实体,或者,如果没有前一段,则从当前作用域导入。花括号可以嵌套,创建路径树,其中每个段分组都与父级逻辑组合以创建完整路径。
#![allow(unused)]
fn main() {
// 创建以下内容的绑定:
// - `std::collections::BTreeSet`
// - `std::collections::hash_map`
// - `std::collections::hash_map::HashMap`
use std::collections::{BTreeSet, hash_map::{self, HashMap}};
}
空花括号不导入任何内容,尽管会验证前导路径是否可访问。
2018 Edition differences
在 2015 版本中,路径相对于 crate 根,因此
use {foo, bar};这样的导入将从 crate 根导入名称foo和bar,而从 2018 开始,这些名称相对于当前作用域。
self 导入
关键字 self 可以在花括号语法中使用,以在其自身名称下创建父实体的绑定。
mod stuff {
pub fn foo() {}
pub fn bar() {}
}
mod example {
// 创建 `stuff` 和 `foo` 的绑定。
use crate::stuff::{self, foo};
pub fn baz() {
foo();
stuff::bar();
}
}
fn main() {}
Note
self也可以用作路径的第一段。将self用作第一段和在use花括号中使用self在逻辑上是相同的;它表示父段的当前模块,或者如果没有父段,则表示当前模块。有关前导self含义的更多信息,请参见路径章节中的self。
self 可以作为 use 路径的最后一段出现,前面加上 ::。P::self 形式的路径等价于 P::{self},P::self as name 等价于 P::{self as name}。
mod m {
pub enum E { V1, V2 }
}
use m::self as _; // 等价于 `use m::{self as _};`。
use m::E::self; // 等价于 `use m::E::{self};`。
fn main() {}
Note
有关前导路径的限制,请参见 paths.qualifiers.mod-self.trailing。
当 self 在花括号语法中使用时,花括号组前面的路径必须解析为模块、枚举或 trait。
mod m {
pub enum E { V1, V2 }
pub trait Tr { fn f(&self); }
}
use m::{self as _}; // OK:模块可以是 `self` 的父级。
use m::E::{self, V1}; // OK:枚举可以是 `self` 的父级。
use m::Tr::{self}; // OK:trait 可以是 `self` 的父级。
fn main() {}
struct S {}
use S::{self as _}; // 错误:结构体不能是 `self` 的父级。
fn main() {}
self 仅从父实体的类型命名空间创建绑定。例如,在以下代码中,仅导入了 foo 模块:
mod bar {
pub mod foo {}
pub fn foo() {}
}
// 这仅导入模块 `foo`。函数 `foo` 位于
// 值命名空间中,没有被导入。
use bar::foo::{self};
fn main() {
foo(); //~ 错误:`foo` 是一个模块
}
通配符导入
* 字符可以作为 use 路径的最后一段,以从前一段的实体中导入所有可导入的实体。例如:
#![allow(unused)]
fn main() {
// 为 `bar` 创建非公开别名。
use foo::*;
mod foo {
fn i_am_private() {}
enum Example {
V1,
V2,
}
pub fn bar() {
// 创建 `Example` 枚举的 `V1` 和 `V2`
// 的本地别名。
use Example::*;
let x = V1;
}
}
}
程序项和命名导入允许遮蔽来自同一命名空间中通配符导入的名称。也就是说,如果同一命名空间中已存在由另一个程序项定义的名称,则通配符导入将被遮蔽。例如:
#![allow(unused)]
fn main() {
// 这创建了对 `clashing::Foo` 元组结构体
// 构造器的绑定,但不会导入其类型,因为
// 这与此处定义的 `Foo` 结构体冲突。
//
// 请注意,此处的定义顺序并不重要。
use clashing::*;
struct Foo {
field: f32,
}
fn do_stuff() {
// 使用 `clashing::Foo` 的构造器。
let f1 = Foo(123);
// 结构体表达式使用上面定义的
// `Foo` 结构体的类型。
let f2 = Foo { field: 1.0 };
// `Bar` 也因通配符导入而在作用域中。
let z = Bar {};
}
mod clashing {
pub struct Foo(pub i32);
pub struct Bar {}
}
}
Note
对于不允许遮蔽的区域,请参见名称解析歧义。
* 不能用作第一段或中间段。
* 不能用于将模块的内容导入自身(如 use self::*;)。
2018 Edition differences
在 2015 版本中,路径相对于 crate 根,因此
use *;这样的导入是有效的,它表示从 crate 根导入所有内容。这不能在 crate 根本身中使用。
下划线导入
可以使用下划线形式 use path as _ 导入程序项而不绑定到名称。这对于导入 trait 以便使用其方法而不导入 trait 的符号特别有用,例如如果 trait 的符号可能与另一个符号冲突。另一个例子是链接外部 crate 而不导入其名称。
星号通配符导入将以其不可命名形式导入以 _ 导入的程序项。
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();
}
唯一的、不可命名的符号在宏展开之后创建,因此宏可以安全地多次发出对 _ 导入的引用。例如,以下不应产生错误:
#![allow(unused)]
fn main() {
macro_rules! m {
($item: item) => { $item $item }
}
m!(use std as _;);
// 这会展开为:
// use std as _;
// use std as _;
}
限制
以下规则是有效 use 声明的限制。
当使用 crate 导入当前 crate 时,必须使用 as 来定义绑定名称。
Example
#![allow(unused)] fn main() { use crate as root; use crate::{self as root2}; // 不允许: // use crate; // use crate::{self}; }
当在宏转录器中使用 $crate 导入当前 crate 时,必须使用 as 来定义绑定名称。
Example
#![allow(unused)] fn main() { macro_rules! import_crate_root { () => { use $crate as my_crate; use $crate::{self as my_crate2}; }; } }
当使用 self 导入当前模块时,必须使用 as 来定义绑定名称。
Example
#![allow(unused)] fn main() { use {self as this_module}; use self as this_module2; use self::{self as this_module3}; // 不允许: // use {self}; // use self; // use self::{self}; }
当使用 super 导入父模块时,必须使用 as 来定义绑定名称。
Example
#![allow(unused)] fn main() { mod a { mod b { use super as parent; use super::{self as parent2}; use self::super as parent3; use super::super as grandparent; use super::super::{self as grandparent2}; // 不允许: // use super; // use super::{self}; // use self::super; // use super::super; // use super::super::{self}; } } }
:: 作为外部预导入不能被导入。
Example
#![allow(unused)] fn main() { use ::{self as root}; //~ 错误 }
2018 Edition differences
在 2015 版本中,前缀
::指向 crate 根,因此use ::{self as root};是允许的,因为它与use crate::{self as root};相同。从 2018 版本开始,::前缀指向外部预导入,不能直接导入。#![allow(unused)] fn main() { use ::{self as root}; //~ OK }
与任何程序项定义一样,use 导入不能在模块或块的同一命名空间中创建同名的重复绑定。
use 路径不能通过类型别名引用枚举变体。
Example
#![allow(unused)] fn main() { enum MyEnum { MyVariant } type TypeAlias = MyEnum; use MyEnum::MyVariant; //~ OK use TypeAlias::MyVariant; //~ 错误 }
函数
Syntax
Function →
FunctionQualifiers fn IDENTIFIER GenericParams?
( FunctionParameters? )
FunctionReturnType? WhereClause?
( BlockExpression | ; )
FunctionQualifiers → const? async?1 ItemSafety?2 ( extern Abi? )?
ItemSafety → safe3 | unsafe
Abi → STRING_LITERAL | RAW_STRING_LITERAL
FunctionParameters →
SelfParam ,?
| ( SelfParam , )? FunctionParam ( , FunctionParam )* ,?
SelfParam → OuterAttribute* ( ShorthandSelf | TypedSelf )
ShorthandSelf → ( & | & Lifetime )? mut? self
FunctionParam → OuterAttribute* ( FunctionParamPattern | ... | Type4 )
FunctionParamPattern → PatternNoTopAlt : ( Type | ... )
FunctionReturnType → -> Type
函数由一个块(即函数的主体)以及一个名称、一组参数和输出类型组成。除名称外,所有这些都是可选的。
函数使用 fn 关键字声明,该关键字在函数所在模块或块的值命名空间中定义给定的名称。
函数可以声明一组输入变量作为参数,调用者通过它们将实参传入函数,以及函数完成后返回给调用者的值的输出类型。
如果输出类型未显式声明,则为单元类型。
当被引用时,函数会产生一个对应的零大小的函数项类型的一等值,调用时会对函数进行直接调用。
例如,这是一个简单函数:
#![allow(unused)]
fn main() {
fn answer_to_life_the_universe_and_everything() -> i32 {
return 42;
}
}
safe 函数在语义上仅在 extern 块中使用时允许。
函数参数
函数参数是不可反驳的模式,因此任何在无 else 的 let 绑定中有效的模式作为参数也是有效的:
#![allow(unused)]
fn main() {
fn first((value, _): (i32, i32)) -> i32 { value }
}
如果第一个参数是 SelfParam,则表示该函数是一个方法。
带有 self 参数的函数只能作为 trait 或实现中的关联函数出现。
带有 ... 标记的参数表示可变参数函数,并且只能用作外部块函数的最后一个参数。可变参数可以有可选的标识符,例如 args: ...。
函数体
函数的主体块在概念上被包裹在另一个块中,该块首先绑定参数模式,然后 return 函数主体的值。这意味着块的尾部表达式(如果被求值)最终会被返回给调用者。像往常一样,函数体内的显式 return 表达式会短路该隐式返回(如果被执行到的话)。
例如,上述函数的行为就像是这样写的:
// argument_0 是调用者实际传入的第一个参数
let (value, _) = argument_0;
return {
value
};
没有主体块的函数以分号结尾。这种形式只能出现在 trait 或外部块中。
泛型函数
泛型函数允许一个或多个参数化类型出现在其签名中。每个类型参数必须在函数名之后、用尖括号括起来的逗号分隔列表中显式声明。
#![allow(unused)]
fn main() {
// foo 对 A 和 B 是泛型的
fn foo<A, B>(x: A, y: B) {
}
}
在函数签名和函数体内,类型参数的名称可以用作类型名。
可以为类型参数指定 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。
类型参数也可以在函数名之后的路径尾段中显式提供。如果上下文不足以确定类型参数,这可能是必要的。例如 mem::size_of::<u32>() == 4。
extern 函数限定符
extern 函数限定符允许提供可以通过特定 ABI 调用的函数定义:
extern "ABI" fn foo() { /* ... */ }
这些通常与外部块程序项结合使用,后者提供函数声明,可用于调用函数而无需提供其定义:
unsafe extern "ABI" {
unsafe fn foo(); /* 没有主体 */
safe fn bar(); /* 没有主体 */
}
unsafe { foo() };
bar();
当 "extern" Abi?* 在函数项中从 FunctionQualifiers 中省略时,ABI "Rust" 被指定。例如:
#![allow(unused)]
fn main() {
fn foo() {}
}
等价于:
#![allow(unused)]
fn main() {
extern "Rust" fn foo() {}
}
函数可以被外部代码调用,使用与 Rust 不同的 ABI 可以(例如)提供可从其他编程语言(如 C)调用的函数:
#![allow(unused)]
fn main() {
// 声明一个具有 "C" ABI 的函数
extern "C" fn new_i32() -> i32 { 0 }
// 声明一个具有 "stdcall" ABI 的函数
#[cfg(any(windows, target_arch = "x86"))]
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;
}
展开
大多数 ABI 字符串有两种变体,一种带有 -unwind 后缀,另一种不带。Rust ABI 始终允许展开,因此没有 Rust-unwind ABI。ABI 的选择与运行时的 panic 处理器一起决定了展开出函数时的行为。
下表指示了展开操作到达每种 ABI 边界(使用相应 ABI 字符串的函数声明或定义)时的行为。请注意,Rust 运行时不受且无法影响完全发生在其他语言运行时内部的任何展开操作的影响,即那些在不触及 Rust ABI 边界的情况下抛出和捕获的展开。
panic-展开列指的是通过 panic! 宏及类似的标准库机制进行panic,以及任何其他导致 panic 的 Rust 操作,例如数组越界索引或整数溢出。
“unwinding” ABI 类别指的是 "Rust"(未标记 extern 的 Rust 函数的隐式 ABI)、"C-unwind" 以及任何其他名称中带有 -unwind 的 ABI。“non-unwinding” ABI 类别指的是所有其他 ABI 字符串,包括 "C" 和 "stdcall"。
原生展开按目标平台定义。在支持抛出和捕获 C++ 异常的目标平台上,它指的是用于实现此功能的机制。某些平台实现了一种称为“强制展开”的展开形式;Windows 上的 longjmp 和 glibc 中的 pthread_exit 以这种方式实现。强制展开被明确排除在下表的“原生展开“列之外。
| panic 运行时 | ABI | panic-展开 | 原生展开(非强制) |
|---|---|---|---|
panic=unwind | unwinding | 展开 | 展开 |
panic=unwind | non-unwinding | 中止(见下文注释) | 未定义行为 |
panic=abort | unwinding | panic 中止而不展开 | 中止 |
panic=abort | non-unwinding | panic 中止而不展开 | 未定义行为 |
当 panic=unwind 时,如果 panic 因非展开 ABI 边界而转为中止,则要么没有析构函数(Drop 调用)会运行,要么直到 ABI 边界的所有析构函数都会运行。这两种行为中具体发生哪一种未指定。
有关跨 FFI 边界展开的其他考虑和限制,请参阅 panic 文档中的相关章节。
const 函数
const 函数的定义请参见 const 函数。
async 函数
函数可以被限定为 async,这也可以与 unsafe 限定符结合使用:
#![allow(unused)]
fn main() {
async fn regular_example() { }
async unsafe fn unsafe_example() { }
}
Async 函数在调用时不执行任何工作:相反,它们将参数捕获到一个 future 中。当被轮询时,该 future 将执行函数的主体。
Async 函数大致等价于一个返回 impl Future 并以 async move 块作为主体的函数:
#![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() }
}
}
实际的脱糖更为复杂:
- 脱糖中的返回类型假设会捕获来自
async fn声明的所有生命周期参数。这可以从上面的脱糖示例中看到,它显式地存活(并因此捕获)'a。
- 主体中的
async move块 捕获所有函数参数,包括那些未使用或绑定到_模式的参数。这确保了函数参数的释放顺序与函数不是 async 时相同,只是释放发生在返回的 future 被完全 await 之后。
关于 async 效果的更多信息,请参见 async 块。
2018 Edition differences
Async 函数仅从 Rust 2018 开始可用。
结合 async 和 unsafe
声明一个既是 async 又是 unsafe 的函数是合法的。生成的函数调用时是不安全的,并且(像任何 async 函数一样)返回一个 future。这个 future 只是一个普通的 future,因此不需要 unsafe 上下文来“await“它:
#![allow(unused)]
fn main() {
// 返回一个 future,当被 await 时会解引用 `x`。
//
// 安全性条件:`x` 必须安全可解引用,直到
// 返回的 future 完成。
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 在 async 函数上的使用方式与在其他函数上的使用方式完全相同:它表示函数对调用者施加了一些额外的义务以确保安全性。与任何其他 unsafe 函数一样,这些条件可能超出初始调用本身——例如,在上面的代码片段中,unsafe_example 函数接受一个指针 x 作为参数,然后(在被 await 时)解引用该指针。这意味着 x 必须有效直到 future 完成执行,而调用者有责任确保这一点。
函数上的属性
函数上允许使用外部属性。内部属性 允许直接在其主体块的 { 之后使用。
此示例展示了函数上的内部属性。该函数仅用单词 “Example” 进行文档化。
#![allow(unused)]
fn main() {
fn documented() {
#![doc = "Example"]
}
}
Note
除了 lint 之外,在函数项上习惯上只使用外部属性。
对函数有意义的属性有:
cfg_attrcfgcolddeprecateddocexport_nameinlinelink_sectionmust_useno_mangle- Lint 检查属性
- 过程宏属性
- 测试属性
函数参数上的属性
函数参数上允许使用外部属性,允许的内置属性仅限于 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 流中移除。
#[some_proc_macro_attribute]
fn foo_oof(#[some_inert_attribute] arg: u8) {
}
-
在 2015 版本中不允许
async限定符。 ↩ -
与早于 Rust 2024 的版本相关:在
extern块中,仅当extern被限定为unsafe时才允许safe或unsafe函数限定符。 ↩ -
safe函数限定符仅在extern块中语义上允许使用。 ↩
类型别名
Syntax
TypeAlias →
type IDENTIFIER GenericParams? ( : Bounds )?
WhereClause?
( = Type WhereClause? )? ;
类型别名在其所在模块或块的类型命名空间中为现有类型定义一个新名称。类型别名用关键字 type 声明。每个值都有单一、特定的类型,但可以实现多个不同的 trait,并且可以与多个不同的类型约束兼容。
例如,以下将类型 Point 定义为类型 (u8, u8) 的同义词,即无符号 8 位整数对的类型:
#![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且不能包含 Bounds。
类型别名在用作 trait 中的关联类型时,不能包含 Type 的规格说明,但可以包含 Bounds。
类型别名在用作 trait impl 中的关联类型时,必须包含 Type 的规格说明,且不能包含 Bounds。
在 trait impl 中,类型别名的等号前的 where 子句(如 type TypeAlias<T> where T: Foo = Bar<T>)已被弃用。建议使用等号后的 where 子句(如 type TypeAlias<T> = Bar<T> where T: Foo)。
结构体
Syntax
Struct →
StructStruct
| TupleStruct
StructStruct →
struct IDENTIFIER GenericParams? WhereClause? ( { StructFields? } | ; )
TupleStruct →
struct IDENTIFIER GenericParams? ( TupleFields? ) WhereClause? ;
StructFields → StructField ( , StructField )* ,?
StructField → OuterAttribute* Visibility? IDENTIFIER : Type
TupleFields → TupleField ( , TupleField )* ,?
结构体是用关键字 struct 定义的具名结构体类型。
结构体声明在其所在模块或块的类型命名空间中定义给定的名称。
一个 struct 程序项及其使用的示例:
#![allow(unused)]
fn main() {
struct Point {x: i32, y: i32}
let p = Point {x: 10, y: 11};
let px: i32 = p.x;
}
元组结构体是具名的元组类型,同样用关键字 struct 定义。除了定义类型外,它还在值命名空间中定义一个同名的构造器。构造器是一个可以调用以创建结构体新实例的函数。例如:
#![allow(unused)]
fn main() {
struct Point(i32, i32);
let p = Point(10, 11);
let px: i32 = match p { Point(x, _) => x };
}
类单元结构体是没有字段的结构体,通过完全省略字段列表来定义。这种结构体隐式定义了一个与其类型同名的常量。例如:
#![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 属性来指定特定的布局。
枚举
Syntax
Enumeration →
enum IDENTIFIER GenericParams? WhereClause? { EnumVariants? }
EnumVariants → EnumVariant ( , EnumVariant )* ,?
EnumVariant →
OuterAttribute* Visibility?
IDENTIFIER ( EnumVariantTuple | EnumVariantStruct )? EnumVariantDiscriminant?
EnumVariantTuple → ( TupleFields? )
EnumVariantStruct → { StructFields? }
枚举(enumeration),也称 enum,是具名枚举类型以及一组构造器的同时定义,这些构造器可用于创建或模式匹配相应枚举类型的值。
枚举使用 enum 关键字声明。
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 是类结构体枚举变体,而 Dog 简称枚举变体。
没有构造器包含字段的枚举称为*无字段枚举*。例如,这是一个无字段枚举:
#![allow(unused)]
fn main() {
enum Fieldless {
Tuple(),
Struct{},
Unit,
}
}
如果无字段枚举只包含单元变体,则该枚举称为*纯单元枚举*。例如:
#![allow(unused)]
fn main() {
enum Enum {
Foo = 3,
Bar = 2,
Baz = 1,
}
}
变体构造器类似于结构体定义,可以通过枚举名称的路径引用,包括在 use 声明中。
每个变体在类型命名空间中定义其类型,尽管该类型不能用作类型说明符。类元组和类单元变体还在值命名空间中定义一个构造器。
类结构体变体可以用结构体表达式实例化。
#![allow(unused)]
fn main() {
enum Examples {
UnitLike,
TupleLike(i32),
StructLike { value: i32 },
}
use Examples::*; // 创建所有变体的别名。
let x = UnitLike; // const 项的路径表达式。
let x = UnitLike {}; // 结构体表达式。
let y = TupleLike(123); // 调用表达式。
let y = TupleLike { 0: 123 }; // 使用整数字段名的结构体表达式。
let z = StructLike { value: 123 }; // 结构体表达式。
}
判别值
每个枚举实例都有一个判别值:一个在逻辑上与之关联的整数,用于确定它持有哪个变体。
在 Rust 表示法下,判别值被解释为 isize 值。但是,编译器允许在实际内存布局中使用更小的类型(或其他区分变体的方式)。
指定判别值
显式判别值
在两种情况下,变体的判别值可以通过在变体名后跟 = 和一个常量表达式来显式设置:
- 如果枚举是“纯单元枚举“。
-
如果使用了原始表示法。例如:
#![allow(unused)] fn main() { #[repr(u8)] enum Enum { Unit = 3, Tuple(u16), Struct { a: u8, b: u16, } = 1, } }
隐式判别值
如果没有为变体指定判别值,则将其设置为声明中前一个变体的判别值加一。如果声明中第一个变体的判别值未指定,则设置为零。
#![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,但这会导致枚举溢出。
}
}
显式枚举判别值初始化器不能使用来自封闭枚举的泛型参数。
#![allow(unused)]
fn main() {
#[repr(u32)]
enum E<'a, T, const N: u32> {
Lifetime(&'a T) = {
let a: &'a (); // 错误。
1
},
Type(T) = {
let x: T; // 错误。
2
},
Const = N, // 错误。
}
}
访问判别值
通过 mem::discriminant
std::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 FieldlessWithDiscriminants {
First = 10,
Tuple(),
Second = 20,
Struct{},
Unit,
}
assert_eq!(10, FieldlessWithDiscriminants::First as u8);
assert_eq!(11, FieldlessWithDiscriminants::Tuple() as u8);
assert_eq!(20, FieldlessWithDiscriminants::Second as u8);
assert_eq!(21, FieldlessWithDiscriminants::Struct{} as u8);
assert_eq!(22, FieldlessWithDiscriminants::Unit as u8);
}
指针强制转换
如果枚举指定了原始表示法,则可以通过不安全的指针强制转换来可靠地访问判别值:
#![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());
}
零变体枚举
零变体的枚举称为零变体枚举。由于它们没有有效值,因此无法被实例化。
#![allow(unused)]
fn main() {
enum ZeroVariants {}
}
零变体枚举等价于 never 类型,但不能强制转换为其他类型。
#![allow(unused)]
fn main() {
enum ZeroVariants {}
let x: ZeroVariants = panic!();
let y: u32 = x; // 类型不匹配错误
}
变体可见性
枚举变体在语法上允许 Visibility 注解,但在枚举验证时会被拒绝。这允许在不同使用上下文中用统一的语法解析程序项。
#![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 },
}
}
联合体
Syntax
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 来手动定义其被释放时的行为。
没有任何字段的联合体不被编译器接受,但可以被宏接受。
联合体的初始化
联合体类型的值可以使用与结构体类型相同的语法创建,但必须指定恰好一个字段:
#![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 };
}
读写联合体字段
联合体没有“活动字段“的概念。相反,每次联合体访问只是将存储解释为用于访问的字段的类型。
读取联合体字段会以该字段的类型读取联合体的位。
字段可能有非零偏移(除非使用了 C 表示法);在这种情况下,会读取从字段偏移处开始的位。
程序员有责任确保数据在该字段的类型下是有效的。未能做到这一点会导致未定义行为。例如,从布尔类型的字段中读取值 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 胶水代码,因此联合体字段写入永远不会隐式地释放任何东西。)
联合体上的模式匹配
访问联合体字段的另一种方式是使用模式匹配。
联合体字段上的模式匹配使用与结构体模式相同的语法,只是模式必须指定恰好一个字段。
由于模式匹配类似于用特定字段读取联合体,因此也必须放在 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); }
}
}
}
}
模式匹配可以将联合体作为更大结构的字段进行匹配。特别是,当通过 FFI 使用 Rust 联合体实现 C 标记联合体时,这允许同时匹配标记和相应字段:
#![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,
}
}
}
}
联合体字段的引用
由于联合体字段共享公共存储,获得联合体一个字段的写入访问权限可能会获得其所有其余字段的写入访问权限。
借用检查规则必须进行调整以考虑这一事实。因此,如果联合体的一个字段被借用了,其所有其余字段也会在同一生命周期内被借用。
#![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 语言的许多未提及的方面也是如此(如隐私、名称解析、类型推断、泛型、trait 实现、固有实现、一致性、模式检查等等)。
常量项
Syntax
ConstantItem →
const ( IDENTIFIER | _ ) : Type ( = Expression )? ;
常量项是一个可选的带名称的*常量值*,不与程序中的特定内存位置关联。
常量本质上在使用的每个地方都会被内联,这意味着在使用时它们会被直接复制到相关上下文中。这包括使用来自外部 crate 的常量,以及非 Copy 类型。对同一常量的引用不一定保证引用相同的内存地址。
常量声明在其所在模块或块的值命名空间中定义常量值。
常量必须显式指定类型。该类型必须具有 'static 生命周期:初始化器中的任何引用都必须具有 'static 生命周期。常量类型中的引用默认为 '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 定义中省略。
带析构函数的常量
常量可以包含析构函数。析构函数在值离开作用域时运行。
#![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."。
}
}
未命名常量
与关联常量不同,自由常量可以通过使用下划线代替名称来取消命名。例如:
#![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 _: () = ();
}
求值
自由常量总是在编译时被求值以显示 panic。即使在未使用的函数中也会发生:
#![allow(unused)]
fn main() {
// 编译时 panic
const PANIC: () = std::unimplemented!();
fn unused_generic_function<T>() {
// 一个失败的编译时断言
const _: () = assert!(usize::BITS == 0);
}
}
静态项
Syntax
StaticItem →
ItemSafety?1 static mut? IDENTIFIER : Type ( = Expression )? ;
静态项类似于常量,不同之处在于它表示程序中的一个分配,该分配由初始化器表达式初始化。对静态项的所有引用和原始指针指向同一个分配。
静态项具有 static 生命周期,它比 Rust 程序中的所有其他生命周期都长。静态项在程序结束时不会调用 drop。
如果 static 的大小至少为 1 字节,则此分配与所有其他此类 static 分配以及堆分配和栈分配变量不相交。但是,不可变 static 项的存储可以与本身没有唯一地址的分配(例如提升项和 const 项)重叠。
静态声明在其所在模块或块的值命名空间中定义静态值。
静态初始化器是在编译时求值的常量表达式。静态初始化器可以引用和读取其他静态项。当读取可变静态项时,它们读取该静态项的初始值。
不包含内部可变类型的非 mut 静态项可以被放置在只读内存中。
对静态项的所有访问都是安全的,但对静态项有一些限制:
- 类型必须具有
Synctrait 约束以允许线程安全访问。
初始化器表达式在外部块中必须省略,而对于自由静态项则必须提供。
safe 和 unsafe 限定符在语义上仅允许在外部块中使用。
静态项与泛型
在泛型作用域(例如在 blanket 实现或默认实现中)中定义的静态项将导致恰好定义一个静态项,就像静态定义被从当前作用域提取到模块中一样。不会对每个单态化产生一个程序项。
以下代码:
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
可变静态项
如果静态项使用 mut 关键字声明,则程序可以修改它。Rust 的目标之一是使并发 bug 难以出现,而这显然是一个非常大的竞态条件或其他 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_unsafe() -> u32 {
unsafe {
let ret = LEVELS;
LEVELS += 1;
return ret;
}
}
// 作为 `bump_levels_unsafe` 的替代方案,此函数是安全的,
// 假设我们有一个返回旧值的 atomic_add 函数。此
// 函数只有在没有其他代码以非原子方式访问该静态项时才是安全的。
// 如果此类访问是可能的(例如 `bump_levels_unsafe` 中),
// 那么这需要是 `unsafe` 的,以向调用者表明他们
// 仍然必须防范并发访问。
fn bump_levels_safe() -> u32 {
unsafe {
return atomic_add(&raw mut LEVELS, 1);
}
}
}
可变静态项具有与普通静态项相同的限制,只是类型不必实现 Sync trait。
使用静态项还是常量
是否应该使用常量项还是静态项可能会令人困惑。通常应该优先使用常量而不是静态项,除非以下情况之一为真:
- 存储大量数据。
- 需要静态项的单地址属性。
- 需要内部可变性。
-
safe和unsafe函数限定符仅在extern块中语义上允许使用。 ↩
trait
Syntax
Trait →
unsafe? trait IDENTIFIER GenericParams? ( : Bounds? )? WhereClause?
{
InnerAttribute*
AssociatedItem*
}
trait 描述一个类型可以实现的抽象接口。该接口由关联程序项组成,这些程序项有三种类型:
trait 声明在其所在模块或块的类型命名空间中定义一个 trait。
关联程序项被定义为 trait 的成员,位于各自的命名空间中。关联类型在类型命名空间中定义。关联常量和关联函数在值命名空间中定义。
所有 trait 都定义一个隐式的类型参数 Self,它指代“实现此接口的类型“。trait 还可以包含额外的类型参数。这些类型参数(包括 Self)可像通常一样受到其他 trait 等的约束。
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 约束
泛型程序项可以使用 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);
}
}
Dyn 兼容性
dyn 兼容的 trait 可以作为 trait 对象的基础 trait。如果 trait 具有以下特性,则它是dyn 兼容的:
- 所有超 trait 也必须是 dyn 兼容的。
Sized不能是超 trait。换句话说,它不能要求Self: Sized。
- 不能有任何关联常量。
- 不能有任何带泛型的关联类型。
- 所有关联函数必须要么可从 trait 对象分派,要么显式地不可分派:
- 可分发函数必须:
- 显式不可分派函数要求:
- 具有
where Self: Sized约束(Self的接收器类型(即self)隐含此约束)。
- 具有
AsyncFn、AsyncFnMut和AsyncFnOncetrait 不是 dyn 兼容的。
Note
此概念以前称为对象安全性。
#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Arc;
use std::pin::Pin;
// dyn 兼容方法的示例。
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 是 dyn 兼容的,但这些方法不能在 trait 对象上分派。
trait NonDispatchable {
// 非方法不能分派。
fn foo() where Self: Sized {}
// Self 类型直到运行时才知道。
fn returns(&self) -> Self where Self: Sized;
// `other` 可能是接收器的不同具体类型。
fn param(&self, other: Self) where Self: Sized {}
// 泛型与虚函数表不兼容。
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;
// dyn 不兼容 trait 的示例。
trait DynIncompatible {
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 DynIncompatible for S {
fn returns(&self) -> Self { S }
}
let obj: Box<dyn DynIncompatible> = Box::new(S); // 错误
}
#![allow(unused)]
fn main() {
// `Self: Sized` trait 是 dyn 不兼容的。
trait TraitWithSize where Self: Sized {}
struct S;
impl TraitWithSize for S {}
let obj: Box<dyn TraitWithSize> = Box::new(S); // 错误
}
#![allow(unused)]
fn main() {
// 如果 `Self` 是类型参数,则 dyn 不兼容。
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` 类型参数
}
超 trait
超 trait 是类型要实现特定 trait 所必须实现的 trait。此外,当泛型或 trait 对象受到 trait 约束时,它可以访问其超 trait 的关联程序项。
超 trait 通过对 trait 的 Self 类型的 trait 约束以及这些 trait 约束中声明的 trait 的超 trait 传递地声明。trait 作为自身的超 trait 是错误的。
带有超 trait 的 trait 称为其超 trait 的子 trait。
以下是将 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 `Shape` 调用 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 trait
以 unsafe 关键字开头的 trait 程序项表示实现该 trait 可能是不安全的。使用正确实现的 unsafe trait 是安全的。trait 实现也必须以 unsafe 关键字开头。
Sync 和 Send 是 unsafe trait 的示例。
参数模式
不带主体的关联函数中的参数只允许使用 IDENTIFIER 或 _ 通配符 模式,以及 SelfParam 允许的形式。mut IDENTIFIER 目前是允许的,但已被弃用,将来会变成硬错误。
#![allow(unused)]
fn main() {
trait T {
fn f1(&self);
fn f2(x: Self, _: i32);
}
}
#![allow(unused)]
fn main() {
trait T {
fn f2(&x: &i32); // 错误:不带主体的函数中不允许使用模式
}
}
带主体的关联函数中的参数只允许使用不可反驳的模式。
#![allow(unused)]
fn main() {
trait T {
fn f1((a, b): (i32, i32)) {} // OK:是不可反驳的
}
}
#![allow(unused)]
fn main() {
trait T {
fn f1(123: i32) {} // 错误:模式是可反驳的
fn f2(Some(x): Option<i32>) {} // 错误:模式是可反驳的
}
}
2018 Edition differences
在 2018 版本之前,关联函数参数的模式是可选的:
#![allow(unused)] fn main() { // 2015 版本 trait T { fn f(i32); // OK:参数标识符不是必需的 } }从 2018 版本开始,模式不再是可选的。
2018 Edition differences
在 2018 版本之前,带主体的关联函数中的参数仅限于以下类型的模式:
- IDENTIFIER
mutIDENTIFIER_&IDENTIFIER&&IDENTIFIER#![allow(unused)] fn main() { // 2015 版本 trait T { fn f1((a, b): (i32, i32)) {} // 错误:不允许的模式 } }从 2018 开始,所有不可反驳的模式都允许,如 items.traits.params.patterns-with-body 中所述。
程序项可见性
trait 程序项在语法上允许 Visibility 注解,但在 trait 验证时会被拒绝。这允许在不同使用上下文中用统一的语法解析程序项。作为示例,空的 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();
}
实现
Syntax
Implementation → InherentImpl | TraitImpl
InherentImpl →
impl GenericParams? Type WhereClause? {
InnerAttribute*
AssociatedItem*
}
TraitImpl →
unsafe? impl GenericParams? !? TypePath for Type
WhereClause?
{
InnerAttribute*
AssociatedItem*
}
实现是将程序项与实现类型关联起来的程序项。实现用关键字 impl 定义,包含属于被实现类型的实例或静态地属于该类型的函数。
有两种类型的实现:
- 固有实现
- trait 实现
固有实现
固有实现的定义是:impl 关键字、泛型类型声明、指向具名类型的路径、where 子句以及一对花括号内的一组可关联程序项。
具名类型称为实现类型,可关联程序项是实现类型的关联程序项。
固有实现将包含的程序项与实现类型关联起来。
它们不能包含关联类型别名。
关联程序项的路径是指向实现类型的任意路径,后跟关联程序项的标识符作为最后的路径组件。
一个类型也可以有多个固有实现。实现类型必须与原始类型定义在同一个 crate 中定义。
pub mod color {
pub struct Color(pub u8, pub u8, pub u8);
impl Color {
pub const WHITE: Color = Color(255, 255, 255);
}
}
mod values {
use super::color::Color;
impl Color {
pub fn red() -> Color {
Color(255, 0, 0)
}
}
}
pub use self::color::Color;
fn main() {
// 实际路径到同一模块中的实现类型和 impl。
color::Color::WHITE;
// 不同模块中的 impl 块仍然通过指向类型的路径来访问。
color::Color::red();
// 指向实现类型的重导出路径也有效。
Color::red();
// 无效,因为 `values` 中的 use 不是 pub。
// values::Color::red();
}
trait 实现
trait 实现的定义方式与固有实现类似,不同之处在于可选的泛型类型声明后跟一个 trait,再后跟关键字 for,再后跟指向具名类型的路径。
trait 称为被实现的 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 实现的一致性
如果孤儿规则检查失败或存在重叠的实现实例,则 trait 实现被认为是不一致的。
当实现所针对的 trait 的交集非空,并且这些实现可以用相同的类型实例化时,两个 trait 实现会重叠。
孤儿规则
孤儿规则规定,只有 trait 或实现中的至少一个类型在当前 crate 中定义时,才允许该 trait 实现。它防止了跨不同 crate 的冲突 trait 实现,是确保一致性的关键。
孤儿实现是指为外部类型实现外部 trait 的实现。如果这些被自由允许,两个 crate 可能会以不兼容的方式为同一类型实现相同的 trait,从而造成添加或更新依赖项可能因冲突实现而导致编译中断的情况。
孤儿规则使库作者能够为其 trait 添加新的实现,而不必担心会破坏下游代码。如果没有这些限制,库就不能添加 impl<T: Display> MyTrait for T 这样的实现,而不会与下游实现产生潜在冲突。
给定 impl<P1..=Pn> Trait<T1..=Tn> for T0,只有当以下至少一项成立时,impl 才有效:
只有未覆盖类型参数的出现受限。
请注意,就一致性而言,基本类型是特殊的。Box<T> 中的 T 不被视为已覆盖,而 Box<LocalType> 被视为本地类型。
泛型实现
实现可以接受泛型参数,这些参数可以在实现的其余部分中使用。实现参数直接写在 impl 关键字之后。
#![allow(unused)]
fn main() {
trait Seq<T> { fn dummy(&self, _: T) { } }
impl<T> Seq<T> for Vec<T> {
/* ... */
}
impl Seq<bool> for u32 {
/* 将整数视为位序列 */
}
}
如果泛型参数至少出现在以下一项中,则该泛型参数约束该实现:
类型参数和 const 参数必须始终约束该实现。生命周期参数如果用于关联类型中则必须约束该实现。
约束情况的示例:
#![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() {
// 以下都是错误,因为它们具有不约束的类型或 const 参数。
// T 不约束,因为它根本没有出现。
impl<T> Struct { /* ... */ }
// N 不约束,原因相同。
impl<const N: usize> Struct { /* ... */ }
// 在实现内部使用 T 不约束该 impl。
impl<T> Struct {
fn uses_t(t: &T) { /* ... */ }
}
// T 在 U 的约束中用作关联类型,但 U 不约束。
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;
}
}
实现上的属性
实现可以在 impl 关键字之前包含外部属性,在包含关联程序项的花括号内包含内部属性。内部属性必须在任何关联程序项之前。此处有意义的属性有 cfg、deprecated、doc 和 lint 检查属性。
外部块
Syntax
ExternBlock →
unsafe?1 extern Abi? {
InnerAttribute*
ExternalItem*
}
ExternalItem →
OuterAttribute* (
MacroInvocationSemi
| Visibility? StaticItem
| Visibility? Function
)
外部块提供不在当前 crate 中定义的程序项的声明,是 Rust 外部函数接口的基础。这类似于未检查的导入。
调用在外部块中声明的 unsafe 函数或访问 unsafe 静态项只允许在 unsafe 上下文中进行。
外部块在其所在模块或块的值命名空间中定义其函数和静态项。
在语义上,unsafe 关键字必须出现在外部块的 extern 关键字之前。
2024 Edition differences
在 2024 版本之前,
unsafe关键字是可选的。只有当外部块本身被标记为unsafe时,才允许使用safe和unsafe程序项限定符。
函数
外部块中的函数声明方式与其他 Rust 函数相同,不同之处在于它们不能有主体,而是以分号结束。
参数中不允许使用模式,只能使用 IDENTIFIER 或 _。
允许使用 safe 和 unsafe 函数限定符,但不允许使用其他函数限定符(例如 const、async、extern)。
外部块中的函数可以被 Rust 代码调用,就像在 Rust 中定义的函数一样。Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行翻译。
在 extern 块中声明的函数隐式是 unsafe 的,除非存在 safe 函数限定符。
当强制转换为函数指针时,extern 块中声明的函数具有类型 for<'l1, ..., 'lm> extern "abi" fn(A1, ..., An) -> R,其中 'l1、…'lm 是其生命周期参数,A1、…、An 是其参数的声明类型,R 是声明的返回类型。
静态项
外部块中的静态项声明方式与外部块外的静态项相同,不同之处在于没有初始化其值的表达式。
除非在 extern 块中声明的静态项被限定为 safe,否则访问该程序项是 unsafe 的,无论其是否可变,因为无法保证该静态项内存中的位模式对于其声明的类型是有效的,因为某个任意的(例如 C)代码负责初始化该静态项。
extern 静态项可以像外部块外的静态项一样是不可变的或可变的。
不可变静态项必须在任何 Rust 代码执行之前被初始化。仅在该静态项在 Rust 代码读取它之前被初始化是不够的。一旦 Rust 代码运行,修改不可变静态项(从 Rust 内部或外部)是 UB,除非修改发生在 UnsafeCell 内部的字节上。
ABI
extern 关键字后可以跟一个可选的 ABI 字符串。ABI 指定块中函数的调用约定。调用约定定义了函数的低级接口,例如参数如何放置在寄存器或栈上、返回值如何传递以及谁负责清理栈。
Example
#![allow(unused)] fn main() { // Windows API 的接口。 unsafe extern "system" { /* ... */ } }
如果未指定 ABI 字符串,则默认为 "C"。
Note
不带显式 ABI 的
extern语法正被逐步淘汰,因此最好始终显式地写出 ABI。详情请参见 Rust issue #134986。
以下 ABI 字符串在所有平台上都受支持:
unsafe extern "Rust"— Rust 函数和闭包的本机调用约定。这是声明函数而不使用extern fn时的默认值。Rust ABI 不提供稳定性保证。
unsafe extern "C"— “C” ABI 与目标平台的占主导地位的 C 编译器选择的默认 ABI 相匹配。
-
unsafe extern "system"— 这等价于extern "C",但在 Windows x86_32 上等价于非可变参数函数的"stdcall",对于可变参数函数等价于"C"。Note
由于 Windows 上正确的底层 ABI 是目标平台特定的,因此在尝试链接不使用显式定义 ABI 的 Windows API 函数时,最好使用
extern "system"。
extern "C-unwind"和extern "system-unwind"— 分别与"C"和"system"相同,但在被调用方展开(通过 panic 或抛出 C++ 风格的异常)时具有不同的行为。
还有一些特定于平台的 ABI 字符串:
-
unsafe extern "cdecl"— 通常与 x86_32 C 代码一起使用的调用约定。- 仅在 x86_32 目标上可用。
- 对应于 MSVC 的
__cdecl以及 GCC 和 clang 的__attribute__((cdecl))。
-
unsafe extern "stdcall"— x86_32 上的 Win32 API 通常使用的调用约定。- 仅在 x86_32 目标上可用。
- 对应于 MSVC 的
__stdcall以及 GCC 和 clang 的__attribute__((stdcall))。
-
unsafe extern "win64"— Windows x64 ABI。- 仅在 x86_64 目标上可用。
- “win64” 与 Windows x86_64 目标上的 “C” ABI 相同。
- 对应于 GCC 和 clang 的
__attribute__((ms_abi))。
-
unsafe extern "sysv64"— System V ABI。- 仅在 x86_64 目标上可用。
- “sysv64” 与非 Windows x86_64 目标上的 “C” ABI 相同。
- 对应于 GCC 和 clang 的
__attribute__((sysv_abi))。
-
unsafe extern "aapcs"— ARM 的 soft-float ABI。- 仅在 ARM32 目标上可用。
- “aapcs” 与 soft-float ARM32 上的 “C” ABI 相同。
- 对应于 clang 的
__attribute__((pcs("aapcs")))。
Note
详情请参见:
-
unsafe extern "fastcall"— stdcall 的一种“快速“变体,通过寄存器传递某些参数。- 仅在 x86_32 目标上可用。
- 对应于 MSVC 的
__fastcall以及 GCC 和 clang 的__attribute__((fastcall))。
-
unsafe extern "thiscall"— x86_32 MSVC 上 C++ 类成员函数通常使用的调用约定。- 仅在 x86_32 目标上可用。
- 对应于 MSVC 的
__thiscall以及 GCC 和 clang 的__attribute__((thiscall))。
unsafe extern "efiapi"— 用于 UEFI 函数的 ABI。- 仅在 x86 和 ARM 目标(32 位和 64 位)上可用。
与 "C" 和 "system" 一样,大多数特定于平台的 ABI 字符串也有相应的 -unwind 变体;具体来说,这些是:
"aapcs-unwind""cdecl-unwind""fastcall-unwind""stdcall-unwind""sysv64-unwind""thiscall-unwind""win64-unwind"
可变参数函数
外部块中的函数可以通过将 ... 指定为最后一个参数来成为可变参数函数。可变参数可以有选择地指定一个标识符。
#![allow(unused)]
fn main() {
unsafe extern "C" {
unsafe fn foo(...);
unsafe fn bar(x: i32, ...);
unsafe fn with_name(format: *const u8, args: ...);
// 安全性:此函数保证不会访问
// 可变参数。
safe fn ignores_variadic_arguments(x: i32, ...);
}
}
Warning
不应在
extern块中的函数上使用safe限定符,除非该函数保证完全不会访问可变参数。向可变参数函数传递意外数量的参数或意外类型的参数可能导致未定义行为。
可变参数只能在具有以下 ABI 字符串或其相应 -unwind 变体的 extern 块中指定:
"aapcs""C""cdecl""efiapi""system""sysv64""win64"
外部块上的属性
以下属性控制外部块的行为。
link 属性
link 属性指定编译器应为 extern 块中的程序项与之链接的本地库的名称。
它使用 MetaListNameValueStr 语法来指定其输入。name 键是要链接的本地库的名称。kind 键是一个可选值,用于指定库的种类,可能的值如下:
dylib— 表示动态库。如果未指定kind,这是默认值。
static— 表示静态库。
framework— 表示 macOS 框架。仅对 macOS 目标有效。
raw-dylib— 表示动态库,编译器将生成导入库进行链接(详见下文的dylib与raw-dylib)。仅对 Windows 目标有效。
如果指定了 kind,则必须包含 name 键。
可选的 modifiers 参数提供了一种为要链接的库指定链接修饰符的方式。
修饰符以逗号分隔的字符串形式指定,每个修饰符以 + 或 - 为前缀,分别表示该修饰符是启用还是禁用。
目前不支持在单个 link 属性中指定多个 modifiers 参数,或在同一个 modifiers 参数中指定多个相同的修饰符。示例:#[link(name = "mylib", kind = "static", modifiers = "+whole-archive")]。
wasm_import_module 键可用于在从宿主环境导入符号时指定 extern 块中程序项的 WebAssembly 模块名称。如果未指定 wasm_import_module,则默认模块名为 env。
#[link(name = "crypto")]
unsafe extern {
// …
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern {
// …
}
#[link(wasm_import_module = "foo")]
unsafe extern {
// …
}
在空外部块上添加 link 属性是有效的。你可以使用这种方式来满足代码中其他地方(包括上游 crate)的外部块的链接要求,而不是在每个外部块上都添加属性。
链接修饰符:bundle
此修饰符仅与 static 链接种类兼容。使用任何其他种类将导致编译器错误。
在构建 rlib 或 staticlib 时,+bundle 表示本地静态库将被打包到 rlib 或 staticlib 归档中,然后在最终二进制文件的链接过程中从那里取出。
在构建 rlib 时,-bundle 表示本地静态库按名称注册为该 rlib 的依赖项,其中的目标文件仅在最终二进制文件的链接过程中被包含,按名称文件搜索也在最终链接时执行。在构建 staticlib 时,-bundle 表示本地静态库根本不包含在归档中,需要某个更高级的构建系统在最终二进制文件的链接过程中稍后添加它。
此修饰符在构建其他目标(如可执行文件或动态库)时无效。
此修饰符的默认值为 +bundle。
关于此修饰符的更多实现细节可以在 bundle 的 rustc 文档中找到。
链接修饰符:whole-archive
此修饰符仅与 static 链接种类兼容。使用任何其他种类将导致编译器错误。
+whole-archive 表示将静态库作为完整归档链接,不丢弃任何目标文件。
此修饰符的默认值为 -whole-archive。
关于此修饰符的更多实现细节可以在 whole-archive 的 rustc 文档中找到。
链接修饰符:verbatim
此修饰符与所有链接种类兼容。
+verbatim 表示 rustc 本身不会向库名称添加任何目标平台指定的库前缀或后缀(如 lib 或 .a),并会尽量要求链接器也这样做。
-verbatim 表示 rustc 在将库名称传递给链接器之前会添加目标平台特定的前缀和后缀,或者不会阻止链接器隐式添加它们。
此修饰符的默认值为 -verbatim。
关于此修饰符的更多实现细节可以在 verbatim 的 rustc 文档中找到。
dylib 与 raw-dylib
在 Windows 上,链接动态库需要向链接器提供一个导入库:这是一个特殊的静态库,它声明了动态库导出的所有符号,以使得链接器知道它们必须在运行时动态加载。
指定 kind = "dylib" 指示 Rust 编译器根据 name 键链接一个导入库。然后链接器将使用其正常的库解析逻辑来查找该导入库。或者,指定 kind = "raw-dylib" 指示编译器在编译期间生成一个导入库并将其提供给链接器。
raw-dylib 仅在 Windows 上受支持。在面向其他平台时使用它将导致编译器错误。
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 上受支持。在面向其他平台时使用它将导致编译器错误。
link_name 属性
link_name 属性 可以应用于 extern 块中的声明,以指定为给定函数或静态项导入的符号。
Example
#![allow(unused)] fn main() { unsafe extern "C" { #[link_name = "actual_symbol_name"] safe fn name_in_rust(); } }
link_name 属性使用 MetaNameValueStr 语法。
link_name 属性只能应用于 extern 块中的函数或静态项。
Note
rustc会忽略在其他位置的使用但会给出 lint 警告。这将来可能变成错误。
只有程序项上首次使用的 link_name 才会生效。
Note
rustc会对首次之后的任何使用以未来兼容性警告的形式给出 lint 警告。这将来可能变成错误。
link_name 属性不能与 link_ordinal 属性一起使用。
link_ordinal 属性
link_ordinal 属性可以应用于 extern 块中的声明,以指示在生成导入库进行链接时要使用的数字序号。序号是 Windows 上动态库中每个导出符号的唯一编号,可以在加载库时使用,以查找该符号,而不必按名称查找。
Warning
link_ordinal应仅在已知符号的序号是稳定的情况下使用:如果符号的序号在构建其所在的二进制文件时未显式设置,则会自动分配一个序号,并且该分配的序号可能在二进制文件的构建之间发生变化。
#![allow(unused)]
fn main() {
#[cfg(all(windows, target_arch = "x86"))]
#[link(name = "exporter", kind = "raw-dylib")]
unsafe extern "stdcall" {
#[link_ordinal(15)]
safe fn imported_function_stdcall(i: i32);
}
}
此属性仅与 raw-dylib 链接种类一起使用。使用任何其他种类将导致编译器错误。
将此属性与 link_name 属性一起使用将导致编译器错误。
函数参数上的属性
extern 函数参数上的属性遵循与常规函数参数相同的规则和限制。
-
从 2024 版本开始,
unsafe关键字在语义上是必需的。 ↩
泛型参数
Syntax
GenericParams → < ( GenericParam ( , GenericParam )* ,? )? >
GenericParam → OuterAttribute* ( LifetimeParam | TypeParam | ConstParam )
LifetimeParam → Lifetime ( : LifetimeBounds )?
TypeParam → IDENTIFIER ( : Bounds? )? ( = Type )?
ConstParam →
const IDENTIFIER : Type
( = ( BlockExpression | IDENTIFIER | -? LiteralExpression ) )?
函数、类型别名、结构体、枚举、联合体、trait和实现可以由类型、常量和生命周期来参数化。这些参数列在尖括号(<...>)中,通常紧接在程序项名称之后、定义之前。对于没有名称的实现,它们紧随 impl 之后。
泛型参数的顺序限制为先生命周期参数,然后类型参数和 const 参数交错排列。
同一参数名称不能在 GenericParams 列表中声明多次。
一些带有类型、const 和生命周期参数的程序项示例:
#![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);
}
泛型参数在声明它们的程序项定义内处于作用域中。它们对于函数体内声明的程序项不在作用域中,如程序项声明中所述。有关更多细节,请参见泛型参数作用域。
引用、原始指针、数组、切片、元组和函数指针也具有生命周期或类型参数,但不使用路径语法引用。
'_ 和 'static 不是有效的生命周期参数名称。
const 泛型
const 泛型参数允许程序项在常量值上是泛型的。
const 标识符在值命名空间中为常量参数引入一个名称,并且该程序项的所有实例都必须用给定类型的值进行实例化。
const 参数唯一允许的类型是 u8、u16、u32、u64、u128、usize、i8、i16、i32、i64、i128、isize、char 和 bool。
const 参数可以在任何可以使用 const 项的地方使用,但当在类型或数组重复表达式中使用时,它必须是独立的(如下所述)。也就是说,它们允许在以下位置使用:
- 作为构成相关程序项签名的任何类型的应用 const。
- 作为用于定义关联常量的 const 表达式的一部分,或作为关联类型的参数。
- 作为程序项中任何函数主体内任何运行时表达式中的值。
- 作为程序项中任何函数主体内使用的任何类型的参数。
- 作为程序项中任何字段的类型的一部分。
#![allow(unused)]
fn main() {
// const 泛型参数可以使用的示例。
// 用在程序项本身的签名中。
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() {
// const 泛型参数不能使用的示例。
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]);
}
}
作为进一步的限制,const 参数只能作为独立参数出现在类型或数组重复表达式内部。在这些上下文中,它们只能用作单段路径表达式,可能在一个块内部(如 N 或 {N})。也就是说,它们不能与其他表达式组合。
#![allow(unused)]
fn main() {
// const 参数不能使用的示例。
// 不允许在类型中与其他表达式组合,如此处
// 返回类型中的算术表达式。
fn bad_function<const N: usize>() -> [u8; {N + 1}] {
// 对于数组重复表达式也是如此。
[1; {N + 1}]
}
}
路径中的 const 实参指定用于该程序项的 const 值。
实参必须是推断 const 或属于分配给 const 参数类型的常量表达式。除非是单路径段(IDENTIFIER)或字面量(可以带有前导 - 标记),否则常量表达式必须是一个块表达式(用花括号包围)。
Note
这种语法限制是必要的,以避免在解析类型内的表达式时需要无限前瞻。
#![allow(unused)]
fn main() {
struct S<const N: i64>;
const C: i64 = 1;
fn f<const N: i64>() -> S<N> { S }
let _ = f::<1>(); // 字面量。
let _ = f::<-1>(); // 负数字面量。
let _ = f::<{ 1 + 2 }>(); // 常量表达式。
let _ = f::<C>(); // 单段路径。
let _ = f::<{ C + 1 }>(); // 常量表达式。
let _: S<1> = f::<_>(); // 推断 const。
let _: S<1> = f::<(((_)))>(); // 推断 const。
}
Note
在泛型实参列表中,推断 const 被解析为推断类型,但在语义上被当作一种单独的 const 泛型实参处理。
在需要 const 实参的地方,可以使用 _(可选地由任意数量的匹配括号包围),称为推断 const(路径规则,数组表达式规则)。这会要求编译器在可能的情况下根据周围信息推断 const 实参。
#![allow(unused)]
fn main() {
fn make_buf<const N: usize>() -> [u8; N] {
[0; _]
// ^ 推断 `N`。
}
let _: [u8; 1024] = make_buf::<_>();
// ^ 推断 `1024`。
}
Note
推断 const 在语义上不是表达式,因此在花括号内不被接受。
#![allow(unused)] fn main() { fn f<const N: usize>() -> [u8; N] { [0; _] } let _: [_; 1] = f::<{ _ }>(); // ^ 错误:此处不允许 `_` }
推断 const 不能在程序项签名中使用。
#![allow(unused)]
fn main() {
fn f<const N: usize>(x: [u8; N]) -> [u8; _] { x }
// ^ 错误:不允许
}
当存在歧义时,如果泛型实参可能被解析为类型或 const 实参,则始终解析为类型。将实参放在块表达式中可以强制将其解释为 const 实参。
#![allow(unused)]
fn main() {
type N = u32;
struct Foo<const N: usize>;
// 以下是错误,因为 `N` 被解释为类型别名 `N`。
fn foo<const N: usize>() -> Foo<N> { todo!() } // 错误
// 可以通过用花括号包裹来修复,以强制将其解释为
// const 参数 `N`:
fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok
}
与类型和生命周期参数不同,const 参数可以在参数化的程序项外部声明而不被使用,但泛型实现中描述的实现除外:
#![allow(unused)]
fn main() {
// ok
struct Foo<const N: usize>;
enum Bar<const M: usize> { A, B }
// 错误:未使用的参数
struct Baz<T>;
struct Biz<'a>;
struct Unconstrained;
impl<const N: usize> Unconstrained {}
}
在解决 trait 约束义务时,在确定约束是否满足时不考虑 const 参数的所有实现的穷尽性。例如,在以下代码中,即使 bool 类型的所有可能的 const 值都已实现,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); // 错误:trait 约束 `Foo<B>: Bar` 不满足
}
}
where 子句
Syntax
WhereClause → where ( WhereClauseItem , )* WhereClauseItem?
WhereClauseItem →
LifetimeWhereClauseItem
| TypeBoundWhereClauseItem
where 子句提供了另一种为类型和生命周期参数指定约束的方式,以及为非类型参数的类型指定约束的方式。
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。此位置没有执行任何操作的内置属性,但自定义 derive 属性可能赋予其意义。
此示例展示了使用自定义 derive 属性来修改泛型参数的含义。
// 假设 MyFlexibleClone 的 derive 声明了 `my_flexible_clone` 为
// 它能理解的属性。
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
a: *const H
}
关联程序项
Syntax
AssociatedItem →
OuterAttribute* (
MacroInvocationSemi
| ( Visibility? ( TypeAlias | ConstantItem | Function ) )
)
关联程序项是在 trait 中声明或在实现中定义的程序项。之所以这样称呼,是因为它们定义在关联类型上——即实现中的类型。
它们是可以在模块中声明的程序项种类的子集。具体来说,有关联函数(包括方法)、关联类型和关联常量。
当关联程序项与关联它的程序项在逻辑上相关时,关联程序项非常有用。例如,Option 上的 is_some 方法与 Option 在本质上是相关的,因此应该被关联。
每种关联程序项有两种形式:包含实际实现的定义和声明定义签名的声明。
构成 trait 契约和泛型上可用内容的正是声明。
关联函数和方法
关联函数是与类型关联的函数。
关联函数声明声明关联函数定义的签名。它像函数项一样编写,只是函数体被替换为 ;。
标识符是函数的名称。
关联函数的泛型、参数列表、返回类型和 where 子句必须与关联函数声明的一致。
关联函数定义定义与另一个类型关联的函数。它的编写方式与函数项相同。
Note
一个常见的例子是名为
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);
}
方法
第一个参数名为 self 的关联函数称为方法,可以使用方法调用运算符(例如 x.foo())以及通常的函数调用表示法来调用。
如果指定了 self 参数的类型,则它限于由以下语法生成的类型(其中 'lt 表示某个任意生命周期):
P = &'lt S | &'lt mut S | Box<S> | Rc<S> | Arc<S> | Pin<P>
S = Self | P
此语法中的 Self 终结符表示解析为实现类型的类型。这也可以包括上下文类型别名 Self、其他类型别名或解析为实现类型的关联类型投影。
#![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 |
Note
使用此简写时,生命周期可以省略,通常也的确被省略。
如果 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 的实现的值都可以调用其 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();
}
2018 Edition differences
在 2015 版本中,可以使用匿名参数声明 trait 方法(例如
fn foo(u8))。这在 2018 版本中已被弃用并成为错误。所有参数必须有实参名称。
方法参数上的属性
方法参数上的属性遵循与常规函数参数相同的规则和限制。
关联类型
关联类型是与另一个类型关联的类型别名。
关联类型不能在固有实现中定义,也不能在 trait 中给出默认实现。
关联类型声明声明关联类型定义的签名。它以以下形式之一编写,其中 Assoc 是关联类型的名称,Params 是以逗号分隔的类型、生命周期或 const 参数列表,Bounds 是以加号分隔的关联类型必须满足的 trait 约束列表,WhereBounds 是以逗号分隔的参数必须满足的约束列表:
type Assoc;
type Assoc: Bounds;
type Assoc<Params>;
type Assoc<Params>: Bounds;
type Assoc<Params> where WhereBounds;
type Assoc<Params>: Bounds where WhereBounds;
标识符是声明的类型别名的名称。
可选的 trait 约束必须由类型别名的实现来满足。
关联类型有一个隐式的 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 可以在类型参数中使用。
关联类型可以包括泛型参数和 where 子句;这些通常称为泛型关联类型(GAT)。如果类型 Thing 有来自 trait Trait 的关联类型 Item,且具有泛型 <'a>,则该类型可以命名为 <Thing as Trait>::Item<'x>,其中 'x 是作用域中的某个生命周期。在这种情况下,'x 将用于 impl 中关联类型定义中 'a 出现的任何位置。
trait AssociatedType {
// 关联类型声明
type Assoc;
}
struct Struct;
struct OtherStruct;
impl AssociatedType for Struct {
// 关联类型定义
type Assoc = OtherStruct;
}
impl OtherStruct {
fn new() -> OtherStruct {
OtherStruct
}
}
fn main() {
// 使用关联类型将 OtherStruct 引用为 <Struct as AssociatedType>::Assoc
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);
}
关联类型容器示例
考虑以下 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); }
}
}
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。
泛型关联类型上必需的 where 子句
trait 上的泛型关联类型声明当前可能需要一组 where 子句,这取决于 trait 中的函数以及 GAT 的使用方式。这些规则将来可能会放宽;更新信息可以在泛型关联类型倡议仓库中找到。
简而言之,这些 where 子句是必需的,以最大限度地扩大 impl 中关联类型的允许定义。为此,在 GAT 作为输入或输出出现的函数上(使用函数或 trait 的参数)可以证明成立的任何子句也必须写在 GAT 自身上。
#![allow(unused)]
fn main() {
trait LendingIterator {
type Item<'x> where Self: 'x;
fn next<'a>(&'a mut self) -> Self::Item<'a>;
}
}
在上面的例子中,在 next 函数上,我们可以从 &'a mut self 的隐含约束中证明 Self: 'a;因此,我们必须在 GAT 自身上编写等价的约束:where Self: 'x。
当 trait 中有多个函数使用 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 被注释掉,则 where T: 'x 约束在 Checker 上是必需的。
关联类型上的约束也会传播必需的 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>;
}
}
这里,Item 上需要 where Self: 'a 是因为 iter。然而,由于 Item 在 Iterator 的约束中使用,因此那里也需要 where Self: 'a 子句。
最后,trait 中 GAT 上对 'static 的任何显式使用不计入所需约束。
#![allow(unused)]
fn main() {
trait StaticReturn {
type Y<'a>;
fn foo(&self) -> Self::Y<'static>;
}
}
关联常量
关联常量是与类型关联的常量。
关联常量声明声明关联常量定义的签名。它写成 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;
}
关联常量示例
一个基本示例:
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);
}
属性
Syntax
InnerAttribute → # ! [ Attr ]
OuterAttribute → # [ Attr ]
Attr →
SimplePath AttrInput?
| unsafe ( SimplePath AttrInput? )
属性是一种通用的、自由格式的元数据,其解释取决于名称、约定、语言和编译器版本。属性的模型来自 ECMA-335 中的 Attributes,语法来自 ECMA-334(C#)。
内部属性,在井号 (#) 后写有感叹号 (!),应用于该属性声明所在的代码形式。
Example
#![allow(unused)] fn main() { // General metadata applied to the enclosing module or crate. #![crate_type = "lib"] // Inner attribute applies to the entire function. fn some_unused_variables() { #![allow(unused_variables)] let x = (); let y = (); let z = (); } }
外部属性,在井号后没有感叹号,应用于该属性之后的代码形式。
Example
#![allow(unused)] fn main() { // A function marked as a unit test #[test] fn test_foo() { /* ... */ } // A conditionally-compiled module #[cfg(target_os = "linux")] mod bar { /* ... */ } // A lint attribute used to suppress a warning/error #[allow(non_camel_case_types)] type int8_t = i8; }
属性由属性的路径组成,后跟一个可选的分隔 token 树,其解释由该属性定义。除宏属性之外的属性还允许输入为等号 (=) 后跟一个表达式。更多细节请参见下面的元项语法。
属性可能是不安全的应用。为了避免使用这些属性时出现未定义行为,必须满足编译器无法检查的某些义务。为了断言这些义务已被满足,属性被包裹在 unsafe(..) 中,例如 #[unsafe(no_mangle)]。
以下属性是不安全的:
属性可以分为以下几类:
属性可以应用于语言中的多种形式:
- 所有项声明接受外部属性,而外部块、函数、实现和模块接受内部属性。
- 大多数语句接受外部属性(关于表达式语句的限制,请参阅表达式属性)。
- 块表达式接受外部和内部属性,但仅当它们是表达式语句的外部表达式或另一个块表达式的最终表达式时。
- 枚举变体以及 struct 和 union 字段接受外部属性。
- Match 表达式分支接受外部属性。
- 泛型生命周期或类型参数接受外部属性。
- 表达式在有限的情况下接受外部属性,详情请参阅表达式属性。
- 函数、闭包和函数指针参数接受外部属性。这包括函数指针和外部块中用
...表示的可变参数上的属性。 - 内联汇编模板字符串和操作数接受外部属性。只有某些属性在语义上被接受;详情请参阅 asm.attributes.supported-attributes。
元项属性语法
“元项“是大多数内置属性为 Attr 规则所使用的语法。它具有以下文法:
Syntax
MetaItem →
SimplePath
| SimplePath = Expression
| SimplePath ( MetaSeq? )
MetaSeq →
MetaItemInner ( , MetaItemInner )* ,?
元项中的表达式必须可以宏展开为字面量表达式,并且不能包含整数或浮点类型后缀。非字面量表达式在语法上会被接受(并可以传递给过程宏),但在解析后会被拒绝。
注意,如果属性出现在另一个宏中,它将在那个外部宏展开后才展开。例如,以下代码将首先展开 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() {}
各种内置属性使用元项语法的不同子集来指定其输入。以下文法规则展示了一些常用形式:
Syntax
MetaWord →
IDENTIFIER
MetaNameValueStr →
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") |
活跃属性与惰性属性
属性要么是活跃的,要么是惰性的。在属性处理期间,活跃属性会从其所附着的代码形式中移除自身,而惰性属性会保留。
cfg 和 cfg_attr 属性是活跃的。属性宏是活跃的。所有其他属性都是惰性的。
工具属性
编译器可以允许外部工具的属性,其中每个工具驻留在工具预导入中的自己的模块中。属性路径的第一个段是工具的名称,可以有一个或多个额外的段,其解释由工具决定。
当工具未被使用时,该工具的属性会在没有警告的情况下被接受。当工具被使用时,由该工具负责处理和解释其属性。
如果使用了 no_implicit_prelude 属性,则工具属性不可用。
#![allow(unused)]
fn main() {
// 告诉 rustfmt 工具不要格式化后面的元素。
#[rustfmt::skip]
struct S {
}
// 控制 clippy 工具的"圈复杂度"阈值。
#[clippy::cyclomatic_complexity = "100"]
pub fn f() {}
}
Note
rustc目前识别工具 “clippy”、“rustfmt”、“diagnostic”、“miri” 和 “rust_analyzer”。
内置属性索引
以下是所有内置属性的索引。
-
条件编译
-
测试
test— 将函数标记为测试。ignore— 禁用一个测试函数。should_panic— 指示测试应产生 panic。
-
派生
derive— 自动 trait 实现。automatically_derived— 由derive创建的实现的标记。
-
宏
macro_export— 导出macro_rules宏以供跨 crate 使用。macro_use— 扩展宏可见性,或从其他 crate 导入宏。proc_macro— 定义类函数宏。proc_macro_derive— 定义派生宏。proc_macro_attribute— 定义属性宏。
-
诊断
allow、expect、warn、deny、forbid— 修改默认 lint 级别。deprecated— 生成弃用提示。must_use— 为未使用的值生成 lint。diagnostic::on_unimplemented— 提示编译器在 trait 未实现时发出特定的错误消息。diagnostic::do_not_recommend— 提示编译器不要在错误消息中显示某个 trait 实现。
-
ABI、链接、符号和 FFI
link— 指定与extern块链接的原生库。link_name— 指定extern块中函数或静态变量的符号名称。link_ordinal— 指定extern块中函数或静态变量的符号序号。no_link— 阻止链接外部 crate。repr— 控制类型布局。crate_type— 指定 crate 类型(库、可执行文件等)。no_main— 禁用生成main符号。export_name— 指定函数或静态变量的导出符号名称。link_section— 指定函数或静态变量使用的目标文件段。no_mangle— 禁用符号名称修饰。used— 强制编译器在输出目标文件中保留静态项。crate_name— 指定 crate 名称。
-
代码生成
inline— 提示内联代码。cold— 提示函数不太可能被调用。naked— 阻止编译器生成函数序言和尾声。no_builtins— 禁用某些内置函数的使用。target_feature— 配置平台特定的代码生成。track_caller— 将父调用位置传递给std::panic::Location::caller()。instruction_set— 指定用于生成函数代码的指令集。
-
文档
doc— 指定文档。更多信息请参阅 The Rustdoc Book。文档注释会转换为doc属性。
-
预导入
no_std— 从预导入中移除 std。no_implicit_prelude— 在模块内禁用预导入查找。
-
模块
path— 指定模块的文件名。
-
限制
recursion_limit— 设置某些编译时操作的最大递归限制。type_length_limit— 设置多态类型的最大大小。
-
运行时
panic_handler— 设置处理 panic 的函数。global_allocator— 设置全局内存分配器。windows_subsystem— 指定要链接的 Windows 子系统。
-
特性
feature— 用于启用不稳定或实验性的编译器特性。有关rustc中实现的特性,请参阅 The Unstable Book。
-
类型系统
non_exhaustive— 指示类型在未来会有更多字段/变体。
-
调试器
debugger_visualizer— 嵌入一个文件,为类型指定调试器输出。collapse_debuginfo— 控制宏调用在调试信息中的编码方式。
测试属性
以下属性用于指定执行测试的函数。在“test“模式下编译 crate 会启用测试函数的构建以及用于执行测试的测试框架。启用测试模式还会启用 test 条件编译选项。
test 属性
test 属性 将一个函数标记为作为测试执行。
Example
#![allow(unused)] fn main() { pub fn add(left: u64, right: u64) -> u64 { left + right } #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } }
test 属性使用 MetaWord 语法。
test 属性只能应用于单态的、不接收参数的自由函数,并且其返回类型必须实现 Termination trait。
Note
实现
Terminationtrait 的一些类型包括:
()Result<T, E> where T: Termination, E: Debug
只有第一次在函数上使用 test 才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
test 属性从标准库预导入中导出为 std::prelude::v1::test。
这些函数仅在测试模式下编译。
Note
测试模式通过向
rustc传递--test参数或使用cargo test启用。
测试框架调用返回值的 report 方法,根据结果 ExitCode 是否表示成功终止将测试分类为通过或失败。
特别是:
- 返回
()的测试只要终止且不发生 panic 就通过。 - 返回
Result<(), E>的测试只要返回Ok(())就通过。 - 返回
ExitCode::SUCCESS的测试通过,返回ExitCode::FAILURE的测试失败。 - 不终止的测试既不通不过也不失败。
Example
#![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(()) } }
ignore 属性
ignore 属性 可以与 test 属性一起使用,告知测试框架不要将该函数作为测试执行。
Example
#![allow(unused)] fn main() { #[test] #[ignore] fn check_thing() { // … } }
Note
rustc测试框架支持--include-ignored标志来强制运行被忽略的测试。
ignore 属性使用 MetaWord 和 MetaNameValueStr 语法。
ignore 属性的 MetaNameValueStr 形式提供了一种指定测试被忽略原因的方法。
Example
#![allow(unused)] fn main() { #[test] #[ignore = "not yet implemented"] fn mytest() { // … } }
ignore 属性只能应用于标注了 test 属性的函数。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
只有第一次在函数上使用 ignore 才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
被忽略的测试在测试模式下仍会被编译,但不会被运行。
should_panic 属性
should_panic 属性 使测试仅在应用该属性的测试函数发生 panic 时通过。
Example
#![allow(unused)] fn main() { #[test] #[should_panic(expected = "values don't match")] fn mytest() { assert_eq!(1, 2, "values don't match"); } }
should_panic 属性有以下形式:
-
Example
#![allow(unused)] fn main() { #[test] #[should_panic] fn mytest() { panic!("error: some message, and more"); } } -
MetaNameValueStr — 给定的字符串必须出现在 panic 消息中,测试才能通过。
Example
#![allow(unused)] fn main() { #[test] #[should_panic = "some message"] fn mytest() { panic!("error: some message, and more"); } } -
MetaListNameValueStr — 与 MetaNameValueStr 语法一样,给定的字符串必须出现在 panic 消息中。
Example
#![allow(unused)] fn main() { #[test] #[should_panic(expected = "some message")] fn mytest() { panic!("error: some message, and more"); } }
should_panic 属性只能应用于标注了 test 属性的函数。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
只有第一次在函数上使用 should_panic 才有效。
Note
rustc会对第一次之后的使用发出未来兼容性警告的 lint。这可能在将来成为错误。
当使用 MetaNameValueStr 形式或带有 expected 键的 MetaListNameValueStr 形式时,给定的字符串必须出现在 panic 消息的某处,测试才能通过。
测试函数的返回类型必须是 ()。
派生
derive 属性 调用一个或多个派生宏,允许为数据结构自动生成新的程序项。你可以使用过程宏创建 derive 宏。
Example
PartialEq派生宏为Foo<T> where T: PartialEq生成PartialEq的实现。Clone派生宏类似地为Clone生成实现。#![allow(unused)] fn main() { #[derive(PartialEq, Clone)] struct Foo<T> { a: i32, b: T, } }生成的
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 } } impl<T: Clone> Clone for Foo<T> { fn clone(&self) -> Self { Foo { a: self.a.clone(), b: self.b.clone() } } } }
derive 属性使用 MetaListPaths 语法来指定要调用的派生宏的路径列表。
derive 属性可以在一个项上使用任意次数。所有属性中列出的所有派生宏都会被调用。
derive 属性在标准库中导出为:
内置派生宏定义在语言预导入中。内置派生宏的列表如下:
内置派生宏在其生成的实现中包含 automatically_derived 属性。
在宏展开期间,对于派生列表中的每个元素,相应的派生宏展开为零个或多个程序项。
automatically_derived 属性
automatically_derived 属性 用于标注一个实现,以表明它是由派生宏自动创建的。它没有直接影响,但可以被工具和诊断 lint 用于检测这些自动生成的实现。
Example
给定
struct Example上的#[derive(Clone)],派生宏可能产生:#![allow(unused)] fn main() { struct Example; #[automatically_derived] impl ::core::clone::Clone for Example { #[inline] fn clone(&self) -> Self { Example } } }
automatically_derived 属性使用 MetaWord 语法。
automatically_derived 属性只能应用于实现。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
在一个实现上多次使用 automatically_derived 的效果与使用一次相同。
Note
rustc会对第一次之后的使用发出 lint 警告。
automatically_derived 属性没有行为。
诊断属性
以下属性用于控制或生成编译期间的诊断消息。
Lint 检查属性
Lint 检查命名了一种潜在的不受欢迎的编码模式,例如不可达代码或遗漏的文档。
lint 属性 allow、expect、warn、deny 和 forbid 使用 MetaListPaths 语法来指定一个 lint 名称列表,以更改应用该属性的实体的 lint 级别。
对于任何 lint 检查 C:
#[allow(C)]覆盖对C的检查,使得违规不会被报告。
#[expect(C)]指示预期会发出 lintC。该属性将抑制C的发出,或者如果预期未被满足则发出警告。
#[warn(C)]对C的违规发出警告但继续编译。
#[deny(C)]在遇到C的违规后发出错误信号,
#[forbid(C)]与deny(C)相同,但也禁止之后更改 lint 级别,
Note
rustc支持的 lint 检查可通过rustc -W help找到,以及它们的默认设置,并在 rustc 书中有文档记录。
#![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(除了 deny,在 forbid 上下文中允许但被忽略)。前一个属性是语法树中更高级别的属性,或按从左到右源顺序在同一实体上的前一个属性。
此示例展示了如何使用 allow 和 warn 来切换特定检查的开关:
#![allow(unused)]
fn main() {
#[warn(missing_docs)]
pub mod m2 {
#[allow(missing_docs)]
pub mod nested {
// 此处忽略缺少文档
pub fn undocumented_one() -> i32 { 1 }
// 此处缺少文档发出警告,
// 尽管上面有 allow。
#[warn(missing_docs)]
pub fn undocumented_two() -> i32 { 2 }
}
// 此处缺少文档发出警告
pub fn undocumented_too() -> i32 { 3 }
}
}
此示例展示了如何使用 forbid 来禁止对该 lint 检查使用 allow 或 expect:
#![allow(unused)]
fn main() {
#[forbid(missing_docs)]
pub mod m3 {
// 尝试切换警告在此处发出错误
#[allow(missing_docs)]
/// 返回 2。
pub fn undocumented_too() -> i32 { 2 }
}
}
Lint 原因
所有 lint 属性都支持一个额外的 reason 参数,用于说明添加某个属性的原因。如果 lint 在定义的级别上发出,此原因将作为 lint 消息的一部分显示。
#![allow(unused)]
fn main() {
// `keyword_idents` 默认是 allow 的。此处我们将其 deny 以避免
// 在更新版次时迁移标识符。
#![deny(
keyword_idents,
reason = "we want to avoid these idents to be future compatible"
)]
// 此名称在 Rust 2015 版次中是允许的。我们仍然希望避免
// 此名称以保持未来兼容性并且不混淆最终用户。
fn dyn() {}
}
另一个示例,其中 lint 被 allow 并带有原因:
#![allow(unused)]
fn main() {
use std::path::PathBuf;
pub fn get_path() -> PathBuf {
// `allow` 属性上的 `reason` 参数充当读者的文档。
#[allow(unused_mut, reason = "this is only modified on some platforms")]
let mut file_name = PathBuf::from("git");
#[cfg(target_os = "windows")]
file_name.set_extension("exe");
file_name
}
}
#[expect] 属性
#[expect(C)] 属性为 lint C 创建一个 lint 预期。如果同一位置的 #[warn(C)] 属性会导致 lint 发出,则该预期将被满足。如果预期未满足,因为 lint C 不会被发出,则 unfulfilled_lint_expectations lint 将在该属性处发出。
fn main() {
// 此 `#[expect]` 属性创建一个 lint 预期,即以下语句会发出
// `unused_variables` lint。此预期未满足,因为 `question` 变量被
// `println!` 宏使用。因此,`unfulfilled_lint_expectations` lint
// 将在该属性处发出。
#[expect(unused_variables)]
let question = "who lives in a pineapple under the sea?";
println!("{question}");
// 此 `#[expect]` 属性创建一个将被满足的 lint 预期,因为
// `answer` 变量从未被使用。通常会发出的 `unused_variables` lint
// 被抑制。不会为该语句或属性发出警告。
#[expect(unused_variables)]
let answer = "SpongeBob SquarePants!";
}
Lint 预期仅由已被 expect 属性抑制的 lint 发出来满足。如果在该作用域中使用其他级别属性(如 allow 或 warn)修改了 lint 级别,则 lint 发出将相应地处理,而预期将保持未满足。
#![allow(unused)]
fn main() {
#[expect(unused_variables)]
fn select_song() {
// 这将在 warn 级别发出 `unused_variables` lint,
// 如 `warn` 属性所定义。这不会满足函数上方的预期。
#[warn(unused_variables)]
let song_name = "Crab Rave";
// `allow` 属性抑制 lint 发出。这不会满足预期,因为它
// 已被 `allow` 属性抑制,而不是函数上方的 `expect` 属性。
#[allow(unused_variables)]
let song_creator = "Noisestorm";
// 此 `expect` 属性将抑制此变量处的 `unused_variables` lint 发出。
// 函数上方的 `expect` 属性仍然不会被满足,因为此 lint 发出
// 已被局部的 expect 属性抑制。
#[expect(unused_variables)]
let song_version = "Monstercat Release";
}
}
如果 expect 属性包含多个 lint,每个 lint 被单独预期。对于 lint 组,只要组内有一个 lint 被发出就足够了:
#![allow(unused)]
fn main() {
// 此预期将被函数内部的未使用值满足,因为发出的
// `unused_variables` lint 在 `unused` lint 组内。
#[expect(unused)]
pub fn thoughts() {
let unused = "I'm running out of examples";
}
pub fn another_example() {
// 此属性创建两个 lint 预期。`unused_mut` lint 将被抑制,
// 从而满足第一个预期。`unused_variables` 不会被发出,
// 因为变量被使用了。因此该预期将未被满足,并将发出警告。
#[expect(unused_mut, unused_variables)]
let mut link = "https://www.rust-lang.org/";
println!("Welcome to our community: {link}");
}
}
Note
#[expect(unfulfilled_lint_expectations)]的行为目前定义为始终生成unfulfilled_lint_expectationslint。
Lint 组
Lint 可以组织成命名组,以便相关 lint 的级别可以一起调整。使用命名组等效于列出该组内的 lint。
#![allow(unused)]
fn main() {
// 这将 allow "unused" 组中的所有 lint。
#[allow(unused)]
// 这将覆盖 "unused" 组中的 "unused_must_use" lint 为 deny。
#[deny(unused_must_use)]
fn example() {
// 这不生成警告,因为 "unused_variables" lint
// 在 "unused" 组中。
let x = 1;
// 这生成一个错误,因为结果未被使用且
// "unused_must_use" 被标记为 "deny"。
std::fs::remove_file("some_file"); // 错误:未使用的必须被使用的 `Result`
}
}
有一个特殊的组名为 “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` 警告已被提升为 "deny"。
unsafe { an_unsafe_fn() } // 错误:使用了 `unsafe` 块
}
}
工具 lint 属性
工具 lint 允许使用作用域限定 lint,以 allow、warn、deny 或 forbid 某些工具的 lint。
工具 lint 仅在关联的工具处于活动状态时才被检查。如果 lint 属性(如 allow)引用了一个不存在的工具 lint,编译器不会警告该不存在的 lint,直到你使用该工具。
否则,它们的工作方式与常规 lint 属性完全相同:
// 将整个 `pedantic` clippy lint 组设置为 warn
#![warn(clippy::pedantic)]
// 静默 `filter_map` clippy lint 的警告
#![allow(clippy::filter_map)]
fn main() {
// ...
}
// 仅为这个函数静默 `cmp_nan` clippy lint
#[allow(clippy::cmp_nan)]
fn foo() {
// ...
}
deprecated 属性
deprecated 属性将项标记为已弃用。rustc 将在使用 #[deprecated] 项时发出警告。rustdoc 将显示项弃用信息,包括 since 版本和 note(如果可用)。
deprecated 属性有多种形式:
deprecated— 发出一条通用消息。deprecated = "message"— 在弃用消息中包含给定的字符串。- MetaListNameValueStr 语法,带有两个可选字段:
since— 指定项被弃用的版本号。rustc目前不解释该字符串,但外部工具如 Clippy 可能会检查值的有效性。note— 指定应包含在弃用消息中的字符串。这通常用于提供关于弃用的解释和首选的替代方案。
deprecated 属性可以应用于任何项、trait 项、枚举变体、结构体字段、外部块项或宏定义。它不能应用于 trait 实现项。当应用于包含其他项的项(如模块或实现)时,所有子项继承该弃用属性。
以下是一个示例:
#![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 包含动机和更多细节。
must_use 属性
must_use 属性 标记一个应该被使用的值。
must_use 属性使用 MetaWord 和 MetaNameValueStr 语法。
Example
#![allow(unused)] fn main() { #[must_use] fn use_me1() -> u8 { 0 } #[must_use = "explanation of why it should be used"] fn use_me2() -> u8 { 0 } }
must_use 属性可以应用于:
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
must_use 属性在一个项上只能使用一次。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
must_use 属性可以使用 MetaNameValueStr 语法包含一条消息,例如 #[must_use = "example message"]。该消息可能作为 lint 的一部分发出。
当该属性应用于结构体、枚举或联合体时,如果表达式语句的表达式具有该类型,则使用会触发 unused_must_use lint。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
struct MustUse();
MustUse(); // 错误:必须被使用的未使用值。
}
作为 attributes.diagnostics.must_use.type 的例外,当 E 是无人居住的或 B 是无人居住的时,对于 Result<(), E> 或 ControlFlow<B, ()> 不触发该 lint。来自外部 crate 的 #[non_exhaustive] 类型在此目的下不被视为无人居住的,因为它可能在未来获得构造函数。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
use core::ops::ControlFlow;
enum Empty {}
fn f1() -> Result<(), Empty> { Ok(()) }
f1(); // 正确:`Empty` 是无人居住的。
fn f2() -> ControlFlow<Empty, ()> { ControlFlow::Continue(()) }
f2(); // 正确:`Empty` 是无人居住的。
}
如果表达式语句的表达式是调用表达式或方法调用表达式,且其函数操作数是应用了该属性的函数,则使用会触发 unused_must_use lint。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}
f(); // 错误:必须被使用的未使用返回值。
}
如果表达式语句的表达式是调用表达式或方法调用表达式,且其函数操作数是一个返回 impl trait 或 dyn trait 类型的函数,而该类型的约束中有一个或多个 trait 被标记了该属性,则使用会触发 unused_must_use lint。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
trait Tr {}
impl Tr for () {}
fn f() -> impl Tr {}
f(); // 错误:必须被使用的未使用实现者。
}
当该属性应用于 trait 声明中的函数时,attributes.diagnostics.must_use.fn 中描述的规则在调用表达式或方法调用表达式的函数操作数是该函数的实现时也适用。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
trait Tr {
#[must_use]
fn use_me(&self);
}
impl Tr for () {
fn use_me(&self) {}
}
().use_me(); // 错误:必须被使用的未使用返回值。
}
#![allow(unused)]
fn main() {
#![deny(unused_must_use)]
trait Tr {
#[must_use]
fn use_me(&self);
}
impl Tr for () {
fn use_me(&self) {}
}
<() as Tr>::use_me(&());
// ^^^^^^^^^^^ 错误:必须被使用的未使用返回值。
}
在针对 attributes.diagnostics.must_use.type、attributes.diagnostics.must_use.fn、attributes.diagnostics.must_use.trait 和 attributes.diagnostics.must_use.trait-function 检查表达式语句的表达式时,该 lint 会穿透块表达式(包括 unsafe 块和带标签块表达式)到每个块的尾部表达式。这递归地适用于嵌套块表达式。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}
{ f() }; // 错误:lint 穿透块表达式。
unsafe { f() }; // 错误:lint 穿透 `unsafe` 块。
{ { f() } }; // 错误:lint 穿透嵌套块。
}
当用于 trait 实现中的函数时,该属性没有任何作用。
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
trait Tr {
fn f(&self);
}
impl Tr for () {
#[must_use] // 这没有效果。
fn f(&self) {}
}
().f(); // 正确。
}
Note
rustc会对 trait 实现中的函数使用发出 lint 警告。这可能在将来成为错误。
Note
将
#[must_use]函数的结果包装在某种表达式中可以抑制基于函数的检查,因为表达式语句的表达式不是对#[must_use]函数的调用表达式或方法调用表达式。如果整体表达式的类型是#[must_use],则基于类型的检查仍然适用。#![allow(unused)] #![deny(unused_must_use)] fn main() { #[must_use] fn f() {} // 基于函数的检查不会对以下任何情况触发,因为 // 表达式语句的表达式不是对 `#[must_use]` 函数的调用。 (f(),); // 表达式是元组,不是调用。 Some(f()); // 被调用者 `Some` 不是 `#[must_use]`。 if true { f() } else {}; // 表达式是 `if`,不是调用。 match true { // 表达式是 `match`,不是调用。 _ => f() }; }#![allow(unused)] #![deny(unused_must_use)] fn main() { #[must_use] struct MustUse; fn g() -> MustUse { MustUse } // 尽管 `if` 表达式不是调用,基于类型的检查会触发, // 因为表达式的类型是 `MustUse`,该类型具有 // `#[must_use]` 属性。 if true { g() } else { MustUse }; // 错误:必须被使用。 }
Note
当有意丢弃一个 must-used 值时,使用模式为
_的 let 语句或解构赋值是惯用的。#![allow(unused)] #![deny(unused_must_use)] fn main() { #[must_use] fn f() {} let _ = f(); // 正确。 _ = f(); // 正确。 }
diagnostic 工具属性命名空间
#[diagnostic] 属性命名空间是影响编译时错误消息的属性的集合。这些属性提供的提示不保证被使用。
此命名空间中的未知属性被接受,尽管可能发出未使用属性的警告。此外,对已知属性的无效输入通常将是警告(详见属性定义)。这意味着允许在未来添加或丢弃属性和更改输入,而无需保持无意义的属性或选项正常工作。
diagnostic::on_unimplemented 属性
#[diagnostic::on_unimplemented] 属性是对编译器的提示,用于补充在需要 trait 但类型未实现该 trait 的情况下通常会生成的错误消息。
该属性应放置在 trait 声明上,尽管放在其他位置也不是错误。
该属性使用 MetaListNameValueStr 语法来指定其输入,尽管对属性任何格式错误的输入不被视为错误,以提供向前和向后兼容性。
以下键具有给定的含义:
message— 顶层错误消息的文本。label— 在错误消息的损坏代码中内联显示的标签文本。note— 提供附加注释。
note 选项可以出现多次,这将导致发出多条注释消息。
如果任何其他选项出现多次,相关选项的第一次出现指定实际使用的值。后续出现会生成警告。
任何未知键都会生成警告。
所有三个选项都接受字符串作为参数,使用与 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
diagnostic::do_not_recommend 属性
#[diagnostic::do_not_recommend] 属性是对编译器的提示,不要在诊断消息中显示标注的 trait 实现。
Note
如果你知道该推荐通常对程序员没有帮助,抑制推荐可能会有用。这通常发生在广泛的毯式 impl 上。推荐可能会将程序员引向错误的方向,或者 trait 实现可能是你不想暴露的内部细节,或者约束可能无法被程序员满足。
例如,在关于类型未实现所需 trait 的错误消息中,编译器可能会找到一个如果没有 trait 实现中的特定约束就能满足要求的 trait 实现。编译器可能会告诉用户存在一个 impl,但问题在于 trait 实现中的约束。
#[diagnostic::do_not_recommend]属性可以用来告诉编译器不要告诉用户关于该 trait 实现的信息,而是简单地告诉用户该类型未实现所需的 trait。
该属性应放置在 trait 实现项上,尽管放在其他位置也不是错误。
该属性不接受任何参数,但意外参数不被视为错误。
在以下示例中,有一个称为 AsExpression 的 trait,用于将任意类型转换为 SQL 库中使用的 Expression 类型。有一个名为 check 的方法,它接受一个 AsExpression。
pub trait Expression {
type SqlType;
}
pub trait AsExpression<ST> {
type Expression: Expression<SqlType = ST>;
}
pub struct Text;
pub struct Integer;
pub struct Bound<T>(T);
pub struct SelectInt;
impl Expression for SelectInt {
type SqlType = Integer;
}
impl<T> Expression for Bound<T> {
type SqlType = T;
}
impl AsExpression<Integer> for i32 {
type Expression = Bound<Integer>;
}
impl AsExpression<Text> for &'_ str {
type Expression = Bound<Text>;
}
impl<T> Foo for T where T: Expression {}
// 取消此行注释以更改推荐。
// #[diagnostic::do_not_recommend]
impl<T, ST> AsExpression<ST> for T
where
T: Expression<SqlType = ST>,
{
type Expression = T;
}
trait Foo: Expression + Sized {
fn check<T>(&self, _: T) -> <T as AsExpression<<Self as Expression>::SqlType>>::Expression
where
T: AsExpression<Self::SqlType>,
{
todo!()
}
}
fn main() {
SelectInt.check("bar");
}
SelectInt 类型的 check 方法期望一个 Integer 类型。用 i32 类型调用它可以工作,因为它通过 AsExpression trait 被转换为 Integer。然而,用字符串调用它不会工作,并生成可能如下所示的错误:
error[E0277]: the trait bound `&str: Expression` is not satisfied
--> src/main.rs:53:15
|
53 | SelectInt.check("bar");
| ^^^^^ the trait `Expression` is not implemented for `&str`
|
= help: the following other types implement trait `Expression`:
Bound<T>
SelectInt
note: required for `&str` to implement `AsExpression<Integer>`
--> src/main.rs:45:13
|
45 | impl<T, ST> AsExpression<ST> for T
| ^^^^^^^^^^^^^^^^ ^
46 | where
47 | T: Expression<SqlType = ST>,
| ------------------------ unsatisfied trait bound introduced here
通过将 #[diagnostic::do_not_recommend] 属性添加到 AsExpression 的毯式 impl 中,消息变为:
error[E0277]: the trait bound `&str: AsExpression<Integer>` is not satisfied
--> src/main.rs:53:15
|
53 | SelectInt.check("bar");
| ^^^^^ the trait `AsExpression<Integer>` is not implemented for `&str`
|
= help: the trait `AsExpression<Integer>` is not implemented for `&str`
but trait `AsExpression<Text>` is implemented for it
= help: for that trait implementation, expected `Text`, found `Integer`
第一条错误消息包含有关 &str 和 Expression 关系以及毯式 impl 中未满足的 trait 约束的有些令人困惑的错误消息。添加 #[diagnostic::do_not_recommend] 后,它不再为该推荐考虑毯式 impl。消息应该更清晰一些,表明字符串无法转换为 Integer。
代码生成属性
以下属性用于控制代码生成。
inline 属性
inline 属性 建议是将带属性函数的代码副本放置在调用者中,还是生成对函数的调用。
Example
#![allow(unused)] fn main() { #[inline] pub fn example1() {} #[inline(always)] pub fn example2() {} #[inline(never)] pub fn example3() {} }
Note
rustc在认为值得时会自动内联函数。请谨慎使用此属性,因为关于内联哪些内容的不当决定可能会降低程序速度。
inline 属性的语法如下:
Syntax
InlineAttribute →
inline ( always )
| inline ( never )
| inline
inline 属性只能应用于具有函数体的函数 — 闭包、async 块、自由函数、固有 impl 或 trait impl 中的关联函数,以及具有默认定义时 trait 定义中的关联函数。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
只有第一次在函数上使用 inline 才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
inline 属性支持以下模式:
#[inline]建议执行内联展开。#[inline(always)]建议始终执行内联展开。#[inline(never)]建议绝不执行内联展开。
Note
在每种形式中,该属性都是一个提示。编译器可能会忽略它。
当 inline 应用于 trait 中的函数时,它仅适用于默认定义的代码。
当 inline 应用于 async 函数 或 async 闭包 时,它仅适用于生成的 poll 函数的代码。
Note
更多细节,请参见 Rust issue #129347。
如果函数通过 no_mangle 或 export_name 外部导出,则 inline 属性被忽略。
cold 属性
cold 属性 建议带属性的函数不太可能被调用,这可能帮助编译器生成更好的代码。
Example
#![allow(unused)] fn main() { #[cold] pub fn example() {} }
cold 属性使用 MetaWord 语法。
cold 属性只能应用于具有函数体的函数。
只有第一次在函数上使用 cold 才有效。
当 cold 应用于 trait 中的函数时,它仅适用于默认定义的代码。
naked 属性
naked 属性 阻止编译器为带属性的函数生成函数序言和尾声。
函数体必须恰好由一个 naked_asm! 宏调用组成。
不会为带属性的函数生成函数序言或尾声。naked_asm! 块中的汇编代码构成裸函数的完整函数体。
naked 属性是一个 unsafe 属性。使用 #[unsafe(naked)] 标注函数附带的安全性义务是:函数体必须遵守函数的调用约定、履行其签名,并且要么返回要么发散(即不越过汇编代码的末尾而掉落)。
汇编代码可以假设在入口时调用栈和寄存器状态根据函数的签名和调用约定是有效的。
汇编代码不能被编译器复制,除非在单态化多态函数时。
Note
保证汇编代码何时可能被复制或不被复制对于定义符号的裸函数很重要。
unused_variables lint 在裸函数中被抑制。
inline 属性不能应用于裸函数。
track_caller 属性不能应用于裸函数。
测试属性不能应用于裸函数。
no_builtins 属性
no_builtins 属性 禁用与调用假定存在的库函数相关的某些代码模式的优化。
Example
#![allow(unused)] #![no_builtins] fn main() { }
no_builtins 属性使用 MetaWord 语法。
no_builtins 属性只能应用于 crate 根。
只有第一次使用 no_builtins 属性才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。
target_feature 属性
target_feature 属性 可以应用于函数,以为特定平台架构特性启用该函数的代码生成。它使用 MetaListNameValueStr 语法,带有单个键 enable,其值是一个以逗号分隔的要启用的特性名称字符串。
#![allow(unused)]
fn main() {
#[cfg(target_feature = "avx2")]
#[target_feature(enable = "avx2")]
fn foo_avx2() {}
}
每个目标架构都有一组可以启用的特性。为 crate 未编译的目标架构指定特性是错误的。
在 target_feature 标注的函数内定义的闭包从外围函数继承该属性。
调用使用当前平台不支持的特定平台特性编译的函数是未定义行为,除非平台明确文档说明这是安全的。
除非以下平台规则另有说明,否则适用以下限制:
- 安全的
#[target_feature]函数(以及继承该属性的闭包)只能在启用被调用者启用的所有target_feature的调用者中安全调用。此限制不适用于unsafe上下文。 - 安全的
#[target_feature]函数(以及继承该属性的闭包)只能在启用被强制转换者启用的所有target_feature的上下文中被强制转换为安全的函数指针。此限制不适用于unsafe函数指针。
隐式启用的特性包含在此规则中。例如,一个 sse2 函数可以调用标记为 sse 的函数。
#![allow(unused)]
fn main() {
#[cfg(target_feature = "sse2")] {
#[target_feature(enable = "sse")]
fn foo_sse() {}
fn bar() {
// 在此处调用 `foo_sse` 是不安全的,因为我们必须首先确保 SSE 可用,
// 即使目标平台默认启用了 `sse` 或作为编译器标志手动启用。
unsafe {
foo_sse();
}
}
#[target_feature(enable = "sse")]
fn bar_sse() {
// 在此处调用 `foo_sse` 是安全的。
foo_sse();
|| foo_sse();
}
#[target_feature(enable = "sse2")]
fn bar_sse2() {
// 在此处调用 `foo_sse` 是安全的,因为 `sse2` 隐含 `sse`。
foo_sse();
}
}
}
具有 #[target_feature] 属性的函数从不实现 Fn trait 族,尽管从外围函数继承特性的闭包会实现。
#[target_feature] 属性不允许出现在以下位置:
main函数panic_handler函数- 安全的 trait 方法
- trait 中的安全默认函数
标记为 target_feature 的函数不会内联到不支持给定特性的上下文中。#[inline(always)] 属性不能与 target_feature 属性一起使用。
可用特性
以下是可用特性名称的列表。
x86 或 x86_64
在此平台上执行不支持的特性的代码是未定义行为。因此在此平台上使用 #[target_feature] 函数遵循上述限制。
| 特性 | 隐式启用 | 描述 |
|---|---|---|
adx | ADX — 多精度加法进位指令扩展 | |
aes | sse2 | AES — 高级加密标准 |
avx | sse4.2 | AVX — 高级向量扩展 |
avx2 | avx | AVX2 — 高级向量扩展 2 |
avx512bf16 | avx512bw | AVX512-BF16 — 高级向量扩展 512 位 - Bfloat16 扩展 |
avx512bitalg | avx512bw | AVX512-BITALG — 高级向量扩展 512 位 - 位算法 |
avx512bw | avx512f | AVX512-BW — 高级向量扩展 512 位 - 字节和字指令 |
avx512cd | avx512f | AVX512-CD — 高级向量扩展 512 位 - 冲突检测指令 |
avx512dq | avx512f | AVX512-DQ — 高级向量扩展 512 位 - 双字和四字指令 |
avx512f | avx2, fma, f16c | AVX512-F — 高级向量扩展 512 位 - 基础 |
avx512fp16 | avx512bw | AVX512-FP16 — 高级向量扩展 512 位 - Float16 扩展 |
avx512ifma | avx512f | AVX512-IFMA — 高级向量扩展 512 位 - 整数融合乘加 |
avx512vbmi | avx512bw | AVX512-VBMI — 高级向量扩展 512 位 - 向量字节操作指令 |
avx512vbmi2 | avx512bw | AVX512-VBMI2 — 高级向量扩展 512 位 - 向量字节操作指令 2 |
avx512vl | avx512f | AVX512-VL — 高级向量扩展 512 位 - 向量长度扩展 |
avx512vnni | avx512f | AVX512-VNNI — 高级向量扩展 512 位 - 向量神经网络指令 |
avx512vp2intersect | avx512f | AVX512-VP2INTERSECT — 高级向量扩展 512 位 - 向量对交集到一对掩码寄存器 |
avx512vpopcntdq | avx512f | AVX512-VPOPCNTDQ — 高级向量扩展 512 位 - 向量人口计数指令 |
avxifma | avx2 | AVX-IFMA — 高级向量扩展 - 整数融合乘加 |
avxneconvert | avx2 | AVX-NE-CONVERT — 高级向量扩展 - 无异常浮点转换指令 |
avxvnni | avx2 | AVX-VNNI — 高级向量扩展 - 向量神经网络指令 |
avxvnniint16 | avx2 | AVX-VNNI-INT16 — 高级向量扩展 - 16 位整数向量神经网络指令 |
avxvnniint8 | avx2 | AVX-VNNI-INT8 — 高级向量扩展 - 8 位整数向量神经网络指令 |
bmi1 | BMI1 — 位操作指令集 | |
bmi2 | BMI2 — 位操作指令集 2 | |
cmpxchg16b | cmpxchg16b — 原子地比较和交换 16 字节(128 位)数据 | |
f16c | avx | F16C — 16 位浮点转换指令 |
fma | avx | FMA3 — 三操作数融合乘加 |
fxsr | fxsave 和 fxrstor — 保存和恢复 x87 FPU、MMX 技术和 SSE 状态 | |
gfni | sse2 | GFNI — 伽罗瓦域新指令 |
kl | sse2 | KEYLOCKER — Intel Key Locker 指令 |
lzcnt | lzcnt — 前导零计数 | |
movbe | movbe — 交换字节后移动数据 | |
pclmulqdq | sse2 | pclmulqdq — 打包无进位乘法四字 |
popcnt | popcnt — 设置为 1 的位数计数 | |
rdrand | rdrand — 读取随机数 | |
rdseed | rdseed — 读取随机种子 | |
sha | sse2 | SHA — 安全哈希算法 |
sha512 | avx2 | SHA512 — 512 位摘要的安全哈希算法 |
sm3 | avx | SM3 — 商密 3 哈希算法 |
sm4 | avx2 | SM4 — 商密 4 密码算法 |
sse | SSE — 流式 SIMD 扩展 | |
sse2 | sse | SSE2 — 流式 SIMD 扩展 2 |
sse3 | sse2 | SSE3 — 流式 SIMD 扩展 3 |
sse4.1 | ssse3 | SSE4.1 — 流式 SIMD 扩展 4.1 |
sse4.2 | sse4.1 | SSE4.2 — 流式 SIMD 扩展 4.2 |
sse4a | sse3 | SSE4a — 流式 SIMD 扩展 4a |
ssse3 | sse3 | SSSE3 — 补充流式 SIMD 扩展 3 |
tbm | TBM — 尾部位操作 | |
vaes | avx2, aes | VAES — 向量 AES 指令 |
vpclmulqdq | avx, pclmulqdq | VPCLMULQDQ — 向量无进位四字乘法 |
widekl | kl | KEYLOCKER_WIDE — Intel Wide Keylocker 指令 |
xsave | xsave — 保存处理器扩展状态 | |
xsavec | xsavec — 以压缩保存处理器扩展状态 | |
xsaveopt | xsaveopt — 优化保存处理器扩展状态 | |
xsaves | xsaves — 以管理员模式保存处理器扩展状态 |
aarch64
在此平台上使用 #[target_feature] 函数遵循上述限制。
有关这些特性的更多文档可以在 ARM 架构参考手册 或 developer.arm.com 上找到。
Note
以下特性对应如果使用,应同时标记为启用或禁用:
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 | neon | FEAT_DotProd — 高级 SIMD Int8 点积指令 |
dpb | FEAT_DPB — 数据缓存清理到持久化点 | |
dpb2 | dpb | FEAT_DPB2 — 数据缓存清理到深度持久化点 |
f32mm | sve | FEAT_F32MM — SVE 单精度 FP 矩阵乘法指令 |
f64mm | sve | FEAT_F64MM — SVE 双精度 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 转换指令 |
lor | FEAT_LOR — 有限排序区域扩展 | |
lse | FEAT_LSE — 大型系统扩展 | |
mte | FEAT_MTE & FEAT_MTE2 — 内存标记扩展 | |
neon | FEAT_AdvSimd & FEAT_FP — 浮点和高级 SIMD 扩展 | |
paca | FEAT_PAUTH — 指针认证(地址认证) | |
pacg | FEAT_PAUTH — 指针认证(通用认证) | |
pan | FEAT_PAN — 特权访问禁止扩展 | |
pmuv3 | FEAT_PMUv3 — 性能监视器扩展(v3) | |
rand | FEAT_RNG — 随机数生成器 | |
ras | FEAT_RAS & FEAT_RASv1p1 — 可靠性、可用性和可维护性扩展 | |
rcpc | FEAT_LRCPC — 释放一致处理器一致 | |
rcpc2 | rcpc | FEAT_LRCPC2 — 带立即偏移的 RcPc |
rdm | neon | FEAT_RDM — 舍入双倍乘累加 |
sb | FEAT_SB — 推测屏障 | |
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 — 推测存储绕过安全 | |
sve | neon | FEAT_SVE — 可扩展向量扩展 |
sve2 | sve | FEAT_SVE2 — 可扩展向量扩展 2 |
sve2-aes | sve2, aes | FEAT_SVE_AES & FEAT_SVE_PMULL128 — SVE AES 指令 |
sve2-bitperm | sve2 | FEAT_SVE2_BitPerm — SVE 位排列 |
sve2-sha3 | sve2, sha3 | FEAT_SVE2_SHA3 — SVE SHA3 指令 |
sve2-sm4 | sve2, sm4 | FEAT_SVE2_SM4 — SVE SM4 指令 |
tme | FEAT_TME — 事务内存扩展 | |
vh | FEAT_VHE — 虚拟化主机扩展 |
loongarch
在此平台上使用 #[target_feature] 函数遵循上述限制。
| 特性 | 隐式启用 | 描述 |
|---|---|---|
f | F — 单精度浮点指令 | |
d | f | D — 双精度浮点指令 |
frecipe | FRECIPE — 倒数近似指令 | |
lasx | lsx | LASX — 256 位向量指令 |
lbt | LBT — 二进制翻译指令 | |
lsx | d | LSX — 128 位向量指令 |
lvz | LVZ — 虚拟化指令 | |
div32 | DIV32 — 接受非符号扩展 32 位操作数的除法指令 | |
lam-bh | LAM-BH — 字节和半字的原子交换和加法指令 | |
lamcas | LAMCAS — 字节、半字、字和双字的原子比较并交换指令 | |
ld-seq-sa | LD-SEQ-SA — 对同一地址的加载操作的顺序排序 | |
scq | SCQ — 存储条件四字指令 |
riscv32 或 riscv64
在此平台上使用 #[target_feature] 函数遵循上述限制。
rest of RISC-V table kept same as English source
wasm32 或 wasm64
在 Wasm 平台上,安全的 #[target_feature] 函数始终可以在安全上下文中使用。无法通过 #[target_feature] 属性导致未定义行为,因为尝试使用 Wasm 引擎不支持的指令将在加载时失败,而不会有被以不同于编译器预期的方式解释的风险。
rest of wasm table kept same as English source
s390x
在 s390x 目标上,使用 #[target_feature] 属性的函数遵循上述限制。
rest of s390x table kept same as English source
附加信息
请参见 target_feature 条件编译选项 以根据编译时设置选择性地启用或禁用代码编译。请注意,此选项不受 target_feature 属性的影响,仅由对整个 crate 启用的特性驱动。
可以使用标准库中的平台特定宏在运行时检查特性是否启用,例如 is_x86_feature_detected 或 is_aarch64_feature_detected。
Note
rustc为每个目标和 CPU 设置了一组默认启用的特性。CPU 可以通过-C target-cpu标志选择。可以通过-C target-feature标志为整个 crate 启用或禁用各个特性。
track_caller 属性
track_caller 属性可以应用于任何具有 "Rust" ABI 的函数,但入口点 fn main 除外。
当应用于 trait 声明中的函数和方法时,该属性适用于所有实现。如果 trait 提供了具有该属性的默认实现,则该属性也适用于覆盖实现。
当应用于 extern 块中的函数时,该属性也必须应用于任何链接的实现,否则会导致未定义行为。当应用于对 extern 块可用的函数时,extern 块中的声明也必须具有该属性,否则会导致未定义行为。
行为
将属性应用于函数 f 允许 f 内的代码获取导致 f 被调用的“最顶层“跟踪调用的 Location 的提示。在观察点,实现的行为如同从 f 的帧向上遍历栈以查找最近的未标注函数 outer 的帧,并返回 outer 中跟踪调用的 Location。
#![allow(unused)]
fn main() {
#[track_caller]
fn f() {
println!("{}", std::panic::Location::caller());
}
}
Note
core提供core::panic::Location::caller用于观察调用者位置。它包装了由rustc实现的core::intrinsics::caller_location内部函数。
Note
因为生成的
Location是一个提示,实现可以提前停止向上遍历栈。请参见限制了解重要注意事项。
示例
当 f 被 calls_f 直接调用时,f 中的代码观察到其在 calls_f 中的调用点。
当 f 被另一个带属性的函数 g 调用,而 g 又被 calls_g 调用时,f 和 g 中的代码观察到 g 在 calls_g 中的调用点。
限制
此信息是一个提示,实现不需要保留它。
特别地,将带有 #[track_caller] 的函数强制转换为函数指针会创建一个垫片,观察者看来它是在带属性的函数定义点被调用的,从而在虚调用中丢失实际的调用者信息。一个常见的强制转换示例是创建方法带有属性的 trait 对象。
Note
函数指针的上述垫片是必需的,因为
rustc通过将隐式参数附加到函数 ABI 在代码生成上下文中实现track_caller,但这对于间接调用将是不健全的,因为该参数不是函数类型的一部分,并且给定的函数指针类型可能引用或不引用具有该属性的函数。创建垫片对函数指针的调用者隐藏了隐式参数,保持了健全性。
instruction_set 属性
instruction_set 属性 指定函数在代码生成期间将使用的指令集。这允许在单个程序中混合多个指令集。
Example
#[instruction_set(arm::a32)] fn arm_code() {} #[instruction_set(arm::t32)] fn thumb_code() {}
instruction_set 属性使用 MetaListPaths 语法来指定由架构族名称和指令集名称组成的单个路径。
instruction_set 属性只能应用于具有函数体的函数。
instruction_set 属性在一个函数上只能使用一次。
instruction_set 属性只能用于支持给定值的目标。
当使用 instruction_set 属性时,函数中的任何内联汇编必须使用指定的指令集而不是目标默认的指令集。
ARM 上的 instruction_set
当面向 ARMv4T 和 ARMv5te 架构时,instruction_set 的支持值为:
arm::a32— 将函数生成为 A32 “ARM” 代码。arm::t32— 将函数生成为 T32 “Thumb” 代码。
如果函数的地址被取为函数指针,地址的低位将取决于所选的指令集:
- 对于
arm::a32(“ARM”),将为 0。 - 对于
arm::t32(“Thumb”),将为 1。
限制
以下属性影响编译时限制。
recursion_limit 属性
recursion_limit 属性可以应用于 crate 级别,以设置可能无限递归的编译时操作(如宏展开或自动解引用)的最大深度。
它使用 MetaNameValueStr 语法来指定递归深度。
Note
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);
}
type_length_limit 属性
type_length_limit 属性 设置在单态化期间构造具体类型时允许的最大类型替换次数。
Note
rustc仅在 nightly 的-Zenforce-type-length-limit标志激活时强制执行该限制。更多信息请参见 Rust PR #127670。
Example
#![type_length_limit = "4"] fn f<T>(x: T) {} // 这无法编译,因为单态化为 // `f::<((((i32,), i32), i32), i32)>` 需要超过 4 个类型元素。 f(((((1,), 2), 3), 4));
Note
rustc中的默认值是1048576。
type_length_limit 属性使用 MetaNameValueStr 语法。字符串中的值必须是非负数。
type_length_limit 属性只能应用于 crate 根。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
只有第一次在项上使用 type_length_limit 才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
类型系统属性
以下属性用于改变类型的使用方式。
non_exhaustive 属性
non_exhaustive 属性指示一个类型或变体可能在将来添加更多字段或变体。
non_exhaustive 属性使用 MetaWord 语法,因此不接受任何输入。
在定义的 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 的类型有一些限制,以保持添加新字段或变体时的向后兼容性。
非穷尽类型不能在定义的 crate 外部构造:
- 非穷尽变体(
struct或enumvariant)不能使用 StructExpression (包括功能更新语法) 构造。 - 单元结构体隐式定义的同名常量,或元组结构体的同名构造函数,其可见性不超过
pub(crate)。 也就是说,如果结构体的可见性是pub,则常量或构造函数的可见性是pub(crate),否则两者的可见性相同(与没有#[non_exhaustive]的情况一样)。 enum实例可以构造。
以下构造示例在定义的 crate 外部无法编译:
// 这些是在上游 crate 中定义并标注了
// `#[non_exhaustive]` 的类型。
use upstream::{Config, Token, Id, Error, Message};
// 无法构造 `Config` 的实例;如果在 `upstream` 的新版本中添加了新字段,
// 则此代码将无法编译,因此不允许。
let config = Config { window_width: 640, window_height: 480 };
// 无法构造 `Token` 的实例;如果添加了新字段,它将不再是单元结构体,
// 因此由其作为单元结构体创建的同名常量在 crate 外部不是 public 的;
// 此代码无法编译。
let token = Token;
// 无法构造 `Id` 的实例;如果添加了新字段,其构造函数签名将改变,
// 因此其构造函数在 crate 外部不是 public 的;此代码无法编译。
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` 的实例;如果将其转换为元组枚举变体,
// 则此代码将无法编译。
let message = Message::Quit;
在定义的 crate 外部对非穷尽类型进行模式匹配时存在限制:
- 在对非穷尽变体(
struct或enumvariant)进行模式匹配时,必须使用包含..的 StructPattern。元组枚举变体的构造函数的可见性降低到不超过pub(crate)。 - 在对非穷尽
enum进行模式匹配时,匹配一个变体不会贡献于分支的穷尽性。以下匹配示例在定义的 crate 外部无法编译:
// 这些是在上游 crate 中定义并标注了
// `#[non_exhaustive]` 的类型。
use upstream::{Config, Token, Id, 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 } => { },
// 不能匹配非穷尽元组或单元枚举变体。
Message::Reaction(type) => { },
Message::Quit => { },
}
也不允许对包含任何非穷尽变体的枚举使用数值强制转换(as)。
例如,以下枚举可以进行强制转换,因为它不包含任何非穷尽变体:
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum Example {
First,
Second,
}
}
但是,如果枚举包含哪怕一个非穷尽变体,强制转换将导致错误。考虑此枚举的修改版本:
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum EnumWithNonExhaustiveVariants {
First,
#[non_exhaustive]
Second,
}
}
use othercrate::EnumWithNonExhaustiveVariants;
// 错误:当枚举在另一个 crate 中定义时,不能对包含非穷尽变体的枚举进行强制转换
let _ = EnumWithNonExhaustiveVariants::First as u8;
非穷尽类型在下游 crate 中始终被视为有人居住的。
调试器属性
以下属性用于增强使用第三方调试器(如 GDB 或 WinDbg)时的调试体验。
debugger_visualizer 属性
debugger_visualizer 属性 可用于将调试器可视化文件嵌入到调试信息中。这改善了显示值时的调试器体验。
Example
#![debugger_visualizer(natvis_file = "Example.natvis")] #![debugger_visualizer(gdb_script_file = "example.py")]
debugger_visualizer 属性使用 MetaListNameValueStr 语法来指定其输入。必须指定以下键之一:
debugger_visualizer 属性只能应用于模块或 crate 根。
debugger_visualizer 属性可以在一个形式上使用任意次数。所有指定的可视化文件都将被加载。
将 debugger_visualizer 与 Natvis 一起使用
Natvis 是一个基于 XML 的框架,适用于微软调试器(如 Visual Studio 和 WinDbg),使用声明式规则来自定义类型的显示。有关 Natvis 格式的详细信息,请参考微软的 Natvis 文档。
此属性仅支持在 -windows-msvc 目标上嵌入 Natvis 文件。
Natvis 文件的路径通过 natvis_file 键指定,该路径是相对于源文件的路径。
Example
#![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)
将 debugger_visualizer 与 GDB 一起使用
GDB 支持使用结构化 Python 脚本,称为美化打印器(pretty printer),来描述类型在调试器视图中应该如何显示。有关美化打印器的详细信息,请参考 GDB 的美化打印文档。
Note
在 GDB 下调试二进制文件时,嵌入的美化打印器不会自动加载。
有两种方法可以启用自动加载嵌入的美化打印器:
- 使用额外参数启动 GDB,显式地将目录或二进制文件添加到自动加载安全路径:
gdb -iex "add-auto-load-safe-path safe-path path/to/binary" path/to/binary。有关更多信息,请参见 GDB 的自动加载文档。- 在
$HOME/.config/gdb下创建一个名为gdbinit的文件(如果目录尚不存在,可能需要创建)。将以下行添加到该文件:add-auto-load-safe-path path/to/binary。
这些脚本使用 gdb_script_file 键嵌入,该路径是相对于源文件的路径。
Example
#![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.
collapse_debuginfo 属性
collapse_debuginfo 属性 控制在为调用此宏的代码生成调试信息时,是否将宏定义中的代码位置折叠为与宏调用点关联的单个位置。
Example
#![allow(unused)] fn main() { #[collapse_debuginfo(yes)] macro_rules! example { () => { println!("hello!"); }; } }在使用调试器时,调用
example宏可能看起来像是调用一个函数。也就是说,当你步进到调用点时,它可能显示宏调用而不是展开的代码。
collapse_debuginfo 属性的语法如下:
Syntax
CollapseDebuginfoAttribute → collapse_debuginfo ( CollapseDebuginfoOption )
CollapseDebuginfoOption →
yes
| no
| external
collapse_debuginfo 属性只能应用于 macro_rules 定义。
collapse_debuginfo 属性在一个宏上只能使用一次。
collapse_debuginfo 属性接受以下选项:
#[collapse_debuginfo(yes)]— 调试信息中的代码位置被折叠。#[collapse_debuginfo(no)]— 调试信息中的代码位置不被折叠。#[collapse_debuginfo(external)]— 仅当宏来自不同的 crate 时,调试信息中的代码位置才被折叠。
对于没有此属性的宏,external 行为是默认的,除非它们是内置宏。对于内置宏,默认是 yes。
Note
rustc有一个-C collapse-macro-debuginfoCLI 选项,可以覆盖默认行为以及任何#[collapse_debuginfo]属性的值。
-
注意:这假设你使用
rust-gdb脚本,该脚本为像String这样的标准库类型配置了美化打印器。 ↩
语句与表达式
Rust 主要是一门表达式语言。这意味着大多数产生值或引发效果的求值过程都由统一的表达式语法类别来主导。每种表达式通常可以互相嵌套,表达式求值的规则包括指定表达式产生的值以及其子表达式的求值顺序。
相比之下,语句的主要作用是承载表达式并明确地编排它们的求值顺序。
语句
Syntax
Statement →
;
| Item
| LetStatement
| ExpressionStatement
| OuterAttribute* MacroInvocationSemi
语句是块的一个组成部分,而块又是外部表达式或函数的一个组成部分。
声明语句
声明语句是在所包含的语句块中引入一个或多个名称的语句。声明的名称可以表示新的变量或新的项。
声明语句有两种:项声明和 let 语句。
项声明
在语句块中声明一个项会将其作用域限制在包含该语句的块中。该项不会被赋予规范路径,其声明的任何子项也不会。
例外情况是,由实现定义的关联项在外部作用域中仍然可以访问,只要该项以及(如果适用)trait 是可访问的。在其他方面,其含义与在模块中声明该项完全相同。
不会隐式捕获包含该函数的泛型参数、参数和局部变量。例如,inner 不能访问 outer_var。
#![allow(unused)]
fn main() {
fn outer() {
let outer_var = true;
fn inner() { /* outer_var 在此作用域中不可见 */ }
inner();
}
}
let 语句
Syntax
LetStatement →
OuterAttribute* let PatternNoTopAlt ( : Type )?
(
= Expression
| = Expressionexcept LazyBooleanExpression or end with a }
else BlockExpressionNoInnerAttributes
)? ;
let 语句通过模式引入一组新的变量。模式后面可以选择跟上类型标注,然后要么结束,要么跟上一个初始化表达式以及可选的 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 { // 此模式是不可反驳的,因此编译器
// 会发出 lint 警告,因为 else 块是多余的。
panic!();
};
}
表达式语句
Syntax
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: expected `()` because of default return type
// if true {
// 1
// }
// good: 块的类型是 i32
if true {
1
} else {
2
};
}
语句上的属性
语句接受外部属性。对语句有意义的属性包括 cfg 和 lint 检查属性。
表达式
Syntax
Expression →
ExpressionWithoutBlock
| ExpressionWithBlock
ExpressionWithoutBlock →
OuterAttribute* ExpressionWithoutBlockNoAttrs
ExpressionWithoutBlockNoAttrs →
LiteralExpression
| PathExpression
| OperatorExpression
| GroupedExpression
| ArrayExpression
| AwaitExpression
| IndexExpression
| TupleExpression
| TupleIndexingExpression
| StructExpression
| CallExpression
| MethodCallExpression
| FieldExpression
| ClosureExpression
| AsyncBlockExpression
| ContinueExpression
| BreakExpression
| RangeExpression
| ReturnExpression
| UnderscoreExpression
| MacroInvocation
ExpressionWithBlock →
OuterAttribute* ExpressionWithBlockNoAttrs
ExpressionWithBlockNoAttrs →
BlockExpression
| ConstBlockExpression
| UnsafeBlockExpression
| LoopExpression
| IfExpression
| MatchExpression
表达式可以有两种角色:它总是产生一个值,并且可能具有效果(也称为“副作用“)。
表达式求值为一个值,并在求值期间产生效果。
许多表达式包含子表达式,称为表达式的操作数。
每种表达式的含义决定了以下几件事:
- 在求值表达式时是否要求值其操作数
- 求值操作数的顺序
- 如何组合操作数的值以获得表达式的值
这样,表达式的结构就决定了执行的结构。块只是另一种表达式,因此块、语句、表达式和块可以相互递归嵌套,达到任意深度。
Note
我们为表达式的操作数命名以便讨论,但这些名称并不稳定,可能会发生变化。
表达式优先级
Rust 运算符和表达式的优先级按从强到弱的顺序排列如下。处于同一优先级的二元运算符按其结合性给定的顺序分组。
| 运算符/表达式 | 结合性 |
|---|---|
| 路径 | |
| 方法调用 | |
| 字段表达式 | 从左到右 |
| 函数调用、数组索引 | |
? | |
一元 - ! * 借用 | |
as | 从左到右 |
* / % | 从左到右 |
+ - | 从左到右 |
<< >> | 从左到右 |
& | 从左到右 |
^ | 从左到右 |
| | 从左到右 |
== != < > <= >= | 需要括号 |
&& | 从左到右 |
|| | 从左到右 |
.. ..= | 需要括号 |
= += -= *= /= %= &= |= ^= <<= >>= | 从右到左 |
return break 闭包 |
操作数的求值顺序
以下列表中的表达式都以相同的方式求值其操作数,如下文所述。其他表达式要么不接收操作数,要么根据其各自页面的描述有条件地求值。
- 解引用表达式
- 错误传播表达式
- 取反表达式
- 算术和逻辑二元运算符
- 比较运算符
- 类型转换表达式
- 分组表达式
- 数组表达式
- Await 表达式
- 索引表达式
- 元组表达式
- 元组索引表达式
- 结构体表达式
- 调用表达式
- 方法调用表达式
- 字段表达式
- Break 表达式
- 区间表达式
- Return 表达式
这些表达式的操作数在应用表达式效果之前被求值。接受多个操作数的表达式按源代码中从左到右的顺序求值。
Note
哪些子表达式是表达式的操作数由前一节中的表达式优先级决定。
例如,两个 next 方法调用将始终以相同的顺序被调用:
#![allow(unused)]
fn main() {
// 使用 vec 而不是数组以避免引用
// 因为在编写此示例时尚无稳定的自有数组迭代器
// 在编写此示例时。
let mut one_two = vec![1, 2].into_iter();
assert_eq!(
(1, 2),
(one_two.next().unwrap(), one_two.next().unwrap())
);
}
Note
由于这是递归应用的,这些表达式也从最内层到最外层求值,忽略兄弟节点,直到没有内部子表达式为止。
位置表达式和值表达式
表达式分为两大类:位置表达式和值表达式;还有第三类次要的表达式,称为赋值目标表达式。在每个表达式内部,操作数同样可以出现在位置上下文或值上下文中。表达式的求值取决于其自身的类别以及它所处的上下文。
位置表达式是表示内存位置的表达式。
这些表达式包括引用局部变量的路径、静态变量、解引用(*expr)、数组索引表达式(expr[expr])、字段引用(expr.f)和带括号的位置表达式。
所有其他表达式都是值表达式。
值表达式是表示实际值的表达式。
以下上下文是位置表达式上下文:
- 复合赋值表达式的左操作数。
- 一元借用、裸借用或解引用运算符的操作数。
- 字段表达式的操作数。
- 数组索引表达式的索引操作数。
- 元组索引表达式的元组操作数。
- 任何隐式借用的操作数。
- let 语句的初始化器。
if let、match或while let表达式的受检者。- 函数式更新结构体表达式的基值。
Note
历史上,位置表达式曾被称为lvalues,值表达式曾被称为rvalues。
赋值目标表达式是出现在赋值表达式左操作数中的表达式。具体来说,赋值目标表达式包括:
在赋值目标表达式内部允许任意加括号。
移动和复制类型
当位置表达式在值表达式上下文中求值,或在模式中以值绑定时,它表示该内存位置中持有的值。
如果该值的类型实现了 Copy,则该值将被复制。
在其余情况下,如果该类型是 Sized,则可能可以移动该值。
只有以下位置表达式可以被移出:
从求值为局部变量的位置表达式中移出后,该位置被反初始化,在重新初始化之前不能再被读取。
在所有其他情况下,尝试在值表达式上下文中使用位置表达式是错误的。
可变性
要使一个位置表达式能够被赋值、可变借用、隐式可变借用或绑定到包含 ref mut 的模式,它必须是可变的。我们称这些为可变位置表达式。相反,其他位置表达式称为不可变位置表达式。
以下表达式可以是可变位置表达式上下文:
- 当前未被借用的可变变量。
- 可变
static项。 - 临时值。
- 字段:这在可变位置表达式上下文中求值子表达式。
- 对
*mut T指针的解引用。 - 对类型为
&mut T的变量或变量的字段的解引用。注意:这是下一条规则要求的例外。 - 对实现了
DerefMut的类型的解引用:这要求被解引用的值在可变位置表达式上下文中求值。 - 对实现了
IndexMut的类型的数组索引:这在可变位置表达式上下文中求值被索引的值,但不求值索引。
临时值
在大多数位置表达式上下文中使用值表达式时,会创建一个临时的无名内存位置并初始化为该值。表达式求值为该位置,除非被提升为 static。临时值的丢弃作用域通常是包围语句的末尾。
超级宏
某些内置宏可能会创建临时值,其作用域可以被延长。这些临时值是超级临时值,这些宏是超级宏。这些宏的调用是超级宏调用表达式。这些宏的参数可以是超级操作数。
Note
当超级宏调用表达式是延长表达式时,其超级操作数是延长表达式,并且超级临时值的作用域被延长。参见 destructors.scope.lifetime-extension.exprs。
format_args!
除格式字符串参数外,传递给 format_args! 的所有参数都是超级操作数。
#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
// 由于该调用是延长表达式且参数是超级操作数,
// 内部块是延长表达式,因此在其尾部表达式中创建的
// 临时值的作用域被延长。
let _ = format_args!("{}", { &temp() }); // OK
}
format_args! 的超级操作数被隐式借用,因此是位置表达式上下文。当传递值表达式作为参数时,它创建一个超级临时值。
#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
let x = format_args!("{}", temp());
x; // <-- 临时值被延长了,允许在此处使用。
}
format_args! 调用的展开有时会创建其他内部的超级临时值。
#![allow(unused)]
fn main() {
let x = {
// 此调用创建一个内部临时值。
let x = format_args!("{:?}", 0);
x // <-- 临时值被延长了,允许在此处使用。
}; // <-- 临时值在此处被丢弃。
x; // 错误
}
#![allow(unused)]
fn main() {
// 此调用不创建内部临时值。
let x = { let x = format_args!("{}", 0); x };
x; // OK
}
Note
format_args!何时创建或不创建内部临时值的细节目前尚未规定。
pin!
pin! 的参数是超级操作数。
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 与上面的 `format_args!` 相同。
let _ = pin!({ &temp() }); // OK
}
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 参数被求值为一个超级临时值。
let x = pin!(temp());
// 临时值被延长了,允许在此处使用。
x; // OK
}
隐式借用
某些表达式会将一个表达式视为位置表达式,通过隐式借用它。例如,可以直接比较两个非固定大小的切片是否相等,因为 == 运算符会隐式借用其操作数:
#![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;
// 等价形式:
::std::cmp::PartialEq::eq(&*a, &*b);
}
隐式借用可能在以下表达式中发生:
- 方法调用表达式中的左操作数。
- 字段表达式中的左操作数。
- 调用表达式中的左操作数。
- 数组索引表达式中的左操作数。
- 解引用运算符(
*)的操作数。 - 比较的操作数。
- 复合赋值的左操作数。
- 传递给
format_args!的参数(格式字符串除外)。
重载 trait
以下许多运算符和表达式也可以使用 std::ops 或 std::cmp 中的 trait 为其他类型进行重载。这些 trait 也以相同的名称存在于 core::ops 和 core::cmp 中。
表达式属性
表达式前允许外部属性的情况仅限于以下几种:
绝不允许在以下表达式之前:
- 区间表达式。
- 二元运算符表达式(ArithmeticOrLogicalExpression、ComparisonExpression、LazyBooleanExpression、TypeCastExpression、AssignmentExpression、CompoundAssignmentExpression)。
字面量表达式
Syntax
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
字面量表达式是由单个记号而非记号序列组成的表达式,它直接且立即表示它所求值为的值,而不是通过名称或其他求值规则来引用它。
字面量是常量表达式的一种形式,因此(主要在)编译时求值。
前面描述的每种词法字面量形式都可以构成字面量表达式,关键字 true 和 false 也可以。
#![allow(unused)]
fn main() {
"hello"; // 字符串类型
'5'; // 字符类型
5; // 整数类型
}
在下面的描述中,记号的字符串表示是输入中与该记号在词法器语法片段中的产生式相匹配的字符序列。
Note
此字符串表示永远不会包含紧跟在
U+000A(LF)之前的字符U+000D(CR):这对字符会预先转换为单个U+000A(LF)。
转义
以下文本字面量表达式的描述中使用了多种形式的转义。
每种形式的转义具有以下特征:
- 一个转义序列:一个字符序列,始终以
U+005C(\)开头 - 一个转义值:单个字符或空字符序列
在下面的转义定义中:
- 八进制数字是范围 [
0-7] 中的任何字符。 - 十六进制数字是范围 [
0-9]、[a-f] 或 [A-F] 中的任何字符。
简单转义
下表中第一列出现的每个字符序列都是一个转义序列。
在每种情况下,转义值是第二列相应条目中给出的字符。
| 转义序列 | 转义值 |
|---|---|
\0 | U+0000 (NUL) |
\t | U+0009 (HT) |
\n | U+000A (LF) |
\r | U+000D (CR) |
\" | U+0022 (QUOTATION MARK) |
\' | U+0027 (APOSTROPHE) |
\\ | U+005C (REVERSE SOLIDUS) |
8 位转义
转义序列由 \x 后跟两个十六进制数字组成。
转义值是其 Unicode 标量值等于将转义序列最后两个字符解释为十六进制整数的字符,如同使用 u8::from_str_radix 以基数 16 解释。
Note
因此,转义值的 Unicode 标量值在
u8的范围内。
7 位转义
转义序列由 \x 后跟一个八进制数字和一个十六进制数字组成。
转义值是其 Unicode 标量值等于将转义序列最后两个字符解释为十六进制整数的字符,如同使用 u8::from_str_radix 以基数 16 解释。
Unicode 转义
转义序列由 \u{ 后跟一串字符(每个字符是十六进制数字或 _)再后跟 } 组成。
转义值是其 Unicode 标量值等于将转义序列中包含的十六进制数字解释为十六进制整数的字符,如同使用 u32::from_str_radix 以基数 16 解释。
Note
CHAR_LITERAL 或 STRING_LITERAL 记号的允许形式确保存在这样一个字符。
字符串续行转义
转义序列由 \ 后紧跟 U+000A(LF)以及下一个非空白字符之前的所有空白字符组成。为此,空白字符是 U+0009(HT)、U+000A(LF)、U+000D(CR)和 U+0020(SPACE)。
转义值是一个空字符序列。
Note
这种转义形式的效果是字符串续行会跳过后续的空白字符,包括额外的换行符。因此
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 中)可能令人困惑和意外。这种行为将来可能会调整。在做出决定之前,建议避免依赖用行续行跳过多个换行符。有关更多信息,请参阅此议题。
字符字面量表达式
字符字面量表达式由单个 CHAR_LITERAL 记号组成。
该表达式的类型是原始 char 类型。
记号不能有后缀。
记号的字面量内容是在记号字符串表示中跟在第一个 U+0027(')之后且在最后一个 U+0027(')之前的字符序列。
字面量表达式的表示字符按如下方式从字面量内容派生:
- 如果字面量内容是以下形式的转义序列之一,则表示字符是该转义序列的转义值:
- 否则,表示字符是构成字面量内容的单个字符。
表达式的值是与表示字符的 Unicode 标量值相对应的 char。
Note
CHAR_LITERAL 记号的允许形式确保这些规则总是产生单个字符。
字符字面量表达式示例:
#![allow(unused)]
fn main() {
'R'; // R
'\''; // '
'\x52'; // R
'\u{00E6}'; // 拉丁文小写字母 AE (U+00E6)
}
字符串字面量表达式
字符串字面量表达式由单个 STRING_LITERAL 或 RAW_STRING_LITERAL 记号组成。
该表达式的类型是对原始 str 类型的共享引用(具有 static 生命周期)。即类型为 &'static str。
记号不能有后缀。
记号的字面量内容是在记号字符串表示中跟在第一个 U+0022(")之后且在最后一个 U+0022(")之前的字符序列。
字面量表达式的表示字符串是按如下方式从字面量内容派生的字符序列:
-
如果记号是 STRING_LITERAL,则字面量内容中出现的以下任何形式的每个转义序列都被该转义序列的转义值替换。
这些替换按从左到右的顺序进行。例如,记号
"\\x41"被转换为字符\x41。
- 如果记号是 RAW_STRING_LITERAL,则表示字符串与字面量内容完全相同。
表达式的值是对一个静态分配的 str 的引用,该 str 包含表示字符串的 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 记号组成。
该表达式的类型是原始 u8 类型。
记号不能有后缀。
记号的字面量内容是在记号字符串表示中跟在第一个 U+0027(')之后且在最后一个 U+0027(')之前的字符序列。
字面量表达式的表示字符按如下方式从字面量内容派生:
- 否则,表示字符是构成字面量内容的单个字符。
表达式的值是表示字符的 Unicode 标量值。
Note
BYTE_LITERAL 记号的允许形式确保这些规则总是产生单个字符,其 Unicode 标量值在
u8的范围内。
字节字面量表达式示例:
#![allow(unused)]
fn main() {
b'R'; // 82
b'\''; // 39
b'\x52'; // 82
b'\xA0'; // 160
}
字节串字面量表达式
字节串字面量表达式由单个 BYTE_STRING_LITERAL 或 RAW_BYTE_STRING_LITERAL 记号组成。
该表达式的类型是对一个数组的共享引用(具有 static 生命周期),其元素类型为 u8。即类型为 &'static [u8; N],其中 N 是下文所述表示字符串中的字节数。
记号不能有后缀。
记号的字面量内容是在记号字符串表示中跟在第一个 U+0022(")之后且在最后一个 U+0022(")之前的字符序列。
字面量表达式的表示字符串是按如下方式从字面量内容派生的字符序列:
-
如果记号是 BYTE_STRING_LITERAL,则字面量内容中出现的以下任何形式的每个转义序列都被该转义序列的转义值替换。
这些替换按从左到右的顺序进行。例如,记号
b"\\x41"被转换为字符\x41。
- 如果记号是 RAW_BYTE_STRING_LITERAL,则表示字符串与字面量内容完全相同。
表达式的值是对一个静态分配的数组的引用,该数组按相同顺序包含表示字符串中每个字符的 Unicode 标量值。
Note
BYTE_STRING_LITERAL 和 RAW_BYTE_STRING_LITERAL 记号的允许形式确保这些规则始终产生在
u8范围内的数组元素值。
字节串字面量表达式示例:
#![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 字符串字面量表达式
C 字符串字面量表达式由单个 C_STRING_LITERAL 或 RAW_C_STRING_LITERAL 记号组成。
该表达式的类型是对标准库 CStr 类型的共享引用(具有 static 生命周期)。即类型为 &'static core::ffi::CStr。
记号不能有后缀。
记号的字面量内容是在记号字符串表示中跟在第一个 " 之后且在最后一个 " 之前的字符序列。
字面量表达式的表示字节是按如下方式从字面量内容派生的字节序列:
- 如果记号是 C_STRING_LITERAL,字面量内容被视为一个项序列,每一项要么是除
\之外的单个 Unicode 字符,要么是一个转义。该序列按如下方式转换为字节序列:- 每个单个 Unicode 字符贡献其 UTF-8 表示。
- 每个简单转义贡献其转义值的 Unicode 标量值。
- 每个 8 位转义贡献一个包含其转义值的 Unicode 标量值的单个字节。
- 每个 Unicode 转义贡献其转义值的 UTF-8 表示。
- 每个字符串续行转义不贡献任何字节。
- 如果记号是 RAW_C_STRING_LITERAL,则表示字节是字面量内容的 UTF-8 编码。
Note
C_STRING_LITERAL 和 RAW_C_STRING_LITERAL 记号的允许形式确保表示字节绝不包含空字节。
表达式的值是对一个静态分配的 CStr 的引用,其字节数组包含表示字节后跟一个空字节。
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"æ"; // 拉丁文小写字母 AE (U+00E6)
c"\u{00E6}"; // 拉丁文小写字母 AE (U+00E6)
c"\xC3\xA6"; // 拉丁文小写字母 AE (U+00E6)
c"\xE6".to_bytes(); // [230]
c"\u{00E6}".to_bytes(); // [195, 166]
}
整数字面量表达式
整数字面量表达式由单个 INTEGER_LITERAL 记号组成。
如果记号有后缀,则该后缀必须是原始整数类型之一:u8、i8、u16、i16、u32、i32、u64、i64、u128、i128、usize 或 isize,并且表达式具有该类型。
如果记号没有后缀,则表达式的类型通过类型推断确定:
- 如果可以从周围的程序上下文唯一地确定一个整数类型,则表达式具有该类型。
- 如果程序上下文对类型的约束不足,则默认为有符号 32 位整数
i32。
- 如果程序上下文对类型的约束过多,则被视为静态类型错误。
整数字面量表达式示例:
#![allow(unused)]
fn main() {
123; // 类型 i32
123i32; // 类型 i32
123u32; // 类型 u32
123_u32; // 类型 u32
let a: u64 = 123; // 类型 u64
0xff; // 类型 i32
0xff_u8; // 类型 u8
0o70; // 类型 i32
0o70_i16; // 类型 i16
0b1111_1111_1001_0000; // 类型 i32
0b1111_1111_1001_0000i64; // 类型 i64
0usize; // 类型 usize
}
表达式的值按如下方式从记号的字符串表示确定:
-
通过检查字符串的前两个字符选择一个整数基数,规则如下:
0b表示基数为 20o表示基数为 80x表示基数为 16- 否则基数为 10。
- 如果基数不是 10,则从字符串中删除前两个字符。
- 从字符串中删除任何后缀。
- 从字符串中删除任何下划线。
- 将字符串转换为
u128值,如同使用u128::from_str_radix并以所选基数转换。如果该值不适合u128,则为编译错误。
- 通过数值转换将
u128值转换为表达式的类型。
Note
如果字面量的值不适合表达式类型,最终转换将截断该值。
rustc包含一个名为overflowing_literals的 lint 检查,默认值为deny,当发生这种情况时拒绝表达式。
浮点字面量表达式
浮点字面量表达式有两种形式之一:
- 一个单独的 FLOAT_LITERAL 记号
- 一个有后缀且无基数指示符的单独 INTEGER_LITERAL 记号
如果记号有后缀,则该后缀必须是原始浮点类型之一:f32 或 f64,并且表达式具有该类型。
如果记号没有后缀,则表达式的类型通过类型推断确定:
- 如果可以从周围的程序上下文唯一地确定一个浮点类型,则表达式具有该类型。
- 如果程序上下文对类型的约束不足,则默认为
f64。
- 如果程序上下文对类型的约束过多,则被视为静态类型错误。
浮点字面量表达式示例:
#![allow(unused)]
fn main() {
123.0f64; // 类型 f64
0.1f64; // 类型 f64
0.1f32; // 类型 f32
12E+99_f64; // 类型 f64
5f32; // 类型 f32
let x: f64 = 2.; // 类型 f64
}
表达式的值按如下方式从记号的字符串表示确定:
- 从字符串中删除任何后缀。
- 从字符串中删除任何下划线。
- 将字符串转换为表达式的类型,如同使用
f32::from_str或f64::from_str。
Note
例如
-1.0是取反运算符应用于字面量表达式1.0,而不是单个浮点字面量表达式。
Note
inf和NaN不是字面量记号。可以使用f32::INFINITY、f64::INFINITY、f32::NAN和f64::NAN常量来代替字面量表达式。在rustc中,足够大到被求值为无穷大的字面量将触发overflowing_literalslint 检查。
布尔字面量表达式
布尔字面量表达式由关键字 true 或 false 之一组成。
表达式的类型是原始布尔类型,其值为:
- 如果关键字是
true,则为 true - 如果关键字是
false,则为 false
路径表达式
用作表达式上下文的路径表示局部变量或项。
解析为局部变量或静态变量的路径表达式是位置表达式;其他路径是值表达式。
使用 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 块的处理方式相同。
块表达式
Syntax
BlockExpression →
{
InnerAttribute*
Statements?
}
BlockExpressionNoInnerAttributes →
{
Statements?
}
Statements →
Statement+
| Statement+ ExpressionWithoutBlock
| ExpressionWithoutBlock
块表达式(或称块)是一种控制流表达式,也是项和变量声明的匿名命名空间作用域。
作为控制流表达式,块按顺序执行其组成的非项声明语句,然后是其可选的最终表达式。
作为匿名命名空间作用域,项声明仅在块自身内部有效,由 let 语句声明的变量从下一条语句开始直到块结束都有效。更多细节参见作用域章节。
块的语法是 {,然后是一些内部属性,然后是一些语句,然后是一个可选的表达式(称为最终操作数),最后是 }。
语句通常要求后跟分号,有两个例外:
- 项声明语句不需要后跟分号。
- 表达式语句通常要求后跟分号,除非其外围表达式是控制流表达式。
此外,语句之间允许额外的分号,但这些分号不影响语义。
在求值块表达式时,每条语句(除了项声明语句)按顺序执行。
然后执行最终操作数(如果有给出的话)。
当块包含最终操作数时,块具有该最终操作数的类型和值。
#![allow(unused)]
fn main() {
let x: u8 = { 0u8 }; // `0u8` 是最终操作数。
assert_eq!(x, 0);
let x: u8 = { (); 0u8 }; // 同上。
assert_eq!(x, 0);
}
#![allow(unused)]
fn main() {
let x: () = {}; // 没有最终操作数。
assert_eq!(x, ());
let x: () = { 0u8; }; // 同上。
assert_eq!(x, ());
}
当块不包含最终操作数且块发散时,块具有永不类型且没有最终值(因为其类型是无人居住的)。
#![allow(unused)]
fn main() {
fn f() -> ! { loop {}; } // 发散且没有最终操作数。
// ^^^^^^^^^^^^
// 函数体是块表达式。
}
Note
注意,块没有最终操作数与有显式的单元类型最终操作数是不同的。例如,即使此块发散,块的类型也是单元而非永不。
#![allow(unused)] fn main() { fn f() -> ! { loop {}; () } // 错误:类型不匹配。 // ^^^^^^^^^^^^^^^ 此块具有单元类型。 }
Note
作为控制流表达式,如果块表达式是表达式语句的外围表达式,则期望类型为
(),除非其后紧跟分号。
如果一个块的所有可达控制流路径都包含一个发散表达式,则该块被认为是发散的,除非该表达式是未被读取的位置表达式。
#![allow(unused)]
fn main() {
#![ feature(never_type) ]
fn no_control_flow() -> ! {
// 没有条件语句,因此整个函数体是发散的。
loop {}
}
fn control_flow_diverging() -> ! {
// 所有路径都是发散的,因此整个函数体是发散的。
if true {
loop {}
} else {
loop {}
}
}
fn control_flow_not_diverging() -> () {
// 某些路径不是发散的,因此整个块不是发散的。
if true {
()
} else {
loop {}
}
}
// 注意:这里使用了不稳定的 never 类型,该类型仅在
// Rust 的 nightly 通道上可用。这只是为了说明目的。
// 在稳定版 Rust 中也可能遇到这种情况,但需要更
// 复杂的示例。
struct Foo {
x: !,
}
fn make<T>() -> T { loop {} }
fn diverging_place_read() -> ! {
let foo = Foo { x: make() };
// 读取位置表达式产生一个发散块。
let _x = foo.x;
}
}
#![allow(unused)]
fn main() {
#![ feature(never_type) ]
fn make<T>() -> T { loop {} }
struct Foo {
x: !,
}
fn diverging_place_not_read() -> ! {
let foo = Foo { x: make() };
// 赋值给 `_` 意味着该位置未被读取。
let _ = foo.x;
} // 错误:类型不匹配。
}
块始终是值表达式,并在值表达式上下文中求值最后一个操作数。
Note
这可以用于在确实需要时强制移动值。例如,下面的示例在调用
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 块
Syntax
AsyncBlockExpression → async move? BlockExpression
async 块是块表达式的一种变体,它求值为一个 future。
块的最终表达式(如果有的话)决定 future 的结果值。
执行 async 块类似于执行闭包表达式:其直接效果是产生并返回一个匿名类型。
然而,闭包返回一个实现 std::ops::Fn trait 族之一的类型,而 async 块返回的类型实现 std::future::Future trait。
此类型的具体数据格式未作规定。
Note
rustc 生成的 future 类型大致等价于一个枚举,每个
await点对应一个变体,每个变体存储从其对应点恢复所需的数据。
2018 Edition differences
Async 块仅从 Rust 2018 起可用。
捕获模式
Async 块使用与闭包相同的捕获模式从环境中捕获变量。与闭包类似,当写为 async { .. } 时,每个变量的捕获模式将根据块的内容推断。而 async move { .. } 块则会将所有引用的变量移动到生成的 future 中。
异步上下文
因为 async 块构造一个 future,它们定义了一个异步上下文,其中又可以包含 await 表达式。异步上下文由 async 块以及异步函数体建立,异步函数的语义是通过 async 块来定义的。
控制流运算符
Async 块像函数边界一样运作,很像闭包。
因此,? 运算符和 return 表达式都影响 future 的输出,而不是外围函数或其他上下文。也就是说,async 块内的 return <expr> 将返回 <expr> 的结果作为 future 的输出。类似地,如果 <expr>? 传播错误,该错误将作为 future 的结果传播。
最后,break 和 continue 关键字不能用于从 async 块中跳出。因此以下代码是非法的:
#![allow(unused)]
fn main() {
loop {
async move {
break; // 错误[E0267]:`async` 块内的 `break`
}
}
}
const 块
Syntax
ConstBlockExpression → const BlockExpression
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 块表达式在运行时被执行,则常量保证被求值,即使其返回值被忽略:
#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
// 如果此代码曾经被执行,则断言肯定已在编译时被求值。
const { assert!(std::mem::size_of::<T>() > 0); }
// 此处我们可以有依赖于类型非零大小的 unsafe 代码。
/* ... */
42
}
}
如果 const 块表达式不在运行时被执行,它可能被求值也可能不被求值:
#![allow(unused)]
fn main() {
if false {
// 当程序构建时,panic 可能发生也可能不发生。
const { panic!(); }
}
}
unsafe 块
Syntax
UnsafeBlockExpression → unsafe BlockExpression
有关何时使用 unsafe 的更多信息,请参见 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() };
}
带标签的块表达式
带标签的块表达式在循环和其他可中断表达式一节中描述。
块表达式上的属性
在以下情况下,允许在块表达式的开花括号后直接放置内部属性:
- 函数和方法体。
- 循环体(
loop、while和for)。 - 用作语句的块表达式。
- 作为数组表达式、元组表达式、调用表达式和元组式结构体表达式元素的块表达式。
- 作为另一个块表达式的尾部表达式的块表达式。
在块表达式上有意义的属性是 cfg 和 lint 检查属性。
例如,此函数在 unix 平台上返回 true,在其他平台上返回 false。
#![allow(unused)]
fn main() {
fn is_unix_platform() -> bool {
#[cfg(unix)] { true }
#[cfg(not(unix))] { false }
}
}
运算符表达式
Syntax
OperatorExpression →
BorrowExpression
| DereferenceExpression
| TryPropagationExpression
| NegationExpression
| ArithmeticOrLogicalExpression
| ComparisonExpression
| LazyBooleanExpression
| TypeCastExpression
| AssignmentExpression
| CompoundAssignmentExpression
运算符由 Rust 语言为内置类型定义。
以下许多运算符也可以通过使用 std::ops 或 std::cmp 中的 trait 进行重载。
溢出
整数运算符在调试模式下编译时,溢出时会 panic。-C debug-assertions 和 -C overflow-checks 编译器标志可用于更直接地控制此行为。以下情况被视为溢出:
- 当
+、*或二元-创建的值大于可存储的最大值,或小于可存储的最小值时。
- 使用
/或%,其中左侧参数是有符号整数类型的最小整数,右侧参数是-1。由于历史原因,即使禁用-C overflow-checks也会执行这些检查。
- 使用
<<或>>,其中右侧参数大于或等于左侧参数类型中的位数,或者为负数。
Note
一元
-后字面量表达式的例外意味着像-128_i8或let j: i8 = -(128)这样的形式永远不会导致 panic,并且具有预期的值 -128。在这些情况下,字面量表达式已经具有其类型的最负值(例如,
128_i8的值为 -128),因为根据整数字面量表达式中的描述,整数字面量会被截断到其类型。由于二进制补码溢出惯例,对这些最负值的取反操作保持值不变。
在
rustc中,这些最负表达式也会被overflowing_literalslint 检查忽略。
借用运算符
Syntax
BorrowExpression →
( & | && ) Expression
| ( & | && ) mut Expression
| ( & | && ) raw const Expression
| ( & | && ) raw 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;
}
}
尽管 && 是单个记号(惰性 ‘and’ 运算符),但在借用表达式的上下文中使用时,它相当于两次借用:
#![allow(unused)]
fn main() {
// 相同的含义:
let a = && 10;
let a = & & 10;
// 相同的含义:
let a = &&&& mut 10;
let a = && && mut 10;
let a = & & & & mut 10;
}
裸借用运算符
&raw const 和 &raw mut 是裸借用运算符。
这些运算符的操作数表达式在位置表达式上下文中求值。
&raw const expr 创建一个类型为 *const T 的指向给定位置的 const 裸指针,&raw mut expr 创建一个类型为 *mut T 的可变裸指针。
每当位置表达式可能求值为未正确对齐的位置或未按类型存储有效值的位置时,或者每当创建引用会引入不正确的别名假设时,必须使用裸借用运算符而不是借用运算符。在这些情况下,使用借用运算符会因创建无效引用而导致未定义行为,但裸指针仍然可以被构造。
以下是通过 packed 结构体创建指向未对齐位置的裸指针的示例:
#![allow(unused)]
fn main() {
#[repr(packed)]
struct Packed {
f1: u8,
f2: u16,
}
let packed = Packed { f1: 1, f2: 2 };
// `&packed.f2` 会创建一个未对齐的引用,因此是未定义行为!
let raw_f2 = &raw const packed.f2;
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);
}
以下是创建指向不包含有效值的位置的裸指针的示例:
#![allow(unused)]
fn main() {
use std::mem::MaybeUninit;
struct Demo {
field: bool,
}
let mut uninit = MaybeUninit::<Demo>::uninit();
// `&uninit.as_mut().field` 会创建一个指向未初始化 `bool` 的引用,
// 因此是未定义行为!
let f1_ptr = unsafe { &raw mut (*uninit.as_mut_ptr()).field };
unsafe { f1_ptr.write(true); }
let init = unsafe { uninit.assume_init() };
}
解引用运算符
Syntax
DereferenceExpression → * Expression
*(解引用)运算符也是一元前缀运算符。
如果表达式的类型为 &mut T、*mut T 或 Box<T>,并且是局部变量、局部变量的(嵌套)字段或可变的位置表达式,则可以赋值给结果内存位置。
解引用裸指针需要 unsafe。
对于非指针类型,*x 等价于不可变位置表达式上下文中的 *std::ops::Deref::deref(&x) 和可变位置表达式上下文中的 *std::ops::DerefMut::deref_mut(&mut x),不同之处在于当 *x 经历临时值生命周期延长时,被解引用的表达式 x 的临时值作用域也会被延长。
#![allow(unused)]
fn main() {
struct NoCopy;
let a = &7;
assert_eq!(*a, 7);
let b = &mut 9;
*b = 11;
assert_eq!(*b, 11);
let c = Box::new(NoCopy);
let d: NoCopy = *c;
}
#![allow(unused)]
fn main() {
// 持有 `String::new()` 结果的临时值被延长到块的末尾,
// 因此 `x` 可以在后续语句中使用。
let x = &*String::new();
x;
}
#![allow(unused)]
fn main() {
// 持有 `String::new()` 结果的临时值在语句末尾被丢弃,
// 因此之后使用 `y` 是错误的。
let y = &*std::ops::Deref::deref(&String::new()); // 错误
y;
}
try 传播表达式
Syntax
TryPropagationExpression → Expression ?
try 传播表达式使用内部表达式的值和 Try trait 来决定是否产生一个值,如果要产生,则产生什么值,或者是否向调用者返回一个值,如果要返回,则返回什么值。
Example
#![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()) }#![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); }use std::ops::ControlFlow; pub struct TreeNode<T> { value: T, left: Option<Box<TreeNode<T>>>, right: Option<Box<TreeNode<T>>>, } impl<T> TreeNode<T> { pub fn traverse_inorder<B>(&self, f: &mut impl FnMut(&T) -> ControlFlow<B>) -> ControlFlow<B> { if let Some(left) = &self.left { left.traverse_inorder(f)?; } f(&self.value)?; if let Some(right) = &self.right { right.traverse_inorder(f)?; } ControlFlow::Continue(()) } } fn main() { let n = TreeNode { value: 1, left: Some(Box::new(TreeNode{value: 2, left: None, right: None})), right: None, }; let v = n.traverse_inorder(&mut |t| { if *t == 2 { ControlFlow::Break("found") } else { ControlFlow::Continue(()) } }); assert_eq!(v, ControlFlow::Break("found")); }
Note
Trytrait 目前是不稳定的,因此不能为用户类型实现。try 传播表达式目前大致等价于:
#![allow(unused)] fn main() { #![ feature(try_trait_v2) ] fn example() -> Result<(), ()> { let expr = Ok(()); match core::ops::Try::branch(expr) { core::ops::ControlFlow::Continue(val) => val, core::ops::ControlFlow::Break(residual) => return core::ops::FromResidual::from_residual(residual), } Ok(()) } }
Note
try 传播运算符有时被称为问号运算符、
?运算符或try 运算符。
try 传播运算符可以应用于以下类型的表达式:
Result<T, E>Result::Ok(val)求值为val。Result::Err(e)返回Result::Err(From::from(e))。
Option<T>Option::Some(val)求值为val。Option::None返回Option::None。
ControlFlow<B, C>ControlFlow::Continue(c)求值为c。ControlFlow::Break(b)返回ControlFlow::Break(b)。
Poll<Result<T, E>>Poll::Ready(Ok(val))求值为Poll::Ready(val)。Poll::Ready(Err(e))返回Poll::Ready(Err(From::from(e)))。Poll::Pending求值为Poll::Pending。
Poll<Option<Result<T, E>>>Poll::Ready(Some(Ok(val)))求值为Poll::Ready(Some(val))。Poll::Ready(Some(Err(e)))返回Poll::Ready(Some(Err(From::from(e))))。Poll::Ready(None)求值为Poll::Ready(None)。Poll::Pending求值为Poll::Pending。
取反运算符
Syntax
NegationExpression →
- Expression
| ! Expression
这是最后两个一元运算符。
此表总结了它们在原始类型上的行为以及用于为其他类型重载这些运算符的 trait。请记住,有符号整数始终使用二进制补码表示。所有这些运算符的操作数都在值表达式上下文中求值,因此会被移动或复制。
| 符号 | 整数 | bool | 浮点数 | 重载 Trait |
|---|---|---|---|---|
- | 取反* | 取反 | std::ops::Neg | |
! | 按位 NOT | 逻辑 NOT | std::ops::Not |
* 仅对有符号整数类型。
以下是这些运算符的一些示例
#![allow(unused)]
fn main() {
let x = 6;
assert_eq!(-x, -6);
assert_eq!(!x, -7);
assert_eq!(true, !false);
}
算术和逻辑二元运算符
Syntax
ArithmeticOrLogicalExpression →
Expression + Expression
| Expression - Expression
| Expression * Expression
| Expression / Expression
| Expression % Expression
| Expression & Expression
| Expression | Expression
| Expression ^ Expression
| Expression << Expression
| Expression >> Expression
二元运算符表达式都使用中缀表示法书写。
此表总结了算术和逻辑二元运算符在原始类型上的行为以及用于为其他类型重载这些运算符的 trait。请记住,有符号整数始终使用二进制补码表示。所有这些运算符的操作数都在值表达式上下文中求值,因此会被移动或复制。
| 符号 | 整数 | bool | 浮点数 | 重载 Trait | 重载复合赋值 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 | |
% | 取余**† | 取余 | std::ops::Rem | std::ops::RemAssign | |
& | 按位 AND | 逻辑 AND | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位 OR | 逻辑 OR | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位 XOR | 逻辑 XOR | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移*** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法向零舍入。
** Rust 使用的取余定义为截断除法。给定 remainder = dividend % divisor,余数将与被除数具有相同的符号。
*** 对有符号整数类型进行算术右移,对无符号整数类型进行逻辑右移。
† 对于整数类型,除以零会 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);
}
比较运算符
Syntax
ComparisonExpression →
Expression == Expression
| Expression != Expression
| Expression > Expression
| Expression < Expression
| Expression >= Expression
| Expression <= Expression
比较运算符也是既为原始类型定义,也为标准库中的许多类型定义。
链接比较运算符时需要使用括号。例如,表达式 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);
}
这意味着操作数不必被移出。
| 符号 | 含义 | 重载方法 |
|---|---|---|
== | 等于 | 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");
}
惰性布尔运算符
Syntax
LazyBooleanExpression →
Expression || Expression
| Expression && Expression
运算符 || 和 && 可以应用于布尔类型的操作数。|| 运算符表示逻辑“或“,&& 运算符表示逻辑“与“。
它们与 | 和 & 的不同之处在于,只有当左侧操作数尚未确定表达式的结果时,才求值右侧操作数。即,只有当左侧操作数求值为 false 时,|| 才求值其右侧操作数;只有当左侧操作数求值为 true 时,&& 才求值其右侧操作数。
#![allow(unused)]
fn main() {
let x = false || true; // true
let y = false && panic!(); // false,不求值 `panic!()`
}
类型转换表达式
Syntax
TypeCastExpression → Expression as 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 可用于显式执行强制转换,以及以下附加的转换。任何不符合强制转换规则或表中条目的转换都是编译错误。此处 *T 表示 *const T 或 *mut T。m 表示引用类型中的可选 mut 以及指针类型中的 mut 或 const。
e 的类型 | U | e as U 执行的转换 |
|---|---|---|
| 整数或浮点类型 | 整数或浮点类型 | 数值转换 |
| 枚举 | 整数类型 | 枚举转换 |
bool 或 char | 整数类型 | 原始类型到整数转换 |
u8 | char | u8 到 char 转换 |
*T | *V(当兼容时) | 指针到指针转换 |
*T,其中 T: Sized | 整数类型 | 指针到地址转换 |
| 整数类型 | *V,其中 V: Sized | 地址到指针转换 |
&m₁ [T; n] | *m₂ T 1 | 数组到指针转换 |
*m₁ [T; n] | *m₂ T 1 | 数组到指针转换 |
| 函数项 | 函数指针 | 函数项到函数指针转换 |
| 函数项 | *V,其中 V: Sized | 函数项到指针转换 |
| 函数项 | 整数 | 函数项到地址转换 |
| 函数指针 | *V,其中 V: Sized | 函数指针到指针转换 |
| 函数指针 | 整数 | 函数指针到地址转换 |
| 闭包 2 | 函数指针 | 闭包到函数指针转换 |
语义
数值转换
-
相同大小的两个整数之间的转换(例如 i32 -> u32)是无操作(Rust 使用二进制补码处理固定整数的负值)
#![allow(unused)] fn main() { assert_eq!(42i8 as u8, 42u8); assert_eq!(-1i8 as u8, 255u8); assert_eq!(255u8 as i8, -1i8); assert_eq!(-1i16 as u16, 65535u16); }
-
从较大整数到较小整数的转换(例如 u32 -> u8)将截断
#![allow(unused)] fn main() { assert_eq!(42u16 as u8, 42u8); assert_eq!(1234u16 as u8, 210u8); assert_eq!(0xabcdu16 as u8, 0xcdu8); assert_eq!(-42i16 as i8, -42i8); assert_eq!(1234u16 as i8, -46i8); assert_eq!(0xabcdi32 as i8, -51i8); }
-
从较小整数到较大整数的转换(例如 u8 -> u32)将
- 如果源是无符号的,则零扩展
- 如果源是有符号的,则符号扩展
#![allow(unused)] fn main() { assert_eq!(42i8 as i16, 42i16); assert_eq!(-17i8 as i16, -17i16); assert_eq!(0b1000_1010u8 as u16, 0b0000_0000_1000_1010u16, "零扩展"); assert_eq!(0b0000_1010i8 as i16, 0b0000_0000_0000_1010i16, "符号扩展 0"); assert_eq!(0b1000_1010u8 as i8 as i16, 0b1111_1111_1000_1010u16 as i16, "符号扩展 1"); }
-
从浮点数到整数的转换将向零舍入
NaN将返回0- 大于最大整数值的值(包括
INFINITY)将饱和到整数类型的最大值。 - 小于最小整数值的值(包括
NEG_INFINITY)将饱和到整数类型的最小值。
#![allow(unused)] fn main() { assert_eq!(42.9f32 as i32, 42); assert_eq!(-42.9f32 as i32, -42); assert_eq!(42_000_000f32 as i32, 42_000_000); assert_eq!(std::f32::NAN as i32, 0); assert_eq!(1_000_000_000_000_000f32 as i32, 0x7fffffffi32); assert_eq!(std::f32::NEG_INFINITY as i32, -0x80000000i32); }
-
从整数到浮点数的转换将产生最接近的可能浮点数 *
- 如有必要,舍入根据
roundTiesToEven模式进行 *** - 溢出时,产生无穷大(与输入符号相同)
- 注意:在当前数值类型集合中,溢出仅在
u128 as f32中发生,当值大于或等于f32::MAX + (0.5 ULP)时
#![allow(unused)] fn main() { assert_eq!(1337i32 as f32, 1337f32); assert_eq!(123_456_789i32 as f32, 123_456_790f32, "已舍入"); assert_eq!(0xffffffff_ffffffff_ffffffff_ffffffff_u128 as f32, std::f32::INFINITY); } - 如有必要,舍入根据
-
从 f32 到 f64 的转换是完美且无损失的
#![allow(unused)] fn main() { assert_eq!(1_234.5f32 as f64, 1_234.5f64); assert_eq!(std::f32::INFINITY as f64, std::f64::INFINITY); assert!((std::f32::NAN as f64).is_nan()); }
-
从 f64 到 f32 的转换将产生最接近的可能 f32 **
- 如有必要,舍入根据
roundTiesToEven模式进行 *** - 溢出时,产生无穷大(与输入符号相同)
#![allow(unused)] fn main() { assert_eq!(1_234.5f64 as f32, 1_234.5f32); assert_eq!(1_234_567_891.123f64 as f32, 1_234_567_890f32, "已舍入"); assert_eq!(std::f64::INFINITY as f32, std::f32::INFINITY); assert!((std::f64::NAN as f32).is_nan()); } - 如有必要,舍入根据
* 如果硬件本身不支持具有此舍入模式和溢出行为的整数到浮点数转换,这些转换可能会比预期慢。
** 如果硬件本身不支持具有此舍入模式和溢出行为的 f64 到 f32 转换,这些转换可能会比预期慢。
*** 如 IEEE 754-2008 §4.3.1 定义:选择最接近的浮点数,如果恰好处于两个浮点数中间,则选择最低有效位为偶数的那个。
枚举转换
将枚举转换为其判别值,然后根据需要应用数值转换。转换仅限于以下类型的枚举:
#![allow(unused)]
fn main() {
enum Enum { A, B, C }
assert_eq!(Enum::A as i32, 0);
assert_eq!(Enum::B as i32, 1);
assert_eq!(Enum::C as i32, 2);
}
如果枚举实现了 Drop,则不允许转换。
原始类型到整数转换
false转换为0,true转换为1char转换为码点值,然后根据需要应用数值转换。
#![allow(unused)]
fn main() {
assert_eq!(false as i32, 0);
assert_eq!(true as i32, 1);
assert_eq!('A' as i32, 65);
assert_eq!('Ö' as i32, 214);
}
u8 到 char 转换
转换为具有对应码点的 char。
#![allow(unused)]
fn main() {
assert_eq!(65u8 as char, 'A');
assert_eq!(214u8 as char, 'Ö');
}
指针到地址转换
从裸指针到整数的转换产生所引用内存的机器地址。如果整数类型小于指针类型,地址可能被截断;使用 usize 可避免此问题。
地址到指针转换
从整数到裸指针的转换将整数解释为内存地址,并产生一个引用该内存的指针。
Warning
这与 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);
}
指针到指针转换
*const T / *mut T 可以转换为 *const U / *mut U,具有以下行为:
-
如果
T和U都是固定大小的,则指针原样返回不变。Example
#![allow(unused)] fn main() { let x: i32 = 42; let p1: *const i32 = &x; let p2: *const u8 = p1 as *const u8; // 指针地址保持不变。 assert_eq!(p1 as usize, p2 as usize); }
-
如果
T是非固定大小的而U是固定大小的,该转换会丢弃完成宽指针T的所有元数据,并产生一个由非固定大小指针的数据部分组成的瘦指针U。Example
#![allow(unused)] fn main() { let slice: &[i32] = &[1, 2, 3]; let ptr: *const [i32] = slice as *const [i32]; // 从宽指针 (*const [i32]) 到瘦指针 (*const i32) 的转换 // 丢弃长度元数据。 let data_ptr: *const i32 = ptr as *const i32; assert_eq!(unsafe { *data_ptr }, 1); }
- 如果
T和U都是非固定大小的,指针也原样返回不变。特别地,元数据被精确保留。只有根据以下规则元数据兼容时才能执行转换:
-
当
T和U是具有切片元数据的非固定大小类型时,它们始终兼容。切片的元数据是元素数量,因此将*[u16] -> *[u8]转换是合法的,但会导致字节数减半。Example
#![allow(unused)] fn main() { let slice: &[u16] = &[1, 2, 3]; let ptr: *const [u16] = slice as *const [u16]; let byte_ptr: *const [u8] = ptr as *const [u8]; assert_eq!(byte_ptr.len(), 3); }
- 当
T和U是具有 trait 对象元数据的非固定大小类型时,仅当满足以下所有条件时元数据才兼容:-
主 trait 必须相同。
Example
#![allow(unused)] fn main() { trait Foo {} trait Bar {} impl Foo for i32 {} impl Bar for i32 {} let x: i32 = 42; let ptr_foo: *const dyn Foo = &x as *const dyn Foo; // 不能转换到不同的主 trait。 let ptr_bar: *const dyn Bar = ptr_foo as *const dyn Bar; // 错误 } -
自动 trait 可以被移除。
Example
#![allow(unused)] fn main() { trait Foo {} struct S; impl Foo for S {} unsafe impl Send for S {} let s = S; let ptr_send: *const (dyn Foo + Send) = &s; // 移除自动 trait。 let ptr_no_send: *const dyn Foo = ptr_send as *const dyn Foo; } -
自动 trait 仅当它们是主 trait 的超级 trait 时才可以被添加。
Example
#![allow(unused)] fn main() { trait Foo: Send {} struct S; impl Foo for S {} unsafe impl Send for S {} let s = S; let ptr_no_send: *const dyn Foo = &s; // 添加自动 trait。 let ptr_send: *const (dyn Foo + Send) = ptr_no_send as *const (dyn Foo + Send); }#![allow(unused)] fn main() { trait Foo {} struct S; impl Foo for S {} unsafe impl Send for S {} let s = S; let ptr_no_send: *const dyn Foo = &s; // 同上,除了 trait Foo 没有 Send 作为超级 trait。 let ptr_send: *const (dyn Foo + Send) = ptr_no_send as *const (dyn Foo + Send); // 错误 } -
尾部生命周期只能被缩短。
Example
#![allow(unused)] fn main() { trait Foo {} fn shorten_lifetime<'long: 'short, 'short>( ptr: *const (dyn Foo + 'long), ) -> *const (dyn Foo + 'short) { // 缩短生命周期是允许的。 ptr as *const (dyn Foo + 'short) } }#![allow(unused)] fn main() { trait Foo {} fn lengthen_lifetime<'long: 'short, 'short>( ptr: *const (dyn Foo + 'short), ) -> *const (dyn Foo + 'long) { // 不允许转换到更长的生命周期。 ptr as *const (dyn Foo + 'long) // 错误 } } -
泛型(包括生命周期)和关联类型必须完全匹配。
Example
#![allow(unused)] fn main() { trait Generic<T> {} impl Generic<i32> for () {} impl Generic<u32> for () {} let x = (); let ptr_i32: *const dyn Generic<i32> = &x; // 不能转换到不同的泛型参数。 let ptr_u32: *const dyn Generic<u32> = ptr_i32 as *const dyn Generic<u32>; // 错误 }#![allow(unused)] fn main() { trait HasType { type Output; } trait Generic<'x, T> {} fn cast_via_associated<'a, 'b, A, B>( ptr: *const dyn Generic<'a, A::Output>, ) -> *const dyn Generic<'b, B::Output> where 'a: 'b, 'b: 'a, A: HasType, B: HasType<Output = A::Output>, // 强制相等 { ptr as *const dyn Generic<'b, B::Output> } }
-
-
当
T或U是其最后一个字段为非固定大小的结构体或元组类型时,它具有与其最后一个字段相同的元数据和兼容性规则。Example
#![allow(unused)] fn main() { struct Wrapper(u32, [u8]); let slice: &[u8] = &[1, 2, 3]; let ptr: *const [u8] = slice; // 转换为最后一个字段为非固定大小类型 `[u8]` 的 // 结构体时,元数据(长度 3)被保留。 let wrapper_ptr: *const Wrapper = ptr as *const Wrapper; // 转换回来时也被保留。 let ptr_back: *const [u8] = wrapper_ptr as *const [u8]; assert_eq!(ptr_back.len(), 3); }
赋值表达式
Syntax
AssignmentExpression → Expression = Expression
赋值表达式将一个值移动到指定位置。
赋值表达式由一个可变的赋值目标表达式(赋值目标操作数)、后跟一个等号(=)和一个值表达式(所赋值的值操作数)组成。
在其最基本的形式中,赋值目标表达式是位置表达式,我们首先讨论这种情况。
下面将讨论解构赋值的更通用情况,但这种情况下总是分解为对位置表达式的顺序赋值,后者可以被视为更基本的情况。
基本赋值
求值赋值表达式从求值其操作数开始。所赋值的值操作数首先被求值,然后求值赋值目标表达式。
对于解构赋值,赋值目标表达式的子表达式从左到右求值。
Note
这与其他表达式不同,因为右侧操作数在左侧之前求值。
然后它的效果是首先丢弃赋值位置上的值,除非该位置是未初始化的局部变量或局部变量的未初始化字段。
接下来它将所赋值的值复制或移动到赋值位置。
赋值表达式始终产生单元值。
示例:
#![allow(unused)]
fn main() {
let mut x = 0;
let y = 0;
x = y;
}
解构赋值
解构赋值是变量声明的解构模式匹配的对应物,允许赋值给复杂值,例如元组或结构体。例如,我们可以交换两个可变变量:
#![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;
}
}
不禁止标识符在单个赋值目标表达式中多次使用。
注意,默认绑定模式不适用于脱糖后的表达式。
Note
脱糖限制了赋值值操作数(RHS)的临时值作用域。
在基本赋值中,临时值在外围临时值作用域的末尾被丢弃。下面是语句的末尾。因此,赋值和使用是允许的。
#![allow(unused)] fn main() { fn temp() {} fn f<T>(x: T) -> T { x } let x; (x = f(&temp()), x); // OK }反之,在解构赋值中,临时值在脱糖中
let语句的末尾被丢弃。由于这发生在我们尝试赋值给x之前,以下代码失败。#![allow(unused)] fn main() { fn temp() {} fn f<T>(x: T) -> T { x } let x; [x] = [f(&temp())]; // 错误 }这会脱糖为:
#![allow(unused)] fn main() { fn temp() {} fn f<T>(x: T) -> T { x } let x; { let [_x] = [f(&temp())]; // ^ // 临时值在此处被丢弃。 x = _x; // 错误 } }
Note
由于脱糖,解构赋值的赋值值操作数(RHS)是新引入块内的一个延长表达式。
下面,由于临时值作用域被延长到此引入块的末尾,赋值是允许的。
#![allow(unused)] fn main() { fn temp() {} let x; [x] = [&temp()]; // OK }这会脱糖为:
#![allow(unused)] fn main() { fn temp() {} let x; { let [_x] = [&temp()]; x = _x; } // OK }然而,如果我们尝试使用
x,即使在同一条语句内,也会得到错误,因为临时值在此引入块的末尾被丢弃。#![allow(unused)] fn main() { fn temp() {} let x; ([x] = [&temp()], x); // 错误 }这会脱糖为:
#![allow(unused)] fn main() { fn temp() {} let x; ( { let [_x] = [&temp()]; x = _x; }, // <-- 临时值在此处被丢弃。 x, // 错误 ); }
复合赋值表达式
Syntax
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);
}
复合赋值的语法是一个可变的位置表达式(被赋值操作数),后跟一个运算符和一个 = 作为一个整体记号(无空白),再后跟一个值表达式(修改操作数)。
与其他位置操作数不同,被赋值的位置操作数必须是位置表达式。
尝试使用值表达式是编译错误,而不是将其提升为临时值。
复合赋值表达式的求值取决于操作数的类型。
如果在单态化之前已知两个操作数的类型都是原始类型,则首先求值右侧,然后求值左侧,通过将运算符应用于两侧的值来修改左侧求值给出的位置。
use core::{num::Wrapping, ops::AddAssign};
trait Equate {}
impl<T> Equate for (T, T) {}
fn f1(x: (u8,)) {
let mut order = vec![];
// 先求值 RHS,因为两个操作数都是原始类型。
{ order.push(2); x }.0 += { order.push(1); x }.0;
assert!(order.is_sorted());
}
fn f2(x: (Wrapping<u8>,)) {
let mut order = vec![];
// 先求值 LHS,因为 `Wrapping<_>` 不是原始类型。
{ order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
assert!(order.is_sorted());
}
fn f3<T: AddAssign<u8> + Copy>(x: (T,)) where (T, u8): Equate {
let mut order = vec![];
// 先求值 LHS,因为操作数之一是泛型参数,即使该泛型参数
// 可以因 where 子句约束而与原始类型统一。
{ order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
assert!(order.is_sorted());
}
fn main() {
f1((0u8,));
f2((Wrapping(0u8),));
// 我们提供原始类型作为泛型参数,但这不影响
// 单态化时 `f3` 中的求值顺序。
f3::<u8>((0u8,));
}
Note
这是不寻常的。在其他地方,从左到右求值是常态。
有关更多示例,请参见求值顺序测试。
否则,此表达式是使用运算符对应 trait 的语法糖(参见 expr.arith-logic.behavior),并以左侧作为接收者、右侧作为下一个参数来调用其方法。
例如,以下两条语句是等价的:
#![allow(unused)]
fn main() {
use std::ops::AddAssign;
fn f<T: AddAssign + Copy>(mut x: T, y: T) {
x += y; // 语句 1。
x.add_assign(y); // 语句 2。
}
}
Note
令人惊讶的是,将其进一步脱糖为完全限定方法调用是不等价的,因为当通过自动引用获取对第一个操作数的可变引用时,借检查器有特殊行为。
#![allow(unused)] fn main() { use std::ops::AddAssign; fn f<T: AddAssign + Copy>(mut x: T) { // 这里我们将 `x` 同时用作 LHS 和 RHS。因为调用 trait 方法 // 所需的 LHS 可变引用是通过自动引用隐式获取的,所以这是可以的。 x += x; //~ OK x.add_assign(x); //~ OK } }#![allow(unused)] fn main() { use std::ops::AddAssign; fn f<T: AddAssign + Copy>(mut x: T) { // 我们不能将上述代码脱糖为以下代码,因为一旦我们获取了 `x` // 的可变引用来传递第一个参数,我们就不能在第二个参数中 // 按值传递 `x`,因为可变引用仍然存活。 <T as AddAssign>::add_assign(&mut x, x); //~^ 错误:无法使用 `x`,因为它已被可变借用 } }#![allow(unused)] fn main() { use std::ops::AddAssign; fn f<T: AddAssign + Copy>(mut x: T) { // 同上。 (&mut x).add_assign(x); //~^ 错误:无法使用 `x`,因为它已被可变借用 } }
与普通赋值表达式一样,复合赋值表达式始终产生单元值。
Warning
避免编写依赖于复合赋值中操作数求值顺序的代码,因为它可能不寻常且令人惊讶。
分组表达式
Syntax
GroupedExpression → ( Expression )
括号表达式包裹一个单独的表达式,求值为该表达式。括号表达式的语法是 (,然后是一个表达式(称为被括操作数),然后是 )。
括号表达式求值为被括操作数的值。
如果被括操作数是位置表达式,则括号表达式也是位置表达式;如果被括操作数是值表达式,则括号表达式也是值表达式。
括号可用于显式修改表达式中子表达式的优先级顺序。
括号表达式的示例:
#![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");
}
数组和数组索引表达式
数组表达式
Syntax
ArrayExpression → [ ArrayElements? ]
ArrayElements →
Expression ( , Expression )* ,?
| Expression ; Expression
数组表达式构造数组。数组表达式有两种形式。
第一种形式列出数组中的每个值。
此形式的语法是用方括号括起来的、以逗号分隔的同一类型表达式列表。
这将生成一个包含这些值的数组,顺序与它们的书写顺序一致。
第二种形式的语法是用方括号括起来的、由分号(;)分隔的两个表达式。
; 之前的表达式称为重复操作数。
; 之后的表达式称为长度操作数。
长度操作数必须是推断常量或类型为 usize 的常量表达式(例如字面量或常量项)。
#![allow(unused)]
fn main() {
const C: usize = 1;
let _: [u8; C] = [0; 1]; // 字面量。
let _: [u8; C] = [0; C]; // 常量项。
let _: [u8; C] = [0; _]; // 推断常量。
let _: [u8; C] = [0; (((_)))]; // 推断常量。
}
Note
在数组表达式中,推断常量被解析为表达式,但在语义上被视为一种单独的const 泛型参数。
这种形式的数组表达式创建一个数组,长度等于长度操作数的值,每个元素都是重复操作数的副本。即 [a; b] 创建一个包含 b 个 a 值副本的数组。
如果长度操作数的值大于 1,则要求重复操作数的类型实现 Copy,或是一个 const 块表达式,或是一个指向常量项的路径。
当重复操作数是 const 块或指向常量项的路径时,它会被求值长度操作数指定的次数。
如果该值为 0,则 const 块或常量项根本不会被求值。
对于既不是 const 块也不是指向常量项的路径的表达式,它会被正好求值一次,然后将结果复制长度操作数的值次。
#![allow(unused)]
fn main() {
[1, 2, 3, 4];
["a", "b", "c", "d"];
[0; 128]; // 包含 128 个零的数组
[0u8, 0u8, 0u8, 0u8,];
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 二维数组
const EMPTY: Vec<i32> = Vec::new();
[EMPTY; 2];
}
数组和切片索引表达式
Syntax
IndexExpression → Expression [ Expression ]
数组和切片类型的值可以通过在其后跟一个用方括号括起来的类型为 usize 的表达式(索引)来索引。当数组可变时,所产生的内存位置可以被赋值。
对于其他类型,索引表达式 a[b] 等价于 *std::ops::Index::index(&a, b),或在可变位置表达式上下文中等价于 *std::ops::IndexMut::index_mut(&mut a, b),不同之处在于当索引表达式经历临时值生命周期延长时,被索引的表达式 a 的临时值作用域也会被延长。与方法一样,Rust 也会在 a 上重复插入解引用操作以找到实现。
#![allow(unused)]
fn main() {
// 持有 `vec![()]` 结果的临时值被延长到块的末尾,
// 因此 `x` 可以在后续语句中使用。
let x = &vec![()][0];
x;
}
#![allow(unused)]
fn main() {
// 持有 `vec![()]` 结果的临时值在语句末尾被丢弃,
// 因此之后使用 `y` 是错误的。
let y = &*std::ops::Index::index(&vec![()], 0); // 错误
y;
}
数组和切片的索引从零开始。
数组访问是常量表达式,因此可以在编译时使用常量索引值检查边界。否则将在运行时进行检查,如果失败将使线程进入panic 状态。
#![allow(unused)]
fn main() {
// lint 默认为 deny。
#![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;
let y = (["a", "b"])[n]; // panic
let arr = ["a", "b"];
arr[10]; // 警告:索引越界
}
数组索引表达式可以通过实现 Index 和 IndexMut trait 为非数组和切片类型实现。
元组和元组索引表达式
元组表达式
Syntax
TupleExpression → ( TupleElements? )
TupleElements → ( Expression , )+ Expression?
元组表达式构造元组值。
元组表达式的语法是用括号括起来的、以逗号分隔的表达式列表,称为元组初始化操作数。
1 元元组表达式需要在其元组初始化操作数后面加一个逗号,以消除与括号表达式的歧义。
元组表达式是值表达式,求值为一个新构造的元组类型值。
元组初始化操作数的数量是所构造元组的元数。
没有任何元组初始化操作数的元组表达式产生单元元组。
对于其他元组表达式,第一个写入的元组初始化操作数初始化字段 0,后续操作数初始化下一个更高编号的字段。例如,在元组表达式 ('a', 'b', 'c') 中,'a' 初始化字段 0 的值,'b' 初始化字段 1,'c' 初始化字段 2。
元组表达式及其类型示例:
| 表达式 | 类型 |
|---|---|
() | ()(单元) |
(0.0, 4.5) | (f64, f64) |
("x".to_string(), ) | (String, ) |
("a", 4usize, true) | (&'static str, usize, bool) |
元组索引表达式
Syntax
TupleIndexingExpression → Expression . TUPLE_INDEX
元组索引表达式的语法是一个表达式(称为元组操作数),后跟一个 .,最后是一个元组索引。
元组索引的语法是一个十进制字面量,没有前导零、下划线或后缀。例如 0 和 2 是有效的元组索引,但 01、0_ 和 0i32 不是。
元组索引必须是元组操作数类型的一个字段名。
元组索引表达式的求值除了求值其元组操作数外没有副作用。作为位置表达式,它求值为元组操作数中与元组索引同名的字段的位置。
元组索引表达式示例:
#![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);
}
Note
与字段访问表达式不同,元组索引表达式可以是调用表达式的函数操作数,因为它不会与方法调用混淆,因为方法名不能是数字。
Note
尽管数组和切片也有元素,但必须使用数组或切片索引表达式或切片模式来访问它们的元素。
结构体表达式
Syntax
StructExpression →
PathInExpression { ( StructExprFields | StructBase )? }
StructExprFields →
StructExprField ( , StructExprField )* ( , StructBase | ,? )
StructExprField →
OuterAttribute*
(
IDENTIFIER
| ( IDENTIFIER | TUPLE_INDEX ) : Expression
)
StructBase → .. Expression
结构体表达式创建一个结构体、枚举或联合体值。它由一个指向结构体、枚举变体或联合体项的路径以及该项字段的值组成。
以下是结构体表达式的示例:
#![allow(unused)]
fn main() {
struct Point { x: f64, y: f64 }
struct NothingInMe { }
mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } }
enum Enum { Variant {} }
Point {x: 10.0, y: 20.0};
NothingInMe {};
let u = game::User {name: "Joe", age: 35, score: 100_000};
Enum::Variant {};
}
Note
元组结构体和元组枚举变体通常使用调用表达式来实例化,该表达式引用值命名空间中的构造器。这与使用花括号引用类型命名空间中构造器的结构体表达式是不同的。
#![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` 值。 enum Version { Triple(i32, i32, i32) }; Version::Triple(0, 0, 0); let f = Version::Triple; let ver = f(8, 6, 7); }调用路径的最后一段不能引用类型别名:
#![allow(unused)] fn main() { trait Tr { type T; } impl<T> Tr for T { type T = T; } struct Tuple(); enum Enum { Tuple() } // <Unit as Tr>::T(); // 导致错误 -- `::T` 是类型,不是值 <Enum as Tr>::T::Tuple(); // OK }
单元结构体和单元枚举变体通常使用路径表达式来实例化,该表达式引用值命名空间中的常量。
#![allow(unused)] fn main() { struct Gamma; // Gamma 单元值,引用值命名空间中的常量。 let a = Gamma; // 与 `a` 完全相同的值,但使用引用类型命名空间的 // 结构体表达式构造。 let b = Gamma {}; enum ColorSpace { Oklch } let c = ColorSpace::Oklch; let d = ColorSpace::Oklch {}; }
字段结构体表达式
用花括号括起字段的结构体表达式允许你以任意顺序为每个单独的字段指定值。字段名与其值之间用冒号分隔。
联合体类型的值只能使用此语法创建,并且必须恰好指定一个字段。
函数式更新语法
构造结构体类型值的结构体表达式可以用 .. 后跟一个表达式作为结尾,以表示函数式更新。
.. 后面的表达式(基值)必须具有与正在构造的新结构体类型相同的结构体类型。
整个表达式使用为已指定字段给定的值,并移动或复制基值表达式中其余字段的值。
与所有结构体表达式一样,结构体的所有字段必须是可见的,即使那些没有被显式命名的字段也是如此。
#![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 或 match 表达式的受检者。但是,如果结构体表达式位于另一个表达式内部(例如在括号内),则可以在这些情况下使用。
字段名可以是十进制整数值以指定用于构造元组结构体的索引。这可以与基值结构体一起使用,以填充未指定的其余索引:
#![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}; // 使用基值结构体填充所有其他字段。
}
结构体字段初始化简写
在初始化具有命名(而非编号)字段的数据结构(结构体、枚举、联合体)时,允许将 fieldname 作为 fieldname: fieldname 的简写。这允许使用更紧凑的语法并减少重复。例如:
#![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 };
}
调用表达式
Syntax
CallExpression → Expression ( CallParams? )
CallParams → Expression ( , Expression )* ,?
调用表达式调用一个函数。调用表达式的语法是一个表达式(称为函数操作数),后跟一个用括号括起来的、以逗号分隔的表达式列表(称为参数操作数)。
如果函数最终返回,则表达式完成。
对于非函数类型,表达式 f(...) 根据函数操作数使用以下 trait 之一上的方法:
Fn或AsyncFn— 共享引用。FnMut或AsyncFnMut— 可变引用。FnOnce或AsyncFnOnce— 值。
如果需要,会自动进行借用。函数操作数也会根据需要被自动解引用。
一些调用表达式的示例:
#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 { 0 }
let three: i32 = add(1i32, 2i32);
let name: &'static str = (|| "Rust")();
}
消歧函数调用
所有函数调用都是更显式的完全限定语法的语法糖。
函数调用可能需要完全限定,这取决于调用的歧义性以及作用域内的项。
Note
过去,术语“无歧义函数调用语法“、“通用函数调用语法“或“UFCS“曾被用于文档、议题、RFC 和其他社区著作中。然而,这些术语缺乏描述力,并且可能混淆手头的问题。我们在此提及它们是为了便于搜索。
经常出现的一些情况会导致方法或关联函数调用的接收者或被引用者的歧义。这些情况可能包括:
- 多个作用域内的 trait 为相同类型定义了同名方法
- 不希望自动
deref;例如,区分智能指针本身的方法和指针所指对象的方法 - 不接受参数的方法,如
default(),以及返回类型属性的方法,如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。
方法调用表达式
Syntax
MethodCallExpression → Expression . PathExprSegment ( CallParams? )
方法调用由一个表达式(接收者)后跟一个点号、一个表达式路径段和一个带括号的表达式列表组成。
方法调用被解析为特定 trait 上的关联方法,如果左侧的精确 self-类型已知,则静态分派到方法;如果左侧表达式是间接的 trait 对象,则动态分派。
#![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)
}
在查找方法调用时,接收者可能会被自动解引用或借用以调用某个方法。这需要一个比其他函数更复杂的查找过程,因为可能有多种可能的方法可调用。使用以下过程:
第一步是构建一个候选接收者类型列表。通过重复解引用接收者表达式的类型来获取这些类型,将遇到的每个类型添加到列表中,然后最后尝试一次数组非固定大小强制转换,如果成功则添加结果类型。
然后,对于每个候选类型 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 约束提供的方法。然后查找作用域内的所有其他方法。
Note
查找按顺序对每个类型进行,这偶尔会导致令人惊讶的结果。下面的代码将打印 “In trait impl!”,因为首先查找
&self方法,在找到结构体的&mut self方法之前就找到了 trait 方法。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 Edition differences
在 2021 版本之前,在搜索可见方法的过程中,如果候选接收者类型是数组类型,则标准库
IntoIteratortrait 提供的方法会被忽略。为此目的使用的版本由表示方法名称的记号确定。
这种特殊情况将来可能会被移除。
Warning
对于 trait 对象,如果存在与 trait 方法同名的固有方法,则在方法调用表达式中尝试调用该方法时将给出编译错误。相反,你可以使用消歧函数调用语法调用该方法,在这种情况下,它调用的是 trait 方法,而不是固有方法。无法调用固有方法。只要不在 trait 对象上定义与 trait 方法同名的固有方法,就不会有问题。
字段访问表达式
Syntax
FieldExpression → Expression . IDENTIFIER
字段表达式是一种位置表达式,它求值为结构体或联合体字段的位置。
当操作数是可变的时,字段表达式也是可变的。
字段表达式的语法是一个表达式(称为容器操作数),然后是一个 .,最后是一个标识符。
字段表达式后面不能跟括号括起来的、以逗号分隔的表达式列表,因为这会被解析为方法调用表达式。也就是说,它们不能是调用表达式的函数操作数。
Note
将字段表达式用括号表达式括起来,以在调用表达式中使用。
#![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)() // 包含字段表达式的调用表达式
自动解引用
如果容器操作数的类型实现了 Deref 或 DerefMut(取决于操作数是否是可变的),它将被自动解引用,解引用的次数根据需要,以使字段访问成为可能。此过程也简称为自动解引用。
借用
结构体或结构体引用的字段在借用时被视为独立的实体。如果结构体未实现 Drop 且存储在局部变量中,这也适用于移出其每个字段。如果自动解引用是通过 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 移出
}
闭包表达式
Syntax
ClosureExpression →
async?1
move?
( || | | ClosureParameters? | )
( Expression | -> TypeNoBounds BlockExpression )
ClosureParameters → ClosureParam ( , ClosureParam )* ,?
ClosureParam → OuterAttribute* PatternNoTopAlt ( : Type )?
闭包表达式(也称为 lambda 表达式或 lambda)定义一个闭包类型,并求值为该类型的一个值。闭包表达式的语法是可选的 async 关键字、可选的 move 关键字,然后是一组用管道符号(|)括起来的、以逗号分隔的模式列表(称为闭包参数,每个参数后面可选地跟有 : 和类型),然后是可选的 -> 和类型(称为返回类型),然后是一个表达式(称为闭包体操作数)。
每个模式后的可选类型是该模式的类型标注。
如果有返回类型,闭包体必须是一个块。
闭包表达式表示一个将参数列表映射到跟随参数的表达式的函数。与 let 绑定一样,闭包参数是不可反驳的模式,其类型标注是可选的,如果未给出将从上下文中推断。
每个闭包表达式都有一个唯一的匿名类型。
值得注意的是,闭包表达式捕获其环境,而常规函数定义则不会。
没有 move 关键字时,闭包表达式推断如何从环境中捕获每个变量,优先通过共享引用捕获,有效地借用闭包体内提到的所有外部变量。
如果需要,编译器将推断应该改为使用可变引用,或者应该从环境中移动或复制值(取决于它们的类型)。
可以通过在闭包前加上 move 关键字来强制闭包通过复制或移走值来捕获其环境。这通常用于确保闭包的生命周期为 'static。
闭包 trait 实现
闭包类型实现哪些 trait 取决于变量的捕获方式、被捕获变量的类型以及 async 的存在。关于闭包如何以及何时实现 Fn、FnMut 和 FnOnce,请参见调用 trait 和强制章节。如果每个被捕获变量的类型也实现了该 trait,则闭包类型也实现 Send 和 Sync。
Async 闭包
标记有 async 关键字的闭包表示它们是异步的,其方式类似于异步函数。
调用 async 闭包不执行任何工作,而是求值为一个实现 Future 的值,该值对应于闭包体的计算。
#![allow(unused)]
fn main() {
async fn takes_async_callback(f: impl AsyncFn(u64)) {
f(0).await;
f(1).await;
}
async fn example() {
takes_async_callback(async |i| {
core::future::ready(i).await;
println!("done with {i}.");
}).await;
}
}
2018 Edition differences
Async 闭包仅从 Rust 2018 起可用。
示例
在此示例中,我们定义了一个接受高阶函数参数的函数 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));
// 带有类型标注
ten_times(|j: i32| -> () { println!("hello, {}", j) });
let word = "konnichiwa".to_owned();
ten_times(move |j| println!("{}, {}", word, j));
}
闭包参数上的属性
闭包参数上的属性遵循与常规函数参数相同的规则和限制。
-
async限定符在 2015 版本中不允许使用。 ↩
循环和其他可中断表达式
Syntax
LoopExpression →
LoopLabel? (
InfiniteLoopExpression
| PredicateLoopExpression
| IteratorLoopExpression
| LabelBlockExpression
)
Rust 支持四种循环表达式:
loop表达式表示无限循环。while表达式在谓词为 false 之前循环。for表达式从迭代器中提取值,循环直到迭代器为空。- 带标签的块表达式恰好运行循环一次,但允许使用
break提前退出循环。
除带标签的块表达式之外,所有类型都支持 continue 表达式。
只有 loop 和带标签的块表达式支持求值为非平凡值。
无限循环
Syntax
InfiniteLoopExpression → loop BlockExpression
loop 表达式持续重复执行其主体:loop { println!("I live."); }。
没有关联 break 表达式的 loop 表达式是发散的,具有类型 !。
包含关联 break 表达式的 loop 表达式可能终止,并且必须具有与 break 表达式的值兼容的类型。
谓词循环
Syntax
PredicateLoopExpression → while Conditions BlockExpression
while 循环表达式允许在一组条件保持为 true 时重复求值一个块。
条件操作数必须是一个具有布尔类型的表达式或一个条件 let 匹配。如果所有条件操作数都求值为 true 且所有 let 模式都成功匹配其受检者,则执行循环体块。
循环体成功执行后,重新求值条件操作数以确定是否应再次执行循环体。
如果任何条件操作数求值为 false 或任何 let 模式未匹配其受检者,循环体不被执行,执行继续到 while 表达式之后。
while 表达式求值为 ()。
示例:
#![allow(unused)]
fn main() {
let mut i = 0;
while i < 10 {
println!("hello");
i = i + 1;
}
}
while let 模式
while 条件中的 let 模式允许在模式成功匹配时将新变量绑定到作用域中。以下示例展示了使用 let 模式的绑定:
#![allow(unused)]
fn main() {
let mut x = vec![1, 2, 3];
while let Some(y) = x.pop() {
println!("y = {}", y);
}
while let _ = 5 {
println!("Irrefutable patterns are always true");
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);
}
}
while 条件链
多个条件操作数可以用 && 分隔。这些具有与 if 条件链相同的语义和限制。
以下是链式多个表达式的示例,混合了 let 绑定和布尔表达式,并且表达式可以引用先前表达式的模式绑定:
fn main() {
let outer_opt = Some(Some(1i32));
while let Some(inner_opt) = outer_opt
&& let Some(number) = inner_opt
&& number == 1
{
println!("Peek a boo");
break;
}
}
迭代器循环
Syntax
IteratorLoopExpression →
for Pattern in Expressionexcept StructExpression BlockExpression
for 表达式是一种语法结构,用于遍历由 std::iter::IntoIterator 实现提供的元素。
如果迭代器产生一个值,该值与不可反驳的模式匹配,则执行循环体,然后控制返回到 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 始终是标准库中的项,而不是当前作用域中这些名称解析到的任何东西。
变量名 next、iter 和 val 仅用于说明,它们实际上没有用户可以输入的名字。
Note
外层的
match用于确保iter_expr中的任何临时值不会在循环完成之前被丢弃。next在被赋值之前声明,因为这更经常地使类型被正确推断。
循环标签
Syntax
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 表达式
Syntax
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 循环关联,但可以使用标签来指定影响哪个外围循环。示例:
#![allow(unused)]
fn main() {
'outer: loop {
while true {
break 'outer;
}
}
}
break 表达式仅允许在循环体内部,并具有以下形式之一:break、break 'label 或(见下文)break EXPR 或 break 'label EXPR。
在带有 break 表达式的 loop 或带标签的块表达式中,不含表达式的 break 等价于 break ()。
带标签的块表达式
Syntax
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
};
}
带标签的块表达式的类型是所有 break 操作数和最终操作数的最小上界。如果省略了最终操作数,则最终操作数的类型默认为单元类型,除非块发散,在这种情况下它是永不类型。
Example
#![allow(unused)] fn main() { fn example(condition: bool) { let s = String::from("owned"); let _: &str = 'block: { if condition { break 'block &s; // &String 通过 Deref 强制为 &str } break 'block "literal"; // &'static str 强制为 &str }; } }
continue 表达式
Syntax
ContinueExpression → continue LIFETIME_OR_LABEL?
当遇到 continue 时,关联循环体的当前迭代立即终止,将控制返回到循环头部。
对于 while 循环,头部是控制循环的条件操作数。
对于 for 循环,头部是控制循环的调用表达式。
与 break 类似,continue 通常与最内层的外围循环关联,但可以使用 continue 'label 来指定受影响的循环。
continue 表达式仅允许在循环体内部。
break 和循环值
当与 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);
}
具有关联 break 表达式的 loop 的类型是所有 break 操作数的最小上界。
Example
#![allow(unused)] fn main() { fn example(condition: bool) { let s = String::from("owned"); let _: &str = loop { if condition { break &s; // &String 通过 Deref 强制为 &str } break "literal"; // &'static str 强制为 &str }; } }
如果所有 break 操作数中没有任何一个是不发散的,则具有关联 break 表达式的 loop 不发散。如果所有 break 操作数都发散,则 loop 表达式也发散。
Example
#![allow(unused)] fn main() { fn diverging_loop_with_break(condition: bool) -> ! { // 此循环是发散的,因为所有 `break` 操作数都是发散的。 loop { if condition { break loop {}; } else { break panic!(); } } } }#![allow(unused)] fn main() { fn loop_with_non_diverging_break(condition: bool) -> ! { // 此循环的类型是 i32,即使其中一个 break 是发散的。 loop { if condition { break loop {}; } else { break 123i32; } } // 错误:期望 `!`,找到 `i32` } }
区间表达式
Syntax
RangeExpression →
RangeExpr
| RangeFromExpr
| RangeToExpr
| RangeFullExpr
| RangeInclusiveExpr
| RangeToInclusiveExpr
RangeExpr → Expression .. Expression
RangeFromExpr → Expression ..
RangeToExpr → .. Expression
RangeFullExpr → ..
.. 和 ..= 运算符将根据下表构造 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 表达式
Syntax
IfExpression →
if Conditions BlockExpressionNoInnerAttributes
( else ( BlockExpressionNoInnerAttributes | IfExpression ) )?
Conditions →
Expressionexcept StructExpression
| LetChain
LetChain → LetChainCondition ( && LetChainCondition )*
LetChainCondition →
Expressionexcept ExcludedConditions
| OuterAttribute* let Pattern = Scrutineeexcept ExcludedConditions
ExcludedConditions →
StructExpression
| LazyBooleanExpression
| RangeExpr
| RangeFromExpr
| RangeInclusiveExpr
| AssignmentExpression
| CompoundAssignmentExpression
if 表达式的语法是由 && 分隔的一个或多个条件操作数的序列,后跟一个结果块,任意数量的 else if 条件和块,以及一个可选的尾部 else 块。
条件操作数必须是一个具有布尔类型的表达式或一个条件 let 匹配。
如果所有条件操作数都求值为 true 且所有 let 模式都成功匹配其受检者,则执行结果块,并跳过任何后续的 else if 或 else 块。
如果任何条件操作数求值为 false 或任何 let 模式未匹配其受检者,则跳过结果块,并求值任何后续的 else if 条件。
如果所有 if 和 else if 条件都求值为 false,则执行 else 块(如果有的话)。
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");
}
// `if` 可以用作表达式。
let y = if 12 * 15 > 150 {
"Bigger"
} else {
"Smaller"
};
assert_eq!(y, "Bigger");
}
如果条件表达式发散或所有分支都发散,则 if 表达式发散。
#![allow(unused)]
fn main() {
fn diverging_condition() -> ! {
// 因为条件表达式发散而发散
if loop {} {
()
} else {
()
};
// 上面的分号很重要:`if` 表达式的类型是 `()`,
// 尽管它发散了。当最终体表达式被省略时,体的类型
// 被推断为 !,因为函数体发散。如果没有分号,
// `if` 将是类型为 `()` 的尾部表达式,这将无法匹配返回类型 `!`。
}
fn diverging_arms() -> ! {
// 因为所有分支都发散而发散
if true {
loop {}
} else {
loop {}
}
}
}
if let 模式
if 条件中的 let 模式允许在模式成功匹配时将新变量绑定到作用域中。
以下示例展示了使用 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!("Irrefutable patterns are always true");
}
}
可以使用 | 运算符指定多个模式。这与 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);
}
}
条件链
多个条件操作数可以用 && 分隔。
类似于 && LazyBooleanExpression,每个操作数从左到右求值,直到某个操作数求值为 false 或 let 匹配失败,在这种情况下后续操作数不会被求值。
每个模式的绑定被放入作用域,以供下一个条件操作数和结果块使用。
以下是链式多个表达式的示例,混合了 let 绑定和布尔表达式,并且表达式可以引用先前表达式的模式绑定:
#![allow(unused)]
fn main() {
fn single() {
let outer_opt = Some(Some(1i32));
if let Some(inner_opt) = outer_opt
&& let Some(number) = inner_opt
&& number == 1
{
println!("Peek a boo");
}
}
}
上面的代码等价于以下不使用条件链的代码:
#![allow(unused)]
fn main() {
fn nested() {
let outer_opt = Some(Some(1i32));
if let Some(inner_opt) = outer_opt {
if let Some(number) = inner_opt {
if number == 1 {
println!("Peek a boo");
}
}
}
}
}
如果任何条件操作数是 let 模式,则由于与 let 受检者的歧义和优先级,所有条件操作数都不能是 || 惰性布尔运算符表达式。如果需要 || 表达式,可以使用括号。例如:
#![allow(unused)]
fn main() {
let foo = Some(123);
let condition1 = true;
let condition2 = false;
// 此处需要括号。
if let Some(x) = foo && (condition1 || condition2) { /*...*/ }
}
2024 Edition differences
在 2024 版本之前,不支持 let 链。即 LetChain 语法在
if表达式中是不允许的。
match 表达式
Syntax
MatchExpression →
match Scrutinee {
InnerAttribute*
MatchArms?
}
Scrutinee → Expressionexcept StructExpression
MatchArms →
( MatchArm => ( ExpressionWithoutBlock , | ExpressionWithBlock ,? ) )*
MatchArm => Expression ,?
MatchArm → OuterAttribute* Pattern MatchArmGuard?
MatchArmGuard → if MatchConditions
MatchConditions →
MatchGuardChain
| Expression
MatchGuardChain → MatchGuardCondition ( && MatchGuardCondition )*
MatchGuardCondition →
Expressionexcept ExcludedMatchConditions
| OuterAttribute* let Pattern = MatchGuardScrutinee
MatchGuardScrutinee → Expressionexcept ExcludedMatchConditions
ExcludedMatchConditions →
LazyBooleanExpression
| RangeExpr
| RangeFromExpr
| RangeInclusiveExpr
| AssignmentExpression
| CompoundAssignmentExpression
match 表达式基于模式进行分支。所发生的匹配的确切形式取决于模式。
match 表达式有一个*受检者表达式*,即要与模式进行比较的值。
受检者表达式和模式必须具有相同的类型。
match 的行为取决于受检者表达式是位置表达式还是值表达式。
如果受检者表达式是值表达式,它首先被求值到一个临时位置,然后结果值按顺序与分支中的模式进行比较,直到找到匹配。第一个具有匹配模式的分支被选为 match 的分支目标,模式绑定的任何变量被赋值给分支块中的局部变量,控制进入该块。
当受检者表达式是位置表达式时,match 不分配临时位置;然而,按值绑定可能会从内存位置复制或移动。如果可能,最好对位置表达式进行匹配,因为这类匹配的生命周期继承位置表达式的生命周期,而不是被限制在 match 内部。
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"),
}
}
模式内绑定的变量的作用域是匹配守卫和分支表达式。
绑定模式(移动、复制或引用)取决于模式。
可以使用 | 运算符连接多个匹配模式。每个模式将按从左到右的顺序进行测试,直到找到成功的匹配。
#![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!(),
}
}
每个 | 分隔模式中的每个绑定都必须出现在该分支的所有模式中。
每个同名的绑定必须具有相同的类型和相同的绑定模式。
整个 match 表达式的类型是各个匹配分支的最小上界。
如果没有匹配分支,则 match 表达式是发散的,类型为 !。
Example
#![allow(unused)] fn main() { fn make<T>() -> T { loop {} } enum Empty {} fn diverging_match_no_arms() -> ! { let e: Empty = make(); match e {} } }
如果受检者表达式或所有匹配分支发散,则整个 match 表达式也发散。
匹配守卫
匹配分支可以接受匹配守卫以进一步细化匹配某个情况的条件。
模式守卫出现在 if 关键字之后的模式之后,由一个具有布尔类型的表达式或一个条件 let 匹配组成。
当模式成功匹配时,模式守卫被执行。如果所有守卫条件操作数都求值为 true 且所有 let 模式都成功匹配其受检者,则该匹配分支被成功匹配,并执行分支体。
否则,测试下一个模式,包括同一分支中使用 | 运算符的其他匹配。
#![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!(),
};
}
Note
使用
|运算符的多重匹配可能导致模式守卫及其副作用被多次执行。例如:#![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); }
模式守卫可以引用在其后模式内绑定的变量。
在求值守卫之前,会对受检者中变量匹配的部分获取共享引用。在求值守卫期间,访问变量时使用此共享引用。
只有当守卫成功求值时,值才从受检者移动或复制到变量中。这允许在守卫内部使用共享借用,而如果在守卫未匹配的情况下不必从受检者中移出。
此外,通过在求值守卫时持有共享引用,也防止了守卫内部的修改。
守卫可以使用 let 模式来有条件地匹配受检者,并在模式成功匹配时将新变量绑定到作用域中。
Example
在此示例中,守卫条件
let Some(first_char) = name.chars().next()被求值。如果let模式成功匹配(即字符串至少有一个字符),则执行分支体。否则,模式匹配继续到下一个分支。
let模式创建一个新绑定(first_char),它可以在分支体中与原始模式绑定(name)一起使用。#![allow(unused)] fn main() { enum Command { Run(String), Stop, } let cmd = Command::Run("example".to_string()); match cmd { Command::Run(name) if let Some(first_char) = name.chars().next() => { // 此处 `name` 和 `first_char` 都可用 println!("Running: {name} (starts with '{first_char}')"); } Command::Run(name) => { println!("{name} is empty"); } _ => {} } }
匹配守卫链
多个守卫条件操作数可以用 && 分隔。
Example
#![allow(unused)] fn main() { let foo = Some([123]); let already_checked = false; match foo { Some(xs) if let [single] = xs && !already_checked => { dbg!(single); } _ => {} } }
类似于 && LazyBooleanExpression,每个操作数从左到右求值,直到某个操作数求值为 false 或 let 匹配失败,在这种情况下后续操作数不会被求值。
每个 let 模式的绑定被放入作用域,以供下一个条件操作数和匹配分支体使用。
如果任何守卫条件操作数是 let 模式,则由于与 let 受检者的歧义和优先级,所有条件操作数都不能是 || 惰性布尔运算符表达式。
Example
如果需要
||表达式,可以使用括号。例如:#![allow(unused)] fn main() { let foo = Some([123]); match foo { // 此处需要括号。 Some(xs) if let [x] = xs && (x < -100 || x > 20) => {} _ => {} } }
匹配分支上的属性
匹配分支上允许外部属性。在匹配分支上有意义的唯一属性是 cfg 和 lint 检查属性。
在匹配表达式开花括号后直接允许内部属性,其所在表达式上下文与块表达式上的属性相同。
return 表达式
Syntax
ReturnExpression → return Expression?
return 表达式用关键字 return 表示。
求值 return 表达式将其参数移动到当前函数调用的指定输出位置,销毁当前函数激活帧,并将控制转移到调用者帧。
return 表达式示例:
#![allow(unused)]
fn main() {
fn max(a: i32, b: i32) -> i32 {
if a > b {
return a;
}
return b;
}
}
Await 表达式
Syntax
AwaitExpression → Expression . await
await 表达式是一种语法结构,用于挂起由 std::future::IntoFuture 实现提供的计算,直到给定的 future 准备好产生一个值。
await 表达式的语法是一个类型实现了 IntoFuture trait 的表达式(称为future 操作数),然后是 . 记号,再然后是 await 关键字。
Await 表达式仅在异步上下文(如 async fn、async 闭包或 async 块)中合法。
更具体地说,await 表达式具有以下效果。
- 通过在 future 操作数上调用
IntoFuture::into_future来创建 future。 - 将该 future 求值为一个 future
tmp; - 使用
Pin::new_unchecked固定tmp; - 然后通过调用
Future::poll方法并将当前的任务上下文传入来对此固定的 future 进行轮询; - 如果对
poll的调用返回Poll::Pending,则 future 返回Poll::Pending,挂起其状态,以便当外围异步上下文被重新轮询时,执行回到第 3 步; - 否则对
poll的调用必定返回了Poll::Ready,在这种情况下,包含在Poll::Ready变体中的值被用作await表达式本身的结果。
2018 Edition differences
Await 表达式仅从 Rust 2018 起可用。
任务上下文
任务上下文指的是当异步上下文本身被轮询时提供给当前异步上下文的 Context。因为 await 表达式仅在异步上下文中合法,所以必须有某个任务上下文可用。
近似脱糖
实际上,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 引用从异步环境中获取的上下文。
_ 表达式
Syntax
UnderscoreExpression → _
下划线表达式,用符号 _ 表示,用于在解构赋值中表示占位符。
它们只能出现在赋值的左侧。
注意,这与通配符模式不同。
_ 表达式示例:
#![allow(unused)]
fn main() {
let p = (1, 2);
let mut a = 0;
(_, a) = p;
struct Position {
x: u32,
y: u32,
}
Position { x: a, y: _ } = Position{ x: 2, y: 3 };
// 未使用的结果,赋值给 `_` 用于声明意图并移除警告
_ = 2 + 2;
// 触发 unused_must_use 警告
// 2 + 2;
// 在 let 绑定中使用通配符模式的等价技术
let _ = 2 + 2;
}
模式
Syntax
Pattern → |? PatternNoTopAlt ( | PatternNoTopAlt )*
PatternNoTopAlt →
PatternWithoutRange
| RangePattern
PatternWithoutRange →
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);
}
}
模式用于:
解构
模式可用于解构 structs、enums 和 tuples。解构将一个值分解为其组成部分。使用的语法几乎与创建此类值时的语法相同。
在被检查值表达式具有 struct、enum 或 tuple 类型的模式中,通配符模式(_)代表单个数据字段,而省略模式或剩余模式(..)代表特定变体的所有剩余字段。
在解构具有命名(而非编号)字段的数据结构时,允许使用 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);
}
};
}
可反驳性
当一个模式有可能不被所要匹配的值匹配时,称该模式是可反驳的(refutable)。而不可反驳的模式则始终与其匹配的值匹配。示例:
#![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);
}
}
字面量模式
Syntax
LiteralPattern → -? LiteralExpression
字面量模式精确匹配与字面量创建的值相同的值。由于负数不是字面量,模式中的字面量可以带有可选的前导负号,相当于取反运算符。
Warning
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"),
}
}
}
标识符模式
Syntax
IdentifierPattern → ref? mut? IDENTIFIER ( @ PatternNoTopAlt )?
标识符模式将其匹配的值绑定到值命名空间中的一个变量。
标识符在模式内必须唯一。
该变量将遮蔽作用域中任何同名的变量。新绑定的作用域取决于使用该模式的上下文(如 let 绑定或 match 分支)。
仅由一个标识符组成的模式(可能带有 mut)匹配任何值并将其绑定到该标识符。这是变量声明以及函数和闭包参数中最常用的模式。
#![allow(unused)]
fn main() {
let mut variable = 10;
fn sum(x: i32, y: i32) -> i32 {
x + y
}
}
要将匹配到的值绑定到一个变量,可以使用语法 variable @ subpattern。例如,以下代码将值 2 绑定到 e(不是整个范围:这里的范围是一个范围子模式)。
#![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) => (),
}
}
在第一个 match 表达式中,值被复制(或移动)。在第二个 match 中,对同一内存位置的引用被绑定到变量 value。这种语法是必要的,因为在解构子模式中,& 运算符不能应用于值的字段。例如,以下代码是无效的:
#![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 不是某种被匹配的东西。它的目标仅仅是使匹配的绑定成为引用,而不是潜在地复制或移动匹配到的内容。
路径模式优先于标识符模式。
Note
当模式是单段标识符时,语法存在歧义:它是 IdentifierPattern 还是 PathPattern。此歧义只能在名称解析之后解决。
#![allow(unused)] fn main() { const EXPECTED_VALUE: u8 = 42; // ^^^^^^^^^^^^^^ 这个常量在作用域中会影响下面模式的处理方式。 fn check_value(x: u8) -> Result<u8, u8> { match x { EXPECTED_VALUE => Ok(x), // ^^^^^^^^^^^^^^ 解析为 `PathPattern`,解析到常量 `42`。 other_value => Err(x), // ^^^^^^^^^^^ 解析为 `IdentifierPattern`。 } } // 如果 `EXPECTED_VALUE` 在上面被当作 `IdentifierPattern`, // 那么该模式将始终匹配,使得函数无论输入如何都返回 `Ok(_)`。 assert_eq!(check_value(42), Ok(42)); assert_eq!(check_value(43), Err(43)); }
如果指定了 ref 或 ref mut 且标识符遮蔽了一个常量,则这是一个错误。
如果 @ 子模式不可反驳或未指定子模式,则标识符模式是不可反驳的。
绑定模式
为了提供更好的人体工学,模式在不同的绑定模式下运作,以便更容易地将引用绑定到值。当引用值被非引用模式匹配时,它将被自动处理为 ref 或 ref mut 绑定。示例:
#![allow(unused)]
fn main() {
let x: &Option<i32> = &Some(3);
if let Some(y) = x {
// y 被转换为 `ref y`,其类型为 &i32
}
}
非引用模式包括除绑定、通配符模式(_)、引用类型的 const 模式以及引用模式之外的所有模式。
如果绑定模式没有显式地带有 ref、ref mut 或 mut,则它使用默认绑定模式来决定变量如何绑定。
默认绑定模式从 “move” 模式开始,即使用移动语义。
在匹配模式时,编译器从模式的外部开始向内工作。
每次一个引用被非引用模式匹配时,它会自动解引用该值并更新默认绑定模式。
引用会将默认绑定模式设置为 ref。
可变引用会将模式设置为 ref mut,除非模式已经是 ref,此时它保持为 ref。
如果自动解引用后的值仍然是一个引用,则继续解引用,此过程会重复。
只有当默认绑定模式为 “move” 时,绑定模式才能显式指定 ref 或 ref mut 绑定模式,或用 mut 指定可变性。例如,以下代码不被接受:
#![allow(unused)]
fn main() {
let [mut x] = &[()]; //~ 错误
let [ref x] = &[()]; //~ 错误
let [ref mut x] = &mut [()]; //~ 错误
}
2024 Edition differences
在 2024 版之前,即使默认绑定模式不是 “move”,绑定也可以显式指定
ref或ref mut绑定模式,并且可以在此类绑定上用mut指定可变性。在这些版次中,在绑定上指定mut会将绑定模式设置为 “move”,无论当前的默认绑定模式是什么。
类似地,引用模式只能在默认绑定模式为 “move” 时出现。例如,以下代码不被接受:
#![allow(unused)]
fn main() {
let [&x] = &[&()]; //~ 错误
}
2024 Edition differences
在 2024 版之前,即使默认绑定模式不是 “move”,引用模式也可以出现,并且同时具有匹配被检查值的效果和将默认绑定模式重置为 “move” 的效果。
移动绑定和引用绑定可以在同一模式中混用。这样做会导致对所绑定对象的部分移动,且该对象在此之后不能使用。这仅适用于类型不能复制的情况。
在下面的示例中,name 从 person 移出。尝试将 person 整体或 person.name 使用将导致错误,因为发生了部分移动。
示例:
#![allow(unused)]
fn main() {
struct Person {
name: String,
age: u8,
}
let person = Person{ name: String::from("John"), age: 23 };
// `name` 从 person 移动,`age` 被引用
let Person { name, ref age } = person;
}
通配符模式
Syntax
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 {}
}
通配符模式始终是不可反驳的。
剩余模式
Syntax
RestPattern → ..
剩余模式(.. token)充当可变长度模式,匹配零个或多个之前和之后尚未被匹配的元素。
它只能用于元组、元组结构体和切片模式,并且在这些模式中只能作为元素之一出现一次。它也允许在仅用于切片模式的标识符模式中。
剩余模式始终是不可反驳的。
示例:
#![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"),
}
}
范围模式
Syntax
RangePattern →
RangeExclusivePattern
| RangeInclusivePattern
| RangeFromPattern
| RangeToExclusivePattern
| RangeToInclusivePattern
| ObsoleteRangePattern1
RangeExclusivePattern →
RangePatternBound .. RangePatternBound
RangeInclusivePattern →
RangePatternBound ..= RangePatternBound
RangeFromPattern →
RangePatternBound ..
RangeToExclusivePattern →
.. RangePatternBound
RangeToInclusivePattern →
..= RangePatternBound
ObsoleteRangePattern →
RangePatternBound ... RangePatternBound
范围模式匹配由其边界定义的范围内的标量值。它们由标记符号(.. 或 ..=)以及一侧或两侧的边界组成。
标记符号左侧的边界称为下界。右侧的边界称为上界。
排除范围模式匹配从下界开始到上界(但不包含上界)的所有值。它的书写形式是下界,后跟 ..,再跟在上界。
例如,模式 'm'..'p' 仅匹配 'm'、'n' 和 'o',特别地不包括 'p'。
包含范围模式匹配从下界开始到上界(包含上界)的所有值。它的书写形式是下界,后跟 ..=,再跟上界。
例如,模式 'm'..='p' 仅匹配值 'm'、'n'、'o' 和 'p'。
从范围模式匹配所有大于等于下界的值。它的书写形式是下界后跟 ..。
例如,1.. 将匹配任何大于等于 1 的整数,如 1、9 或 9001,或 9007199254740991(如果其大小合适),但不匹配 0,对于有符号整数也不匹配负数。
到排除范围模式匹配所有小于上界的值。它的书写形式是 .. 后跟上界。
例如,..10 将匹配任何小于 10 的整数,如 9、1、0,对于有符号整数类型还包括所有负数。
到包含范围模式匹配所有小于等于上界的值。它的书写形式是 ..= 后跟上界。
例如,..=10 将匹配任何小于等于 10 的整数,如 10、1、0,对于有符号整数类型还包括所有负数。
范围模式必须非空;它必须在其类型的可能值集合中跨越至少一个值。换句话说:
- 在
a..=b中,必须满足 a ≤ b。例如,范围模式10..=0是错误的,但10..=10是允许的。 - 在
a..b中,必须满足 a < b。例如,范围模式10..0或10..10是错误的。 - 在
..b中,b 不能是其类型的最小值。例如,范围模式..-128i8或..f64::NEG_INFINITY是错误的。
边界书写为以下之一:
- 字符、字节、整数或浮点字面量。
- 一个
-后跟整数或浮点字面量。 - 一个路径。
Note
我们在语法上接受的比 RangePatternBound 更多。我们稍后会在语义上拒绝其他内容。
如果边界书写为路径,在宏解析之后,该路径必须解析为类型为 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.. => "positive number!",
};
// 使用常量路径:
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!("It fits and occupies {} bytes", 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}'。
RangeFromPattern 不能用作切片模式中子模式的顶层模式。例如,模式 [1.., _] 不是有效的模式。
2021 Edition differences
在 2021 版之前,同时具有下界和上界的范围模式也可以使用
...代替..=来书写,含义相同。
引用模式
Syntax
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);
}
引用模式的文法产生式必须匹配 token && 以匹配引用的引用,因为它本身就是一个 token,而不是两个 & token。
添加 mut 关键字会解引用可变引用。可变性必须与引用的可变性匹配。
引用模式始终是不可反驳的。
结构体模式
Syntax
StructPattern →
PathInExpression {
StructPatternElements?
}
StructPatternElements →
StructPatternFields ( , | , StructPatternEtCetera )?
| StructPatternEtCetera
StructPatternFields →
StructPatternField ( , StructPatternField )*
StructPatternField →
OuterAttribute*
(
TUPLE_INDEX : Pattern
| IDENTIFIER : Pattern
| ref? mut? IDENTIFIER
)
结构体模式匹配满足其子模式定义的所有条件的结构体、枚举和联合体值。它们也用于解构结构体、枚举或联合体值。
在结构体模式中,字段可以通过名称、索引(对于元组结构体)来引用,或通过 .. 忽略:
#![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: _} => (),
}
}
用于匹配联合体的结构体模式必须恰好指定一个字段(参见联合体上的模式匹配)。
IDENTIFIER 语法匹配任何值并将其绑定到与给定字段同名的变量。它是 fieldname: fieldname 的简写形式。可以包含 ref 和 mut 限定符,其行为如 patterns.ident.ref 所述。
#![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, b, c } = struct_value;
}
如果 PathInExpression 解析为具有多个变体的枚举的构造器,或其某个子模式是可反驳的,则结构体模式是可反驳的。
结构体模式在类型命名空间中匹配其构造器由 PathInExpression 解析到的结构体、联合体或枚举变体。更多细节请参见 patterns.tuple-struct.namespace。
元组结构体模式
Syntax
TupleStructPattern → PathInExpression ( TupleStructItems? )
TupleStructItems → Pattern ( , Pattern )* ,?
元组结构体模式匹配满足其子模式定义的所有条件的元组结构体和枚举值。它们也用于解构元组结构体或枚举值。
如果 PathInExpression 解析为具有多个变体的枚举的构造器,或其某个子模式是可反驳的,则元组结构体模式是可反驳的。
元组结构体模式在值命名空间中匹配其构造器由 PathInExpression 解析到的元组结构体或类元组枚举变体。
Note
反之,用于元组结构体或类元组枚举变体的结构体模式,例如
S { 0: _ },在类型命名空间中匹配其构造器被解析到的元组结构体或变体。enum E1 { V(u16) } enum E2 { V(u32) } // 仅从类型命名空间导入 `E1::V`。 mod _0 { const V: () = (); // 用于命名空间掩蔽。 pub(super) use super::E1::*; } use _0::*; // 仅从值命名空间导入 `E2::V`。 mod _1 { struct V {} // 用于命名空间掩蔽。 pub(super) use super::E2::*; } use _1::*; fn f() { // 此结构体模式匹配其构造器在类型命名空间中 // 找到的类元组枚举变体。 let V { 0: ..=u16::MAX } = (loop {}) else { loop {} }; // 此元组结构体模式匹配其构造器在值命名空间中 // 找到的类元组枚举变体。 let V(..=u32::MAX) = (loop {}) else { loop {} }; } // Required due to the odd behavior of `super` within functions. fn main() {}语言团队已经做出了某些决策,例如 PR #138458 中的决策,这引发了对以这种方式在模式中使用值命名空间的合理性的质疑,如 PR #140593 中所述。在你的代码中有意依赖这种细微差别可能是不明智的。
元组模式
Syntax
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");
}
分组模式
Syntax
GroupedPattern → ( Pattern )
将模式括在括号中可用于显式控制复合模式的优先级。例如,紧邻范围模式的引用模式(如 &0..=5)是歧义的且不被允许,但可以用括号来表达。
#![allow(unused)]
fn main() {
let int_reference = &3;
match int_reference {
&(0..=5) => (),
_ => (),
}
}
切片模式
Syntax
SlicePattern → [ SlicePatternItems? ]
SlicePatternItems → Pattern ( , Pattern )* ,?
切片模式既可以匹配固定大小的数组,也可以匹配动态大小的切片。
#![allow(unused)]
fn main() {
// 固定大小
let arr = [1, 2, 3];
match arr {
[1, _, _] => "starts with one",
[a, b, c] => "starts with something else",
};
}
#![allow(unused)]
fn main() {
// 动态大小
let v = vec![1, 2, 3];
match v[..] {
[a, b] => { /* 此分支不会应用,因为长度不匹配 */ }
[a, b, c] => { /* 此分支会应用 */ }
_ => { /* 需要此通配符,因为长度在静态时未知 */ }
};
}
当匹配数组时,如果每个元素都是不可反驳的,则切片模式是不可反驳的。
当匹配切片时,仅当形式是单个 .. 剩余模式或带有 .. 剩余模式作为子模式的标识符模式时,它才是不可反驳的。
在切片内部,没有同时指定下界和上界的范围模式必须括在括号中,如 (a..),以明确其意图是匹配单个切片元素。同时具有下界和上界的范围模式,如 a..=b,不需要括在括号中。
路径模式
Syntax
PathPattern → PathExpression
路径模式是引用常量值或没有字段的结构体或枚举变体的模式。
非限定路径模式可以引用:
- 枚举变体
- 结构体
- 常量
- 关联常量
限定路径模式只能引用关联常量。
当路径模式引用结构体或只有一个变体的枚举变体,或引用类型不可反驳的常量时,它们是不可反驳的。当引用可反驳的常量或具有多个变体的枚举变体时,它们是可反驳的。
常量模式
当类型为 T 的常量 C 被用作模式时,我们首先检查 T: PartialEq。
此外我们要求 C 的值具有(递归)结构相等性,递归定义如下:
- 整数以及
str、bool和char值始终具有结构相等性。
- 如果元组、数组和切片的所有字段/元素都具有结构相等性,则它们也具有结构相等性。(特别地,
()和[]始终具有结构相等性。)
- 如果引用指向的值具有结构相等性,则该引用具有结构相等性。
- 如果
struct或enum类型的PartialEq实例是通过#[derive(PartialEq)]派生得到的,并且所有字段(对于枚举:活跃变体的字段)具有结构相等性,则该类型的值具有结构相等性。
- 如果裸指针被定义为常量整数(然后被 cast/transmute),则该裸指针具有结构相等性。
- 如果浮点值不是
NaN,则它具有结构相等性。
- 其他任何内容都不具有结构相等性。
特别地,C 的值必须在模式构建时(即单态化之前)已知。这意味着涉及泛型参数的关联常量不能用作模式。
C 的值不能包含任何对可变静态变量(static mut 项或内部可变的 static 项)或 extern 静态变量的引用。
在确保满足所有条件后,常量值被转换为模式,并且现在其行为完全如同直接书写了该模式一样。特别是,它完全参与穷尽性检查。(对于裸指针,常量是书写此类模式的唯一方式。对于这些类型,只有 _ 被认为是有穷尽性的。)
或模式
或模式是匹配两个或多个子模式之一的模式(例如 A | B | C)。它们可以任意嵌套。在语法上,或模式允许出现在任何允许其他模式的地方(由 Pattern 产生式表示),但 let 绑定以及函数和闭包参数除外(由 PatternNoTopAlt 产生式表示)。
静态语义
-
给定某个深度处的模式
p | q,对于任意模式p和q,如果满足以下条件,则该模式被认为是病态的:- 为
p推断的类型与为q推断的类型不统一,或 p和q中没有引入相同的绑定集合,或p和q中任何两个具有相同名称的绑定的类型在类型或绑定模式方面不统一。
在所有上述情况下,类型的统一是精确的,不适用隐式类型自动强转。
- 为
- 在对表达式
match e_s { a_1 => e_1, ... a_n => e_n }进行类型检查时,对于包含形式为p_i | q_i的模式的每个 match 分支a_i,如果在其存在的深度d处,深度d处的e_s的片段的类型不与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的嵌套模式。注意,这里所说的*“构造器”*并非指元组结构体模式,而是指任何乘积类型的模式。这包括枚举变体、元组结构体、具有命名字段的结构体、数组、元组和切片。
动态语义
- 在深度
d处,将被检查值表达式e_s与模式c(p | q, ..rest)进行模式匹配的动态语义(其中c是某个构造器,p和q是任意模式,rest可选地是c中的任何剩余潜在因子)被定义为与c(p, ..rest) | c(q, ..rest)的动态语义相同。
与其他无分隔模式之间的优先级
如本章其他部分所示,有几种语法上无分隔的模式类型,包括标识符模式、引用模式和或模式。或模式始终具有最低优先级。这使我们能够为将来可能的类型标注特性预留语法空间,并减少歧义。例如,x @ A(..) | B(..) 将导致错误,因为 x 并未在所有模式中被绑定。&A(x) | B(x) 将导致不同子模式中 x 的类型不匹配。
-
ObsoleteRangePattern 语法已在 2021 版中移除。 ↩
类型系统
类型
Rust 程序中的每个变量、项和值都有一个类型。值的类型定义了其内存的解释方式以及可对该值执行的操作。
内置类型与语言紧密集成,以非常规的方式实现,用户定义类型无法模拟。
用户定义类型的功能有限。
类型的分类如下:
- 原始类型:
- 序列类型:
- 用户定义类型:
- 函数类型:
- 指针类型:
- Trait 类型:
类型表达式
Syntax
Type →
TypeNoBounds
| ImplTraitType
| TraitObjectType
TypeNoBounds →
ParenthesizedType
| ImplTraitTypeOneBound
| TraitObjectTypeOneBound
| TypePath
| TupleType
| NeverType
| RawPointerType
| ReferenceType
| ArrayType
| SliceType
| InferredType
| QualifiedPathInType
| BareFunctionType
| MacroInvocation
类型表达式是上述 Type 语法规则所定义的引用类型的语法。它可以引用:
- 推断类型,请求编译器来确定类型。
- 括号,用于消除歧义。
- Trait 类型:Trait 对象 和 impl trait。
- never 类型。
- 宏,展开为一个类型表达式。
括号类型
Syntax
ParenthesizedType → ( Type )
在某些情况下,类型的组合可能存在歧义。在类型周围使用括号可以消除歧义。例如,引用类型 中的 + 运算符对类型边界的作用域可能不明确,因此必须使用括号。需要此消歧方式的语法规则使用 TypeNoBounds 规则而非 Type。
#![allow(unused)]
fn main() {
use std::any::Any;
type T<'a> = &'a (dyn Any + Send);
}
递归类型
名义类型——结构体、枚举和联合体——可以是递归的。也就是说,每个 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))));
}
布尔类型
#![allow(unused)]
fn main() {
let b: bool = true;
}
布尔类型或 bool 是一种原始数据类型,可以取两个值之一,称为 true 和 false。
此类型的值可以使用字面量表达式通过关键字 true 和 false 来创建,分别对应同名的值。
布尔类型的对象具有1的大小和对齐。
值 false 的位模式为 0x00,值 true 的位模式为 0x01。布尔类型的对象具有任何其它位模式是未定义行为。
布尔类型是多种表达式中许多操作数的类型:
- 惰性布尔运算符表达式中的操作数
Note
布尔类型的行为类似于枚举类型,但不是枚举类型。在实践中,这主要意味着构造函数不与类型关联(例如
bool::true)。
与所有原始类型一样,布尔类型实现了 trait Clone、Copy、Sized、Send 和 Sync。
Note
有关库操作,请参阅标准库文档。
布尔值的运算
当使用某些运算符表达式对布尔类型的操作数进行运算时,它们按照布尔逻辑的规则求值。
逻辑非
b | !b |
|---|---|
true | false |
false | true |
逻辑或
a | b | a | b |
|---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
逻辑与
a | b | a & b |
|---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
逻辑异或
a | b | a ^ b |
|---|---|---|
true | true | false |
true | false | true |
false | true | true |
false | false | false |
比较
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相同
位有效性
bool 的单个字节保证被初始化(换句话说,transmute::<bool, u8>(...) 始终是可靠的——但由于某些位模式是无效的 bool,反过来并不总是可靠的)。
数值类型
整数类型
无符号整数类型包括:
| 类型 | 最小值 | 最大值 |
|---|---|---|
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 |
浮点类型
IEEE 754-2008 “binary32” 和 “binary64” 浮点类型分别为 f32 和 f64。
依赖机器的整数类型
usize 类型是一种无符号整数类型,其位数与平台的指针类型相同。它可以表示进程中的每个内存地址。
isize 类型是一种有符号二进制补码整数类型,其位数与平台的指针类型相同。对象和数组大小的理论上限是最大 isize 值。这确保了 isize 可用于计算指向同一对象或数组的指针之间的差值,并且可以寻址对象内的每个字节以及对象末尾之后一个字节。
usize 和 isize 至少为 16 位宽。
Note
Rust 代码的许多部分可能假定指针、
usize和isize是 32 位或 64 位。因此,16 位指针的支持是有限的,可能需要库显式注意和确认才能支持。
位有效性
对于每个数值类型 T,T 的位有效性等价于 [u8; size_of::<T>()] 的位有效性。未初始化的字节不是有效的 u8。
字符类型
char 类型表示单个 Unicode 标量值(即不是代理项对的码位)。
Example
#![allow(unused)] fn main() { let c: char = 'a'; let emoji: char = '😀'; let unicode: char = '\u{1F600}'; }
Note
有关
char类型的实现信息,请参阅标准库文档。
char 类型的值表示为一个 32 位无符号字,取值范围在 0x0000 到 0xD7FF 或 0xE000 到 0x10FFFF 之间。创建超出此范围的 char 将立即构成未定义行为。
char 在所有平台上保证与 u32 具有相同的大小和对齐。
char 的每个字节都保证被初始化。换句话说,transmute::<char, [u8; size_of::<char>()]>(...) 始终是可靠的——但由于某些位模式是无效的 char,反过来并不总是可靠的。
字符串切片类型
字符串切片(str)类型表示一个字符序列。
#![allow(unused)]
fn main() {
let greeting1: &str = "Hello, world!";
let greeting2: &str = "你好,世界";
}
Note
有关
str类型的实现信息,请参阅标准库文档。
str 类型的值以与 [u8](8 位无符号字节切片)相同的方式表示。
Note
标准库对
str有额外的假定:操作str的方法假定并确保其中包含的数据是有效的 UTF-8。使用非 UTF-8 缓冲区调用str方法可能现在或将来引发未定义行为。
str 是动态大小类型。它只能通过指针类型(如 &str)进行实例化。&str 的布局与 &[u8] 的布局相同。
Never 类型
Syntax
NeverType → !
never 类型 ! 是一种没有值的类型,表示永远不会完成的求值结果。
类型为 ! 的表达式可以被强制转换为任何其他类型。
! 类型目前只能出现在函数返回类型中,表示它是一个永不返回的发散函数。
#![allow(unused)]
fn main() {
fn foo() -> ! {
panic!("此调用永远不会返回。");
}
}
#![allow(unused)]
fn main() {
unsafe extern "C" {
pub safe fn no_return_extern_func() -> !;
}
}
元组类型
元组类型是其他类型的异构列表的一族结构类型1。
元组类型的语法由括号括起的、逗号分隔的类型列表构成。
1-元组需要在元素类型之后添加逗号,以与括号类型消除歧义。
元组类型的字段数量等于类型列表的长度。这个字段数量确定了元组的元数。具有 n 个字段的元组称为 n-元组。例如,具有 2 个字段的元组是 2-元组。
元组的字段使用提高的数字名称来命名,与其在类型列表中的位置对应。第一个字段是 0,第二个字段是 1,以此类推。每个字段的类型是元组类型列表中相同位置的类型。
为方便和历史原因,没有字段的元组类型(())通常被称为单元类型。它的唯一值也称为单元值。
元组类型的一些示例:
()(单元)(i32,)(1-元组)(f64, f64)(String, i32)(i32, String)(与上一个示例不同类型)(i32, f64, Vec<String>, Option<bool>)
此类型的值使用元组表达式构造。此外,如果没有其他有意义的求值结果,各种表达式将产生单元值。
数组类型
Syntax
ArrayType → [ Type ; Expression ]
数组是包含 N 个类型为 T 的元素的固定大小序列。数组类型写作 [T; N]。
示例:
#![allow(unused)]
fn main() {
// 栈分配的数组
let array: [i32; 3] = [1, 2, 3];
// 堆分配的数组,强制转换为切片
let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);
}
数组的所有元素始终是初始化的,并且在安全方法和运算符中访问数组时始终进行边界检查。
Note
Vec<T>标准库类型提供了一种堆分配的可变大小数组类型。
切片类型
切片是一种动态大小类型,表示类型为 T 的元素序列的一个“视图“。切片类型写作 [T]。
切片类型通常通过指针类型使用。例如:
&[T]:一个“共享切片“,通常直接简称为“切片“。它不拥有所指向的数据;它只是借用。&mut [T]:一个“可变切片“。它可变地借用所指向的数据。Box<[T]>:一个“装箱切片“
示例:
#![allow(unused)]
fn main() {
// 堆分配的数组,强制转换为切片
let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);
// 数组上的(共享)切片
let slice: &[i32] = &boxed_array[..];
}
切片的所有元素始终是初始化的,并且在安全方法和运算符中访问切片时始终进行边界检查。
结构体类型
struct 类型是其他类型的异构积,称为类型的字段。1
struct 的新实例可以使用结构体表达式构造。
struct 的内存布局默认是未定义的,以便编译器进行字段重排等优化,但可以使用 repr 属性来固定。在两种情况下,字段都可以在相应的结构体表达式中以任意顺序给出;生成的 struct 值始终具有相同的内存布局。
struct 的字段可以受可见性修饰符限定,以允许在模块外部访问结构体中的数据。
元组结构体类型与结构体类型类似,只是字段是匿名的。
类单元结构体类型类似于结构体类型,只是它没有字段。由关联的结构体表达式构造的那个值是在这样的类型中存在的唯一值。
-
struct类型类似于 C 中的struct类型、ML 家族中的 record 类型或 Lisp 家族中的 struct 类型。 ↩
枚举类型
枚举类型是一种名义上的、异构的不交和类型,由 enum 项的名称表示。1
enum 项声明了该类型和若干变体,每个变体各自有独立的名称,并使用结构体、元组结构体或类单元结构体的语法。
enum 的新实例可以使用结构体表达式构造。
任何 enum 值消耗的内存与其对应 enum 类型中最大的变体一样多,外加存储判别值所需的大小。
枚举类型不能以结构方式作为类型来表示,而必须通过对 enum 项的命名引用来表示。
-
enum类型类似于 Haskell 中的data构造声明,或 Limbo 中的 pick ADT。 ↩
联合体类型
联合体类型是一种名义上的、异构的类 C 联合体,由 union 项的名称来表示。
联合体没有“活动字段“的概念。相反,每次联合体访问会将联合体内容的部分按所访问字段的类型进行转换(transmute)。
由于转换可能导致意外或未定义行为,读取联合体字段需要 unsafe。
联合体字段类型也限制为一组确保它们永远不需要被丢弃的类型。详见该项的文档。
union 的内存布局默认是未定义的(特别是字段不必须在偏移量 0 处),但是 #[repr(...)] 属性可以用于固定布局。
函数项类型
当被引用时,函数项,或类元组结构体或枚举变体的构造函数,会产生一个其函数项类型的零大小值。
该类型显式地标识了函数——其名称、类型参数和早期绑定生命周期参数(但不包括晚期绑定生命周期参数,它们仅在函数被调用时赋值)——因此该值不需要包含实际的函数指针,调用函数时也不需要间接调用。
没有直接引用函数项类型的语法,但编译器会在错误消息中将类型显示为类似 fn(u32) -> i32 {fn_name} 的形式。
由于函数项类型显式地标识了函数,不同函数的项类型——不同的项,或具有不同泛型的同一项——是不同的,混合它们将产生类型错误:
#![allow(unused)]
fn main() {
fn foo<T>() { }
let x = &mut foo::<i32>;
*x = foo::<u32>; //~ 错误:类型不匹配
}
不过,存在从函数项到具有相同签名的函数指针的强制转换,这种转换不仅在函数项直接用于期望函数指针的位置时触发,也在具有相同签名的不同函数项类型出现在同一 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` 同样也是------类型检查通过。
let foo_ptr_2 = if want_i32 {
foo::<i32>
} else {
foo::<u32>
};
}
所有函数项都实现了 Copy、Clone、Send 和 Sync。
Fn、FnMut 和 FnOnce 被实现,除非函数具有以下任何一项:
unsafe限定符target_feature属性- 除
"Rust"之外的 ABI
闭包类型
闭包表达式生成一个闭包值,其类型是唯一的、匿名的,无法被写出。闭包类型大致等价于一个包含被捕获值的结构体。例如,以下闭包:
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point { x: i32, y: i32 }
struct Rectangle { left_top: Point, right_bottom: Point }
fn f<F : FnOnce() -> String> (g: F) {
println!("{}", g());
}
let mut rect = Rectangle {
left_top: Point { x: 1, y: 1 },
right_bottom: Point { x: 0, y: 0 }
};
let c = || {
rect.left_top.x += 1;
rect.right_bottom.x += 1;
format!("{:?}", rect.left_top)
};
f(c); // 打印 "Point { x: 2, y: 1 }"。
}
生成一个大致如下的闭包类型:
// 注意:这不是精确的转换方式,仅用于说明。
struct Closure<'a> {
left_top : &'a mut Point,
right_bottom_x : &'a mut i32,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.left_top.x += 1;
*self.right_bottom_x += 1;
format!("{:?}", self.left_top)
}
}
因此对 f 的调用如同:
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.right_bottom.x });
捕获模式
捕获模式决定了环境中的位置表达式如何被借用或移动到闭包中。捕获模式有:
- 不可变借用 (
ImmBorrow) — 位置表达式被捕获为共享引用。 - 唯一不可变借用 (
UniqueImmBorrow) — 类似于不可变借用,但必须是唯一的,如下文所述。 - 可变借用 (
MutBorrow) — 位置表达式被捕获为可变引用。 - 移动 (
ByValue) — 位置表达式通过移动值的方式被捕获到闭包中。
环境中的位置表达式从第一个与闭包体内捕获值的使用方式兼容的模式开始被捕获。模式不受闭包周围代码的影响,例如所涉及的变量或字段的生命周期,或闭包自身的生命周期。
Copy 值
实现了 Copy 并且被移动到闭包中的值以 ImmBorrow 模式捕获。
#![allow(unused)]
fn main() {
let x = [0; 1024];
let c = || {
let y = x; // x 以 ImmBorrow 捕获
};
}
异步输入捕获
异步闭包始终捕获所有输入参数,无论它们是否在闭包体内被使用。
捕获精度
捕获路径是一个序列,以环境中的变量开始,后面跟随零个或多个从该变量开始的位置投影。
位置投影是字段访问、元组索引、解引用(和自动解引用)、数组或切片索引表达式,或应用于变量的模式解构。
Note
在
rustc中,模式解构会脱糖为一系列解引用和字段或元素访问。
闭包借用或移动捕获路径,该路径可能根据下述规则被截断。
例如:
#![allow(unused)]
fn main() {
struct SomeStruct {
f1: (i32, i32),
}
let s = SomeStruct { f1: (1, 2) };
let c = || {
let x = s.f1.1; // s.f1.1 以 ImmBorrow 捕获
};
c();
}
此处捕获路径是局部变量 s,后跟字段访问 .f1,再后跟元组索引 .1。此闭包捕获 s.f1.1 的不可变借用。
共享前缀
当一条捕获路径及其某个祖先路径同时被闭包捕获时,祖先路径将以两者中较高的捕获模式捕获,CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode),使用如下严格弱序:
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
注意,这可能需要递归应用。
#![allow(unused)]
fn main() {
// 此例中有三条不同的捕获路径共享同一祖先:
fn move_value<T>(_: T){}
let s = String::from("S");
let t = (s, String::from("T"));
let mut u = (t, String::from("U"));
let c = || {
println!("{:?}", u); // u 以 ImmBorrow 捕获
u.1.truncate(0); // u.1 以 MutBorrow 捕获
move_value(u.0.0); // u.0.0 以 ByValue 捕获
};
c();
}
总体而言,此闭包将以 ByValue 捕获 u。
最右侧共享引用截断
捕获路径在路径中最右侧的解引用处截断,前提是该解引用作用于共享引用。
允许此截断是因为通过共享引用读取的字段始终通过共享引用或复制来读取。这有助于在额外精度从借用检查角度而言没有收益时减少捕获的大小。
之所以是最右侧解引用,是为了帮助避免不必要的更短生命周期。考虑以下示例:
#![allow(unused)]
fn main() {
struct Int(i32);
struct B<'a>(&'a i32);
struct MyStruct<'a> {
a: &'static Int,
b: B<'a>,
}
fn foo<'a, 'b>(m: &'a MyStruct<'b>) -> impl FnMut() + 'static {
let c = || drop(&m.a.0);
c
}
}
如果此处捕获 m,则闭包将不再能存活超过 'static,因为 m 受 'a 约束。相反,它以 ImmBorrow 捕获 (*(*m).a)。
通配符模式绑定
闭包仅捕获需要被读取的数据。使用通配符模式绑定值不会读取该值,因此该位置不会被捕获。
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
let x = S;
let c = || {
let _ = x; // 不捕获 `x`。
};
let c = || match x {
_ => (), // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
}
解构元组、结构体和单变体枚举本身不会导致读取或捕获该位置。
Note
标记有
#[non_exhaustive]的枚举始终被视为具有多个变体。参见 type.closure.capture.precision.discriminants.non_exhaustive。
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
// 解构元组不会导致读取或捕获。
let x = (S,);
let c = || {
let (..) = x; // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
// 解构单元结构体不会导致读取或捕获。
let x = S;
let c = || {
let S = x; // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
// 解构结构体不会导致读取或捕获。
struct W<T>(T);
let x = W(S);
let c = || {
let W(..) = x; // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
// 解构单变体枚举不会导致读取或捕获。
enum E<T> { V(T) }
let x = E::V(S);
let c = || {
let E::V(..) = x; // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
}
匹配 RestPattern (..) 或 StructPatternEtCetera(也是 ..)的字段不会被读取,这些字段不会被捕获。
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
let x = (S, S);
let c = || {
let (x0, ..) = x; // 以 `ByValue` 捕获 `x.0`。
};
// 只有第一个元组字段被闭包捕获。
x.1; // 正确:`x.1` 可以在这里移动。
c();
}
不支持对数组和切片的部分捕获;即使使用通配符模式匹配、索引或子切片,整个切片或数组也始终被捕获。
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
let mut x = [S, S];
let c = || {
let [x0, _] = x; // 以 `ByValue` 捕获整个 `x`。
};
let _ = &mut x[1]; // 错误:借用已移动的值。
}
使用通配符匹配的值仍然必须被初始化。
#![allow(unused)]
fn main() {
let x: u8;
let c = || {
let _ = x; // 错误:绑定 `x` 未初始化。
};
}
判别值读取的捕获
如果模式匹配读取了判别值,则包含该判别值的位置以 ImmBorrow 被捕获。
匹配具有多个变体的枚举的某个变体将读取判别值,以 ImmBorrow 捕获该位置。
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
let mut x = (Some(S), S);
let c = || match x {
(None, _) => (),
// ^^^^
// 此模式需要读取判别值,这导致
// `x.0` 以 `ImmBorrow` 被捕获。
_ => (),
};
let _ = &mut x.0; // 错误:无法将 `x.0` 作为可变借用。
// ^^^
// 闭包仍然存活,因此这里 `x.0` 仍然
// 被不可变借用。
c();
}
#![allow(unused)]
fn main() {
struct S; // 非 `Copy` 类型。
let x = (Some(S), S);
let c = || match x { // 以 `ImmBorrow` 捕获 `x.0`。
(None, _) => (),
_ => (),
};
// 虽然 `x.0` 因判别值读取而被捕获,
// 但 `x.1` 没有被捕获。
x.1; // 正确:`x.1` 可以在这里移动。
c();
}
匹配单变体枚举的唯一变体不会读取判别值,也不会捕获该位置。
#![allow(unused)]
fn main() {
enum E<T> { V(T) } // 单变体枚举。
let x = E::V(());
let c = || {
let E::V(_) = x; // 不捕获 `x`。
};
x; // 正确:`x` 可以在这里移动。
c();
}
如果 #[non_exhaustive] 应用于某个枚举,就判断是否发生读取而言,该枚举被视为具有多个变体,即使它实际上只有一个变体。
即使除所匹配的变体之外的所有变体都是不可居住的,使得该模式不可反驳,判别值仍然会被读取(如果本来应读取的话)。
#![allow(unused)]
fn main() {
enum Empty {}
let mut x = Ok::<_, Empty>(42);
let c = || {
let Ok(_) = x; // 以 `ImmBorrow` 捕获 `x`。
};
let _ = &mut x; // 错误:无法将 `x` 作为可变借用。
c();
}
范围模式的捕获
匹配范围模式会读取被匹配的位置,即使该范围包含类型的所有可能值,也会以 ImmBorrow 捕获该位置。
#![allow(unused)]
fn main() {
let mut x = 0u8;
let c = || {
let 0..=u8::MAX = x; // 以 `ImmBorrow` 捕获 `x`。
};
let _ = &mut x; // 错误:无法将 `x` 作为可变借用。
c();
}
切片模式的捕获
将切片与除仅包含单个剩余模式(即 [..])之外的切片模式匹配,视为从切片读取长度,并以 ImmBorrow 捕获该切片。
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // 以 `ImmBorrow` 捕获 `*x`。
&mut [] => (),
// ^^
// 这匹配一个恰好零元素的切片。要判断被检查值是否
// 匹配,必须读取长度,导致切片被捕获。
_ => (),
};
let _ = &mut *x; // 错误:无法将 `*x` 作为可变借用。
c();
}
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // 不捕获 `*x`。
[..] => (),
// ^^ 剩余模式。
};
let _ = &mut *x; // 正确:`*x` 可以在这里借用。
c();
}
Note
也许令人惊讶的是,尽管长度包含在指向切片的(宽)指针中,但被视为读取并捕获的是指向对象(切片)的位置。
#![allow(unused)] fn main() { fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { // 闭包存活期超过 `'l`,因为它捕获了 `**x`。 // 如果它捕获的是 `*x`,则存活时间不足以 // 满足 `impl Fn() + 'l` 的约束。 || match *x { // 以 `ImmBorrow` 捕获 `**x`。 &[] => (), _ => (), } } }这样,行为与在检查值中解引用到切片是一致的。
#![allow(unused)] fn main() { fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { || match **x { // 以 `ImmBorrow` 捕获 `**x`。 [] => (), _ => (), } } }详细信息见 Rust PR #138961。
由于数组的长度由类型确定,将数组与切片模式匹配本身并不会捕获该位置。
#![allow(unused)]
fn main() {
let x: [u8; 1] = [0];
let c = || match x { // 不捕获 `x`。
[_] => (), // 长度是固定的。
};
x; // 正确:`x` 可以在这里移动。
c();
}
move 上下文中捕获引用
由于不允许从引用中移出字段,move 闭包仅会捕获到达引用第一次解引用之前(但不包括)的捕获路径前缀。引用本身将被移动到闭包中。
#![allow(unused)]
fn main() {
struct T(String, String);
let mut t = T(String::from("foo"), String::from("bar"));
let t_mut_ref = &mut t;
let mut c = move || {
t_mut_ref.0.push_str("123"); // 以 ByValue 捕获 `t_mut_ref`
};
c();
}
裸指针解引用
由于解引用裸指针是 unsafe 的,闭包仅会捕获到达裸指针第一次解引用之前(但不包括)的捕获路径前缀。
#![allow(unused)]
fn main() {
struct T(String, String);
let t = T(String::from("foo"), String::from("bar"));
let t_ptr = &t as *const T;
let c = || unsafe {
println!("{}", (*t_ptr).0); // 以 ImmBorrow 捕获 `t_ptr`
};
c();
}
联合体字段
由于访问联合体字段是 unsafe 的,闭包仅会捕获到达联合体本身的捕获路径前缀。
#![allow(unused)]
fn main() {
union U {
a: (i32, i32),
b: bool,
}
let u = U { a: (123, 456) };
let c = || {
let x = unsafe { u.a.0 }; // 以 ByValue 捕获 `u`
};
c();
// 这也包括写入字段。
let mut u = U { a: (123, 456) };
let mut c = || {
u.b = true; // 以 MutBorrow 捕获 `u`
};
c();
}
对未对齐 struct 的引用
由于创建对结构中未对齐字段的引用是未定义行为,闭包仅会捕获到达使用了 packed 表示法的结构体中第一次字段访问之前(但不包括)的捕获路径前缀。这包括所有字段,即使那些是对齐的,以防将来结构体中的任何字段发生变化时产生兼容性问题。
#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(i32, i32);
let t = T(2, 5);
let c = || {
let a = t.0; // 以 ImmBorrow 捕获 `t`
};
// 从 `t` 复制是可以的。
let (a, b) = (t.0, t.1);
c();
}
类似地,获取未对齐字段的地址也会捕获整个结构体:
#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(String, String);
let mut t = T(String::new(), String::new());
let c = || {
let a = std::ptr::addr_of!(t.1); // 以 ImmBorrow 捕获 `t`
};
let a = t.0; // 错误:无法移出 `t.0`,因为它已被借用
c();
}
但如果不是 packed 的就可行,因为它精确地捕获了字段:
#![allow(unused)]
fn main() {
struct T(String, String);
let mut t = T(String::new(), String::new());
let c = || {
let a = std::ptr::addr_of!(t.1); // 以 ImmBorrow 捕获 `t.1`
};
// 这里允许移动。
let a = t.0;
c();
}
Box 与其他 Deref 实现
Box 的 Deref trait 实现与其他 Deref 实现的处理方式不同,因为它被视为特殊实体。
例如,我们来看涉及 Rc 和 Box 的示例。*rc 脱糖为调用 Rc 上定义的 trait 方法 deref,但由于 *box 被特殊处理,可以对 Box 的内容进行精确捕获。
非 move 闭包中的 Box
在非 move 闭包中,如果 Box 的内容没有被移动到闭包体内,则 Box 的内容会被精确捕获。
#![allow(unused)]
fn main() {
struct S(String);
let b = Box::new(S(String::new()));
let c_box = || {
let x = &(*b).0; // 以 ImmBorrow 捕获 `(*b).0`
};
c_box();
// 将 `Box` 与另一个实现了 Deref 的类型对比:
let r = std::rc::Rc::new(S(String::new()));
let c_rc = || {
let x = &(*r).0; // 以 ImmBorrow 捕获 `r`
};
c_rc();
}
但是,如果 Box 的内容被移动到闭包中,则整个 box 被捕获。这样做是为了最小化需要移动到闭包中的数据量。
#![allow(unused)]
fn main() {
// 与上例相同,只是闭包移动值而不是获取引用。
struct S(String);
let b = Box::new(S(String::new()));
let c_box = || {
let x = (*b).0; // 以 ByValue 捕获 `b`
};
c_box();
}
move 闭包中的 Box
与非 move 闭包中移动 Box 内容类似,在 move 闭包中读取 Box 的内容将会整体捕获该 Box。
#![allow(unused)]
fn main() {
struct S(i32);
let b = Box::new(S(10));
let c_box = move || {
let x = (*b).0; // 以 ByValue 捕获 `b`
};
}
唯一不可变借用于捕获
捕获可以通过一种特殊的借用发生,称为唯一不可变借用,它不能在语言的其他任何地方使用,也无法显式写出。当修改可变引用的所指对象时会发生这种借用,如下例所示:
#![allow(unused)]
fn main() {
let mut b = false;
let x = &mut b;
let mut c = || {
// 对 `x` 的 ImmBorrow 和 MutBorrow。
let a = &x;
*x = true; // `x` 以 UniqueImmBorrow 捕获
};
// 下面这行会出错:
// let y = &x;
c();
// 然而下面这行没问题。
let z = &x;
}
在这种情况下,可变借用 x 是不可能的,因为 x 不是 mut。但同时,不可变借用 x 会使赋值非法,因为 & &mut 引用可能不是唯一的,因此不能安全地用于修改值。所以使用了唯一不可变借用:它不可变地借用 x,但像可变借用一样,它必须是唯一的。
在上述示例中,取消 y 声明的注释将产生错误,因为这会违反闭包对 x 的借用的唯一性;z 的声明是有效的,因为闭包的生命周期已在块结束时到期,释放了借用。
调用 trait 与强制转换
闭包类型都实现了 FnOnce,表示它们可通过消耗闭包所有权被调用一次。此外,某些闭包实现了更具体的调用 trait:
- 不移动出任何被捕获变量的闭包实现了
FnMut,表示它可以通过可变引用被调用。
- 不修改也不移动出任何被捕获变量的闭包实现了
Fn,表示它可以通过共享引用被调用。
非捕获闭包是不从环境中捕获任何内容的闭包。非异步、非捕获闭包可以强制转换为具有匹配签名的函数指针(例如 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);
}
异步闭包 trait
异步闭包在是否实现 FnMut 或 Fn 方面有进一步的限制。
异步闭包返回的 Future 具有与闭包类似的捕获特征。它根据捕获值在异步闭包中的使用方式,从异步闭包中捕获位置表达式。如果异步闭包具有以下任一属性,则称其借出给其 Future:
Future包含可变捕获。- 异步闭包按值捕获,除非该值通过解引用投影访问。
如果异步闭包借出给其 Future,则 FnMut 和 Fn 不被实现。FnOnce 总是被实现。
示例:可变捕获的第一个条款可以用以下示例说明:
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {} fn f() { let mut x = 1i32; let c = async || { x = 2; // x 以 MutBorrow 捕获 }; takes_callback(c); // 错误:异步闭包未实现 `FnMut` } }常规值捕获的第二个条款可以用以下示例说明:
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = x + 2; // x 以 ByValue 捕获 }; takes_callback(c); // 错误:异步闭包未实现 `Fn` } }第二个条款的例外可以通过使用解引用来说明,这样确实允许实现
Fn和FnMut:#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = *x + 2; }; takes_callback(c); // 正确:实现了 `Fn` } }
异步闭包以类似于常规闭包实现 Fn、FnMut 和 FnOnce 的方式实现 AsyncFn、AsyncFnMut 和 AsyncFnOnce;即,取决于闭包体内对捕获变量的使用方式。
其他 trait
所有闭包类型都实现了 Sized。此外,闭包类型在其存储的捕获类型允许时实现以下 trait:
Send 和 Sync 的规则与普通结构体类型相同,而 Clone 和 Copy 的行为如同派生一样。对于 Clone,捕获值的克隆顺序未指定。
由于捕获通常是通过引用进行的,因此产生以下一般规则:
- 如果所有捕获的值都是
Sync的,则闭包是Sync的。 - 如果所有通过非唯一不可变引用捕获的值都是
Sync的,并且所有通过唯一不可变或可变引用、复制或移动捕获的值都是Send的,则闭包是Send的。 - 如果闭包没有通过唯一不可变或可变引用捕获任何值,并且它通过复制或移动捕获的所有值分别是
Clone或Copy的,则闭包是Clone或Copy的。
丢弃顺序
如果闭包按值捕获了复合类型(例如结构体、元组和枚举)的字段,则该字段的生命周期现在将绑定到闭包。因此,复合类型的不相交字段可能在不同时间被丢弃。
#![allow(unused)]
fn main() {
{
let tuple =
(String::from("foo"), String::from("bar")); // --+
{ // |
let c = || { // ----------------------------+ |
// tuple.0 被捕获到闭包中 | |
drop(tuple.0); // | |
}; // | |
} // 'c' 和 'tuple.0' 在此处丢弃 --------------+ |
} // tuple.1 在此处丢弃 ------------------------------+
}
2018 及更早版本
闭包类型差异
在 2018 版本及之前,闭包始终整体捕获一个变量,不带其精确捕获路径。这意味着对于闭包类型一节中使用的示例,生成的闭包类型将类似如下:
struct Closure<'a> {
rect : &'a mut Rectangle,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.rect.left_top.x += 1;
self.rect.right_bottom.x += 1;
format!("{:?}", self.rect.left_top)
}
}
对 f 的调用将如下工作:
f(Closure { rect: rect });
捕获精度差异
复合类型(如结构体、元组和枚举)始终被整体捕获,而不是按单个字段捕获。因此,可能需要借用到局部变量才能捕获单个字段:
#![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 已经被借用以进行迭代,代码将无法编译。
如果使用了 move 关键字,则所有捕获都是通过移动或(对于 Copy 类型)复制进行的,无论借用是否可行。move 关键字通常用于允许闭包在捕获值之后继续存活,例如在返回闭包或使用它来生成新线程时。
无论闭包是否会读取数据(即在通配符模式的情况下),如果在闭包内提到了闭包外部定义的变量,该变量将被整体捕获。
丢弃顺序差异
由于复合类型被整体捕获,按值捕获这些复合类型之一的闭包将在闭包被丢弃时同时丢弃整个被捕获的变量。
#![allow(unused)]
fn main() {
{
let tuple =
(String::from("foo"), String::from("bar"));
{
let c = || { // --------------------------+
// tuple 被捕获到闭包中 |
drop(tuple.0); // |
}; // |
} // 'c' 和 'tuple' 在此处丢弃 --------------+
}
}
指针类型
所有指针都是显式的一等值。它们可以被移动或复制,存储在数据结构中,并从函数返回。
引用(& 和 &mut)
Syntax
ReferenceType → & Lifetime? mut? TypeNoBounds
共享引用(&)
共享引用指向由某个其他值拥有的内存。
当创建对值的共享引用时,它会阻止该值的直接修改。内部可变性在某些情况下提供了对此的例外。顾名思义,可以对一个值存在任意数量的共享引用。共享引用类型写作 &type,或者当你需要指定显式生命周期时写作 &'a type。
复制引用是一种“浅“操作:它只涉及复制指针本身,也就是说,指针是 Copy 的。释放引用对其指向的值没有影响,但对临时值的引用会在引用自身的作用域期间保持该临时值的存活。
可变引用(&mut)
可变引用指向由某个其他值拥有的内存。可变引用类型写作 &mut type 或 &'a mut type。
可变引用(未被借出的)是访问其指向值的唯一方式,因此不是 Copy 的。
裸指针(*const 和 *mut)
Syntax
RawPointerType → * ( mut | const ) TypeNoBounds
裸指针是没有安全或活性保证的指针。裸指针写作 *const T 或 *mut T。例如,*const i32 表示指向 32 位整数的裸指针。
复制或丢弃裸指针不会对任何其他值的生命周期产生任何影响。
解引用裸指针是 unsafe 操作。
这也可以用来通过重新借用(&* 或 &mut *)将裸指针转换为引用。通常不鼓励使用裸指针;它们存在是为了支持与外部代码的互操作,以及编写性能关键或底层函数。
比较裸指针时,按地址进行比较,而不是按其指向的内容进行比较。比较指向动态大小类型的裸指针时,还会比较它们的附加数据。
裸指针可以直接使用 &raw const(用于 *const 指针)和 &raw mut(用于 *mut 指针)来创建。
智能指针
标准库包含了引用和裸指针之外的额外“智能指针“类型。
位有效性
尽管在大多数平台上生成的机器码中,指针和引用与 usize 类似,但将引用或指针类型转换为非指针类型的语义目前尚未确定。因此,将指针或引用类型 P 转换为 [u8; size_of::<P>()] 可能不是有效的。
对于瘦裸指针(即对于 T: Sized 的 P = *const T 或 P = *mut T),反向(从整数或整数数组转换为 P)始终是有效的。然而,通过这样的转换产生的指针可能不能被解引用(即使 T 具有零大小也不行)。
函数指针类型
Syntax
BareFunctionType →
ForLifetimes? FunctionTypeQualifiers fn
( FunctionParametersMaybeNamedVariadic? ) BareFunctionReturnType?
FunctionTypeQualifiers → unsafe? ( extern Abi? )?
BareFunctionReturnType → -> TypeNoBounds
FunctionParametersMaybeNamedVariadic →
MaybeNamedFunctionParameters | MaybeNamedFunctionParametersVariadic
MaybeNamedFunctionParameters →
MaybeNamedParam ( , MaybeNamedParam )* ,?
MaybeNamedParam →
OuterAttribute* ( ( IDENTIFIER | _ ) : )? Type
MaybeNamedFunctionParametersVariadic →
( MaybeNamedParam , )* MaybeNamedParam , OuterAttribute* ...
函数指针类型使用 fn 关键字编写,指向一个在编译时不一定知道其标识的函数。
以下示例中 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);
}
函数指针可以通过从函数项和非捕获、非异步闭包强制转换来创建。
unsafe 限定符表示该类型的值是一个不安全函数,extern 限定符表示它是一个外部函数。
要使函数为可变参数函数,其 extern ABI 必须是 items.extern.variadic.conventions 中列出的之一。
函数指针参数上的属性
函数指针参数上的属性遵循与常规函数参数相同的规则和限制。
Trait 对象
Syntax
TraitObjectType → dyn? Bounds
trait 对象是实现了某一组 trait 的另一种类型的不透明值。这组 trait 由一个 dyn 兼容的基础 trait 加上任意数量的自动 trait组成。
Trait 对象实现了基础 trait、其自动 trait 以及基础 trait 的任何超 trait。
Trait 对象写作关键字 dyn 后跟一组 trait 边界,但对 trait 边界有如下限制。
不能有超过一个非自动 trait,不能有超过一个生命周期,并且不允许使用 opt-out 边界(例如 ?Sized)。此外,trait 路径可以用括号括起来。
例如,给定一个 trait Trait,以下所有都是 trait 对象:
dyn Traitdyn Trait + Senddyn Trait + Send + Syncdyn Trait + 'staticdyn Trait + Send + 'staticdyn Trait +dyn 'static + Trait。dyn (Trait)
2021 Edition differences
在 2021 版本之前,
dyn关键字可以省略。
2018 Edition differences
在 2015 版本中,如果 trait 对象的第一个边界是一个以
::开头的路径,则dyn将被视为路径的一部分。可以通过将第一个路径放在括号中来规避此问题。因此,如果你想要一个带有 trait::your_module::Trait的 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的实例的指针 - 一个虚方法表,通常简称为 vtable,对于
SomeTrait及其T实现的超 trait 的每个方法,包含一个指向T的实现(即函数指针)的指针。
trait 对象的目的是允许方法的“晚期绑定“。在 trait 对象上调用方法会导致运行时的虚派发:即从 trait 对象的 vtable 中加载函数指针并间接调用。每个 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 在 print 的类型签名和 main 中的类型转换表达式中均以 trait 对象的形式出现。
Trait 对象生命周期边界
由于 trait 对象可以包含引用,这些引用的生命周期需要作为 trait 对象的一部分来表达。此生命周期写作 Trait + 'a。有一些默认规则允许此生命周期通常被推断为一个合理的选择。
impl Trait
Syntax
ImplTraitType → impl Bounds
ImplTraitTypeOneBound → impl TraitBound
impl Trait 提供了指定实现了特定 trait 的未命名但具体类型的方式。它可以出现在两类位置:参数位置(可以充当函数的匿名类型参数)和返回位置(可以充当抽象返回类型)。
#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}
// 参数位置:匿名类型参数
fn foo(arg: impl Trait) {
}
// 返回位置:抽象返回类型
fn bar() -> impl Trait {
}
}
匿名类型参数
Note
这通常被称为“参数位置的 impl Trait“。(“参数“在此是更准确的用语,但“参数位置的 impl Trait“是该特性开发期间使用的措辞,并且仍保留在实现的某些部分。)
函数可以使用 impl 后跟一组 trait 边界来声明参数具有匿名类型。调用者必须提供一个满足匿名类型参数所声明边界的类型,函数也只能使用通过匿名类型参数的 trait 边界可用的方法。
例如,以下两种形式几乎是等价的:
#![allow(unused)]
fn main() {
trait Trait {}
// 泛型类型参数
fn with_generic_type<T: Trait>(arg: T) {
}
// 参数位置的 impl Trait
fn with_impl_trait(arg: impl Trait) {
}
}
也就是说,参数位置的 impl Trait 是类似 <T: Trait> 的泛型类型参数的语法糖,不同之处在于该类型是匿名的,并且不出现在 GenericParams 列表中。
Note
对于函数参数,泛型类型参数和
impl Trait并不完全等价。对于泛型参数如<T: Trait>,调用者可以选择在调用处使用 GenericArgs 显式指定T的泛型参数,例如foo::<usize>(1)。将参数从一种形式更改为另一种会构成函数的调用者的破坏性变更,因为这改变了泛型参数的数量。
抽象返回类型
Note
这通常被称为“返回位置的 impl Trait“。
函数可以使用 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 暴露为其返回类型的边界,而不是显式指定所有涉及到的其他迭代器类型。
trait 和 trait 实现中的返回位置 impl Trait
trait 中的函数也可以使用 impl Trait 作为匿名关联类型的语法。
trait 的关联函数返回类型中的每个 impl Trait 都被脱糖为一个匿名关联类型。实现函数签名中出现的返回类型用于确定该关联类型的值。
捕获
每个返回位置 impl Trait 抽象类型背后都隐藏着某个具体类型。为了让这个具体类型使用泛型参数,该泛型参数必须被抽象类型捕获。
自动捕获
返回位置 impl Trait 抽象类型自动捕获所有作用域内泛型参数,包括泛型类型、const 和生命周期参数(包括高阶参数)。
2024 Edition differences
在 2024 版本之前,对于自由函数以及固有 impl 的关联函数和方法,未出现在抽象返回类型边界中的泛型生命周期参数不会被自动捕获。
精确捕获
返回位置 impl Trait 抽象类型所捕获的泛型参数集合可以通过 use<..> 边界显式控制。如果存在,则只有在 use<..> 边界中列出的泛型参数才会被捕获。例如:
#![allow(unused)]
fn main() {
fn capture<'a, 'b, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
// ~~~~~~~~~~~~~~~~~~~~~~~
// 仅捕获 `'a` 和 `T`。
(x, y)
}
}
目前,边界列表中只能出现一个 use<..> 边界,所有作用域内的类型和 const 泛型参数都必须被包含,并且出现在抽象类型其他边界中的所有生命周期参数都必须被包含。
在 use<..> 边界内,出现的任何生命周期参数必须位于所有类型和 const 泛型参数之前,如果被省略的生命周期('_)本来可以在 impl Trait 返回类型中出现,则它也可以出现。
由于所有作用域内类型参数必须按名称包含,use<..> 边界不能用于使用参数位置 impl Trait 的项签名中,因为这些项的作用域中有匿名类型参数。
在 trait 定义的关联函数中出现的任何 use<..> 边界都必须包含该 trait 的所有泛型参数,包括 trait 的隐式 Self 泛型类型参数。
泛型与返回位置 impl Trait 的区别
在参数位置,impl Trait 在语义上与泛型类型参数非常相似。然而,在返回位置上两者有显著区别。使用 impl Trait 时,与泛型类型参数不同,是函数选择返回类型,调用者不能选择返回类型。
以下函数:
#![allow(unused)]
fn main() {
trait Trait {}
fn foo<T: Trait>() -> T {
// ...
panic!()
}
}
允许调用者决定返回类型 T,函数返回该类型。
以下函数:
#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}
fn foo() -> impl Trait {
// ...
}
}
不允许调用者决定返回类型。相反,函数选择返回类型,但只承诺它将实现 Trait。
限制
impl Trait 只能作为非 extern 函数的参数或返回类型出现。它不能是 let 绑定的类型、字段类型,也不能出现在类型别名中。
类型参数
在具有类型参数声明的项的定义体内,其类型参数的名称即是类型:
#![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。
推断类型
Syntax
InferredType → _
推断类型要求编译器在可能的情况下根据可用的周围信息来推断类型。
Example
推断类型常用于泛型参数中:
#![allow(unused)] fn main() { let x: Vec<_> = (0..10).collect(); }
推断类型不能用于项签名中。
动态大小类型
大多数类型具有在编译时已知的固定大小,并实现了 Sized trait。大小仅在运行时才知道的类型称为动态大小类型(DST),或非正式地称为无大小类型。切片、trait 对象和 str 是 DST 的示例。
此类类型只能在某些情况下使用:
- 指向 DST 的指针类型具有固定大小,但大小是指向有大小类型的指针的两倍
- 指向切片和
str的指针还存储元素数量。 - 指向 trait 对象的指针还存储一个指向虚表的指针。
- 指向切片和
- DST 可以作为类型实参提供给具有特殊
?Sized约束的泛型类型参数。当对应的关联类型声明具有?Sized约束时,它们也可以用于关联类型定义。默认情况下,任何类型参数或关联类型具有Sized约束,除非使用?Sized放宽。
- 可以为 DST 实现 trait。与泛型类型参数不同,
Self: ?Sized在 trait 定义中默认生效。
- 结构体可以包含一个 DST 作为最后一个字段;这使得结构体本身成为 DST。
类型布局
类型的布局是指其大小、对齐方式以及其字段的相对偏移量。对于枚举,判别式的布局和解读方式也是类型布局的一部分。
类型布局可能随每次编译而改变。与其尝试详尽记录实现细节,我们只记录当前保证的部分。
注意,即使是具有相同布局的类型,其在跨函数边界的传递方式仍可能不同。关于类型的函数调用 ABI 兼容性,请参见此处。
大小与对齐
所有值都具有对齐方式和大小。
值的对齐方式指定了哪些地址可以存储该值。对齐方式为 n 的值只能存储在地址为 n 的倍数的位置。例如,对齐方式为 2 的值必须存储在偶数地址,而对齐方式为 1 的值可以存储在任何地址。对齐方式以字节为单位,必须至少为 1,并且始终是 2 的幂。可以使用 align_of_val 函数检查值的对齐方式。
值的大小是包含该项类型的数组中连续元素之间的偏移量(以字节为单位,包括对齐填充)。值的大小始终是其对齐方式的倍数。注意,有些类型是零大小的;0 被认为是任何对齐方式的倍数(例如,在某些平台上,类型 [u16; 0] 的大小为 0,对齐方式为 2)。可以使用 size_of_val 函数检查值的大小。
所有值均具有相同大小和对齐方式,并且两者在编译时均已知的类型,实现了 Sized trait,并可以通过 size_of 和 align_of 函数进行检查。非 Sized 的类型称为动态大小类型。由于 Sized 类型的所有值共享相同的大小和对齐方式,我们分别将这些共享值称为该类型的大小和对齐方式。
原始数据类型布局
大多数原始类型的大小如下表所示。
| 类型 | 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 位目标上为 4 字节,在 64 位目标上为 8 字节。
usize 和 isize 具有相同的大小和对齐方式。
原始类型的对齐方式是平台相关的。在大多数情况下,它们的对齐方式等于其大小,但也可能更小。特别地,i128 和 u128 通常对齐到 4 或 8 字节,尽管它们的大小为 16;在许多 32 位平台上,i64、u64 和 f64 仅对齐到 4 字节,而不是 8。
保证相同指示大小的定宽有符号和无符号整数变体的对齐方式相同——即,对于给定大小 N,align_of::<uN>() == align_of::<iN>()。
指针与引用布局
指针和引用具有相同的布局。指针或引用的可变性不会改变布局。
指向固定大小类型的指针具有与 usize 相同的大小和对齐方式。
指向非固定大小类型的指针是固定大小的。指向非固定大小类型的指针的大小和对齐方式保证大于或等于指向固定大小类型的指针的大小和对齐方式。
Note
尽管你不应依赖于此,但目前所有指向 DST 的指针都是
usize大小的两倍,并具有相同的对齐方式。
数组布局
[T; N] 数组的大小为 size_of::<T>() * N,与 T 具有相同的对齐方式。数组的布局使得其从零开始的第 n 个元素从数组开头偏移 n * size_of::<T>() 字节。
切片布局
切片与其所切片的数组段具有相同的布局。
Note
这里说的是原生
[T]类型,而非指向切片的指针(&[T]、Box<[T]>等)。
str 布局
字符串切片是字符的 UTF-8 表示形式,与类型 [u8] 的切片具有相同的布局。引用 &str 与引用 &[u8] 具有相同的布局。
元组布局
元组按照 Rust 表示法进行布局。
例外是单元元组(()),它被保证作为零大小类型,大小为 0,对齐方式为 1。
Trait 对象布局
Trait 对象与 trait 对象所指的值具有相同的布局。
Note
这里说的是原生 trait 对象类型,而非指向 trait 对象的指针(
&dyn Trait、Box<dyn Trait>等)。
闭包布局
闭包没有布局保证。
表示法
所有用户定义的复合类型(struct、enum 和 union)都有一个表示法,该表示法指定了该类型的布局。
类型的可能表示法有:
Rust(默认)C- 原始表示法
transparent
可以通过将 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
}
}
Note
作为项上的属性,表示法不依赖于泛型参数。任何两个同名的类型具有相同的表示法。例如,
Foo<Bar>和Foo<Baz>都具有相同的表示法。
类型的表示法可以更改字段之间的填充,但不会更改字段本身的布局。例如,一个具有 C 表示法的结构体包含了一个具有 Rust 表示法的结构体 Inner,这不会改变 Inner 的布局。
Rust 表示法
Rust 表示法是未标注 repr 属性的名义类型的默认表示法。通过 repr 属性显式使用此表示法,保证与完全省略该属性时相同。
此表示法对数据布局的唯一保证是那些为语言健全性所需的保证。它们是:
- 字段的偏移量可以被该字段的对齐方式整除。
- 类型的对齐方式至少是其字段的最大对齐方式。
对于 structs,进一步保证字段不会重叠。也就是说,字段可以排序,使得任何字段的偏移量加上其大小小于等于排序中下一个字段的偏移量。该排序不必须与类型声明中字段的指定顺序相同。
请注意,此保证并不意味着字段具有不同的地址:零大小类型可能与同一结构体中的其他字段具有相同的地址。
此表示法对数据布局没有其他保证。
C 表示法
C 表示法设计用于双重目的。一个目的是创建与 C 语言可互操作的类型。第二个目的是创建可以安全地执行依赖于数据布局的操作的类型,例如将值重新解释为不同的类型。
由于这种双重目的,可以创建对 C 编程语言接口无用的类型。
此表示法可以应用于结构体、联合体和枚举。零变体枚举为例外,对其使用 C 表示法会报错。
#[repr(C)] 结构体
结构体的对齐方式是其内部对齐方式最大的字段的对齐方式。
字段的大小和偏移量由以下算法确定。
从当前偏移量 0 字节开始。
对于结构体中按声明顺序的每个字段,首先确定该字段的大小和对齐方式。如果当前偏移量不是字段对齐方式的倍数,则向当前偏移量添加填充字节,直到其为字段对齐方式的倍数。字段的偏移量就是当前的偏移量数值。然后将当前偏移量增加该字段的大小。
最后,结构体的大小为当前偏移量向上取整到结构体对齐方式的最近倍数。
以下是此算法的伪代码描述。
/// Returns the amount of padding needed after `offset` to ensure that the
/// following address will be aligned to `alignment`.
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 += 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);
Warning
此伪代码为清晰起见使用了忽略溢出问题的朴素算法。要在实际代码中执行内存布局计算,请使用
Layout。
Note
此算法可以产生零大小结构体。在 C 中,像
struct Foo { }这样的空结构体声明是非法的。然而,gcc 和 clang 都支持启用此类结构体的选项,并为其分配大小零。相比之下,C++ 为空结构体赋予大小 1,除非它们来自继承或带有[[no_unique_address]]属性的字段,在这种情况下它们不会增加结构体的总大小。
#[repr(C)] 联合体
用 #[repr(C)] 声明的联合体将具有与目标平台的 C 语言中等价 C 联合体声明相同的大小和对齐方式。
联合体的大小为其所有字段的最大大小向上取整到其对齐方式,对齐方式为其所有字段的最大对齐方式。这些最大值可能来自不同的字段。每个字段都位于从联合体开头偏移 0 字节的位置。
#![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
assert_eq!(std::mem::offset_of!(Union, f1), 0);
assert_eq!(std::mem::offset_of!(Union, f2), 0);
#[repr(C)]
union SizeRoundedUp {
a: u32,
b: [u16; 3],
}
assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 8); // 大小为 6,b 提供,
// 向上取整为 8,来自 a 的对齐方式。
assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // 来自 a
assert_eq!(std::mem::offset_of!(SizeRoundedUp, a), 0);
assert_eq!(std::mem::offset_of!(SizeRoundedUp, b), 0);
}
#[repr(C)] 无字段枚举
对于无字段枚举,C 表示法具有目标平台的 C ABI 下默认 enum 大小和对齐方式的大小和对齐方式。
Note
C 中的枚举表示法是实现定义的,因此这实际上是一个“最佳猜测“。特别是,当感兴趣的 C 代码使用某些标志编译时,这可能是不正确的。
Warning
C 语言中的
enum与具有此表示法的 Rust 无字段枚举之间存在关键差异。C 中的enum主要是一个typedef加上一些命名常量;换句话说,enum类型的对象可以容纳任何整数值。例如,这常用于 C 中的位标志。相比之下,Rust 的无字段枚举只能合法地容纳判别值,其他任何值都是未定义行为。因此,在 FFI 中使用无字段枚举来建模 Cenum通常是错误的。
#[repr(C)] 带字段枚举
带有字段的 repr(C) 枚举的表示法是一个包含两个字段的 repr(C) 结构体,在 C 中也被称为“带标签联合体“:
- 枚举的
repr(C)版本,其所有字段被移除(“标签”)
- 一个
repr(C)联合体,包含每个具有字段的变体的字段所对应的repr(C)结构体(“负载”)
Note
由于
repr(C)结构体和联合体的表示法,如果一个变体只有一个字段,将该字段直接放在联合体中或将其包装在结构体中是没有区别的;任何希望操作此类enum表示法的系统因此可以使用更方便或更一致的形式。
#![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,
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;
}
原始表示法
原始表示法是与原始整数类型同名的表示法。即:u8、u16、u32、u64、u128、usize、i8、i16、i32、i64、i128 和 isize。
原始表示法只能应用于枚举,并且根据枚举是否有字段而具有不同的行为。对零变体枚举使用原始表示法是错误的。将两种原始表示法组合在一起是错误的。
无字段枚举的原始表示法
对于无字段枚举,原始表示法设置大小和对齐方式与同名的原始类型相同。例如,带有 u8 表示法的无字段枚举只能具有 0 到 255(含)之间的判别值。
带字段枚举的原始表示法
原始表示法枚举的表示法是一个 repr(C) 联合体,包含每个具有字段变体的 repr(C) 结构体。联合体中每个结构体的第一个字段是带有原始表示法的枚举的(移除所有字段后的)版本(“标签”),其余字段是该变体的字段。
Note
如果标签在联合体中拥有自己的成员,此表示法不变,这或许能让你操作更清晰(尽管遵循 C++ 标准,标签成员应包装在一个
struct中)。
#![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,
B: MyVariantB,
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);
}
带有字段枚举的原始表示法与 #[repr(C)] 的组合
对于带有字段的枚举,还可以组合 repr(C) 和原始表示法(例如 repr(C, u8))。这会修改 repr(C) 的表示法,将判别式枚举的表示法更改为所选的原始表示法。因此,如果你选择 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 个唯一的判别值(“标签”),而仅带有 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);
}
对齐修饰符
align 和 packed 修饰符可以分别用于提高或降低 struct 和 union 的对齐方式。packed 还可能改变字段之间的填充(尽管不会改变任何字段内部的填充)。单独使用时,align 和 packed 不提供关于结构体布局中字段顺序或枚举变体布局的保证,尽管它们可以与提供此类保证的表示法(如 C)组合使用。
对齐方式以整数参数的形式指定,格式为 #[repr(align(x))] 或 #[repr(packed(x))]。对齐值必须是 2 的幂,范围为 1 到 229。对于 packed,如果未给出值,如 #[repr(packed)],则值为 1。
对于 align,如果指定的对齐方式小于没有 align 修饰符时类型的对齐方式,则对齐方式不受影响。
对于 packed,如果指定的对齐方式大于没有 packed 修饰符时类型的对齐方式,则对齐方式和布局不受影响。
为了定位字段,每个字段的对齐方式取指定对齐方式和该字段类型对齐方式中的较小值。
保证字段间填充是满足每个字段(可能已更改)的对齐方式所需的最小值(尽管请注意,单独使用的 packed 不提供任何关于字段顺序的保证)。这些规则的一个重要结果是,带有 #[repr(packed(1))](或 #[repr(packed)])的类型将没有字段间填充。
align 和 packed 修饰符不能应用于同一类型,且 packed 类型不能传递性地包含另一个 align 类型。align 和 packed 只能应用于 Rust 和 C 表示法。
align 修饰符也可以应用于 enum。当应用时,对 enum 对齐方式的影响与将 enum 包装在带有相同 align 修饰符的 newtype struct 中相同。
Note
不允许引用未对齐的字段,因为这是未定义行为。当字段由于对齐修饰符而变得未对齐时,请考虑以下使用引用和解引用的选项:
#![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 = &raw const e.f2; let value = unsafe { ptr.read_unaligned() }; let mut_ptr: *mut u16 = &raw mut e.f2; unsafe { mut_ptr.write_unaligned(3) } }
transparent 表示法
transparent 表示法只能用于具有单个变体的 struct 或 enum,该变体具有:
- 任意数量的、大小为 0 且对齐方式为 1 的字段(例如
PhantomData<T>),以及 - 至多一个其他字段。
具有此表示法的结构体和枚举具有与唯一的非 0 大小、非 1 对齐字段相同的布局和 ABI(如果存在),否则具有与单元类型相同的布局和 ABI。
这与 C 表示法不同,因为具有 C 表示法的结构体始终具有 C struct 的 ABI,而例如,具有 transparent 表示法且带有原始字段的结构体将具有该原始字段的 ABI。
因为此表示法将类型布局委托给另一个类型,所以它不能与任何其他表示法一起使用。
内部可变性
有时一个类型需要在其存在多个别名的情况下被修改。在 Rust 中,这通过一种称为内部可变性的模式实现。
如果一个类型的内部状态可以通过指向它的共享引用来改变,则该类型具有内部可变性。
这违背了通常的要求,即共享引用指向的值不被修改。
std::cell::UnsafeCell<T> 类型是禁用此要求的唯一允许方式。当 UnsafeCell<T> 被不可变地别名时,仍然可以安全地修改或获取对其包含的 T 的可变引用。
与所有其他类型一样,拥有多个 &mut UnsafeCell<T> 别名是未定义行为。
可以通过使用 UnsafeCell<T> 作为字段来创建其他具有内部可变性的类型。标准库提供了各种提供安全内部可变性 API 的类型。
例如,std::cell::RefCell<T> 使用运行时借用检查来确保围绕多个引用的通常规则。
std::sync::atomic 模块包含包装了仅通过原子操作访问的值的类型,允许该值在线程之间共享和修改。
子类型与型变
子类型关系是隐式的,可以在类型检查或推断的任何阶段发生。
子类型仅限于两种情况:关于生命周期的型变,以及具有高阶生命周期的类型之间的子类型关系。如果我们从类型中抹去生命周期,那么唯一的子类型关系将是基于类型相等的关系。
考虑以下示例:字符串字面量始终具有 '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;
}
型变
型变是泛型类型相对于其参数所具有的一种性质。泛型类型在某个参数上的型变描述了该参数的子类型关系如何影响该类型的子类型关系。
- 如果
T是U的子类型意味着F<T>是F<U>的子类型,则称F<T>对T是协变的(子类型“穿透“)
- 如果
T是U的子类型意味着F<U>是F<T>的子类型,则称F<T>对T是逆变的
- 否则
F<T>对T是不变的(不能推导出子类型关系)
类型的型变按以下规则自动确定:
| 类型 | '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 上不变,使得整个结构体不变
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 约束与生命周期约束
Syntax
Bounds → Bound ( + Bound )* +?
Bound → Lifetime | TraitBound | UseBound
TraitBound →
( ? | ForLifetimes )? TypePath
| ( ( ? | ForLifetimes )? TypePath )
LifetimeBounds → ( Lifetime + )* Lifetime?
Lifetime →
LIFETIME_OR_LABEL
| 'static
| '_
UseBound → use UseBoundGenericArgs
UseBoundGenericArgs →
< >
| < ( UseBoundGenericArg , )* UseBoundGenericArg ,? >
UseBoundGenericArg →
Lifetime
| IDENTIFIER
| Self
Trait 约束和生命周期约束为泛型项提供了一种方式来限制哪些类型和生命周期可以用作它们的参数。约束可以在 where 子句中的任何类型上提供。对于某些常见情况,还有更简短的形式:
- 在声明泛型参数之后写出的约束:
fn f<A: Copy>() {}等同于fn f<A>() where A: Copy {}。 - 在 trait 声明中作为超 trait:
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:
- 在泛型函数体内,可以对
Ty值调用Trait的方法。同样,可以使用Trait上的关联常量。 - 可以使用
Trait的关联类型。 - 具有
T: Trait约束的泛型函数和类型可以使用,其中Ty被用于T。
#![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); // 可以使用泛型函数,因为 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(), // 可以使用关联函数
);
}
}
不使用项的参数或高阶生命周期的约束会在项被定义时检查。如果这样的约束为假,则报错。
Copy、Clone 和 Sized 约束也会在调用项时对某些泛型类型进行检查,即使调用时没有提供具体类型。将 Copy 或 Clone 作为可变引用、trait 对象或切片的约束是错误的。将 Sized 作为 trait 对象或切片的约束是错误的。
#![allow(unused)]
fn main() {
struct A<'a, T>
where
i32: Default, // 允许,但没有用处
i32: Iterator, // 错误:`i32` 不是迭代器
&'a mut T: Copy, // (使用时)错误:trait 约束未满足
[T]: Sized, // (使用时)错误:编译时无法确定大小
{
f: &'a T,
}
struct UsesA<'a, T>(A<'a, T>);
}
trait 约束和生命周期约束也用于命名 trait 对象。
?Sized
? 仅用于放宽对类型参数或关联类型的隐式 Sized trait 约束。?Sized 不能用于对其他类型的约束。
生命周期约束
生命周期约束可以应用于类型或其他生命周期。
'a: 'b 通常读作 'a 存活不短于 'b。'a: 'b 意味着 'a 至少和 'b 一样长,因此引用 &'a () 在 &'b () 有效的任何地方都有效。
#![allow(unused)]
fn main() {
fn f<'a, 'b>(x: &'a i32, mut y: &'b i32) where 'a: 'b {
y = x; // &'a i32 是 &'b i32 的子类型,因为 'a: 'b
let r: &'b &'a i32 = &&0; // &'b &'a i32 是良构的,因为 'a: 'b
}
}
T: 'a 意味着 T 的所有生命周期参数都存活不短于 'a。例如,如果 'a 是一个无约束的生命周期参数,那么 i32: 'static 和 &'static str: 'a 是满足的,但 Vec<&'a ()>: 'static 则不满足。
高阶 trait 约束
Syntax
ForLifetimes → for GenericParams
trait 约束可以是高阶的(higher ranked),即对生命周期进行量化。这些约束指定了一个对所有生命周期都成立的约束。例如,形如 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 进行比较。
只有高阶约束才能在这里使用,因为引用的生命周期比函数上任何可能的生命周期参数都短:
#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) {
let zero = 0;
f(&zero);
}
}
高阶生命周期也可以直接写在 trait 之前:唯一的区别是生命周期参数的作用域,它仅延伸到紧随其后的 trait 结束,而非整个约束。下面这个函数与上一个等价。
#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where F: for<'a> Fn(&'a i32) {
let zero = 0;
f(&zero);
}
}
隐含约束
类型要良构所需满足的生命周期约束有时会被推断出来。
#![allow(unused)]
fn main() {
fn requires_t_outlives_a<'a, T>(x: &'a T) {}
}
类型参数 T 需要存活不短于 'a 才能使类型 &'a T 良构。这会被推断出来,因为函数签名中包含类型 &'a T,而该类型仅在 T: 'a 成立时才有效。
隐含约束会被添加到函数的所有参数和返回值上。在 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>();
}
}
只有生命周期约束会被隐含,trait 约束仍然必须显式添加。因此下面的示例会引发错误:
#![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 块进行推断:
#![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 () {}
// 这段可以编译,因为 `T: 'a` 由 self 类型 `&'a T` 隐含。
impl<'a, T> Trait<'a, T> for &'a T {}
}
Use 约束
某些约束列表可以包含 use<..> 约束来控制哪些泛型参数被 impl Trait 抽象返回类型所捕获。更多细节请参阅精确捕获。
类型自动强转
类型自动强转是更改值的类型的隐式操作。它们在特定位置自动发生,并且对实际强转的类型有高度限制。
自动强转允许的任何转换也可以通过类型转换运算符 as 显式执行。
自动强转最初在 RFC 401 中定义,并在 RFC 1558 中扩展。
强转位置
自动强转只能在程序的特定强转位置发生;这些通常是期望的类型是显式的或可以通过显式类型推导出来的位置(不需要类型推断)。可能的强转位置包括:
-
给出了显式类型的
let语句。例如,在以下代码中
&mut 42被强转为类型&i8:#![allow(unused)] fn main() { let _: &i8 = &mut 42; }
static和const项声明(类似于let语句)。
-
函数调用的参数
被强转的值是实际参数,它被强转为形式参数的类型。
例如,在以下代码中
&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 } }
-
赋值表达式中被赋值的操作数
例如,在以下代码中
y被强转为类型&i8:#![allow(unused)] fn main() { let mut x = &0i8; let y = &mut 42i8; x = y; }
如果这些强转位置之一的表达式是强转传播表达式,则该表达式中的相关子表达式也是强转位置。从这些新的强转位置开始递归传播。传播表达式及其相关子表达式包括:
- 数组字面量,其中数组的类型为
[U; n]。数组字面量中的每个子表达式都是到类型U的强转位置。
- 带有重复语法的数组字面量,其中数组的类型为
[U; n]。重复的子表达式是到类型U的强转位置。
- 元组,其中元组是到类型
(U_0, U_1, ..., U_n)的强转位置。每个子表达式是到各自类型的强转位置,例如第零个子表达式是到类型U_0的强转位置。
- 括号子表达式 (
(e)):如果表达式的类型为U,则子表达式是到U的强转位置。
- 块:如果块的类型为
U,则块中的最后一个表达式(如果没有用分号结束)是到U的强转位置。这包括作为控制流语句一部分的块,如if/else,如果该块具有已知类型的话。
强转类型
允许在以下类型之间进行自动强转:
T到U,如果T是U的子类型(自反情况)
-
T_1到T_3,其中T_1可以强转为T_2,且T_2可以强转为T_3(传递情况)注意,这尚未完全支持。
&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) 是以下之一&T&mut T*const T*mut TBox<T>
并且
U可以通过非固定大小强转由T获得。
- 函数项类型到
fn指针
- 非捕获闭包到
fn指针
!到任何T
非固定大小强转
以下强转称为非固定大小强转,因为它们涉及将类型转换为非固定大小类型(unsized types),并且在上文描述的其他强转不被允许的少数情况下也是被允许的。它们仍然可以在任何允许强转的地方发生。
两个 trait,Unsize 和 CoerceUnsized,用于辅助此过程并在库使用中暴露它。以下强转是内置的,如果 T 可以通过其中之一强转为 U,则将提供 T 对 Unsize<U> 的实现:
[T; n]到[T]。
T到dyn U,当T实现U + Sized,且U是 dyn 兼容的。
dyn T到dyn U,当U是T的超 trait之一时。- 这允许丢弃 auto trait,即
dyn T + Auto到dyn U是允许的。 - 如果主 trait 具有 auto trait 作为超 trait,这允许添加 auto trait,即给定
trait T: U + Send {},则允许dyn T到dyn T + Send或到dyn U + Send的强转。
- 这允许丢弃 auto trait,即
Foo<..., T, ...>到Foo<..., U, ...>,当:Foo是一个结构体。T实现了Unsize<U>。Foo的最后一个字段具有涉及T的类型。- 如果该字段的类型为
Bar<T>,则Bar<T>实现了Unsize<Bar<U>>。 - T 不是任何其他字段类型的一部分。
此外,当 T 实现 Unsize<U> 或 CoerceUnsized<Foo<U>> 时,类型 Foo<T> 可以实现 CoerceUnsized<Foo<U>>。这允许其提供到 Foo<U> 的非固定大小强转。
Note
虽然非固定大小强转的定义及其实现已经稳定,但这些 trait 本身尚未稳定,因此不能在稳定的 Rust 中直接使用。
最小上界强转
在某些上下文中,编译器必须将多个类型一起强转以尝试找到最通用的类型。这称为“最小上界“(Least Upper Bound)强转。LUB 强转仅用于以下情况:
- 为一系列 if 分支找到公共类型。
- 为一系列 match 分支找到公共类型。
- 为数组元素找到公共类型。
- 为带标签块表达式在 break 操作数和最终块操作数之间找到公共类型。
- 为带有 break 表达式的
loop表达式在 break 操作数之间找到公共类型。 - 为具有多个 return 语句的闭包找到返回类型。
- 检查具有多个 return 语句的函数的返回类型。
在每种情况下,有一组类型 T0..Tn 需要相互强转到某个目标类型 T_t,该目标类型开始时是未知的。
LUB 强转的计算是迭代进行的。目标类型 T_t 从类型 T0 开始。对于每个新类型 Ti,我们考虑:
- 如果
Ti可以强转到当前目标类型T_t,则不做更改。
- 否则,检查
T_t是否可以强转到Ti;如果可以,则T_t被更改为Ti。(此检查还取决于到目前为止考虑的所有源表达式是否具有隐式强转。)
- 如果不能,则尝试计算
T_t和Ti的公共超类型,该类型将成为新的目标类型。
示例:
#![allow(unused)]
fn main() {
let (a, b, c) = (0, 1, 2);
// 对于 if 分支
let bar = if true {
a
} else if false {
b
} else {
c
};
// 对于 match 分支
let baw = match 42 {
0 => a,
1 => b,
_ => c,
};
// 对于数组元素
let bax = [a, b, c];
// 对于具有多个 return 语句的闭包
let clo = || {
if true {
a
} else if false {
b
} else {
c
}
};
let baz = clo();
// 对于具有多个 return 语句的函数的类型检查
fn foo() -> i32 {
let (a, b, c) = (0, 1, 2);
match 42 {
0 => a,
1 => b,
_ => c,
}
}
}
在这些示例中,ba* 的类型是通过 LUB 强转找到的。编译器在处理函数 foo 时检查 a、b、c 的 LUB 强转结果是否为 i32。
注意事项
此描述显然是非形式化的。使其更精确的工作预期将作为更精确地规范化 Rust 类型检查器的一般努力的一部分进行。
发散
发散表达式是永远不会完成正常执行的表达式。
#![allow(unused)]
fn main() {
fn diverges() -> ! {
panic!("This function never returns!");
}
fn example() {
let x: i32 = diverges(); // 这一行永远不会完成。
println!("This is never printed: {x}");
}
}
有关特定表达式发散行为的规则,请参见:
- expr.block.diverging — 块表达式。
- expr.if.diverging —
if表达式。 - expr.loop.block-labels.type — 带
break的带标签块表达式。 - expr.loop.break-value.diverging — 带
break的loop表达式。 - expr.loop.break.diverging —
break表达式。 - expr.loop.continue.diverging —
continue表达式。 - expr.loop.infinite.diverging — 无限
loop表达式。 - expr.match.diverging —
match表达式。 - expr.match.empty — 空
match表达式。 - expr.return.diverging —
return表达式。 - type.never.constraint — 返回
!的函数调用。
Note
panic!宏以及相关的生成 panic 的宏(如unreachable!)也具有类型!并且是发散的。
任何类型为 ! 的表达式都是发散表达式。然而,发散表达式不限于类型 !;其他类型的表达式也可能发散(例如,Some(loop {}) 的类型是 Option<!>)。
Note
尽管
!被视为无人居住类型,但类型是无人居住的并不足以使其发散。#![allow(unused)] fn main() { enum Empty {} fn make_never() -> ! {loop{}} fn make_empty() -> Empty {loop{}} fn diverging() -> ! { // 此处的类型为 `!`。 // 因此,整个函数被认为是发散的。 make_never(); // 正确:函数体的类型是 `!`,与返回类型匹配。 } fn not_diverging() -> ! { // 此类型是无人居住的。 // 然而,整个函数不被认为是发散的。 make_empty(); // 错误:函数体的类型是 `()`,但期望的类型是 `!`。 } }
Note
发散可以传播到外围的块。请参见 expr.block.diverging。
回退
如果待推断的类型仅与发散表达式统一,那么该类型将被推断为 !。
Example
#![allow(unused)] fn main() { fn foo() -> i32 { 22 } match foo() { // 错误:trait 约束 `!: Default` 未满足。 4 => Default::default(), _ => return, }; }
2024 Edition differences
在 2024 版次之前,该类型被推断为
()。
Note
重要的是,类型统一可能以结构化方式发生,因此回退到的
!可能是更大类型的一部分。以下代码可以编译:#![allow(unused)] fn main() { fn foo() -> i32 { 22 } // 此处的类型为 `Option<!>`,而非 `!` match foo() { 4 => Default::default(), _ => Some(return), }; }
析构器
当已初始化 变量或临时值离开作用域时,其析构器会被运行,或者说它被丢弃。赋值也会运行其左操作数的析构器(如果它已被初始化)。如果变量已被部分初始化,则仅丢弃其已初始化的字段。
类型 T 的析构器包括:
- 如果
T: Drop,则调用<T as core::ops::Drop>::drop - 递归地运行其所有字段的析构器。
如果必须手动运行析构器(例如在实现你自己的智能指针时),可以使用 core::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("drops when overwritten");
overwritten = PrintOnDrop("drops when scope ends");
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 的作用域结束时,只有第一个字段被丢弃。
}
丢弃作用域
每个变量或临时值都关联到一个丢弃作用域。当控制流离开丢弃作用域时,与该作用域关联的所有变量按声明(对于变量)或创建(对于临时值)的逆序被丢弃。
丢弃作用域可以通过将 for、if 和 while 表达式替换为使用 match、loop 和 break 的等效表达式来确定。
重载运算符与内置运算符不做区分,且不考虑绑定模式。
对于函数或闭包,存在以下丢弃作用域:
- 整个函数
- 每个语句
- 每个表达式
- 每个块,包括函数体
- 对于块表达式,该块的作用域和该表达式的作用域是同一个作用域。
match表达式的每个分支
丢弃作用域按如下方式嵌套。当同时离开多个作用域时(例如从函数返回时),变量从内到外被丢弃。
- 整个函数作用域是最外层作用域。
- 函数体块包含在整个函数的作用域中。
- 表达式语句中的表达式的父作用域是该语句的作用域。
let语句的初始化器的父作用域是该let语句的作用域。
- 语句作用域的父作用域是包含该语句的块的作用域。
match守卫的表达式的父作用域是该守卫所在分支的作用域。
match表达式中=>之后的表达式的父作用域是它所在分支的作用域。
- 分支作用域的父作用域是该分支所属的
match表达式的作用域。
- 所有其他作用域的父作用域是其直接包含的表达式的的作用域。
函数参数的作用域
所有函数参数都在整个函数体的作用域中,因此在计算函数时最后丢弃。每个实际函数参数在该参数模式中引入的绑定之后被丢弃。
#![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")),
);
}
局部变量的作用域
在 let 语句中声明的局部变量关联到包含该 let 语句的块的作用域。
#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
let declared_first = PrintOnDrop("Dropped last in outer scope");
{
let declared_in_block = PrintOnDrop("Dropped in inner scope");
}
let declared_last = PrintOnDrop("Dropped first in outer scope");
}
在 match 表达式或模式匹配的 match 守卫中声明的局部变量关联到它们所在 match 分支的分支作用域。
#![allow(unused)]
fn main() {
#![allow(irrefutable_let_patterns)]
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
match PrintOnDrop("Dropped last in the first arm's scope") {
// 当守卫求值成功时,控制流保留在分支中,
// 值可以从被检查值移动到分支的绑定中,
// 导致它们在分支作用域中被丢弃。
x if let y = PrintOnDrop("Dropped second in the first arm's scope")
&& let z = PrintOnDrop("Dropped first in the first arm's scope") =>
{
let declared_in_block = PrintOnDrop("Dropped in inner scope");
// 模式匹配守卫的绑定和临时值按逆序丢弃,
// 在丢弃每个守卫条件操作数的临时值之前先丢弃其绑定。
// 最后,丢弃由分支模式绑定的变量。
}
_ => unreachable!(),
}
match PrintOnDrop("Dropped in the enclosing temporary scope") {
// 当守卫求值失败时,控制流离开分支作用域,
// 导致更早的模式匹配守卫条件操作数的绑定和临时值被丢弃。
// 这发生在求值下一个分支的守卫或主体之前。
_ if let y = PrintOnDrop("Dropped in the first arm's scope")
&& false => unreachable!(),
// 当由于自重叠或模式导致守卫多次执行时,
// 控制流在守卫失败时离开分支作用域,
// 并在再次执行守卫之前重新进入分支作用域。
_ | _ if let y = PrintOnDrop("Dropped in the second arm's scope twice")
&& false => unreachable!(),
_ => {},
}
}
模式中的变量按模式内声明的逆序丢弃。
#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
let (declared_first, declared_last) = (
PrintOnDrop("Dropped last"),
PrintOnDrop("Dropped first"),
);
}
对于丢弃顺序,或模式按第一个子模式中给出的顺序声明绑定。
#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
// 在 `y` 之前丢弃 `x`。
fn or_pattern_drop_order<T>(
(Ok([x, y]) | Err([y, x])): Result<[T; 2], [T; 2]>
// ^^^^^^^^^^ ^^^^^^^^^^^ 这是第二个子模式。
// |
// 这是第一个子模式。
//
// 在第一个子模式中,`x` 在 `y` 之前声明。由于它是
// 第一个子模式,即使匹配到绑定顺序相反的
// 第二个子模式时,也使用该顺序。
) {}
// 这里我们匹配第一个子模式,丢弃顺序按照
// 第一个子模式中的声明顺序。
or_pattern_drop_order(Ok([
PrintOnDrop("Declared first, dropped last"),
PrintOnDrop("Declared last, dropped first"),
]));
// 这里我们匹配第二个子模式,丢弃顺序仍然
// 按照第一个子模式中的声明顺序。
or_pattern_drop_order(Err([
PrintOnDrop("Declared last, dropped first"),
PrintOnDrop("Declared first, dropped last"),
]));
}
临时值作用域
表达式的临时值作用域是用于在位置上下文中使用该表达式时保存该表达式结果的临时变量的作用域,除非它被提升。
除了生命周期延长的情况外,表达式的临时值作用域是包含该表达式且符合以下条件之一的最小作用域:
- 整个函数。
- 一个语句。
- 一个
if、while或loop表达式的主体。 - 一个
if表达式的else块。 if或while表达式中非模式匹配的条件表达式,或非模式匹配的match守卫条件操作数。match分支的模式匹配守卫(如果存在)和主体表达式。- 惰性布尔表达式的每个操作数。
if的模式匹配条件和随之的主体(destructors.scope.temporary.edition2024)。while的模式匹配条件和循环体。- 块的尾部表达式的整体(destructors.scope.temporary.edition2024)。
Note
match表达式的被检查值不是一个临时值作用域,因此被检查值中的临时值可以在match表达式之后被丢弃。例如,在match 1 { ref mut z => z };中,1的临时值存活到语句结束。
Note
解构赋值的脱糖限制了其所赋值操作数(RHS)的临时值作用域。详情请参见 expr.assign.destructure.tmp-scopes。
2024 Edition differences
2024 版添加了两条新的临时值作用域收窄规则:
if let的临时值在else块之前丢弃,块尾部表达式的临时值在尾部表达式求值后立即丢弃。
一些示例:
#![allow(unused)]
fn main() {
#![allow(irrefutable_let_patterns)]
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!()
};
if let "if let scrutinee" = PrintOnDrop("if let scrutinee").0 {
PrintOnDrop("if let consequent").0
// `if let consequent` 在这里丢弃
}
// `if let scrutinee` 在这里丢弃
else {
PrintOnDrop("if let else").0
// `if let else` 在这里丢弃
};
while let x = PrintOnDrop("while let scrutinee").0 {
PrintOnDrop("while let loop body").0;
break;
// `while let loop body` 在这里丢弃。
// `while let scrutinee` 在这里丢弃。
}
// 在第一个 || 之前丢弃
(PrintOnDrop("first operand").0 == ""
// 在 ) 之前丢弃
|| PrintOnDrop("second operand").0 == "")
// 在分号之前丢弃
|| PrintOnDrop("third operand").0 == "";
// 被检查值在函数末尾丢弃,早于局部变量
// (因为这是函数体块的尾部表达式)。
match PrintOnDrop("Matched value in final expression") {
// 非模式匹配守卫的临时值在条件求值后丢弃
_ if PrintOnDrop("guard condition").0 == "" => (),
// 模式匹配守卫的临时值在离开分支作用域时丢弃
_ if let "guard scrutinee" = PrintOnDrop("guard scrutinee").0 => {
let _ = &PrintOnDrop("lifetime-extended temporary in inner scope");
// `lifetime-extended temporary in inner scope` 在这里丢弃
}
// `guard scrutinee` 在这里丢弃
_ => (),
}
}
操作数
临时值也会被创建以在计算其他操作数时保存表达式操作数的结果。这些临时值关联到具有该操作数的表达式的作用域。由于一旦表达式求值完成,这些临时值就被移走,因此丢弃它们没有效果,除非某个表达式操作数中断、返回或 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"),
);
}
}
常量提升
当值表达式可以写为常量并被借用,且该借用可以在原本书写表达式的位置被解引用而不改变运行时行为时,该表达式可以被提升到 'static 槽位。也就是说,提升后的表达式可以在编译时求值,其结果值不包含内部可变性或析构器(这些属性在可能的情况下根据值确定,例如 &None 始终具有类型 &'static Option<_>,因为它不包含任何禁止的内容)。
临时值生命周期延长
Note
临时值生命周期延长的确切规则可能会变更。这里仅描述当前行为。
let 语句中表达式的临时值作用域有时被延长到包含该 let 语句的块的作用域。当通常的临时值作用域太小时,基于某些语法规则会执行此延长操作。例如:
#![allow(unused)]
fn main() {
let x = &mut 0;
// 通常临时值现在已经丢弃,但 `0` 的临时值存活到块的末尾。
println!("{}", x);
}
生命周期延长也适用于 static 和 const 项,使临时值存活到程序结束。例如:
#![allow(unused)]
fn main() {
const C: &Vec<i32> = &Vec::new();
// 通常这会是一个悬垂引用,因为 `Vec` 只存在于
// `C` 的初始化器表达式中,但借用的生命周期被延长,
// 因此它实际上具有 `'static` 生命周期。
println!("{:?}", C);
}
如果借用、解引用、字段或元组索引表达式具有延长的临时值作用域,则其操作数也具有延长的临时值作用域。如果索引表达式具有延长的临时值作用域,则被索引的表达式也具有延长的临时值作用域。
基于模式的延长
扩展模式是以下之一:
-
通过引用或可变引用进行绑定的标识符模式。
#![allow(unused)] fn main() { fn temp() {} let ref x = temp(); // 通过引用绑定。 x; let ref mut x = temp(); // 通过可变引用绑定。 x; } -
其中至少一个直接子模式是扩展模式的结构体、元组、元组结构体、切片或或模式。
#![allow(unused)] fn main() { use core::sync::atomic::{AtomicU64, Ordering::Relaxed}; static X: AtomicU64 = AtomicU64::new(0); struct W<T>(T); impl<T> Drop for W<T> { fn drop(&mut self) { X.fetch_add(1, Relaxed); } } let W { 0: ref x } = W(()); // 结构体模式。 x; let W(ref x) = W(()); // 元组结构体模式。 x; let (W(ref x),) = (W(()),); // 元组模式。 x; let [W(ref x), ..] = [W(())]; // 切片模式。 x; let (Ok(W(ref x)) | Err(&ref x)) = Ok(W(())); // 或模式。 x; // // 以上所有临时值在这里仍然存活。 assert_eq!(0, X.load(Relaxed)); }
因此 ref x、V(ref x) 和 [ref x, y] 都是扩展模式,但 x、&ref x 和 &(ref x,) 不是。
如果 let 语句中的模式是扩展模式,则初始化器表达式的临时值作用域被延长。
#![allow(unused)]
fn main() {
fn temp() {}
// 这是一个扩展模式,因此临时值作用域被延长。
let ref x = *&temp(); // OK
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 这既不是扩展模式也不是扩展表达式,
// 因此临时值在分号处被丢弃。
let &ref x = *&&temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 这不是扩展模式,但它是扩展表达式,
// 因此临时值存活到 `let` 语句之后。
let &ref x = &*&temp(); // OK
x;
}
基于表达式的延长
对于带有初始化器的 let 语句,扩展表达式是以下之一的表达式:
- 初始化器表达式。
- 扩展借用表达式的操作数。
- 扩展超级宏调用表达式的超级操作数。
- 扩展数组、cast、花括号结构体或元组表达式的操作数。
- 扩展元组结构体或元组枚举变体构造器表达式的参数。
- 扩展块表达式(异步块表达式除外)的最终表达式。
- 扩展
if表达式的后续分支、else if或else块的最终表达式。 - 扩展
match表达式的一个分支表达式。
Note
解构赋值的脱糖使其所赋值操作数(RHS)成为新引入块中的扩展表达式。详情请参见 expr.assign.destructure.tmp-ext。
因此 &mut 0、(&1, &mut 2) 和 Some(&mut 3) 中的借用表达式都是扩展表达式。&0 + &1 和 f(&mut 0) 中的借用则不是。
Note
rustc不将扩展数组表达式的数组重复操作数视为扩展表达式。是否应该这样处理是一个开放性问题。详情请参见 Rust issue #146092。
示例
以下是表达式具有延长临时值作用域的一些示例:
#![allow(unused)]
fn main() {
use core::pin::pin;
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
static X: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)] struct S;
impl Drop for S { fn drop(&mut self) { X.fetch_add(1, Relaxed); } }
const fn temp() -> S { S }
let x = &temp(); // 借用的操作数。
x;
let x = &raw const *&temp(); // 裸借用的操作数。
assert_eq!(X.load(Relaxed), 0);
let x = &temp() as &dyn Send; // cast 的操作数。
x;
let x = (&*&temp(),); // 元组构造器的操作数。
x;
struct W<T>(T);
let x = W(&temp()); // 元组结构体构造器的参数。
x;
let x = Some(&temp()); // 元组枚举变体构造器的参数。
x;
let x = { [Some(&temp())] }; // 块的最终表达式。
x;
let x = const { &temp() }; // `const` 块的最终表达式。
x;
let x = unsafe { &temp() }; // `unsafe` 块的最终表达式。
x;
let x = if true { &temp() } else { &temp() };
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// `if`/`else` 块的最终表达式。
x;
let x = match () { _ => &temp() }; // `match` 分支表达式。
x;
let x = pin!(temp()); // 超级宏调用表达式的超级操作数。
x;
let x = pin!({ &mut temp() }); // 同上。
x;
let x = format_args!("{:?}", temp()); // 同上。
x;
//
// 以上所有临时值在这里仍然存活。
assert_eq!(0, X.load(Relaxed));
}
以下是没有延长临时值作用域的表达式的一些示例:
#![allow(unused)]
fn main() {
fn temp() {}
// 函数调用的参数不是扩展表达式。临时值在分号处丢弃。
let x = core::convert::identity(&temp()); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// 方法调用的接收者不是扩展表达式。
let x = (&temp()).use_temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// match 表达式的被检查值不是扩展表达式。
let x = match &temp() { x => x }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// `async` 块的最终表达式不是扩展表达式。
let x = async { &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 闭包的最终表达式不是扩展表达式。
let x = || &temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 循环 break 的操作数不是扩展表达式。
let x = loop { break &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// break 到标签的操作数不是扩展表达式。
let x = 'a: { break 'a &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// `pin!` 的参数仅在调用是扩展表达式时才被作为扩展表达式。
// 由于它不是,因此内部块不是扩展表达式,所以其尾部
// 表达式中的临时值被立即丢弃。
pin!({ &temp() }); // 错误
}
#![allow(unused)]
fn main() {
fn temp() {}
// 同上。
format_args!("{:?}", { &temp() }); // 错误
}
不运行析构器
手动阻止析构器
core::mem::forget 可用于阻止变量的析构器运行,core::mem::ManuallyDrop 提供了一个包装器来防止变量或字段被自动丢弃。
Note
通过
core::mem::forget或其他方式阻止析构器运行是安全的,即使变量的类型不是'static。除了本文档定义的保证运行析构器的地方之外,类型不能安全地依赖析构器的运行来保证健全性。
不展开的进程终止
有一些终止进程的方式不会进行展开,在这种情况下析构器不会运行。
标准库提供了 std::process::exit 和 std::process::abort 来显式执行此操作。此外,如果 panic 处理器被设置为 abort,则 panic 将始终终止进程而不运行析构器。
还有一个需要知晓的额外情况:当 panic 到达不可展开的 ABI 边界时,要么没有析构器运行,要么直到 ABI 边界的所有析构器都运行。
生命周期省略
Rust 有规则允许在编译器可以推断出合理的默认选择的各种位置省略生命周期。
函数中的生命周期省略
为了使常见模式更加符合人体工程学,生命周期参数可以在函数项、函数指针和闭包 trait 签名中省略。以下规则用于推断省略的生命周期的生命周期参数。
省略无法推断的生命周期参数是错误的。
占位生命周期 '_ 也可以用来以相同的方式推断生命周期。对于路径中的生命周期,首选使用 '_。
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 other_args1<'a>(arg: &str) -> &'a str; // 省略
fn other_args2<'a, 'b>(arg: &'b str) -> &'a str; // 展开
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; // 非法
}
}
默认 trait 对象生命周期
trait 对象持有的引用的假定生命周期称为其默认对象生命周期约束。这些定义在 RFC 599 中,并在 RFC 1156 中修订。
当生命周期约束完全省略时,使用这些默认对象生命周期约束而不是上面定义的生命周期参数省略规则。
如果使用 '_ 作为生命周期约束,则该约束遵循通常的省略规则。
如果 trait 对象用作泛型类型的类型参数,则首先使用包含类型尝试推断约束。
- 如果从包含类型有唯一的约束,则使用该约束作为默认值。
- 如果从包含类型有多个约束,则必须指定显式约束。
如果上述规则都不适用,则使用 trait 上的约束:
- 如果 trait 定义了单个生命周期约束,则使用该约束。
- 如果任何生命周期约束使用了
'static,则使用'static。
- 如果 trait 没有生命周期约束,则在表达式中推断生命周期,在表达式外部为
'static。
#![allow(unused)]
fn main() {
// 对于以下 trait……
trait Foo { }
// 这两个是相同的,因为 Box<T> 没有对 T 的生命周期约束
type T1 = Box<dyn Foo>;
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> 仍然是 &'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 {}
}
const 和 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 借用的
}
特殊类型和 trait
标准库中的某些类型和 trait 为 Rust 编译器所知。本章记录了这些类型和 trait 的特殊特性。
Box<T>
Box<T> 具有一些 Rust 目前不允许用户定义类型使用的特殊特性。
- 方法可以将
Box<Self>作为接收者(receiver)。
- 可以在与
T相同的 crate 中为Box<T>实现 trait,而孤儿规则禁止其他泛型类型这样做。
Rc<T>
Arc<T>
Pin<P>
UnsafeCell<T>
std::cell::UnsafeCell<T> 用于内部可变性。它确保编译器不会对此类类型执行不正确的优化。
它还确保具有内部可变性类型的 static 项不会被放置在标记为只读的内存中。
PhantomData<T>
std::marker::PhantomData<T> 是一个零大小、最小对齐的类型,对于变型、丢弃检查和自动 trait的目的,它被认为是拥有一个 T。
运算符 trait
std::ops 和 std::cmp 中的 trait 用于重载运算符、索引表达式和调用表达式。
Deref 和 DerefMut
除了重载一元 * 运算符外,Deref 和 DerefMut 还用于方法解析和解引用强制转换。
Drop
Drop trait 提供了一个析构函数,在此类型的值将被销毁时运行。
Copy
Copy trait 改变了实现它的类型的语义。
类型实现 Copy 的值在赋值时被复制而不是移动。
Copy 只能为不实现 Drop 且其所有字段都是 Copy 的类型实现。对于枚举,这意味着所有变体的所有字段都必须是 Copy。对于联合体,这意味着所有变体都必须是 Copy。
编译器为以下类型实现 Copy:
Copy类型的元组
- 不捕获值或仅捕获
Copy类型值的闭包
Clone
Clone trait 是 Copy 的超 trait,因此它也需要编译器生成的实现。
编译器为以下类型实现它:
- 具有内置
Copy实现的类型(见上文)
Clone类型的元组
- 仅捕获
Clone类型值或不从环境捕获值的闭包
Send
Send trait 表示此类型的值可以安全地从一个线程发送到另一个线程。
Sync
Sync trait 表示此类型的值可以安全地在多个线程之间共享。
所有在不可变 static 项中使用的类型都必须实现此 trait。
Termination
Termination trait 指示 main 函数和测试函数可接受的返回类型。
自动 trait
Send、Sync、Unpin、UnwindSafe 和 RefUnwindSafe trait 是自动 trait。自动 trait 具有特殊属性。
如果没有为给定类型的自动 trait 编写显式实现或否定实现,则编译器根据以下规则自动实现它:
- 如果
T实现了该 trait,则&T、&mut T、*const T、*mut T、[T; n]和[T]也实现该 trait。
- 函数项类型和函数指针自动实现该 trait。
- 如果结构体、枚举、联合体和元组的所有字段都实现了该 trait,则它们也实现该 trait。
- 如果闭包的所有捕获的类型都实现了该 trait,则闭包也实现该 trait。通过共享引用捕获
T和通过值捕获U的闭包实现了&T和U都实现的任何自动 trait。
对于泛型类型(将上述内置类型视为对 T 泛型),如果存在泛型实现,则编译器不会自动为那些本可以使用该实现但不满足所需 trait 约束的类型实现它。例如,标准库为所有 T 是 Sync 的 &T 实现了 Send;这意味着如果 T 是 Send 但不是 Sync,编译器将不会为 &T 实现 Send。
自动 trait 也可以有否定实现,在标准库文档中显示为 impl !AutoTrait for T,覆盖自动实现。例如 *mut T 具有 Send 的否定实现,因此 *mut T 不是 Send,即使 T 是。目前没有稳定方式来指定额外的否定实现;它们仅存在于标准库中。
自动 trait 可以作为附加约束添加到任何 trait 对象中,即使通常只允许一个 trait。例如,Box<dyn Debug + Send + UnwindSafe> 是有效类型。
Sized
Sized trait 表示此类型的大小在编译时已知;也就是说,它不是动态大小类型。
类型参数(trait 中的 Self 除外)默认是 Sized,关联类型也是如此。
Sized 始终由编译器自动实现,而不是通过实现项。
这些隐式 Sized 约束可以通过使用特殊的 ?Sized 约束来放宽。
名称
实体是一种语言构造,可以在源程序中以某种方式引用,通常通过路径。实体包括类型、程序项、泛型参数、变量绑定、循环标签、生命周期、字段、属性和lint。
声明是一种语法构造,可以引入一个名称来引用实体。实体名称在其作用域内有效——作用域是源文本中可以引用该名称的区域。
某些实体在源代码中显式声明,某些实体作为语言或编译器扩展的一部分隐式声明。
路径用于引用实体,可能位于其他模块或类型中。
生命周期和循环标签使用专用语法,以引号开头。
名称被隔离到不同的命名空间中,允许不同命名空间中的实体共享相同的名称而不会冲突。
名称解析是将路径、标识符和标签绑定到实体声明的编译时过程。
对某些名称的访问可能受其可见性的限制。
显式声明的实体
在源代码中显式引入名称的实体包括:
- 程序项:
let语句模式绑定
macro_use属性可以从另一个 crate 引入宏名称
macro_export属性可以为宏在 crate 根中引入别名
隐式声明的实体
以下实体由语言隐式定义,或由编译器选项和扩展引入:
- 标准库预导入项、属性和宏
- 根模块中的标准库 crate
- 编译器链接的外部 crate
- 派生辅助属性在项内部有效,无需显式导入
'static生命周期
此外,crate 根模块没有名称,但可以通过某些路径限定符或别名来引用。
命名空间
命名空间是已声明名称的逻辑分组。名称根据其引用的实体类型被隔离到不同的命名空间中。命名空间允许一个命名空间中的名称出现不会与另一个命名空间中的相同名称冲突。
存在多个不同的命名空间,每个命名空间包含不同类型的实体。名称的使用将根据上下文在不同的命名空间中查找该名称的声明,如名称解析章节所述。
以下是命名空间及其对应实体的列表:
- 类型命名空间
- 值命名空间
- 宏命名空间
- 生命周期命名空间
- 标签命名空间
一个示例,展示不同命名空间中重叠的名称如何可以被无歧义地使用:
#![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` 引用标签。
break 'Foo;
}
}
}
没有命名空间的命名实体
以下实体具有显式名称,但这些名称不属于任何特定的命名空间。
字段
尽管结构体、枚举和联合体字段是有名称的,但这些命名字段不存在于显式的命名空间中。它们只能通过字段表达式访问,该表达式仅检查所访问特定类型的字段名称。
Use 声明
use 声明具有它导入作用域的命名别名,但 use 项本身不属于特定命名空间。相反,它可以根据导入的项类型将别名引入多个命名空间。
子命名空间
宏命名空间分为两个子命名空间:一个用于叹号风格宏,一个用于属性。解析属性时,作用域中的任何叹号风格宏将被忽略。反之,解析叹号风格宏时,将忽略作用域中的属性宏。这防止了一种风格遮蔽另一种风格。
例如,cfg 属性和 cfg 宏是宏命名空间中两个具有相同名称的不同实体,但它们仍可在各自的上下文中使用。
Note
use导入仍不能在模块或块中创建相同名称的重复绑定,无论子命名空间如何。#[macro_export] macro_rules! mymac { () => {}; } use myattr::mymac; // error[E0252]: 名称 `mymac` 被多次定义。
作用域
作用域是源文本中可以以其名称引用某命名实体的区域。以下各节提供了作用域规则和行为的详细信息,这些取决于实体的类型及其声明位置。名称如何解析为实体的过程在名称解析章节中描述。有关用于运行析构函数的“丢弃作用域“的更多信息,请参见析构函数章节。
项作用域
直接在模块中声明的项的名称具有从模块开头延伸到模块末尾的作用域。这些项也是模块的成员,可以通过从其模块开始的路径引用。
作为语句声明的项的名称具有从该语句所在块的开头延伸到块末尾的作用域。
在同一模块或块内引入与同命名空间中另一个项名称重复的项是错误的。星号 glob 导入对处理重复名称和遮蔽有特殊行为,更多细节请参见链接的章节。
模块中的项可以遮蔽预导入中的项。
外部模块的项名称不在嵌套模块的作用域中。可以使用路径引用另一个模块中的项。
关联项作用域
关联项没有作用域,只能通过从它们关联的类型或 trait 开始的路径引用。方法也可以通过调用表达式引用。
类似于模块或块内的项,在 trait 或实现中引入与同一命名空间中 trait 或 impl 中另一个项重复的项是错误的。
模式绑定作用域
局部变量模式绑定的作用域取决于它使用的上下文:
let语句绑定从let语句之后开始,直到声明它的块结束。
- 函数参数绑定在函数体内部。
- 闭包参数绑定在闭包体内部。
for绑定在循环体内部。
match守卫let绑定在后续守卫条件和匹配分支表达式中有效。
局部变量作用域不扩展到项声明内部。
模式绑定遮蔽
模式绑定允许遮蔽作用域中的任何名称,但以下情况会导致错误:
以下示例说明了局部绑定如何遮蔽项声明:
#![allow(unused)]
fn main() {
fn shadow_example() {
// 由于作用域中还没有局部变量,这将解析为函数。
foo(); // 打印 `function`
let foo = || println!("closure");
fn foo() { println!("function"); }
// 这将解析为局部闭包,因为它遮蔽了该项。
foo(); // 打印 `closure`
}
}
泛型参数作用域
泛型参数在 GenericParams 列表中声明。泛型参数的作用域在其声明的项内部。
所有参数都在泛型参数列表中的作用域内,无论声明顺序如何。以下展示了一些在声明之前引用参数的示例:
#![allow(unused)]
fn main() {
// 'b 约束在其声明之前被引用。
fn params_scope<'a: 'b, 'b>() {}
trait SomeTrait<const Z: usize> {}
// 常量 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) {} // 错误:不能使用外部函数的泛型参数
}
}
泛型参数遮蔽
遮蔽泛型参数是错误的,但函数内部声明的项允许遮蔽该函数的泛型参数名称。
#![allow(unused)]
fn main() {
fn example<'a, T, const N: usize>() {
// 函数内部的项允许遮蔽作用域中的泛型参数。
fn inner_lifetime<'a>() {} // 正确
fn inner_type<T>() {} // 正确
fn inner_const<const N: usize>() {} // 正确
}
}
#![allow(unused)]
fn main() {
trait SomeTrait<'a, T, const N: usize> {
fn example_lifetime<'a>() {} // 错误:'a 已在使用中
fn example_type<T>() {} // 错误:T 已在使用中
fn example_const<const N: usize>() {} // 错误:N 已在使用中
fn example_mixed<const T: usize>() {} // 错误:T 已在使用中
}
}
生命周期作用域
生命周期参数在 GenericParams 列表和高阶 trait 约束中声明。
'static 生命周期和占位生命周期 '_ 具有特殊含义,不能作为参数声明。
生命周期泛型参数作用域
常量和静态项以及 const 上下文 仅允许 'static 生命周期引用,因此没有其他生命周期可以在它们的作用域中。关联常量确实允许引用在其 trait 或实现中声明的生命周期。
高阶 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 限制
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> + use<'a>> {
// ...
Example
}
}
循环标签作用域
循环标签可以由循环表达式声明。循环标签的作用域从它被声明的点开始直到循环表达式的末尾。作用域不会扩展到项、闭包、async 块、常量参数、const 上下文以及定义它的 for 循环的迭代器表达式中。
#![allow(unused)]
fn main() {
'a: for n in 0..3 {
if n % 2 == 0 {
break 'a;
}
fn inner() {
// 在此处使用 'a 会是错误。
// break 'a;
}
}
// 标签在 `while` 循环的表达式中处于作用域中。
'a: while break 'a {} // 循环不运行。
'a: while let _ = break 'a {} // 循环不运行。
// 标签在定义它的 `for` 循环中不在作用域中:
'a: for outer in 0..5 {
// 这将中断外部循环,跳过内部循环并停止外部循环。
'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;
}
}
}
预导入作用域
预导入将实体带入每个模块的作用域。这些实体不是模块的成员,而是在名称解析期间被隐式查询。
预导入名称可以被模块中的声明遮蔽。
预导入是分层的,如果一个预导入包含与另一个预导入同名的实体,则一个可以遮蔽另一个。预导入可能遮蔽其他预导入的顺序如下,其中较前的条目可以遮蔽较后的条目:
macro_rules 作用域
macro_rules 宏的作用域在声明宏章节中描述。行为取决于 macro_use 和 macro_export 属性的使用。
派生宏辅助属性
派生宏辅助属性在其对应 derive 属性指定的项中处于作用域中。作用域从 derive 属性之后开始直到该项目的末尾。
辅助属性遮蔽作用域中同名的其他属性。
Self 作用域
尽管 Self 是具有特殊含义的关键字,但它与名称解析的交互方式类似于普通名称。
在 struct、enum、union、trait 或实现的定义中,隐式的 Self 类型被视为类似于泛型参数,并以与泛型类型参数相同的方式处于作用域中。
在实现的值命名空间中,隐式的 Self 构造函数在实现体(实现的关联项)内部处于作用域中。
#![allow(unused)]
fn main() {
// 结构体定义中的 Self 类型。
struct Recursive {
f1: Option<Box<Self>>
}
// 泛型参数中的 Self 类型。
struct SelfGeneric<T: Into<Self>>(T);
// 实现中的 Self 值构造函数。
struct ImplExample();
impl ImplExample {
fn example() -> Self { // Self 类型
Self() // Self 值构造函数
}
}
}
预导入
预导入是一组自动带入 crate 中每个模块作用域的名称集合。
这些预导入名称不是模块本身的一部分:它们在名称解析期间被隐式查询。例如,尽管像 Box 这样的名称在每个模块中都在作用域内,你不能将其引用为 self::Box,因为它不是当前模块的成员。
存在几种不同的预导入:
标准库预导入
每个 crate 都有一个标准库预导入,由来自单个标准库模块的名称组成。
所使用的模块取决于 crate 的版次以及 no_std 属性是否应用于 crate:
Note
std::prelude::rust_2015和std::prelude::rust_2018与std::prelude::v1具有相同的内容。
core::prelude::rust_2015和core::prelude::rust_2018与core::prelude::v1具有相同的内容。
Note
当
core::panic!或std::panic!之一由于标准库预导入被带入作用域,而用户编写的 glob 导入将另一个带入作用域时,rustc目前允许使用panic!,即使它是有歧义的。用户编写的 glob 导入优先以解决此歧义。详情请参见 names.resolution.expansion.imports.ambiguity.panic-hack。
外部 crate 预导入
在根模块中使用 extern crate 导入的或提供给编译器的(如使用 rustc 的 --extern 标志)外部 crate 被添加到外部 crate 预导入中。如果使用别名导入,例如 extern crate orig_name as new_name,则符号 new_name 被添加到预导入中。
core crate 始终被添加到外部 crate 预导入中。
只要在 crate 根中未指定 no_std 属性,std crate 就会被添加。
2018 Edition differences
在 2015 版次中,外部 crate 预导入中的 crate 不能通过 use 声明引用,因此通常的标准做法是包含
extern crate声明将其带入作用域。从 2018 版次开始,use 声明可以引用外部 crate 预导入中的 crate,因此使用
extern crate被认为是不惯用的。
Note
随
rustc一起分发的其他 crate,例如alloc和test,在使用 Cargo 时不会通过--extern标志自动包含。即使在 2018 版次中,也必须使用extern crate声明将其带入作用域。#![allow(unused)] fn main() { extern crate alloc; use alloc::rc::Rc; }对于仅 proc-macro crate,Cargo 确实会将
proc_macro引入外部 crate 预导入中。
no_std 属性
no_std 属性 导致 std crate 不被自动链接,并且标准库预导入改为使用 core 预导入。
Example
#![no_std]
Note
当 crate 面向不支持标准库的平台,或有目的地不使用标准库的功能时,使用
no_std是有用的。这些功能主要是动态内存分配(如Box和Vec)以及文件和网络功能(如std::fs和std::io)。
Warning
使用
no_std不会阻止标准库被链接。在 crate 或其依赖之一中写入extern crate std仍然是有效的;这将导致编译器将stdcrate 链接到程序中。
no_std 属性使用 MetaWord 语法。
no_std 属性只能应用于 crate 根。
no_std 属性可以在一个形式上使用任意次数。
Note
rustc会对第一次之后的使用发出 lint 警告。
no_std 属性将标准库预导入改为使用 core 预导入而非 std 预导入。
2018 Edition differences
在 2018 版次之前,
std默认被注入到 crate 根中。如果指定了no_std,则改为注入core。从 2018 版次开始,无论是否指定no_std,两者都不会被注入到 crate 根中。
语言预导入
语言预导入包括语言内置的类型和属性名称。语言预导入始终在作用域中。
它包括以下内容:
macro_use 预导入
macro_use 预导入包括通过对 extern crate 应用 macro_use 属性从外部 crate 导入的宏。
工具预导入
工具预导入包括类型命名空间中外部工具的工具名称。更多细节请参见工具属性部分。
no_implicit_prelude 属性
no_implicit_prelude 属性 用于阻止隐式预导入被带入作用域。
Example
#![allow(unused)] fn main() { // 该属性可以应用于 crate 根以影响所有模块。 #![no_implicit_prelude] // 或者可以应用于一个模块以仅影响该模块及其后代。 #[no_implicit_prelude] mod example { // ... } }
no_implicit_prelude 属性使用 MetaWord 语法。
no_implicit_prelude 属性只能应用于 crate 或模块。
Note
rustc忽略其他位置的用法但会发出 lint 警告。这可能在将来成为错误。
no_implicit_prelude 属性可以在一个形式上使用任意次数。
Note
rustc会对第一次之后的使用发出 lint 警告。
no_implicit_prelude 属性阻止标准库预导入、外部 crate 预导入、macro_use 预导入和工具预导入被带入模块及其后代的作用域。
Note
尽管有
#![no_implicit_prelude],rustc目前仍隐式地将某些宏带入作用域。这些宏是:
assert!cfg!cfg_select!column!compile_error!concat!concat_bytes!env!file!format_args!include!include_bytes!include_str!line!module_path!option_env!panic!stringify!unreachable!例如,这可以工作:
#![no_implicit_prelude] fn main() { assert!(true); }不要依赖此行为;它可能在未来被移除。在使用
#![no_implicit_prelude]时,始终显式地将你需要的项带入作用域。详情请参见 Rust PR #62086 和 Rust PR #139493。
no_implicit_prelude 属性不影响语言预导入。
2018 Edition differences
在 2015 版次中,
no_implicit_prelude属性不影响macro_use预导入,标准库导出的所有宏仍包含在macro_use预导入中。从 2018 版次开始,该属性确实移除了macro_use预导入。
路径
路径是由 :: 分隔的一个或多个路径段组成的序列。路径用于引用程序项、值、类型、宏和属性。
仅由标识符段组成的简单路径的两个示例:
x;
x::y::z;
路径的类型
简单路径
Syntax
SimplePath →
::? SimplePathSegment ( :: SimplePathSegment )*
SimplePathSegment →
IDENTIFIER | super | self | crate | $crate
#![allow(unused)]
fn main() {
use std::io::{self, Write};
mod m {
#[clippy::cyclomatic_complexity = "0"]
pub (in super) fn f1() {}
}
}
表达式中的路径
Syntax
PathInExpression →
::? PathExprSegment ( :: PathExprSegment )*
PathExprSegment →
PathIdentSegment ( :: GenericArgs )?
PathIdentSegment →
IDENTIFIER | super | self | Self | crate | $crate
GenericArgs →
< GenericArgList? >
| ( TypeList? ) ( -> TypeNoBounds )?
GenericArgList →
( GenericArg , )* GenericArg ,?
TypeList →
( Type , )* Type ,?
GenericArg →
Lifetime | Type | GenericArgsConst | GenericArgsBinding | GenericArgsBounds
GenericArgsConst →
BlockExpression
| LiteralExpression
| - LiteralExpression
| SimplePathSegment
表达式中的路径允许指定带有泛型参数的路径。它们用于表达式和模式中的各种位置。
在泛型参数的开头 < 之前需要 ::,以避免与小于运算符歧义。这俗称“turbofish“语法。
#![allow(unused)]
fn main() {
(0..10).collect::<Vec<_>>();
Vec::<u8>::with_capacity(1024);
}
泛型参数的顺序限制为生命周期参数,然后是类型参数,然后是常量参数,然后是等式约束。
常量参数必须用花括号包围,除非它们是字面量、推断常量或单段路径。推断常量不能使用花括号包围。
#![allow(unused)]
fn main() {
mod m {
pub const C: usize = 1;
}
const C: usize = m::C;
fn f<const N: usize>() -> [u8; N] { [0; N] }
let _ = f::<1>(); // 字面量。
let _: [_; 1] = f::<_>(); // 推断常量。
let _: [_; 1] = f::<(((_)))>(); // 推断常量。
let _ = f::<C>(); // 单段路径。
let _ = f::<{ m::C }>(); // 多段路径必须使用花括号。
}
#![allow(unused)]
fn main() {
fn f<const N: usize>() -> [u8; N] { [0; _] }
let _: [_; 1] = f::<{ _ }>();
// ^ 错误:此处不允许 `_`
}
对应于 impl Trait 类型的合成类型参数是隐式的,不能显式指定。
限定路径
Syntax
QualifiedPathInExpression → QualifiedPathType ( :: PathExprSegment )+
QualifiedPathType → < Type ( as TypePath )? >
QualifiedPathInType → QualifiedPathType ( :: TypePathSegment )+
完全限定路径允许消除 trait 实现的路径歧义,并允许指定规范路径。在类型规范中使用时,它支持使用下面指定的类型语法。
#![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(); // 调用固有 impl。
<S as T1>::f(); // 调用 T1 trait 函数。
<S as T2>::f(); // 调用 T2 trait 函数。
}
类型中的路径
Syntax
TypePath → ::? TypePathSegment ( :: TypePathSegment )*
TypePathSegment → PathIdentSegment ( ::? GenericArgs )?
类型路径用于类型定义、trait 约束和限定路径中。
尽管在泛型参数之前允许使用 ::,但不是必需的,因为这里不像 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>;
}
路径限定符
路径可以使用各种前导限定符来表示,以改变其解析方式的含义。
Note
use声明对self、super、crate和$crate有额外的行为和限制。
::
以 :: 开头的路径被视为全局路径,路径段的解析起点根据版次不同而有所差异。路径中的每个标识符必须解析为一个项。
2018 Edition differences
在 2015 版次中,标识符从“crate 根“(2018 版次中的
crate::)解析,其中包含各种不同的项,包括外部 crate、默认 crate 如std或core,以及 crate 顶层的项(包括use导入)。从 2018 版次开始,以
::开头的路径从外部 crate 预导入中的 crate 解析。也就是说,它们必须后跟一个 crate 的名称。
#![allow(unused)]
fn main() {
pub fn foo() {
// 在 2018 版次中,这通过外部 crate 预导入访问 `std`。
// 在 2015 版次中,这通过 crate 根访问 `std`。
let now = ::std::time::Instant::now();
println!("{:?}", now);
}
}
// 2015 版次
mod a {
pub fn foo() {}
}
mod b {
pub fn foo() {
::a::foo(); // 调用 `a` 的 foo 函数
// 在 Rust 2018 中,`::a` 将被解释为 crate `a`。
}
}
fn main() {}
self
self 将路径相对于当前模块解析。
self 只能作为路径的第一个段(前面没有 ::)或最后一个段(前面有 ::)使用。
当 self 作为路径的最后一个段出现时,它引用前面段命名的实体。前面的路径必须解析为模块、枚举或 trait。
mod m {
pub enum E { V1 }
pub trait Tr {}
pub(in crate::m::self) fn g() {} // 正确:模块可以是 `self` 的父级。
}
type Ty = m::E::self; // 正确:枚举可以是 `self` 的父级。
fn f<T: m::Tr::self>() {} // 正确:trait 可以是 `self` 的父级。
fn main() { let _: Ty = m::E::V1; }
struct S;
type Ty = S::self; // 错误:结构体不能是 `self` 的父级。
fn main() {}
Note
有关
use声明中self的附加规则,请参见 items.use.self。
在方法体中,仅由单个 self 段组成的路径解析为方法的 self 参数。
fn foo() {}
fn bar() {
self::foo();
}
struct S(bool);
impl S {
fn baz(self) {
self.0;
}
}
fn main() {}
Self
Self(大写 “S”)用于引用当前正在实现或定义的类型。它可以在以下情况下使用:
- 在 trait 定义中,它引用实现该 trait 的类型。
Self 的作用域行为类似于泛型参数;更多细节请参见 Self 作用域部分。
Self 只能作为第一个段使用,前面不能有 ::。
Self 路径不能包含泛型参数(如 Self::<i32>)。
#![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`。
}
}
// `Self` 在 trait 定义的泛型中处于作用域中,
// 用于引用正在定义的类型。
trait Add<Rhs = Self> {
type Output;
// `Self` 也可以引用正在实现的类型的关联项。
fn add(self, rhs: Rhs) -> Self::Output;
}
struct NonEmptyList<T> {
head: T,
// 结构体可以引用自身(只要不是无限递归)。
tail: Option<Box<Self>>,
}
}
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 仅在宏转录器中使用,并且只能作为第一个段使用,前面不能有 ::。
$crate 将展开为访问定义该宏的 crate 顶层项的路径,无论该宏是在哪个 crate 中被调用的。
pub fn increment(x: u32) -> u32 {
x + 1
}
#[macro_export]
macro_rules! inc {
($x:expr) => ( $crate::increment($x) )
}
fn main() { }
规范路径
在模块或实现中定义的每个项都有一个规范路径,对应于其在其 crate 内的定义位置。
所有这些项的其他路径都是别名。
规范路径定义为路径前缀加上该项本身定义的路径段。
实现和 use 声明没有规范路径,尽管实现定义的项有规范路径。在块表达式中定义的项没有规范路径。在没有规范路径的模块中定义的项没有规范路径。在引用没有规范路径的项的实现中定义的关联项(例如作为实现类型、被实现的 trait、类型参数或类型参数上的约束)没有规范路径。
模块的路径前缀是该模块的规范路径。
对于裸实现,它是被实现项的规范路径,用尖括号(<>)包围。
对于 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; // 无
trait OtherTrait { // 无
fn g(&self); // 无
}
impl OtherTrait for OtherStruct {
fn g(&self) {} // 无
}
impl OtherTrait for crate::a::Struct {
fn g(&self) {} // 无
}
impl crate::a::Trait for OtherStruct {
fn f(&self) {} // 无
}
}
}
fn main() {}
名称解析
名称解析是将路径和其他标识符绑定到这些实体声明的过程。名称被隔离到不同的命名空间中,允许不同命名空间中的实体共享相同的名称而不会冲突。每个名称在一个作用域内有效,即源文本中可以引用该名称的区域。对名称的访问可能受其可见性的限制。
名称解析在整个编译过程中分为三个阶段。第一阶段,展开时解析,解析所有 use 声明和宏调用。第二阶段,主解析,解析所有尚未解析且不依赖类型信息来解析的名称。最后一个阶段,类型相关解析,在类型信息可用后解析剩余的名称。
Note
展开时解析也称为早期解析。主解析也称为晚期解析。
概述
本节中的规则适用于名称解析的所有阶段。
作用域
Note
这是一个占位符,用于未来展开关于各种作用域内名称解析的内容。
展开时名称解析
展开时名称解析是完成宏展开和完全生成 crate 的 AST 所必需的名称解析阶段。此阶段需要解析宏调用和 use 声明。解析 use 声明对于通过基于路径的作用域解析的宏调用是必需的。解析宏调用是为了将它们展开。
在展开时名称解析之后,AST 不得包含任何未展开的宏调用。每个宏调用都解析为存在于最终 AST 或外部 crate 中的有效定义。
#![allow(unused)]
fn main() {
m!(); // 错误:在此作用域中找不到宏 `m`。
}
名称的解析必须是稳定的。展开后,完全展开的 AST 中的名称必须解析到相同的定义,无论宏展开和导入解析的顺序如何。
在宏展开期间选择的所有名称解析候选项都被认为是推测性的。一旦 crate 被完全展开,所有推测性的导入解析都会被验证,以确保宏展开没有引入任何新的歧义。
Note
由于宏展开的迭代性质,这会导致所谓的时间旅行歧义,例如当宏或 glob 导入引入了一个与其自身基路径有歧义的项时。
fn main() {} macro_rules! f { () => { mod m { pub(crate) use f; } } } f!(); const _: () = { // 最初,我们推测性地将 `m` 解析为 crate 根中的模块。 // // `f` 的展开在此函数体内引入了第二个 `m` 模块。 // // 展开时解析通过重新解析所有导入和宏调用来最终确定解析结果, // 看到引入的歧义并将其报告为错误。 m::f!(); // 错误:`m` 有歧义。 };
导入
所有 use 声明在此解析阶段完全解析。类型相关路径在此阶段无法解析,将产生错误。
#![allow(unused)]
fn main() {
mod m {
pub const C: () = ();
pub enum E { V }
pub type A = E;
impl E {
pub const C: () = ();
}
}
// 在展开时解析的有效导入:
use m::C; // 正确。
use m::E; // 正确。
use m::A; // 正确。
use m::E::V; // 正确。
// 在类型相关解析期间解析的有效表达式:
let _ = m::A::V; // 正确。
let _ = m::E::C; // 正确。
}
#![allow(unused)]
fn main() {
mod m {
pub const C: () = ();
pub enum E { V }
pub type A = E;
impl E {
pub const C: () = ();
}
}
// 无法在展开时解析的无效类型相关导入:
use m::A::V; // 错误:未解析的导入 `m::A::V`。
use m::E::C; // 错误:未解析的导入 `m::E::C`。
}
通过外部作用域中的 use 声明引入的名称会被内部作用域中相同命名空间的同名候选项遮蔽,除非受到名称解析歧义的限制。
#![allow(unused)]
fn main() {
pub mod m1 {
pub mod ambig {
pub const C: u8 = 1;
}
}
pub mod m2 {
pub mod ambig {
pub const C: u8 = 2;
}
}
// 这在外部作用域中引入了名称 `ambig`。
use m1::ambig;
const _: () = {
// 这在内部作用域中遮蔽了 `ambig`。
use m2::ambig;
// 此处选择内部候选项
// 作为 `ambig` 的解析结果。
use ambig::C;
assert!(C == 2);
};
}
在单个作用域内通过 use 声明引入的名称在以下情况下允许遮蔽:
歧义
在展开时解析中存在某些情况,其中导入或宏调用的名称可能引用多个宏定义、use 声明或模块,而编译器无法一致地确定哪个候选项应遮蔽另一个。在这些情况下不允许遮蔽,编译器会发出歧义错误。
名称不能通过有歧义的 glob 导入解析。只要名称未被使用,glob 导入允许导入相同命名空间中冲突的名称。来自有歧义的 glob 导入的冲突候选项名称仍可被非 glob 导入遮蔽并在不产生错误的情况下使用。错误发生在使用时,而非导入时。
#![allow(unused)]
fn main() {
mod m1 {
pub struct Ambig;
}
mod m2 {
pub struct Ambig;
}
// 正确:这在同一命名空间中引入了冲突的名称
// 但它们尚未被使用。
use m1::*;
use m2::*;
const _: () = {
// 当使用具有冲突候选项的名称时发生错误。
let x = Ambig; // 错误:`Ambig` 有歧义。
};
}
#![allow(unused)]
fn main() {
mod m1 {
pub struct Ambig;
}
mod m2 {
pub struct Ambig;
}
use m1::*;
use m2::*; // 正确:无名称冲突。
const _: () = {
// 这是允许的,因为解析并非通过有歧义的 glob。
struct Ambig;
let x = Ambig; // 正确。
};
}
允许多个 glob 导入导入相同的名称,并且如果导入的是相同的项(跟随重新导出),则允许使用该名称。名称的可见性是导入的最大可见性。
mod m1 {
pub struct Ambig;
}
mod m2 {
// 这从第二个模块重新导出相同的 `Ambig` 项。
pub use super::m1::Ambig;
}
mod m3 {
// 这两者都导入相同的 `Ambig`。
//
// `Ambig` 的可见性是 `pub`,因为这是这两个
// `use` 声明之间的最大可见性。
pub use super::m1::*;
use super::m2::*;
}
mod m4 {
// `Ambig` 可以通过 `m3` 的 globs 使用,并且仍然具有
// `pub` 可见性。
pub use crate::m3::Ambig;
}
const _: () = {
// 因此,我们可以在此处使用它。
let _ = m4::Ambig; // 正确。
};
fn main() {}
当外部作用域中存在另一个候选项时,导入和宏调用中的名称不能通过 glob 导入解析。
Note
当
core::panic!或std::panic!之一由于标准库预导入被带入作用域,而用户编写的 glob 导入将另一个带入作用域时,rustc目前允许使用panic!,即使它是有歧义的。用户编写的 glob 导入优先以解决此歧义。在 Rust 2021 及更高版本中,
core::panic!和std::panic!行为相同。但在更早的版次中,它们有所不同;只有std::panic!接受String作为格式化参数。例如,这是一个错误:
extern crate core; use ::core::prelude::v1::*; fn main() { panic!(std::string::String::new()); // 错误。 }而这是被接受的:
#![no_std] extern crate std; use ::std::prelude::v1::*; fn main() { panic!(std::string::String::new()); // 正确。 }不要依赖此行为;计划将其移除。
详情请参见 Rust issue #147319。
#![allow(unused)]
fn main() {
mod glob {
pub mod ambig {
pub struct Name;
}
}
// 外部 `ambig` 候选项。
pub mod ambig {
pub struct Name;
}
const _: () = {
// 无法通过此 glob 解析 `ambig`,
// 因为上面存在外部 `ambig` 候选项。
use glob::*;
use ambig::Name; // 错误:`ambig` 有歧义。
};
}
#![allow(unused)]
fn main() {
// 同上,但使用宏。
pub mod m {
macro_rules! f {
() => {};
}
pub(crate) use f;
}
pub mod glob {
macro_rules! f {
() => {};
}
pub(crate) use f as ambig;
}
use m::f as ambig;
const _: () = {
use glob::*;
ambig!(); // 错误:`ambig` 有歧义。
};
}
Note
这些歧义错误是展开时解析特有的。在后续解析阶段中,某个名称存在多个候选项不被视为错误。只要导入本身没有歧义,就始终会有一个唯一的无歧义的最接近解析。
#![allow(unused)] fn main() { mod glob { pub const AMBIG: u8 = 1; } mod outer { pub const AMBIG: u8 = 2; } use outer::AMBIG; const C: () = { use glob::*; assert!(AMBIG == 1); // ^---- 此 `AMBIG` 在主解析期间解析。 }; }
名称不能通过有歧义的宏重新导出解析。当宏重新导出会遮蔽外部作用域中相同名称的文本宏候选项时,宏重新导出是有歧义的。
#![allow(unused)]
fn main() {
// 文本宏候选项。
macro_rules! ambig {
() => {}
}
// 基于路径的宏候选项。
macro_rules! path_based {
() => {}
}
pub fn f() {
// 将 `path_based` 宏定义重新导出为 `ambig`
// 不能遮蔽通过文本宏作用域解析的
// `ambig` 宏定义。
use path_based as ambig;
ambig!(); // 错误:`ambig` 有歧义。
}
}
Note
此限制是由于编译器中的实现细节,特别是当前的作用域访问逻辑和支持此行为的复杂性。此歧义错误可能在未来被移除。
宏
宏通过遍历可用作用域以查找可用候选项来解析。宏分为两个子命名空间,一个用于类函数宏,另一个用于属性和派生宏。来自错误子命名空间的解析候选项将被忽略。
可用的作用域类型按以下顺序访问。每种作用域类型代表一个或多个作用域。
Note
编译器将尝试解析在其关联宏将其引入作用域之前使用的派生辅助属性。此作用域在解析正确位于作用域中的派生辅助属性候选项的作用域之后访问。此行为计划被移除。
更多信息请参见派生辅助属性作用域。
Note
此访问顺序可能在未来改变,例如根据文本作用域和基于路径的作用域候选项的词法作用域交错访问它们。
2018 Edition differences
从 2018 版次开始,当存在
#[no_implicit_prelude]时,不会访问#[macro_use]预导入。
名称 cfg 和 cfg_attr 在宏属性子命名空间中是保留的。
歧义
名称不能通过宏展开内部的有歧义的候选项解析。当宏展开内部的候选项会遮蔽来自第一个候选项的宏展开外部的相同名称的候选项,并且被解析的名称的调用也来自第一个候选项的宏展开外部时,宏展开内部的候选项是有歧义的。
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
// 为 `ambig` 宏调用引入外部候选项定义。
macro_rules! ambig {
() => {}
}
// 在宏展开内部为 `ambig` 引入第二个候选项定义。
define_ambig!();
// 来自第二次 `define_ambig` 调用的 `ambig` 定义
// 是最内层的候选项。
//
// 来自第一次 `define_ambig` 调用的 `ambig` 定义
// 是第二个候选项。
//
// 编译器检查第一个候选项是否位于宏展开内部,
// 第二个候选项是否不来自同一宏展开内部,
// 并且被解析的名称是否不来自同一宏展开内部。
ambig!(); // 错误:`ambig` 有歧义。
}
反过来不被视为歧义。
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
// 交换定义顺序。
define_ambig!();
macro_rules! ambig {
() => {}
}
// 最内层的候选项现在展开程度较低,因此它可以遮蔽
// 其上方的宏展开定义。
ambig!();
}
如果被解析的调用位于最内层候选项的展开内部,也不被视为歧义。
#![allow(unused)]
fn main() {
macro_rules! ambig {
() => {}
}
macro_rules! define_and_invoke_ambig {
() => {
// 定义最内层候选项。
macro_rules! ambig {
() => {}
}
// `ambig` 的调用与最内层候选项位于同一展开中。
ambig!(); // 正确
}
}
define_and_invoke_ambig!();
}
两个定义是否来自同一宏的调用并不重要;最外层候选项仍被视为“展开程度较低“,因为它不在包含最内层候选项定义的展开内部。
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
define_ambig!();
define_ambig!();
ambig!(); // 错误:`ambig` 有歧义。
}
这也适用于导入,只要名称的最内层候选项来自宏展开内部。
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
mod ambig {
pub struct Name;
}
}
}
mod ambig {
pub struct Name;
}
const _: () = {
// 在此宏展开中为 `ambig` mod 引入最内层候选项。
define_ambig!();
use ambig::Name; // 错误:`ambig` 有歧义。
};
}
用户定义的属性或派生宏不能遮蔽内置的非宏属性(例如 inline)。
// with-helper/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_derive(WithHelperAttr, attributes(non_exhaustive))]
// ^^^^^^^^^^^^^^
// 用户定义的属性候选项。
// ...
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
// src/lib.rs
#[derive(with_helper::WithHelperAttr)]
#[non_exhaustive] // 错误:`non_exhaustive` 有歧义。
struct S;
Note
无论内置属性是哪个名称的候选项,此规则都适用:
// with-helper/src/lib.rs use proc_macro::TokenStream; #[proc_macro_derive(WithHelperAttr, attributes(helper))] // ^^^^^^ // 用户定义的属性候选项。 // ... pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream { TokenStream::new() }// src/lib.rs use inline as helper; // ^----- 通过重新导出的内置属性候选项。 #[derive(with_helper::WithHelperAttr)] #[helper] // 错误:`helper` 有歧义。 struct S;
主名称解析
Note
这是一个占位符,用于未来展开关于主名称解析的内容。
类型相关解析
Note
这是一个占位符,用于未来展开关于类型相关解析的内容。
可见性和隐私
Syntax
Visibility →
pub
| pub ( crate )
| pub ( self )
| pub ( super )
| pub ( in SimplePath )
这两个术语经常互换使用,它们试图传达的是对“此项目能否在此位置使用?“这个问题的答案。
Rust 的名称解析在全局的命名空间层次结构上运行。层次结构中的每个级别可以被视为某个项。项是上述提到的那几种之一,但也包括外部 crate。声明或定义一个新模块可以被视为在定义位置的层次结构中插入一棵新树。
为了控制接口是否可以跨模块使用,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 需要一个对其自身全局可用的“辅助模块“,但不想将辅助模块暴露为公共 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) 和 pub(self)
除了 public 和 private 之外,Rust 允许用户将项声明为仅在给定作用域内可见。pub 限制的规则如下:
pub(in path)使项在提供的path内可见。path必须是一个简单路径,解析为正在声明其可见性的项的祖先模块。path中的每个标识符必须直接引用模块(而不是通过use语句引入的名称)。
pub(crate)使项在当前 crate 内可见。
pub(super)使项对父模块可见。这等价于pub(in super)。
pub(self)使项对当前模块可见。这等价于pub(in self)或根本不使用pub。
2018 Edition differences
从 2018 版次开始,
pub(in path)的路径必须以crate、self或super开头。2015 版次还可以使用以::或来自 crate 根的模块开头的路径。
以下是一个示例:
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() {
// 此函数可见,因为我们在同一个 `mod` 中
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() }
Note
此语法仅对项的可见性增加了另一种限制。它不保证该项在指定作用域的所有部分都可见。要访问一个项,其所有父项直到当前作用域也必须仍然可见。
重新导出和可见性
Rust 允许通过 pub use 指令公开重新导出项。因为这是一个公共指令,这允许通过上述规则在当前模块中使用该项。它本质上允许对重新导出的项进行公共访问。例如,此程序是有效的:
pub use self::implementation::api;
mod implementation {
pub mod api {
pub fn f() {}
}
}
fn main() {}
这意味着任何引用 implementation::api::f 的外部 crate 将收到隐私违规,而路径 api::f 将被允许。
当重新导出私有项时,可以认为允许通过重新导出“短路“隐私链,而不是像通常那样通过命名空间层次结构传递。
内存模型
Warning
Rust 的内存模型尚不完整,且未完全确定。
字节
Rust 中最基本的内存单元是字节。
Note
尽管字节通常被降低到硬件字节,Rust 使用一种“抽象“的字节概念,可以做出硬件中不存在的区分,例如未初始化或存储了指针的一部分。这些区分可能影响你的程序是否具有未定义行为,因此它们仍然对编译后的 Rust 程序的行为有实际影响。
每个字节可能具有以下值之一:
- 一个已初始化字节,包含一个
u8值和可选的来源,
- 一个未初始化字节。
Note
上述列表尚未保证是详尽无遗的。
内存分配和生命周期
程序的程序项是那些在编译时计算其值并唯一地存储在 Rust 进程的内存映像中的函数、模块和类型。程序项既不会动态分配也不会被释放。
堆是描述 box 的通用术语。堆中分配的生命周期取决于指向它的 box 值的生命周期。由于 box 值本身可以在帧之间传入传出,或存储在堆中,堆分配可能比它们被分配的帧存活更久。堆中的分配保证在分配的整个生命周期中位于堆中的单个位置——它永远不会因为移动 box 值而被重新定位。
变量
变量是栈帧的一个组成部分,可以是具名的函数参数、匿名的临时值,或具名的局部变量。
局部变量(或称栈局部分配)直接持有一个值,分配在栈内存中。该值是栈帧的一部分。
局部变量默认是不可变的,除非另行声明。例如: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; // err: use of possibly uninitialized `uninit_after_if`
}
}
Panic
Rust 提供了一种机制来阻止函数正常返回,而是“panic“,这是对通常不期望在遇到错误上下文中可恢复的错误条件的响应。
某些语言构造,例如越界数组索引,会自动 panic。
还有一些语言特性提供对 panic 行为的一定程度的控制:
Note
标准库提供了通过
panic!宏显式 panic 的能力。
panic_handler 属性
panic_handler 属性可以应用于一个函数以定义 panic 的行为。
panic_handler 属性只能应用于签名为 fn(&PanicInfo) -> ! 的函数。
Note
PanicInfo结构体包含有关 panic 位置的信息。
依赖图中必须有一个单一的 panic_handler 函数。
下面展示了一个 panic_handler 函数,该函数记录 panic 消息然后暂停线程。
#![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();
// 将 "panicked at '$reason', src/main.rs:27:4" 记录到某个 `sink`
let _ = writeln!(sink, "{}", info);
loop {}
}
标准行为
std 提供了两种不同的 panic 处理器:
unwind— 展开调用栈,可能可恢复。abort–– 中止进程,不可恢复。
并非所有目标都提供 unwind 处理器。
Note
链接
std时使用的 panic 处理器可以通过-C panicCLI 标志设置。大多数目标的默认值是unwind。标准库的 panic 行为可以在运行时通过
std::panic::set_hook函数修改。
链接 no_std 二进制文件、dylib、cdylib 或 staticlib 将需要指定你自己的 panic 处理器。
Panic 策略
panic 策略 定义了 crate 构建所支持的 panic 行为类型。
Note
panic 策略可以通过
rustc的-C panicCLI 标志选择。在生成二进制文件、dynamic library、cdylib 或 staticlib 并链接
std时,-C panicCLI 标志也影响使用哪个 panic 处理器。
Note
当使用
abortpanic 策略编译代码时,优化器可以假设跨越 Rust 帧的展开是不可能的,这可以带来代码大小和运行时速度的改进。
Note
有关链接具有不同 panic 策略的 crate 的限制,请参见 link.unwinding。一个推论是,使用
unwind策略构建的 crate 可以使用abortpanic 处理器,但abort策略不能使用unwindpanic 处理器。
展开(Unwinding)
Panic 可以是可恢复的或不可恢复的,尽管可以通过选择非展开 panic 处理器将其配置为始终不可恢复。(反过来不成立:unwind 处理器不保证所有 panic 都是可恢复的,只保证通过 panic! 宏和类似的标准库机制的 panic 是可恢复的。)
当 panic 发生时,unwind 处理器“展开“Rust 帧,就像 C++ 的 throw 展开 C++ 帧一样,直到 panic 到达恢复点(例如在线程边界)。这意味着当 panic 遍历 Rust 帧时,这些帧中的实现了 Drop 的活动对象将调用它们的 drop 方法。因此,当正常执行恢复时,不再可访问的对象将已经被“清理“,就像它们正常地离开作用域一样。
Note
只要保留了这种资源清理的保证,“展开“可以在不实际使用目标平台 C++ 使用的机制的情况下实现。
Note
标准库提供了两种从 panic 恢复的机制,
std::panic::catch_unwind(允许在 panic 线程内恢复)和std::thread::spawn(自动为生成的线程设置 panic 恢复,以便其他线程可以继续运行)。
跨 FFI 边界的展开
可以使用适当的 ABI 声明跨 FFI 边界展开。虽然在某些情况下有用,但这为未定义行为创造了独特的机会,特别是在涉及多个语言运行时的情况下。
使用错误的 ABI 展开是未定义行为:
- 从通过非展开 ABI(如
"C"、"system"等)声明的函数声明或指针调用的外部函数导致展开进入 Rust 代码。(例如,当这样用 C++ 编写的函数抛出一个未被捕获并传播到 Rust 的异常时会发生这种情况。) - 从不支持展开的代码中调用会展开的 Rust
extern函数(使用extern "C-unwind"或其他允许展开的 ABI),例如使用 GCC 或 Clang 的-fno-exceptions编译的代码
使用 std::panic::catch_unwind、std::thread::JoinHandle::join 捕获外部展开操作(如 C++ 异常),或让它传播越过 Rust main() 函数或线程根,将具有以下两种行为之一,且未指定哪种会发生:
- 进程中止。
- 函数返回包含不透明类型的
Result::Err。
Note
使用不同实例的 Rust 标准库编译或链接的 Rust 代码在此保证的目的下被视为“外部异常“。因此,一个使用
panic!并链接到一个版本的 Rust 标准库的库,从使用不同版本标准库的应用程序调用,可能导致整个应用程序中止,即使该库仅在子线程中使用。
目前没有关于外部运行时尝试处置或重新抛出 Rust panic 负载时会发生什么的保证。换句话说,源自 Rust 运行时的展开必须导致进程终止或被同一运行时捕获。
链接
Note
本节更多从编译器角度描述,而非语言角度。
编译器支持多种将 crate 静态和动态链接在一起的方法。本节将探讨将 crate 链接在一起的各种方法,有关原生库的更多信息见本书的 FFI 部分。
在一次编译会话中,编译器可以通过使用命令行标志或 crate_type 属性生成多个产物。如果指定了一个或多个命令行标志,所有 crate_type 属性将被忽略,仅构建命令行指定的产物。
--crate-type=bin、#![crate_type = "bin"]- 将生成一个可运行的可执行文件。这要求 crate 中有一个main函数,当程序开始执行时将运行该函数。这将链接所有 Rust 和原生依赖,生成一个可分发二进制文件。这是默认的 crate 类型。
--crate-type=lib、#![crate_type = "lib"]- 将生成一个 Rust 库。这是一个模糊的概念,因为库可以表现为几种形式。此通用lib选项的目的是生成“编译器推荐“风格的库。输出库始终可被 rustc 使用,但实际的库类型可能随时间变化。其余的产物类型都是不同风格的库,lib类型可以看作是其中之一的别名(但具体是哪一个由编译器定义)。
--crate-type=dylib、#![crate_type = "dylib"]- 将生成一个动态 Rust 库。这与lib产物类型不同,因为它强制生成动态库。生成的动态库可以用作其他库和/或可执行文件的依赖。此产物类型将在 Linux 上创建*.so文件,在 macOS 上创建*.dylib文件,在 Windows 上创建*.dll文件。
-
--crate-type=staticlib、#![crate_type = "staticlib"]- 将生成一个静态系统库。这与其他库产物不同,因为编译器不会尝试链接到staticlib产物。此产物类型的目的是创建一个包含所有本地 crate 代码以及所有上游依赖的静态库。此产物类型将在 Linux、macOS 和 Windows (MinGW) 上创建*.a文件,在 Windows (MSVC) 上创建*.lib文件。此格式推荐用于将 Rust 代码链接到现有非 Rust 应用程序的情况,因为它不会对其他 Rust 代码有动态依赖。请注意,当从某处链接该静态库时,静态库可能具有的任何动态依赖(如对系统库的依赖,或对编译为动态库的 Rust 库的依赖)必须手动指定。
--print=native-static-libs标志可能对此有帮助。请注意,由于生成的静态库包含所有依赖的代码,包括标准库,并导出它们的所有公共符号,将静态库链接到可执行文件或共享库可能需要特别小心。对于共享库,导出的符号列表必须通过例如链接器或符号版本脚本、导出符号列表(macOS)或模块定义文件(Windows)来限制。此外,可以移除未使用的段以移除所有实际未使用的依赖代码(例如 mac 上的
--gc-sections或-dead_strip)。
--crate-type=cdylib、#![crate_type = "cdylib"]- 将生成一个动态系统库。这用于编译要从其他语言加载的动态库。此产物类型将在 Linux 上创建*.so文件,在 macOS 上创建*.dylib文件,在 Windows 上创建*.dll文件。
--crate-type=rlib、#![crate_type = "rlib"]- 将生成一个“Rust 库“文件。这用作中间产物,可以认为是“静态 Rust 库“。与staticlib文件不同,这些rlib文件在未来的链接中由编译器解释。这本质上意味着rustc将在rlib文件中查找元数据,就像在动态库中查找元数据一样。此形式的输出用于生成静态链接的可执行文件以及staticlib产物。
--crate-type=proc-macro、#![crate_type = "proc-macro"]- 产生的输出未指定,但如果提供了-L路径,编译器会将输出产物识别为宏,并且可以加载给程序使用。使用此 crate 类型编译的 crate 必须仅导出过程宏。编译器将自动设置proc_macro配置选项。这些 crate 始终以编译器本身构建的目标进行编译。例如,如果你在 Linux 上使用x86_64CPU 执行编译器,即使该 crate 是另一个为不同目标构建的 crate 的依赖,目标也将是x86_64-unknown-linux-gnu。
请注意,这些产出是可叠加的,如果指定了多个,编译器将生成每种形式的输出而无需重新编译。然而,这仅适用于通过相同方法指定的产出。如果只指定了 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 的输出格式时才可用。
静态和动态 C 运行时
标准库通常努力为目标平台适当支持静态链接和动态链接的 C 运行时。例如,x86_64-pc-windows-msvc 和 x86_64-unknown-linux-musl 目标通常附带两种运行时,用户可以选择他们喜欢的运行时。编译器中的所有目标都有链接到 C 运行时的默认模式。通常目标是默认动态链接的,但有一些例外默认是静态链接的,例如:
arm-unknown-linux-musleabiarm-unknown-linux-musleabihfarmv7-unknown-linux-musleabihfi686-unknown-linux-muslx86_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 运行时链接方式之间切换的目标将忽略此标志。建议在编译器成功编译后检查生成的二进制文件,确保其按预期链接。
Crate 也可以了解 C 运行时的链接方式。例如,MSVC 上的代码需要根据链接的运行时以不同方式编译(例如,使用 /MT 或 /MD)。这目前通过 cfg 属性 target_feature 选项导出:
#![allow(unused)]
fn main() {
#[cfg(target_feature = "crt-static")]
fn foo() {
println!("the C runtime should be statically linked");
}
#[cfg(not(target_feature = "crt-static"))]
fn foo() {
println!("the C runtime should be dynamically linked");
}
}
另请注意,Cargo 构建脚本可以通过环境变量了解此特性。在构建脚本中,你可以通过以下方式检测链接方式:
use std::env;
fn main() {
let linkage = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new());
if linkage.contains("crt-static") {
println!("the C runtime will be statically linked");
} else {
println!("the C runtime will be dynamically linked");
}
}
要本地使用此特性,你通常会使用 RUSTFLAGS 环境变量通过 Cargo 指定编译器标志。例如要在 MSVC 上编译静态链接的二进制文件,你可以执行:
RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-pc-windows-msvc
混合 Rust 和外部代码库
如果你正在将 Rust 与外部代码(如 C、C++)混合使用,并希望创建包含两种类型代码的单个二进制文件,你有两种方法进行最终的二进制链接:
- 使用
rustc。使用-L <directory>和-l<library>rustc 参数以及/或 Rust 代码中的#[link]指令传递任何非 Rust 库。如果需要链接.o文件,你可以使用-Clink-arg=file.o。 - 使用外部链接器。在这种情况下,你首先需要生成一个 Rust
staticlib目标并将其传递给你的外部链接器调用。如果你需要链接多个 Rust 子系统,你需要生成一个单一的staticlib,可能使用大量extern crate语句来包含多个 Rustrlib。多个 Ruststaticlib文件可能会冲突。
目前不支持直接将 rlib 传递给你的外部链接器。
Note
使用 Rust 运行时的不同实例编译或链接的 Rust 代码在本节的目的下被视为“外部代码“。
禁止的链接和展开
只有在二进制文件根据以下规则一致构建时,才能使用 panic 展开。
如果满足以下任一条件,则 Rust 产物被称为可能展开的:
- 该产物使用
unwindpanic 处理器。 - 该产物包含以
unwindpanic 策略构建的 crate,该 crate 调用了一个使用-unwindABI 的函数。 - 该产物发起
"Rust"ABI 调用到运行在另一个 Rust 产物中的代码,该产物具有 Rust 运行时的单独副本,并且该另一个产物是可能展开的。
Note
此定义描述了 Rust 产物中的
"Rust"ABI 调用是否可能展开。
如果 Rust 产物是可能展开的,则其所有 crate 必须使用 unwind panic 策略构建。否则,展开可能导致未定义行为。
Note
如果你使用
rustc链接,这些规则会自动强制执行。如果你不使用rustc链接,你必须注意确保整个二进制文件中的展开处理一致。不使用rustc链接包括使用dlopen或类似设施的情况,其中链接由系统运行时完成,而不涉及rustc。这仅在混合使用不同-C panic标志的代码时才会发生,因此大多数用户不必关注这一点。
Note
为了保证库在链接时无论使用何种 panic 运行时都将是健全的(并且可与
rustc链接),可以使用ffi_unwind_callslint。该 lint 标记对-unwind外部函数或函数指针的任何调用。
内联汇编
内联汇编的支持通过 asm!、naked_asm! 和 global_asm! 宏提供。它可以用于在编译器生成的汇编输出中嵌入手写汇编。
内联汇编的支持在以下架构上已稳定:
- x86 和 x86-64
- ARM
- AArch64 和 Arm64EC
- RISC-V
- LoongArch
- s390x
- PowerPC 和 PowerPC64
在不支持的平台上使用汇编宏时,编译器将发出错误。
示例
#![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);
}
}
语法
以下语法指定了可以传递给 asm!、global_asm! 和 naked_asm! 宏的参数。
Syntax
AsmArgs → AsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?
FormatString → STRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation
AsmAttrFormatString → ( OuterAttribute )* FormatString
AsmOperand →
ClobberAbi
| AsmOptions
| RegOperand
AsmAttrOperand → ( OuterAttribute )* AsmOperand
ClobberAbi → clobber_abi ( Abi ( , Abi )* ,? )
AsmOptions →
options ( ( AsmOption ( , AsmOption )* ,? )? )
AsmOption →
pure
| nomem
| readonly
| preserves_flags
| noreturn
| nostack
| att_syntax
| raw
RegOperand → ( ParamName = )?
(
DirSpec ( RegSpec ) Expression
| DualDirSpec ( RegSpec ) DualDirSpecExpression
| sym PathExpression
| const Expression
| label { Statements? }
)
ParamName → IDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER
DualDirSpecExpression →
Expression
| Expression => Expression
RegSpec → RegisterClass | ExplicitRegister
RegisterClass → IDENTIFIER_OR_KEYWORD
ExplicitRegister → STRING_LITERAL
DirSpec →
in
| out
| lateout
DualDirSpec →
inout
| inlateout
作用域
内联汇编可以以三种方式之一使用。
使用 asm! 宏,汇编代码在函数作用域中发出并集成到编译器生成的函数汇编代码中。此汇编代码必须遵守严格的规则以避免未定义行为。请注意,在某些情况下编译器可能会选择将汇编代码作为单独的函数发出并生成对其的调用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
unsafe { core::arch::asm!("/* {} */", in(reg) 0); }
}
}
使用 naked_asm! 宏,汇编代码在函数作用域中发出并构成函数的完整汇编代码。naked_asm! 宏仅允许在裸函数中使用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "C" fn wrapper() {
core::arch::naked_asm!("/* {} */", const 0);
}
}
}
使用 global_asm! 宏,汇编代码在全局作用域中发出,位于函数外部。这可用于使用汇编代码手写整个函数,并且通常提供更大的自由度来使用任意寄存器和汇编器指令。
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("/* {} */", const 0);
模板字符串参数
汇编器模板使用与格式字符串相同的语法(即占位符由大括号指定)。
相应的参数按顺序、按索引或按名称访问。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
let z: i64;
// 这种方式
unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); }
// ...这种方式
unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); }
// ...和这种方式
unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); }
// 都具有相同的行为
assert_eq!(x, y);
assert_eq!(y, z);
}
}
但是,不支持隐式命名参数(由 RFC #2795 引入)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5;
// 我们不能从作用域直接引用 `x`,我们需要一个像 `in(reg) x` 这样的操作数
unsafe { core::arch::asm!("/* {x} */"); } // ERROR: no argument named x
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
一次 asm! 调用可以有一个或多个模板字符串参数;具有多个模板字符串参数的 asm! 被视作所有字符串之间用 \n 连接。预期用途是每个模板字符串参数对应一行汇编代码。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
// 我们可以将多个字符串分开,就像它们写在一起一样
unsafe { core::arch::asm!("mov eax, 5", "mov ecx, eax", out("rax") x, out("rcx") y); }
assert_eq!(x, y);
}
}
所有模板字符串参数必须出现在任何其他参数之前。
#![allow(unused)]
fn main() {
let x = 5;
#[cfg(target_arch = "x86_64")] {
// 模板字符串需要首先出现在 asm 调用中
unsafe { core::arch::asm!("/* {x} */", x = const 5, "ud2"); } // ERROR: unexpected token
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
与格式字符串一样,位置参数必须出现在命名参数和显式寄存器操作数之前。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 命名操作数需要放在位置操作数之后
unsafe { core::arch::asm!("/* {x} {} */", x = const 5, in(reg) 5); }
// ERROR: positional arguments cannot follow named arguments or explicit register arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们也不能将显式寄存器放在位置操作数之前
unsafe { core::arch::asm!("/* {} */", in("eax") 0, in(reg) 5); }
// ERROR: positional arguments cannot follow named arguments or explicit register arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
显式寄存器操作数不能被模板字符串中的占位符使用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 显式寄存器操作数不会被替换,在字符串中显式使用 `eax`
unsafe { core::arch::asm!("/* {} */", in("eax") 5); }
// ERROR: invalid reference to argument at index 0
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
所有其他命名和位置操作数必须在模板字符串中至少出现一次,否则会生成编译器错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们必须在格式字符串中命名所有操作数
unsafe { core::arch::asm!("", in(reg) 5, x = const 5); }
// ERROR: multiple unused asm arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
确切的汇编代码语法是平台特定的,对编译器是不透明的,除了操作数被替换到模板字符串中以形成传递给汇编器的代码的方式。
目前,所有支持的目标都遵循 LLVM 内部汇编器使用的汇编代码语法,这通常对应于 GNU 汇编器 (GAS) 的语法。在 x86 上,默认使用 GAS 的 .intel_syntax noprefix 模式。在 ARM 上,使用 .syntax unified 模式。这些目标对汇编代码施加了一个额外的限制:任何汇编器状态(例如可以用 .section 更改的当前节)必须在 asm 字符串的末尾恢复到其原始值。不符合 GAS 语法的汇编代码将导致特定于汇编器的行为。内联汇编使用的指令的进一步约束由指令支持指示。
属性
仅 cfg 和 cfg_attr 属性在内联汇编模板字符串和操作数上被语义接受。其他属性会被解析但在汇编宏展开时被拒绝。
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!(
#[cfg(not(panic = "abort"))]
".cfi_startproc",
// ...
"ret",
#[cfg(not(panic = "abort"))]
".cfi_endproc",
);
Note
在
rustc中,汇编宏实现处理这些属性的方式与处理语言中类似属性的常规系统分开。这解释了支持的属性类型的有限性,并可能导致行为的细微差异。
语法上,第一个操作数之前必须至少有一个模板字符串。
#![allow(unused)]
fn main() {
// 这被拒绝,因为 `a = out(reg) x` 不被解析为模板字符串。
core::arch::asm!(
#[cfg(false)]
a = out(reg) x, // ERROR。
"",
);
}
操作数类型
支持几种类型的操作数:
in(<reg>) <expr><reg>可以引用寄存器类别或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器将在汇编代码开始时包含
<expr>的值。 - 分配的寄存器必须在汇编代码结束时包含相同的值(除非为同一寄存器分配了
lateout)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// ``in` 可用于将值传递到内联汇编中……
unsafe { core::arch::asm!("/* {} */", in(reg) 5); }
}
}
Note
如果值的类型小于寄存器,高位比特的值是平台特定的。某些目标将高位清零,而其他目标则保持不变。
out(<reg>) <expr><reg>可以引用寄存器类别或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器将在汇编代码开始时包含一个未定义的值。
<expr>必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容在汇编代码结束时被写入该表达式。- 可以指定下划线(
_)代替表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(有效地充当清除器)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `out` 可用于将值传回给 Rust。
unsafe { core::arch::asm!("/* {} */", out(reg) x); }
}
}
lateout(<reg>) <expr>- 与
out相同,除了寄存器分配器可以重用分配给in的寄存器。 - 你只应在所有输入都被读取后才写入寄存器,否则可能会清除一个输入。
- 与
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `lateout` 与 `out` 相同
// 但编译器知道在覆盖时我们不关心任何输入的值。
unsafe { core::arch::asm!("mov {}, 5", lateout(reg) x); }
assert_eq!(x, 5)
}
}
inout(<reg>) <expr><reg>可以引用寄存器类别或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器将在汇编代码开始时包含
<expr>的值。 <expr>必须是一个可变的已初始化位置表达式,分配的寄存器的内容在汇编代码结束时被写入该表达式。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inout` 可用于在寄存器中修改值
unsafe { core::arch::asm!("inc {}", inout(reg) x); }
assert_eq!(x, 5);
}
}
inout(<reg>) <in expr> => <out expr>- 与
inout相同,除了寄存器的初始值取自<in expr>的值。 <out expr>必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容在汇编代码结束时被写入该表达式。- 可以为
<out expr>指定下划线(_)代替表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(有效地充当清除器)。 <in expr>和<out expr>可以具有不同的类型。
- 与
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `inout` 也可以将值移动到不同的位置
unsafe { core::arch::asm!("inc {}", inout(reg) 4u64=>x); }
assert_eq!(x, 5);
}
}
inlateout(<reg>) <expr>/inlateout(<reg>) <in expr> => <out expr>- 与
inout相同,除了寄存器分配器可以重用分配给in的寄存器(如果编译器知道in与inlateout具有相同的初始值,这可能会发生)。 - 你只应在所有输入都被读取后才写入寄存器,否则可能会清除一个输入。
- 与
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inlateout` 是使用 `lateout` 语义的 `inout`
unsafe { core::arch::asm!("inc {}", inlateout(reg) x); }
assert_eq!(x, 5);
}
}
sym <path><path>必须引用一个fn或static。- 引用该项的装饰过的符号名被替换到 asm 模板字符串中。
- 替换后的字符串不包含任何修饰符(例如 GOT、PLT、重定位等)。
<path>允许指向#[thread_local]静态变量,在这种情况下,汇编代码可以结合重定位(例如@plt、@TPOFF)从线程局部数据中读取。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {
println!("Hello from inline assembly")
}
// `sym` 可用于引用函数(即使它没有我们可以直接编写的外部名称)
unsafe { core::arch::asm!("call {}", sym foo, clobber_abi("C")); }
}
}
const <expr><expr>必须是一个整数常量表达式。此表达式遵循与内联const块相同的规则。- 表达式的类型可以是任何整数类型,但默认与整数字面量一样为
i32。 - 表达式的值被格式化为字符串并直接替换到 asm 模板字符串中。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 交换 [0, 1, 2, 3] => [3, 2, 0, 1]
const SHUFFLE: u8 = 0b01_00_10_11;
let x: core::arch::x86_64::__m128 = unsafe { core::mem::transmute([0u32, 1u32, 2u32, 3u32]) };
let y: core::arch::x86_64::__m128;
// 将常量值传入期望立即数的指令,如 `pshufd`
unsafe {
core::arch::asm!("pshufd {xmm}, {xmm}, {shuffle}",
xmm = inlateout(xmm_reg) x=>y,
shuffle = const SHUFFLE
);
}
let y: [u32; 4] = unsafe { core::mem::transmute(y) };
assert_eq!(y, [3, 2, 0, 1]);
}
}
label <block>- 块的地址被替换到 asm 模板字符串中。汇编代码可以跳转到替换后的地址。
- 对于区分直接跳转和间接跳转的目标(例如启用了
cf-protection的 x86-64),汇编代码不能间接跳转到替换后的地址。 - 块执行完毕后,
asm!表达式返回。 - 块的类型必须是单元类型或
!(never 类型)。 - 块启动一个新的安全性上下文;
label块内的不安全操作必须包装在内部的unsafe块中,即使整个asm!表达式已经被unsafe包装。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
unsafe {
core::arch::asm!("jmp {}", label {
println!("Hello from inline assembly label");
});
}
}
操作数表达式从左到右求值,就像函数调用参数一样。在 asm! 执行完毕后,输出按从左到右的顺序写入。如果两个输出指向同一位置,这很重要:该位置将包含最右侧输出的值。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// y 从第二个输出获取值,而不是第一个
unsafe { core::arch::asm!("mov {}, 0", "mov {}, 1", out(reg) y, out(reg) y); }
assert_eq!(y, 1);
}
}
由于 naked_asm! 定义了整个函数体而编译器无法发出任何额外代码来处理操作数,因此它只能使用 sym 和 const 操作数。
由于 global_asm! 存在于函数外部,因此它只能使用 sym 和 const 操作数。
fn main() {}
// 不允许寄存器操作数,因为我们不在函数中
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("", in(reg) 5);
// ERROR: the `in` operand cannot be used with `global_asm!`
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
fn main() {}
fn foo() {}
#[cfg(target_arch = "x86_64")]
// 然而 `const` 和 `sym` 都是允许的
core::arch::global_asm!("/* {} {} */", const 0, sym foo);
寄存器操作数
输入和输出操作数可以指定为显式寄存器或来自寄存器类别的寄存器,由寄存器分配器从中选择。显式寄存器作为字符串字面量(例如 "eax")指定,而寄存器类别作为标识符(例如 reg)指定。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// 我们可以命名 `reg` 或像 `eax` 这样的显式寄存器来获取整数寄存器
unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); }
assert_eq!(y, 5);
}
}
请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14 与 lr)和寄存器的较小视图(例如 eax 与 rax)视为等同于基寄存器。
将同一显式寄存器用于两个输入操作数或两个输出操作数是编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们不能两次命名 eax
unsafe { core::arch::asm!("", in("eax") 5, in("eax") 4); }
// ERROR: register `eax` conflicts with register `eax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// ...即使使用不同的别名也不行
unsafe { core::arch::asm!("", in("ax") 5, in("rax") 4); }
// ERROR: register `rax` conflicts with register `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
此外,在输入操作数或输出操作数中使用重叠寄存器(例如 ARM VFP)也是编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// al 与 ax 重叠,所以我们不能同时命名两者。
unsafe { core::arch::asm!("", in("ax") 5, in("al") 4i8); }
// ERROR: register `al` conflicts with register `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
仅以下类型允许作为内联汇编的操作数:
- 整数(有符号和无符号)
- 浮点数
- 指针(仅瘦指针)
- 函数指针
- SIMD 向量(使用
#[repr(simd)]定义且实现了Copy的结构体)。这包括在std::arch中定义的架构特定向量类型,如__m128(x86)或int8x16_t(ARM)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {}
// 整数是允许的……
let y: i64 = 5;
unsafe { core::arch::asm!("/* {} */", in(reg) y); }
// 还有指针……
let py = &raw const y;
unsafe { core::arch::asm!("/* {} */", in(reg) py); }
// 浮点数也是……
let f = 1.0f32;
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); }
// 甚至函数指针和 SIMD 向量也行。
let func: extern "C" fn() = foo;
unsafe { core::arch::asm!("/* {} */", in(reg) func); }
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) z); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
struct Foo;
let x: Foo = Foo;
// 像结构体这样的复杂类型是不允许的
unsafe { core::arch::asm!("/* {} */", in(reg) x); }
// ERROR: cannot use value of type `Foo` for inline assembly
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
以下是当前支持的寄存器类别列表:
| 架构 | 寄存器类别 | 寄存器 | 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 | 仅清除器 |
| x86 | x87_reg | st([0-7]) | 仅清除器 |
| x86 | mmx_reg | mm[0-7] | 仅清除器 |
| x86-64 | tmm_reg | tmm[0-7] | 仅清除器 |
| 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 | 仅清除器 |
| Arm64EC | reg | x[0-12], x[15-22], x[25-27], x30 | r |
| Arm64EC | vreg | v[0-15] | w |
| Arm64EC | vreg_low16 | v[0-15] | x |
| 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] (非 RV32E) | r |
| RISC-V | freg | f[0-31] | f |
| RISC-V | vreg | v[0-31] | 仅清除器 |
| LoongArch | reg | $r1, $r[4-20], $r[23,30] | r |
| LoongArch | freg | $f[0-31] | f |
| s390x | reg | r[0-10], r[12-14] | r |
| s390x | reg_addr | r[1-10], r[12-14] | a |
| s390x | freg | f[0-15] | f |
| s390x | vreg | v[0-31] | v |
| s390x | areg | a[2-15] | 仅清除器 |
| PowerPC | reg | r0, r[3-12], r[14-28] | r |
| PowerPC | reg_nonzero | r[3-12], r[14-28] | b |
| PowerPC | spe_acc | spe_acc | 仅清除器 |
| PowerPC64 | reg | r0, r[3-12], r[14-29] | r |
| PowerPC64 | reg_nonzero | r[3-12], r[14-29] | b |
| PowerPC/PowerPC64 | freg | f[0-31] | f |
| PowerPC/PowerPC64 | vreg | v[0-31] | v |
| PowerPC/PowerPC64 | vsreg | vs[0-63] | wa |
| PowerPC/PowerPC64 | cr | cr[0-7], cr | 仅清除器 |
| PowerPC/PowerPC64 | ctr | ctr | 仅清除器 |
| PowerPC/PowerPC64 | lr | lr | 仅清除器 |
| PowerPC/PowerPC64 | xer | xer | 仅清除器 |
Note
- 在 x86 上,我们将
reg_byte与reg区别对待,因为编译器可以分别分配al和ah,而reg会保留整个寄存器。- 在 x86-64 上,高字节寄存器(例如
ah)在reg_byte寄存器类别中不可用。- 某些寄存器类别标记为“仅清除器“,这意味着这些类别中的寄存器不能用于输入或输出,只能使用
out(<explicit register>) _或lateout(<explicit register>) _形式的清除器。spe_acc寄存器仅在 PowerPC SPE 目标上可用。
每个寄存器类别对其可使用的值类型有约束。这是必要的,因为将值加载到寄存器的方式取决于其类型。例如,在大端系统上,将 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 | 仅清除器 |
| x86 | x87_reg | N/A | 仅清除器 |
| x86 | tmm_reg | N/A | 仅清除器 |
| 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 | 仅清除器 |
| Arm64EC | reg | None | i8, i16, i32, f32, i64, f64 |
| Arm64EC | vreg | neon | i8, i16, i32, f32, i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2, f64x1, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| 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 | 仅清除器 |
| LoongArch32 | reg | None | i8, i16, i32, f32 |
| LoongArch64 | reg | None | i8, i16, i32, i64, f32, f64 |
| LoongArch | freg | f | f32 |
| LoongArch | freg | d | f64 |
| s390x | reg, reg_addr | None | i8, i16, i32, i64 |
| s390x | freg | None | f32, f64 |
| s390x | vreg | vector | i32, f32, i64, f64, i128, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| s390x | areg | N/A | 仅清除器 |
| PowerPC | spe_acc | None | 仅清除器 |
| PowerPC/PowerPC64 | reg | None | i8, i16, i32, i64 (仅 PowerPC64) |
| PowerPC/PowerPC64 | reg_nonzero | None | i8, i16, i32, i64 (仅 PowerPC64) |
| PowerPC/PowerPC64 | freg | None | f32, f64 |
| PowerPC/PowerPC64 | vreg | altivec | i8x16, i16x8, i32x4, f32x4 |
| PowerPC/PowerPC64 | vreg | vsx | f32, f64, i64x2, f64x2 |
| PowerPC/PowerPC64 | vsreg | vsx | vsx 和 altivec vreg 类型的并集 |
| PowerPC/PowerPC64 | cr | None | 仅清除器 |
| PowerPC/PowerPC64 | ctr | None | 仅清除器 |
| PowerPC/PowerPC64 | lr | None | 仅清除器 |
| PowerPC/PowerPC64 | xer | None | 仅清除器 |
Note
就上表而言,指针、函数指针和
isize/usize被视为等效的整数类型(根据目标为i16/i32/i64)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5i32;
let y = -1i8;
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
// reg 对 `i32` 有效,reg_byte 对 `i8` 有效,xmm_reg 对 `__m128i` 有效
// 我们不能将 `tmm0` 用作输入或输出,但可以将其清除。
unsafe { core::arch::asm!("/* {} {} {} */", in(reg) x, in(reg_byte) y, in(xmm_reg) z, out("tmm0") _); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
// 我们不能将 `__m128i` 传递给 `reg` 输入
unsafe { core::arch::asm!("/* {} */", in(reg) z); }
// ERROR: type `__m128i` cannot be used with this register class
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
如果值的尺寸小于其分配的寄存器,则对于输入,该寄存器的高位比特将具有未定义的值,对于输出则会被忽略。唯一的例外是 RISC-V 上的 freg 寄存器类别,其中 f32 值按照 RISC-V 架构的要求在 f64 中进行 NaN 装箱。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64;
// 将 32 位值移动到 64 位值中,糟糕。
#[allow(asm_sub_register)] // rustc 对此行为发出警告
unsafe { core::arch::asm!("mov {}, {}", lateout(reg) x, in(reg) 4i32); }
// 高 32 位是不确定的
assert_eq!(x, 4); // 此断言不保证成功
assert_eq!(x & 0xFFFFFFFF, 4); // 然而,这个断言会成功
}
}
当为 inout 操作数指定单独的输入和输出表达式时,两个表达式必须具有相同的类型。唯一的例外是如果两个操作数都是指针或整数,在这种情况下它们只需要具有相同的尺寸。此限制存在是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的关联操作数。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 指针和整数可以混合(只要它们尺寸相同)
let x: isize = 0;
let y: *mut ();
// 使用内联汇编魔法将 `isize` 转换为 `*mut ()`
unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); }
assert!(y.is_null()); // 极其迂回地创建空指针的方式
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let y: f32;
// 但我们不能这样将 `i32` 重新解释为 `f32`
unsafe { core::arch::asm!("/* {} */", inout(reg) x=>y); }
// ERROR: incompatible types for asm inout argument
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
寄存器名称
某些寄存器有多个名称。这些都被编译器视为等同于基寄存器名称。以下是所有支持的寄存器别名列表:
| 架构 | 基寄存器 | 别名 |
|---|---|---|
| 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] |
| Arm64EC | x[0-30] | w[0-30] |
| Arm64EC | x29 | fp |
| Arm64EC | x30 | lr |
| Arm64EC | sp | wsp |
| Arm64EC | xzr | wzr |
| Arm64EC | v[0-15] | b[0-15], h[0-15], s[0-15], d[0-15], q[0-15] |
| 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] |
| PowerPC/PowerPC64 | r1 | sp |
| PowerPC/PowerPC64 | r31 | fp |
| PowerPC/PowerPC64 | r[0-31] | [0-31] |
| PowerPC/PowerPC64 | f[0-31] | fr[0-31] |
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = 0i64;
// rax 是 eax 和 ax 的别名
unsafe { core::arch::asm!("", in("rax") z); }
}
}
某些寄存器不能用于输入或输出操作数:
| 架构 | 不支持的寄存器 | 原因 |
|---|---|---|
| All | sp, r15 (s390x), r1 (PowerPC 和 PowerPC64) | 栈指针必须在汇编代码结束时或跳转到 label 块之前恢复到其原始值。 |
| All | bp (x86), x29 (AArch64 和 Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x), fp (PowerPC 和 PowerPC64) | 帧指针不能用作输入或输出。 |
| ARM | r7 或 r11 | 在 ARM 上,帧指针可以是 r7 或 r11,取决于目标。帧指针不能用作输入或输出。 |
| All | si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 和 Arm64EC), x9 (RISC-V), $s8 (LoongArch), r29 和 r30 (PowerPC), r30 (PowerPC64) | 此寄存器被 LLVM 内部用作具有复杂栈帧的函数的“基指针“。 |
| x86 | ip | 这是程序计数器,不是真正的寄存器。 |
| AArch64 | xzr | 这是一个常量零寄存器,不能被修改。 |
| AArch64 | x18 | 在某些 AArch64 目标上这是 OS 保留的寄存器。 |
| Arm64EC | xzr | 这是一个常量零寄存器,不能被修改。 |
| Arm64EC | x18 | 这是一个 OS 保留的寄存器。 |
| Arm64EC | x13, x14, x23, x24, x28, v[16-31], p[0-15], ffr | 这些是 Arm64EC 不支持的 AArch64 寄存器。 |
| ARM | pc | 这是程序计数器,不是真正的寄存器。 |
| ARM | r9 | 在某些 ARM 目标上这是 OS 保留的寄存器。 |
| RISC-V | x0 | 这是一个常量零寄存器,不能被修改。 |
| RISC-V | gp, tp | 这些寄存器是保留的,不能用作输入或输出。 |
| LoongArch | $r0 或 $zero | 这是一个常量零寄存器,不能被修改。 |
| LoongArch | $r2 或 $tp | 此寄存器保留用于 TLS。 |
| LoongArch | $r21 | 此寄存器由 ABI 保留。 |
| s390x | c[0-15] | 由内核保留。 |
| s390x | a[0-1] | 保留供系统使用。 |
| PowerPC/PowerPC64 | r2, r13 | 这些是系统保留的寄存器。 |
| PowerPC/PowerPC64 | vrsave | vrsave 寄存器不能用作输入或输出。 |
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// bp 是保留的
unsafe { core::arch::asm!("", in("bp") 5i32); }
// ERROR: invalid register `bp`: the frame pointer cannot be used as an operand for inline asm
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
帧指针寄存器和基指针寄存器保留供 LLVM 内部使用。虽然 asm! 语句不能显式指定使用保留寄存器,但在某些情况下 LLVM 会为 reg 操作数分配这些保留寄存器之一。使用保留寄存器的汇编代码应谨慎,因为 reg 操作数可能使用相同的寄存器。
模板修饰符
占位符可以通过修饰符来增强,修饰符在大括号中的 : 之后指定。这些修饰符不影响寄存器分配,但会改变操作数插入模板字符串时的格式化方式。
每个模板占位符只允许一个修饰符。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们不能同时指定 `r` 和 `e`。
unsafe { core::arch::asm!("/* {:er}", in(reg) 5i32); }
// ERROR: asm template modifier must be a single character
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
支持的修饰符是 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/Arm64EC | reg | None | x0 | x |
| AArch64/Arm64EC | reg | w | w0 | w |
| AArch64/Arm64EC | reg | x | x0 | x |
| AArch64/Arm64EC | vreg | None | v0 | None |
| AArch64/Arm64EC | vreg | v | v0 | None |
| AArch64/Arm64EC | vreg | b | b0 | b |
| AArch64/Arm64EC | vreg | h | h0 | h |
| AArch64/Arm64EC | vreg | s | s0 | s |
| AArch64/Arm64EC | vreg | d | d0 | d |
| AArch64/Arm64EC | 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 |
| s390x | reg | None | %r0 | None |
| s390x | reg_addr | None | %r1 | None |
| s390x | freg | None | %f0 | None |
| s390x | vreg | None | %v0 | None |
| PowerPC/PowerPC64 | reg | None | 0 | None |
| PowerPC/PowerPC64 | reg_nonzero | None | 3 | None |
| PowerPC/PowerPC64 | freg | None | 0 | None |
| PowerPC/PowerPC64 | vreg | None | 0 | None |
| PowerPC/PowerPC64 | vsreg | None | 0 | None |
Note
- 在 ARM 上
e/f:此修饰符打印 NEON 四字(128 位)寄存器的低或高双字寄存器名称。- 在 x86 上:我们对无修饰符的
reg的行为与 GCC 不同。GCC 会基于操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。- 在 x86
xmm_reg上:x、t和gLLVM 修饰符尚未在 LLVM 中实现(它们仅受 GCC 支持),但这应该是一个简单的更改。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0x10u16;
// 使用 `xchg` 进行 u16::swap_bytes
// `{x}` 的低半部分由 `{x:l}` 引用,高半部分由 `{x:h}` 引用
unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); }
assert_eq!(x, 0x1000u16);
}
}
如前一节所述,传入小于寄存器宽度的输入值将导致寄存器的高位比特包含未定义的值。如果内联汇编仅访问寄存器的低位比特,这不成问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称(例如 ax 而非 rax)来实现。由于这是一个容易犯的错误,编译器会在适当时根据输入类型建议使用模板修饰符。如果对某个操作数的所有引用都已经有修饰符,则对该操作数的警告将被抑制。
ABI 清除器
clobber_abi 关键字可用于对汇编代码应用一组默认的清除器。这将自动插入必要的清除器约束,以适应使用特定调用约定调用函数的需求:如果调用约定在跨函数调用时不完全保留寄存器的值,则 lateout("...") _ 会隐式添加到操作数列表中(其中 ... 替换为寄存器的名称)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() -> i32 { 0 }
let z: i32;
// 要调用函数,我们必须通知编译器我们要清除被调用者保存的寄存器
unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); }
assert_eq!(z, 0);
}
}
clobber_abi 可以指定任意次数。它将为所有指定调用约定的并集中的每个唯一寄存器插入一个清除器。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "sysv64" fn foo() -> i32 { 0 }
extern "win64" fn bar(x: i32) -> i32 { x + 1 }
let z: i32;
// 我们甚至可以调用具有不同约定和不同保存寄存器的多个函数
unsafe {
core::arch::asm!(
"call {}",
"mov ecx, eax",
"call {}",
sym foo,
sym bar,
out("rax") z,
clobber_abi("sysv64"),
clobber_abi("win64"),
);
}
assert_eq!(z, 1);
}
}
当使用 clobber_abi 时,编译器不允许通用寄存器类别输出:所有输出必须指定显式寄存器。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo(x: i32) -> i32 { 0 }
let z: i32;
// 必须使用显式寄存器以避免意外重叠。
unsafe {
core::arch::asm!(
"mov eax, {:e}",
"call {}",
out(reg) z,
sym foo,
clobber_abi("C")
);
// ERROR: asm with `clobber_abi` must specify explicit registers for outputs
}
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
显式寄存器输出优先于 clobber_abi 插入的隐式清除器:仅当寄存器未被用作输出时才会插入清除器。
以下 ABI 可与 clobber_abi 一起使用:
| 架构 | ABI 名称 | 被清除的寄存器 |
|---|---|---|
| x86-32 | "C", "system", "efiapi", "cdecl", "stdcall", "fastcall" | ax, cx, dx, xmm[0-7], mm[0-7], k[0-7], st([0-7]) |
| x86-64 | "C", "system" (在 Windows 上), "efiapi", "win64" | ax, cx, dx, r[8-11], xmm[0-31], mm[0-7], k[0-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[0-7], st([0-7]), tmm[0-7] |
| AArch64 | "C", "system", "efiapi" | x[0-17], x18*, x30, v[0-31], p[0-15], ffr |
| Arm64EC | "C", "system" | x[0-12], x[15-17], x30, v[0-15] |
| 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" | $r1, $r[4-20], $f[0-23] |
| s390x | "C", "system" | r[0-5], r14, f[0-7], v[0-31], a[2-15] |
Note
- 在 AArch64 上,
x18仅当在目标上不被视为保留寄存器时才包含在清除器列表中。- 在 RISC-V 上,
x[16-17]和x[28-31]仅当在目标上不被视为保留寄存器时才包含在清除器列表中。
随着架构获得新寄存器,每种 ABI 的清除寄存器列表都会在 rustc 中更新:这确保了当 LLVM 在其生成的代码中开始使用这些新寄存器时,asm! 清除器将继续保持正确。
选项
标志用于进一步影响内联汇编代码的行为。目前定义了以下选项:
pure:汇编代码没有副作用,必须最终返回,其输出仅依赖于其直接输入(即值本身,而非它们指向的内容)或从内存读取的值(除非也设置了nomem选项)。这允许编译器减少汇编代码的执行次数(例如将其提升到循环外),甚至如果输出未被使用则完全消除它。pure选项必须与nomem或readonly选项结合使用,否则会发出编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// pure 可用于通过假设汇编没有副作用来进行优化
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure, nomem)); }
assert_eq!(z, 1);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// 必须满足 nomem 或 readonly,以指示是否允许读取内存
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure)); }
// ERROR: the `pure` option must be combined with either `nomem` or `readonly`
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
nomem:汇编代码不从汇编代码外部可访问的任何内存读取或写入。这允许编译器在汇编代码执行期间在寄存器中缓存已修改的全局变量的值,因为它知道它们不会被汇编代码读取或写入。编译器还假设汇编代码不执行任何类型的与其他线程的同步,例如通过屏障。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0i32;
let z: i32;
// 当指定了 `nomem` 时,从汇编中访问外部内存是不允许的
unsafe {
core::arch::asm!("mov {val:e}, dword ptr [{ptr}]",
ptr = in(reg) &mut x,
val = lateout(reg) z,
options(nomem)
)
}
// 当指定了 `nomem` 时,从汇编中写入外部内存也是未定义行为
unsafe {
core::arch::asm!("mov dword ptr [{ptr}], {val:e}",
ptr = in(reg) &mut x,
val = in(reg) z,
options(nomem)
)
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// 但是,如果我们分配自己的内存,例如通过 `push`,仍然可以使用它
unsafe {
core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
x = inout(reg) x => z,
options(nomem)
);
}
assert_eq!(z, 1);
}
}
readonly:汇编代码不写入汇编代码外部可访问的任何内存。这允许编译器在汇编代码执行期间在寄存器中缓存未修改的全局变量的值,因为它知道它们不会被汇编代码写入。编译器还假设此汇编代码不执行任何类型的与其他线程的同步,例如通过屏障。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0;
// 当指定了 `readonly` 时,我们不能修改外部内存
unsafe {
core::arch::asm!("mov dword ptr[{}], 1", in(reg) &mut x, options(readonly))
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// 不过,我们仍然可以从中读取
unsafe {
core::arch::asm!("mov {x}, qword ptr [{x}]",
x = inout(reg) &x => z,
options(readonly)
);
}
assert_eq!(z, 0);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// 与 nomem 相同的例外情况也适用。
unsafe {
core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
x = inout(reg) x => z,
options(readonly)
);
}
assert_eq!(z, 1);
}
}
preserves_flags:汇编代码不修改标志寄存器(在下文规则中定义)。这允许编译器在内联汇编代码执行后避免重新计算条件标志。
noreturn:汇编代码不会穿过结尾;如果穿过则是未定义行为。它仍然可以跳转到label块。如果任何label块返回单元类型,则asm!块将返回单元类型。否则它将返回!(never 类型)。与调用不返回的函数一样,作用域内的局部变量在汇编代码执行之前不会被丢弃。
fn main() -> ! {
#[cfg(target_arch = "x86_64")] {
// 我们可以使用指令在 noreturn 块内捕获执行
unsafe { core::arch::asm!("ud2", options(noreturn)); }
}
#[cfg(not(target_arch = "x86_64"))] panic!("no return");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 你有责任不穿过 noreturn asm 块的结尾
unsafe { core::arch::asm!("", options(noreturn)); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
let _: () = unsafe {
// 你仍然可以跳转到 `label` 块
core::arch::asm!("jmp {}", label {
println!();
}, options(noreturn));
};
}
nostack:汇编代码不向栈推送数据,也不写入栈红色区域(如果目标支持)。如果未使用此选项,则编译器保证在汇编代码开始时栈指针已适当对齐(根据目标 ABI)以进行函数调用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `push` 和 `pop` 在使用 nostack 时是未定义行为
unsafe { core::arch::asm!("push rax", "pop rax", options(nostack)); }
}
}
att_syntax:此选项仅在 x86 上有效,并导致汇编器使用 GNU 汇编器的.att_syntax prefix模式。寄存器操作数在替换时会带有前导%。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32;
let y = 1i32;
// 这里我们需要使用 AT&T 语法。操作数的顺序是 src, dest
unsafe {
core::arch::asm!("mov {y:e}, {x:e}",
x = lateout(reg) x,
y = in(reg) y,
options(att_syntax)
);
}
assert_eq!(x, y);
}
}
raw:这导致模板字符串被解析为原始汇编字符串,对{和}没有特殊处理。这主要在通过include_str!从外部文件包含原始汇编代码时很有用。
编译器对选项执行一些额外的检查:
nomem和readonly选项是互斥的:同时指定两者是编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// nomem 比 readonly 严格得多,它们不能一起指定
unsafe { core::arch::asm!("", options(nomem, readonly)); }
// ERROR: the `nomem` and `readonly` options are mutually exclusive
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
- 在没有输出或仅丢弃输出(
_)的 asm 块上指定pure是编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// pure 块需要至少一个输出
unsafe { core::arch::asm!("", options(pure)); }
// ERROR: asm with the `pure` option must have at least one output
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
- 在有输出且没有标签的 asm 块上指定
noreturn是编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z: i32;
// noreturn 不能有输出
unsafe { core::arch::asm!("mov {:e}, 1", out(reg) z, options(noreturn)); }
// ERROR: asm outputs are not allowed with the `noreturn` option
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
- 在有输出的 asm 块中有任何
label块是编译时错误。
naked_asm! 仅支持 att_syntax 和 raw 选项。其余选项没有意义,因为内联汇编定义了整个函数体。
global_asm! 仅支持 att_syntax 和 raw 选项。其余选项对于全局作用域的内联汇编没有意义。
fn main() {}
#[cfg(target_arch = "x86_64")]
// nomem 在 global_asm! 上毫无用处
core::arch::global_asm!("", options(nomem));
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
内联汇编的规则
为避免未定义行为,在使用函数作用域内联汇编(asm!)时必须遵循以下规则:
- 任何未指定为输入的寄存器在进入汇编代码时将包含未定义的值。
- 内联汇编语境中的“未定义值“意味着该寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM 的
undef不同,后者每次读取时可能具有不同的值(因为汇编代码中不存在这样的概念)。
- 内联汇编语境中的“未定义值“意味着该寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM 的
- 任何未指定为输出的寄存器在退出汇编代码时必须具有与进入时相同的值,否则行为是未定义的。
- 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循特定于目标的规则。
- 请注意,
lateout可能被分配到与in相同的寄存器,在这种情况下此规则不适用。但是,代码不应依赖于此,因为它取决于寄存器分配的结果。
- 如果执行从汇编代码中展开,行为是未定义的。
- 如果汇编代码调用一个函数,然后该函数发生展开,这也适用。
- 汇编代码允许读写的内存位置集合与 FFI 函数允许的相同。
- 如果设置了
readonly选项,则仅允许内存读取。 - 如果设置了
nomem选项,则不允许读取或写入内存。 - 这些规则不适用于汇编代码私有的内存,例如在其中分配的栈空间。
- 如果设置了
- 编译器不能假设汇编代码中的指令就是最终实际执行的指令。
- 这实际上意味着编译器必须将汇编代码视为黑盒,只考虑接口规范,而非指令本身。
- 通过特定于目标的机制允许运行时代码补丁。
- 然而,不能保证源代码中的每个汇编代码块都直接对应于目标文件中的单个指令实例;编译器可以自由地复制或去重
asm!块中的汇编代码。
- 除非设置了
nostack选项,否则汇编代码允许使用栈指针以下的栈空间。- 在进入汇编代码时,栈指针保证已适当对齐(根据目标 ABI)以进行函数调用。
- 你有责任确保不会溢出栈(例如使用栈探测确保碰到保护页)。
- 你应根据目标 ABI 的要求在分配栈内存时调整栈指针。
- 在离开汇编代码之前,栈指针必须恢复到其原始值。
- 除非设置了
nostack选项,否则当目标 ABI 要求在调用者的帧中存储某些值(例如在 PowerPC64 上保存lr时),汇编代码允许修改调用者的栈帧。
- 如果设置了
noreturn选项,则执行穿过汇编代码的结尾是未定义行为。
- 如果设置了
pure选项,则如果asm!除了其直接输出之外还有副作用,行为是未定义的。如果两次执行具有相同输入的asm!代码产生不同的输出,行为也是未定义的。- 当与
nomem选项一起使用时,“输入“仅指asm!的直接输入。 - 当与
readonly选项一起使用时,“输入“包括汇编代码的直接输入以及允许读取的任何内存。
- 当与
- 如果设置了
preserves_flags选项,则以下标志寄存器必须在退出汇编代码时恢复:- 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 和 Arm64EC
- 条件标志(
NZCV寄存器)。 - 浮点状态(
FPSR寄存器)。
- 条件标志(
- RISC-V
fcsr中的浮点异常标志(fflags)。- 向量扩展状态(
vtype,vl,vxsat和vxrm)。
- LoongArch
$fcc[0-7]中的浮点条件标志。
- PowerPC/PowerPC64
fpscr中的浮点状态和粘滞位(DRN, VE, OE, UE, ZE, XE, NI 或 RN 以外的任何字段)。vscr中的向量状态和粘滞位(NJ 以外的任何字段)。
- PowerPC SPE
spefscr的粘滞位和状态位(FINXE, FINVE, FDBZE, FUNFE, FOVFE 或 FRMC 以外的任何字段)。
- s390x
- 条件码寄存器
cc。
- 条件码寄存器
- x86
- 在 x86 上,方向标志(
EFLAGS中的 DF)在进入汇编代码时是清除的,并且必须在退出时也是清除的。- 退出汇编代码时如果方向标志被设置,则行为是未定义的。
- 在 x86 上,x87 浮点寄存器栈必须保持不变,除非所有
st([0-7])寄存器都已使用out("st(0)") _, out("st(1)") _, ...标记为清除。- 如果所有 x87 寄存器都被清除,则 x87 寄存器栈在进入汇编代码时保证为空。汇编代码必须确保在退出汇编代码时 x87 寄存器栈也为空。
#[cfg(target_arch = "x86_64")]
pub fn fadd(x: f64, y: f64) -> f64 {
let mut out = 0f64;
let mut top = 0u16;
// 如果清除了整个 x87 栈,我们可以用 x87 做复杂的事情
unsafe { core::arch::asm!(
"fld qword ptr [{x}]",
"fld qword ptr [{y}])",
"faddp",
"fstp qword ptr [{out}]",
"xor eax, eax",
"fstsw ax",
"shl eax, 11",
x = in(reg) &x,
y = in(reg) &y,
out = in(reg) &mut out,
out("st(0)") _, out("st(1)") _, out("st(2)") _, out("st(3)") _,
out("st(4)") _, out("st(5)") _, out("st(6)") _, out("st(7)") _,
out("eax") top
);}
assert_eq!(top & 0x7, 0);
out
}
pub fn main() {
#[cfg(target_arch = "x86_64")]{
assert_eq!(fadd(1.0, 1.0), 2.0);
}
}
- 在 arm64ec 上,在调用函数时,带有适当转换块的调用检查器是强制性的。
- 恢复栈指针和非输出寄存器到其原始值的要求仅在退出汇编代码时适用。
- 这意味着不穿过结尾且不跳转到任何
label块的汇编代码,即使未标记为noreturn,也不需要保留这些寄存器。 - 当返回到与你进入时不同的
asm!块的汇编代码时(例如用于上下文切换),这些寄存器必须包含你进入你正在退出的asm!块时它们具有的值。- 你不能退出来曾进入的
asm!块的汇编代码。你也不能退出其汇编代码已经退出过的asm!块的汇编代码(而不先再次进入它)。 - 你有责任切换任何特定于目标的状态(例如线程局部存储、栈边界)。
- 你不能从一个
asm!块中的地址跳转到另一个asm!块中的地址,即使在同一函数或块内,除非将它们上下文视为可能不同且需要上下文切换。你不能假设这些上下文中的任何特定值(例如当前栈指针或栈指针以下的临时值)在两个asm!块之间保持不变。 - 你可以访问的内存位置集合是你进入和退出的
asm!块所允许的那些集合的交集。
- 你不能退出来曾进入的
- 这意味着不穿过结尾且不跳转到任何
- 你不能假设源代码中相邻的两个
asm!块(即使它们之间没有任何其他代码)最终会在二进制中连续排列,中间没有任何其他指令。
- 你不能假设一个
asm!块在输出二进制中恰好出现一次。编译器可以实例化asm!块的多个副本,例如当包含它的函数在多个位置被内联时。
- 在 x86 上,内联汇编不能以会应用于编译器生成的指令的指令前缀(如
LOCK)结尾。- 由于内联汇编的编译方式,编译器目前无法检测到这一点,但未来可能会捕获并拒绝这种情况。
Note
作为一般规则,
preserves_flags涵盖的标志是那些在执行函数调用时不被保留的标志。
裸内联汇编的规则
为避免未定义行为,在裸函数中使用函数作用域内联汇编(naked_asm!)时必须遵循以下规则:
- 任何未根据调用约定和函数签名用于函数输入的寄存器在进入
naked_asm!块时将包含未定义的值。- 内联汇编语境中的“未定义值“意味着该寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM 的
undef不同,后者每次读取时可能具有不同的值(因为汇编代码中不存在这样的概念)。
- 内联汇编语境中的“未定义值“意味着该寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM 的
- 所有被调用者保存的寄存器在返回时必须具有与进入时相同的值。
- 调用者保存的寄存器可以自由使用。
- 如果执行穿过汇编代码的结尾,行为是未定义的。
- 汇编代码中的每条路径都期望以返回指令终止或发散。
- 汇编代码允许读写的内存位置集合与 FFI 函数允许的相同。
- 编译器不能假设
naked_asm!块中的指令就是最终实际执行的指令。- 这实际上意味着编译器必须将
naked_asm!视为黑盒,只考虑接口规范,而非指令本身。 - 通过特定于目标的机制允许运行时代码补丁。
- 这实际上意味着编译器必须将
- 允许从
naked_asm!块中展开。- 为获得正确行为,必须使用适当的发出展开元数据的汇编器指令。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
core::arch::naked_asm!(
// "CFI" 在此代表 "call frame information"(调用帧信息)。
".cfi_startproc",
// CFA(规范帧地址)是 `call` 之前的 `rsp` 值,
// 即在返回地址 `rip` 被推入 `rsp` 之前,
// 因此在函数进入时(在 `rip` 被推入之后)
// 它比 `rsp` 在内存中高八个字节。
//
// 这是默认值,所以我们不必写它。
//".cfi_def_cfa rsp, 8",
//
// 传统的做法是保留基指针,所以我们照做。
"push rbp",
// 由于我们现在已将栈向下扩展了 8 个字节,
// 需要将 `rsp` 到 CFA 的偏移量再调整 8 个字节。
".cfi_adjust_cfa_offset 8",
// 我们还标注了调用者的 `rbp` 值存储的位置,
// 相对于 CFA,以便在展开到调用者时能够找到它,
// 以防我们需要它来计算调用者相对于它的 CFA。
//
// 这里,我们将调用者的 `rbp` 存储在 CFA 下方 16 字节处。
// 即,从 CFA 开始,首先是 `rip`(从 CFA 下方 8 字节开始一直延伸到 CFA),
// 然后是我们刚刚推送的调用者的 `rbp`。
".cfi_offset rbp, -16",
// 按照传统,我们将基指针设置为栈指针的值。
// 这样,基指针在整个函数体中保持不变。
"mov rbp, rsp",
// 现在我们可以通过基指针来跟踪 CFA 的偏移量。
// 这意味着在结束之前我们不需要做进一步调整,因为我们不会改变 `rbp`。
".cfi_def_cfa_register rbp",
// 现在我们可以调用一个可能 panic 的函数。
"call {f}",
// 返回时,我们恢复 `rbp` 为自身返回做准备。
"pop rbp",
// 既然我们已经恢复了 `rbp`,必须根据 `rsp` 重新指定 CFA 的偏移量。
".cfi_def_cfa rsp, 8",
// 现在我们可以返回了。
"ret",
".cfi_endproc",
f = sym may_panic,
)
}
extern "sysv64-unwind" fn may_panic() {
panic!("unwind");
}
}
}
Note
有关上述
cfi汇编器指令的更多信息,请参阅以下资源:
正确性与有效性
除上述所有规则外,传递给 asm! 的字符串参数最终必须—在所有其他参数被求值、格式化被执行且操作数被翻译之后—成为对目标架构既语法正确又语义有效的汇编代码。格式化规则允许编译器生成语法正确的汇编代码。有关操作数的规则允许将 Rust 操作数有效地翻译进出汇编代码。遵守这些规则是必要的,但不足以使最终展开的汇编代码既正确又有效。例如:
- 参数可能被放置在格式化后语法上不正确的位置
- 一条指令可能被正确编写,但被赋予了在架构上无效的操作数
- 一条架构上未指定的指令可能被汇编成未定义的代码
- 一组各自正确且有效的指令,如果被紧接放置可能会引起未定义行为
因此,这些规则是非穷尽的。编译器不需要检查初始字符串的正确性和有效性,也不需要检查最终生成的汇编的正确性和有效性。汇编器可能检查正确性和有效性,但不要求这样做。在使用 asm! 时,一个排版错误可能就足以使程序不健全,而汇编的规则可能包含数千页的架构参考手册。程序员应谨慎行事,因为调用此 unsafe 功能意味着承担不违反编译器或架构规则的责任。
指令支持
内联汇编支持 GNU AS 和 LLVM 内部汇编器都支持的指令子集,如下所示。使用其他指令的结果是特定于汇编器的(可能引起错误,也可能按原样接受)。
如果内联汇编包含任何修改后续汇编处理方式的“有状态“指令,则汇编代码必须在内联汇编结束之前撤销这些指令的影响。
以下指令保证受汇编器支持:
.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.insn.lcomm.long.octa.option.p2align.popsection.private_extern.pushsection.quad.scl.section.set.short.size.skip.sleb128.space.string.text.type.uleb128.word
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let bytes: *const u8;
let len: usize;
unsafe {
core::arch::asm!(
"jmp 3f", "2: .ascii \"Hello World!\"",
"3: lea {bytes}, [2b+rip]",
"mov {len}, 12",
bytes = out(reg) bytes,
len = out(reg) len
);
}
let s = unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(bytes, len)) };
assert_eq!(s, "Hello World!");
}
}
目标特定指令支持
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
结构化异常处理
在具有结构化异常处理的目标上,以下附加指令保证受支持:
.seh_endproc.seh_endprologue.seh_proc.seh_pushreg.seh_savereg.seh_setframe.seh_stackalloc
x86(32 位和 64 位)
在 x86 目标(32 位和 64 位)上,以下附加指令保证受支持:
.nops.code16.code32.code64
仅当在退出汇编代码之前将状态重置为默认值时,才支持使用 .code16、.code32 和 .code64 指令。32 位 x86 默认使用 .code32,x86_64 默认使用 .code64。
ARM(32 位)
在 ARM 上,以下附加指令保证受支持:
.even.fnstart.fnend.save.movsp.code.thumb.thumb_func
不安全操作
不安全操作是指那些可能违反 Rust 静态语义的内存安全保证的操作。
以下语言级特性不能在 Rust 的安全子集中使用:
- 解引用裸指针。
- 访问
union的字段,除非是为了赋值。
- 调用不安全的函数。
- 从一个没有启用相应
target_feature特性的函数中,调用带有target_feature标记的安全函数(参见 attributes.codegen.target_feature.safety-restrictions)。
- 实现不安全 trait。
- 对项应用不安全属性。
-
在 2024 版之前,
extern块允许不带unsafe声明。 ↩
unsafe 关键字
unsafe 关键字用于创建或解除证明某事物是安全的义务。具体来说:
- 它用于标记那些定义了在别处必须遵守的额外安全条件的代码。
- 这包括
unsafe fn、unsafe static和unsafe trait。
- 这包括
- 它用于标记程序员断言满足别处所定义安全条件的代码。
- 这包括
unsafe {}、unsafe impl、不带unsafe_op_in_unsafe_fn的unsafe fn、unsafe extern和#[unsafe(attr)]。
- 这包括
下文将逐一讨论这些情况。 请参阅关键字文档了解一些示例说明。
unsafe 关键字可以出现在以下几种不同的上下文中:
- 不安全函数(
unsafe fn) - 不安全块(
unsafe {}) - 不安全 trait(
unsafe trait) - 不安全 trait 实现(
unsafe impl) - 不安全外部块(
unsafe extern) - 不安全外部静态变量(
unsafe static) - 不安全属性(
#[unsafe(attr)])
不安全函数(unsafe fn)
不安全函数是指并非在所有上下文和/或所有可能输入下都安全的函数。
我们说它们具有额外安全条件,即所有调用者必须满足、而编译器不做检查的要求。
例如,get_unchecked 的额外安全条件是索引必须在边界内。
不安全函数应附带文档说明这些额外安全条件是什么。
此类函数必须以 unsafe 关键字为前缀,并且只能在 unsafe 块内调用,或者在未启用 unsafe_op_in_unsafe_fn lint 的情况下,在 unsafe fn 内部调用。
不安全块(unsafe {})
代码块可以用 unsafe 关键字作为前缀,以允许使用不安全操作章节中定义的那些不安全动作,例如调用其他不安全函数或解引用裸指针。
默认情况下,不安全函数体也被视为一个不安全块;
可以通过启用 unsafe_op_in_unsafe_fn lint 来改变此行为。
通过将操作放入不安全块中,程序员声明他们已经处理好满足该块内所有操作的额外安全条件。
不安全块是不安全函数的逻辑对偶:
不安全函数定义了调用者必须履行的证明义务,而不安全块则声明块内调用的所有函数或操作的相关证明义务已被解除。
解除证明义务有多种方式;
例如,可以有运行时检查或数据结构不变量来保证某些性质确实成立,或者不安全块可以位于 unsafe fn 内部,此时该块可以使用该函数的证明义务来解除块内产生的证明义务。
不安全块用于包装外部库、直接使用硬件或实现语言中不直接存在的特性。 例如,Rust 提供了在语言中实现内存安全并发所需的语言特性,但标准库中线程和消息传递的实现使用了不安全块。
Rust 的类型系统是对动态安全需求的保守近似,因此在某些情况下使用安全代码会有性能代价。
例如,双向链表不是树形结构,在安全代码中只能用引用计数指针来表示。
通过使用 unsafe 块将反向链接表示为裸指针,可以在不使用引用计数的情况下实现。
(请参阅“Learn Rust With Entirely Too Many Linked Lists” 来深入探索这个具体示例。)
不安全 trait(unsafe trait)
不安全 trait 是带有trait 实现者必须遵守的额外安全条件的 trait。 不安全 trait 应附带文档说明这些额外安全条件是什么。
此类 trait 必须以 unsafe 关键字为前缀,并且只能由 unsafe impl 块来实现。
不安全 trait 实现(unsafe impl)
在实现不安全 trait 时,需要以 unsafe 关键字作为实现的前缀。
通过写作 unsafe impl,程序员声明他们已经处理好满足该 trait 所要求的额外安全条件。
不安全 trait 实现是不安全 trait 的逻辑对偶:不安全 trait 定义了实现者必须履行的证明义务,而不安全实现则声明所有相关证明义务已被解除。
不安全外部块(unsafe extern)
声明外部块的程序员必须确保其中包含的项的签名是正确的。未能做到这一点可能导致未定义行为。通过书写 unsafe extern 来表明此义务已被履行。
2024 Edition differences
在 2024 版之前,
extern块允许不用unsafe修饰。
不安全属性(#[unsafe(attr)])
不安全属性是指具有额外安全条件、使用时必须遵守的属性。编译器无法检查这些条件是否已被满足。为了断言它们已被满足,这些属性必须包裹在 unsafe(..) 中,例如 #[unsafe(no_mangle)]。
被视为未定义的行为
如果 Rust 代码表现出以下列表中的任何行为,则该代码是错误的。这包括 unsafe 块和 unsafe 函数中的代码。unsafe 仅仅意味着避免未定义行为是程序员的责任;它不改变 Rust 程序绝不能导致未定义行为这一事实。
在编写 unsafe 代码时,程序员有责任确保任何与该 unsafe 代码交互的安全代码不会触发这些行为。对于任何安全客户端都能满足这一属性的 unsafe 代码称为健全的(sound);如果 unsafe 代码可能被安全代码误用而表现出未定义行为,则称为不健全的(unsound)。
Warning
以下列表并非详尽无遗;它可能会增加或减少。对于
unsafe代码中允许和不允许的内容,Rust 没有形式化的语义模型,因此可能有更多行为被视为不安全。我们也保留在未来使该列表中的某些行为变为已定义的权利。换句话说,此列表并不表示任何内容一定在所有未来的 Rust 版本中都始终是未定义的(但我们可能在未来对某些列表项做出此类承诺)。在编写
unsafe代码之前,请阅读 Rustonomicon。
- 数据竞争。
- 执行违反边界内指针算术要求的位置投影。位置投影是字段表达式、元组索引表达式或数组/切片索引表达式。
-
违反指针别名规则。确切的别名规则尚未确定,但以下是一般原则的概述:
&T必须指向在其存活期间不被修改的内存(除了UnsafeCell<U>内部的数据), 而&mut T必须指向在其存活期间不被任何非派生自该引用的指针读取或写入的内存,并且在此期间没有其他引用指向该内存。 就这些规则而言,Box<T>被视作类似于&'static mut T。 确切的存活时长未指定,但存在一些界限:- 对于引用,存活时长以借用检查器分配的语法生命周期为上界;它不能比该生命周期存活更长时间。
- 每次引用或 box 被解引用或重新借用时,它被视为存活。
- 每次引用或 box 被传递到函数或从函数返回时,它被视为存活。
- 当引用(但不是
Box!)被传递到函数时,它至少在该函数调用期间是存活的,除非&T包含UnsafeCell<U>则再次例外。
当这些类型的值以复合类型的(嵌套)字段形式传递时,所有这些也适用,但通过指针间接传递时则不适用。
-
修改不可变字节。 通过常量提升表达式可达的所有字节都是不可变的,以及在
static和const初始化器中已被生命周期延长到'static的借用可达的字节也是不可变的。 不可变绑定或不可变static所拥有的字节是不可变的,除非这些字节属于UnsafeCell<U>的一部分。此外,共享引用指向的字节,包括通过其他引用(共享的和可变的)和
Box的传递,都是不可变的;传递性包括存储在复合类型字段中的那些引用。变更是任何写入超过 0 个字节且与任何相关字节重叠的操作(即使该写入不改变内存内容)。
- 通过编译器内部函数调用未定义行为。
- 执行使用了当前平台不支持的平台特性编译的代码(参见
target_feature),除非平台明确文档说明这是安全的。
- 使用错误的调用 ABI 调用函数,或者展开(unwind)越过不允许展开的栈帧(例如,调用了一个以
"C"函数或函数指针形式导入或 transmute 的"C-unwind"函数)。
- 产生无效值。“产生“一个值发生在任何将值赋值给位置、从位置读取值、将值传递给函数/原始操作或从函数/原始操作返回值的时候。
- 错误地使用内联汇编。更多细节请参考编写使用内联汇编的代码时应遵循的规则。
- 违反 Rust 运行时的假设。Rust 运行时的大多数假设目前没有明确文档化。
- 有关与展开(unwinding)特别相关的假设,请参见 panic 文档。
- 运行时假设 Rust 栈帧在未执行该栈帧拥有的局部变量的析构函数的情况下不会被释放。此假设可能被 C 函数如
longjmp违反。
Note
未定义行为影响整个程序。例如,调用 C 中表现出 C 的未定义行为的函数意味着你的整个程序包含未定义行为,这也会影响 Rust 代码。反之亦然,Rust 中的未定义行为可能对通过任何 FFI 调用其他语言执行的代码产生不利影响。
指向的字节
指针或引用“指向“的字节范围由指针值和指向对象类型的大小(使用 size_of_val)确定。
基于未对齐指针的位置
如果一个位置在位置计算期间最后一次 * 投影是在一个对其类型未对齐的指针上执行的,则称该位置“基于未对齐指针“。(如果位置表达式中没有 * 投影,那么访问的是局部变量或 static 的字段,rustc 将保证正确的对齐。如果存在多个 * 投影,则每个投影都会从内存中加载待解引用的指针本身,每个这样的加载都受对齐约束。请注意,由于自动解引用,一些 * 投影可能在表面 Rust 语法中被省略;我们在此考虑的是完全展开的位置表达式。)
例如,如果 ptr 具有类型 *const S,其中 S 的对齐为 8,则 ptr 必须 8 对齐,否则 (*ptr).f 就是“基于未对齐指针“。即使字段 f 的类型是 u8(即对齐为 1 的类型),这也成立。换句话说,对齐要求源于被解引用的指针的类型,而不是被访问字段的类型。
请注意,基于未对齐指针的位置仅在对其进行加载或存储时才会导致未定义行为。
允许对这种位置使用 &raw const/&raw mut。
对位置使用 &/&mut 要求字段类型的对齐(否则程序将“产生无效值“),这通常比基于对齐指针的要求更宽松。
在字段类型可能比包含它的类型更严格对齐的情况下,即 repr(packed),获取引用将导致编译器错误。这意味着基于对齐指针总是足以确保新引用是对齐的,但不总是必需的。
悬垂指针
如果引用/指针所指向的字节并非全部属于同一个存活分配(因此特别是它们必须全部属于某个分配),则该引用/指针是“悬垂的“。
如果大小为零,则指针平凡地永远不会“悬垂“(即使它是空指针)。
请注意,动态大小类型(如切片和字符串)指向它们的整个范围,因此长度元数据绝不能太大是重要的。
特别是,Rust 值的动态大小(由 size_of_val 确定)绝不能超过 isize::MAX,因为单个分配不可能大于 isize::MAX。
无效值
Rust 编译器假设程序执行期间产生的所有值都是“有效的“,因此产生无效值即构成即时 UB。
一个值是否有效取决于类型:
bool值必须是false(0)或true(1)。
fn指针值必须非空。
char值不能是代理项(surrogate)(即不能位于0xD800..=0xDFFF范围内)且必须等于或小于char::MAX。
!值绝不能存在。
- 整数(
i*/u*)、浮点值(f*)或原始指针必须已初始化,即不能从未初始化内存获取。
str值被视为[u8],即必须已初始化。
enum必须具有有效的判别值,且该判别值所示的变体的所有字段在其各自类型下必须有效。
struct、元组和数组要求所有字段/元素在其各自类型下有效。
- 宽引用、
Box<T>或原始指针的元数据必须与无大小尾部的类型匹配:dyn Trait元数据必须是指向编译器生成的Trait虚表的指针。 (对于原始指针,此要求仍存在一些争议。)- 切片(
[T])元数据必须是有效的usize。 此外,对于宽引用和Box<T>,如果切片元数据使得指向值总大小大于isize::MAX,则是无效的。
-
如果一个类型有自定义的有效值范围,那么有效值必须在该范围内。 在标准库中,这影响
NonNull<T>和NonZero<T>。Note
rustc通过不稳定的rustc_layout_scalar_valid_range_*属性实现这一点。
-
在 const 上下文 中:除了上述内容外,在常量求值期间还适用与来源(provenance)相关的进一步要求。任何持有纯整数数据的值(
i*/u*/f*类型以及bool和char、枚举判别值和切片元数据)不得携带任何来源。任何持有指针数据的值(引用、原始指针、函数指针和dyn Trait元数据)必须要么不携带来源,要么所有字节必须是同一原始指针值的片段且顺序正确。这意味着将指针(引用、原始指针或函数指针)transmute 或以其他方式重新解释为非指针类型(如整数)是未定义行为,如果该指针具有来源的话。
Example
以下所有情况都是 UB:
#![allow(unused)] fn main() { use core::mem::MaybeUninit; use core::ptr; // 我们不能将有来源的指针重新解释为整数, // 因为这样整数的字节将具有来源。 const _: usize = { let ptr = &0; unsafe { (&raw const ptr as *const usize).read() } }; // 我们不能重新排列有来源的指针的字节, // 然后将它们解释为引用,因为这样的话持有指针数据的值 // 将具有顺序错误的指针片段。 const _: &i32 = { let mut ptr = &0; let ptr_bytes = &raw mut ptr as *mut MaybeUninit::<u8>; unsafe { ptr::swap(ptr_bytes.add(1), ptr_bytes.add(2)) }; ptr }; }
注意: 未初始化内存对于任何具有受限有效值集合的类型也是隐式无效的。换句话说,允许读取未初始化内存的唯一情况是在 union 内部和“填充“(类型的字段之间的间隙)中。
不被视为 unsafe 的行为
Rust 编译器不会将以下行为视为不安全,但程序员可能(应该)认为它们是不受欢迎的、意外的或错误的。
- 死锁
- 内存及其他资源的泄漏
- 退出时不调用析构函数
- 通过指针泄漏暴露随机基址
整数溢出
如果程序包含算术溢出,则程序员犯了错误。在下面的讨论中,我们区分算术溢出和回绕算术。前者是错误的,后者是有意的。
当程序员启用了 debug_assert! 断言(例如,通过启用非优化构建),实现必须插入动态检查,在溢出时 panic。其他类型的构建可能根据实现的判定,在溢出时产生 panic 或静默回绕值。
对于隐式回绕溢出的情况,实现必须使用二进制补码溢出约定来提供良好定义(即使仍被认为错误)的结果。
整数类型提供了内置方法,允许程序员显式执行回绕算术。例如,i32::wrapping_add 提供了二进制补码的回绕加法。
标准库还提供了 Wrapping<T> 新型类型,确保 T 的所有标准算术运算都具有回绕语义。
有关错误条件、理由和整数溢出的更多细节,请参见 RFC 560。
逻辑错误
安全代码可能施加额外的逻辑约束,这些约束既不能在编译时也不能在运行时检查。如果程序违反了此类约束,其行为可能是未指定的,但不会导致未定义行为。这可能包括 panic、错误结果、中止和非终止。行为也可能因运行、构建或构建类型的不同而有所差异。
例如,同时实现 Hash 和 Eq 要求被视为相等的值具有相等的哈希值。另一个例子是像 BinaryHeap、BTreeMap、BTreeSet、HashMap 和 HashSet 这样的数据结构,它们描述了当键位于数据结构中时对键修改的约束。违反此类约束不被视为不安全,但程序被认为是有错误的,其行为不可预测。
常量求值
常量求值是在编译期间计算表达式结果的过程。只有所有表达式的一个子集可以在编译时求值。
常量表达式
某些形式的表达式(称为常量表达式)可以在编译时求值。
const 上下文中的表达式必须是常量表达式。
const 上下文中的表达式始终在编译时求值。
在 const 上下文之外,常量表达式可能在编译时求值,但不保证。
如果值必须在编译时求值(即在 const 上下文中),诸如越界数组索引或溢出等行为是编译器错误。否则,这些行为是警告,但在运行时可能会导致 panic。
以下表达式是常量表达式,只要任何操作数也是常量表达式,并且不会导致运行任何 Drop::drop 调用。
- 字面量。
- 常量参数。
-
指向静态项的路径,有以下限制:
- 在任何常量求值上下文中不允许写入
static项。 - 在任何常量求值上下文中不允许读取
extern静态项。 - 如果求值不在
static项的初始化器中进行,则不允许读取任何可变static。可变static是static mut项,或具有内部可变类型的static项。
这些要求仅在常量被求值时检查。换句话说,在 const 上下文中语法上存在此类访问是允许的,只要它们从未被执行。
- 在任何常量求值上下文中不允许写入
- 数组和切片索引表达式,其中索引是
usize。
- 不从环境中捕获变量的闭包表达式。
-
所有形式的借用,包括原始借用,除了那些临时作用域会被延长到程序结束(见临时生命周期延长)的表达式的借用,并且这些表达式要么是:
- 可变借用。
- 对产生具有内部可变性值的表达式的共享借用。
#![allow(unused)] fn main() { // 由于处于尾部位置,此借用将临时值的作用域延长到程序结束。 // 由于借用是可变的,这在 const 表达式中不允许。 const C: &u8 = &mut 0; // 错误,不允许 }#![allow(unused)] fn main() { // Const 块类似于 `const` 项的初始化器。 let _: &u8 = const { &mut 0 }; // 错误,不允许 }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 这是不允许的,因为 1) 临时作用域被延长到程序结束, // 且 2) 临时值具有内部可变性。 const C: &AtomicU8 = &AtomicU8::new(0); // 错误,不允许 }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 同上。 let _: &_ = const { &AtomicU8::new(0) }; // 错误,不允许 }#![allow(unused)] fn main() { #![allow(static_mut_refs)] // 即使此借用是可变的,它也不是临时值的借用,因此允许。 const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; // 正确 }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 即使此借用是对具有内部可变性值的借用, // 它不是临时值的借用,因此允许。 const C: &AtomicU8 = { static S: AtomicU8 = AtomicU8::new(0); &S // 正确 }; }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 此对内部可变临时值的共享借用是允许的, // 因为其作用域未被延长。 const C: () = { _ = &AtomicU8::new(0); }; // 正确 }#![allow(unused)] fn main() { // 即使借用是可变的且临时值由于提升而存活到程序结束,这也是允许的, // 因为借用不在尾部位置,因此临时值的作用域 // 不会通过临时生命周期延长而扩展。 const C: () = { let _: &'static mut [u8] = &mut []; }; // 正确 // ~~ // 已提升的临时值。 }Note
换句话说——关注允许什么而不是不允许什么——对内部可变数据的共享借用和可变借用仅在 const 上下文中被借用的位置表达式是瞬态的、间接的或静态的时才允许。
如果位置表达式是当前 const 上下文局部的变量或其临时作用域包含在当前 const 上下文内的表达式,则该位置表达式是瞬态的。
#![allow(unused)] fn main() { // 借用是对初始化器局部变量的借用,因此此位置表达式是瞬态的。 const C: () = { let mut x = 0; _ = &mut x; }; }#![allow(unused)] fn main() { // 借用是对其作用域未被延长的临时值的借用,因此此位置表达式是瞬态的。 const C: () = { _ = &mut 0u8; }; }#![allow(unused)] fn main() { // 当临时值被提升但生命周期未被延长时,其位置表达式仍被视为瞬态的。 const C: () = { let _: &'static mut [u8] = &mut []; }; }如果位置表达式是解引用表达式,则该位置表达式是间接的。
#![allow(unused)] fn main() { const C: () = { _ = &mut *(&mut 0); }; }如果位置表达式是
static项,则该位置表达式是静态的。#![allow(unused)] fn main() { #![allow(static_mut_refs)] const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; }Note
这些规则的一个令人惊讶的后果是我们允许这种写法,
#![allow(unused)] fn main() { const C: &[u8] = { let x: &mut [u8] = &mut []; x }; // 正确 // ~~~~~~~ // 空数组即使在可变借用之后也会被提升。 }但我们不允许这段类似的代码:
#![allow(unused)] fn main() { const C: &[u8] = &mut []; // 错误 // ~~~~~~~ // 尾部表达式。 }这两者之间的区别在于,在第一种情况下,空数组被提升但其作用域不经历临时生命周期延长,因此我们认为位置表达式是瞬态的(即使在提升之后该位置确实存活到程序结束)。在第二种情况下,空数组临时值的作用域确实经历了生命周期延长,因此由于是对生命周期延长临时值的可变借用而被拒绝(因此借用了非瞬态位置表达式)。
这种效果令人惊讶,因为在这种情况下,临时生命周期延长导致比没有它更少的代码可以编译。
更多细节请参见 issue #143129。
-
#![allow(unused)] fn main() { use core::cell::UnsafeCell; const _: u8 = unsafe { let x: *mut u8 = &raw mut *&mut 0; // ^^^^^^^ // 可变引用的解引用。 *x = 1; // 可变指针的解引用。 *(x as *const u8) // 常量指针的解引用。 }; const _: u8 = unsafe { let x = &UnsafeCell::new(0); *x.get() = 1; // 内部可变值的修改。 *x.get() }; }
- 分组表达式。
- 强制转换表达式,除了
- 指针到地址的强制转换和
- 函数指针到地址的强制转换。
- const 函数和 const 方法的调用。
Const 上下文
const 上下文是以下之一:
数组类型长度表达式、数组重复长度表达式和 const 泛型参数在使用外部泛型参数时受到限制:此类表达式必须是单个 const 泛型参数,或者是不引用任何泛型参数的表达式。
Const 函数
const 函数是可以从 const 上下文调用的函数。它使用 const 限定符定义,也包括元组结构体和元组枚举变体构造函数。
Example
#![allow(unused)] fn main() { const fn square(x: i32) -> i32 { x * x } const VALUE: i32 = square(12); }
从 const 上下文调用时,const 函数由编译器在编译时解释。解释发生在编译目标的环境中,而不是主机环境中。因此,如果你针对 32 位系统编译,usize 是 32 位,无论你是在 64 位还是 32 位系统上构建。
当 const 函数在 const 上下文之外调用时,其行为与没有 const 限定符时相同。
const 函数的主体只能使用常量表达式。
const 函数不允许是 async。
const 函数的参数和返回类型的类型限制为与 const 上下文兼容的类型。
应用程序二进制接口(ABI)
本节记录了影响 crate 编译输出的 ABI 的特性。
相关信息请参阅 extern 函数 以了解如何指定导出函数的 ABI,参阅 外部块 以了解如何指定链接外部库的 ABI。
used 属性
used 属性 强制将一个 static 保留在输出目标文件(.o、.rlib 等,但不包括最终二进制文件)中,即使它在 crate 中从未被其他程序项使用或引用。不过,链接器仍然可以自由地移除它。
Example
#![allow(unused)] fn main() { // lib.rs // 因为有 `#[used]`,此静态项被保留。 #[used] static S1: u8 = 0; // 因为未使用,此静态项可被移除。 #[allow(dead_code)] static S2: u8 = 0; // 因为可被公开访问,此静态项被保留。 pub static S3: u8 = 0; // 因为被一个可公开访问的函数引用,此静态项被保留。 static S4: u8 = 0; #[unsafe(no_mangle)] pub fn f4() -> &'static u8 { &S4 } // 因为只被一个私有的、未使用的(死)函数引用,此静态项可被移除。 static S5: u8 = 0; #[allow(dead_code)] fn f5() -> &'static u8 { &S5 } }$ rustc -O --emit=obj --crate-type=rlib lib.rs $ LC_ALL=C nm -C lib.o 0000000000000000 R lib::S1 0000000000000000 R lib::S3 0000000000000000 r lib::S4 0000000000000000 T f4
used 属性使用 MetaWord 语法。
used 属性只能应用于 static 程序项。
只有程序项上的第一次 used 使用才有效。
Note
rustc会对第一次之后的任何使用给出 lint 警告。
no_mangle 属性
no_mangle 属性可用于任何程序项,以禁用在符号名称上应用标准的名称修饰。该程序项的符号将是该程序项名称的标识符。
此外,该程序项将从生成的库或目标文件中公开导出,类似于 used 属性。
此属性是不安全的,因为未修饰的符号可能与另一个同名符号(或已知符号)冲突,导致未定义行为。
#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
extern "C" fn foo() {}
}
2024 Edition differences
在 2024 版之前,允许在不使用
unsafe限定的情况下使用no_mangle属性。
link_section 属性
link_section 属性指定将函数或 static 的内容放入目标文件的哪个节中。
link_section 属性使用 MetaNameValueStr 语法来指定节名称。
#![allow(unused)]
fn main() {
#[cfg(target_os = "linux")] {
#[unsafe(no_mangle)]
#[unsafe(link_section = ".example_section")]
pub static VAR1: u32 = 1;
}
}
此属性是不安全的,因为它允许用户将数据和代码放入未预期它们的内存节中,例如将可变数据放入只读区域。
只有程序项上的第一次 link_section 使用才有效。
Note
rustc会对第一次之后的任何使用给出未来兼容性警告。这在未来可能成为错误。
2024 Edition differences
在 2024 版之前,允许在不使用
unsafe限定的情况下使用link_section属性。
export_name 属性
export_name 属性指定将在函数或 static 上导出的符号名称。
export_name 属性使用 MetaNameValueStr 语法来指定符号名称。
#![allow(unused)]
fn main() {
#[unsafe(export_name = "exported_symbol_name")]
pub fn name_in_rust() { }
}
此属性是不安全的,因为具有自定义名称的符号可能与另一个同名符号(或已知符号)冲突,导致未定义行为。
只有程序项上的第一次 export_name 使用才有效。
Note
rustc会对第一次之后的任何使用给出未来兼容性警告。这在未来可能成为错误。
2024 Edition differences
在 2024 版之前,允许在不使用
unsafe限定的情况下使用export_name属性。
Rust 运行时
本节记录了定义 Rust 运行时某些方面的特性。
global_allocator 属性
global_allocator 属性 选择一个内存分配器。
Example
#![allow(unused)] fn main() { use core::alloc::{GlobalAlloc, Layout}; use std::alloc::System; struct MyAllocator; unsafe impl GlobalAlloc for MyAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { unsafe { System.alloc(layout) } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { System.dealloc(ptr, layout) } } } #[global_allocator] static GLOBAL: MyAllocator = MyAllocator; }
global_allocator 属性使用 MetaWord 语法。
global_allocator 属性只能应用于类型实现了 GlobalAlloc trait 的静态项。
global_allocator 属性在一个项上只能使用一次。
global_allocator 属性在 crate 图中只能使用一次。
global_allocator 属性从标准库预导入中导出。
windows_subsystem 属性
windows_subsystem 属性 在 Windows 目标上链接时设置子系统。
Example
#![allow(unused)] #![windows_subsystem = "windows"] fn main() { }
windows_subsystem 属性使用 MetaNameValueStr 语法。可接受的值为 "console" 和 "windows"。
windows_subsystem 属性只能应用于 crate 根。
只有第一次使用 windows_subsystem 才有效。
Note
rustc会对第一次之后的使用发出 lint 警告。这可能在将来成为错误。
windows_subsystem 属性在非 Windows 目标和非 bin crate 类型上被忽略。
"console" 子系统是默认的。如果控制台进程从现有控制台运行,它将附加到该控制台;否则将创建一个新的控制台窗口。
"windows" 子系统将与任何现有控制台分离运行。
Note
"windows"子系统通常由不想在启动时显示控制台窗口的 GUI 应用程序使用。
附录
语法摘要
以下是语法产生规则的摘要。有关此语法语法的详细信息,请参见 notation.grammar.syntax。
{{ grammar-summary }}
语法索引
本附录提供了 token 和常见形式的索引,带有指向定义这些元素位置的链接。
关键字
运算符和标点符号
| 符号 | 名称 | 使用 |
|---|---|---|
+ | 加号 | 加法、trait 约束、宏 Kleene 匹配器 |
- | 减号 | 减法、取反 |
* | 星号 | 乘法、解引用、原始指针、宏 Kleene 匹配器、glob 导入 |
/ | 斜线 | 除法 |
% | 百分号 | 取余 |
^ | 脱字符 | 按位与逻辑 XOR |
! | 非 | 按位与逻辑 NOT、宏调用、内部属性、never 类型、否定 impl |
& | 与 | 按位与逻辑 AND、借用、引用、引用模式 |
| | 或 | 按位与逻辑 OR、闭包、或模式、if let、while let |
&& | 与与 | 惰性 AND、借用、引用、引用模式 |
|| | 或或 | 惰性 OR、闭包 |
<< | 左移 | 左移、嵌套泛型 |
>> | 右移 | 右移、嵌套泛型 |
+= | 加等 | 加法赋值 |
-= | 减等 | 减法赋值 |
*= | 星等 | 乘法赋值 |
/= | 斜等 | 除法赋值 |
%= | 百分等 | 取余赋值 |
^= | 脱字符等 | 按位 XOR 赋值 |
&= | 与等 | 按位 AND 赋值 |
|= | 或等 | 按位 OR 赋值 |
<<= | 左移等 | 左移赋值 |
>>= | 右移等 | 右移赋值、嵌套泛型 |
= | 等 | 赋值、let 语句、属性、各种类型定义 |
== | 等等 | 相等 |
!= | 不等 | 不等于 |
> | 大于 | 大于、泛型、路径、use 约束 |
< | 小于 | 小于、泛型、路径、use 约束 |
>= | 大于等于 | 大于或等于、泛型 |
<= | 小于等于 | 小于或等于 |
@ | At | 子模式绑定 |
. | 点 | 字段访问、元组索引、方法调用、await 表达式 |
.. | 点点 | 范围表达式、结构体表达式、rest 模式、范围模式、结构体模式 |
... | 点点点 | 可变参数函数、范围模式 |
..= | 点点等 | 包含范围表达式、范围模式 |
, | 逗号 | 各种分隔符 |
; | 分号 | 各种项和语句的终止符、数组表达式、数组类型 |
: | 冒号 | 各种分隔符 |
:: | 路径分隔符 | 路径分隔符 |
-> | 右箭头 | 函数、闭包、函数指针类型 |
=> | 胖箭头 | match 分支、宏 |
<- | 左箭头 | 左箭头符号自 Rust 1.0 之前就一直未使用,但仍被视为单个 token。 |
# | 井号 | 属性、原始字符串字面量、原始字节字符串字面量、原始 C 字符串字面量 |
$ | 美元 | 宏 |
? | 问号 | try 传播表达式、放宽 trait 约束、宏 Kleene 匹配器 |
~ | 波浪号 | 波浪号运算符自 Rust 1.0 之前就一直未使用,但其 token 仍可使用。 |
注释
其他 token
| Token | 使用 |
|---|---|
ident | 标识符 |
r#ident | 原始标识符 |
'ident | 生命周期和循环标签 |
'r#ident | 原始生命周期和循环标签 |
…u8、…i32、…f64、…usize、… | 数字字面量 |
"…" | 字符串字面量 |
r"…"、r#"…"#、r##"…"##、… | 原始字符串字面量 |
b"…" | 字节字符串字面量 |
br"…"、br#"…"#、br##"…"##、… | 原始字节字符串字面量 |
'…' | 字符字面量 |
b'…' | 字节字面量 |
c"…" | C 字符串字面量 |
cr"…"、cr#"…"#、cr##"…"##、… | 原始 C 字符串字面量 |
宏
属性
表达式
| 表达式 | 使用 |
|---|---|
|…| expr|…| -> Type { … } | 闭包 |
ident::… | 路径 |
::crate_name::… | 显式 crate 路径 |
crate::… | crate 相对路径 |
self::… | 模块相对路径 |
super::… | 父模块路径 |
Type::…<Type as Trait>::ident | 关联项 |
<Type>::… | 限定路径,可用于没有名称的类型,如 <&T>::…、<[T]>::… 等。 |
Trait::method(…)Type::method(…)<Type as Trait>::method(…) | 消歧方法调用 |
method::<…>(…)path::<…> | 泛型参数,又名 turbofish |
() | 单元 |
(expr) | 括号表达式 |
(expr,) | 单元素元组表达式 |
(expr, …) | 元组表达式 |
expr(expr, …) | 调用表达式 |
expr.0、expr.1、… | 元组索引表达式 |
expr.ident | 字段访问表达式 |
{…} | 块表达式 |
Type {…} | 结构体表达式 |
Type(…) | 元组结构体构造函数 |
[…] | 数组表达式 |
[expr; len] | 重复数组表达式 |
expr[..]、expr[a..]、expr[..b]、expr[a..b]、expr[a..=b]、expr[..=b] | 数组和切片索引表达式 |
if expr {…} else {…} | if 表达式 |
match expr { pattern => {…} } | match 表达式 |
loop {…} | 无限循环表达式 |
while expr {…} | 谓词循环表达式 |
for pattern in expr {…} | 迭代器循环 |
&expr&mut expr | 借用表达式 |
&raw const expr&raw mut expr | 原始借用表达式 |
*expr | 解引用表达式 |
expr? | try 传播表达式 |
-expr | 取反表达式 |
!expr | 按位与逻辑 NOT 表达式 |
expr as Type | 类型强制转换表达式 |
项
程序项 是 crate 的组成部分。
| 项 | 使用 |
|---|---|
mod ident;mod ident {…} | 模块 |
use path; | use 声明 |
fn ident(…) {…} | 函数 |
type Type = Type; | 类型别名 |
struct ident {…} | 结构体 |
enum ident {…} | 枚举 |
union ident {…} | 联合体 |
trait ident {…} | trait |
impl Type {…}impl Type for Trait {…} | 实现 |
const ident = expr; | 常量项 |
static ident = expr; | 静态项 |
extern "C" {…} | 外部块 |
fn ident<…>(…) …struct ident<…> {…}enum ident<…> {…}impl<…> Type<…> {…} | 泛型定义 |
类型表达式
类型表达式 用于引用类型。
| 类型 | 使用 |
|---|---|
bool、u8、f64、str、… | 原始类型 |
for<…> | 高阶 trait 约束 |
T: TraitA + TraitB | trait 约束 |
T: 'a + 'b | 生命周期约束 |
T: TraitA + 'a | trait 和生命周期约束 |
T: ?Sized | 放宽 trait 约束 |
[Type; len] | 数组类型 |
(Type, …) | 元组类型 |
[Type] | 切片类型 |
(Type) | 括号类型 |
impl Trait | impl trait 类型、匿名类型参数 |
dyn Trait | trait 对象类型 |
identident::… | 类型路径(可以引用结构体、枚举、联合体、类型别名、trait、泛型等) |
Type<…>Trait<…> | 泛型参数(例如 Vec<u8>) |
Trait<ident = Type> | 关联类型绑定(例如 Iterator<Item = T>) |
Trait<ident: …> | 关联类型约束(例如 Iterator<Item: Send>) |
&Type&mut Type | 引用类型 |
*mut Type*const Type | 原始指针类型 |
fn(…) -> Type | 函数指针类型 |
_ | 推断类型、推断常量 |
'_ | 占位生命周期 |
! | never 类型 |
模式
模式 用于匹配值。
| 模式 | 使用 |
|---|---|
"foo"、'a'、123、2.4、… | 字面量模式 |
ident | 标识符模式 |
_ | 通配符模式 |
.. | rest 模式 |
a..、..b、a..b、a..=b、..=b | 范围模式 |
&pattern&mut pattern | 引用模式 |
path {…} | 结构体模式 |
path(…) | 元组结构体模式 |
(pattern, …) | 元组模式 |
(pattern) | 分组模式 |
[pattern, …] | 切片模式 |
CONST、Enum::Variant、… | 路径模式 |
附录:宏跟随集歧义形式规范
本页记录了声明宏的跟随规则的形式规范。它们最初在 RFC 550 中指定,本文大部分内容复制自那里,并在后续 RFC 中扩展。
定义和约定
macro:任何可以在源代码中作为foo!(...)调用的东西。MBE:声明宏,由macro_rules定义的宏。matcher:macro_rules调用中某条规则的左手边,或其子部分。macro parser:Rust 解析器中用于使用从所有匹配器派生的语法解析输入的代码部分。fragment:给定匹配器将接受(或“匹配“)的 Rust 语法类别。repetition:一个遵循常规重复模式的片段NT:非终结符,可以出现在匹配器中的各种“元变量“或重复匹配器,在 MBE 语法中以前导$字符指定。simple NT:一种“元变量“非终结符(下文进一步讨论)。complex NT:一种重复匹配非终结符,通过重复运算符(*、+、?)指定。token:匹配器的原子元素;即标识符、运算符、开/闭分隔符,以及简单 NT。token tree:由 token(叶子)、复杂 NT 和有限序列的 token tree 组成的树结构。delimiter token:一个 token,旨在分隔一个片段的结束和下一个片段的开始。separator token:复杂 NT 中可选的定界 token,分隔匹配重复中每对元素。separated complex NT:具有自己的分隔 token 的复杂 NT。delimited sequence:具有适当的开分隔符和闭分隔符的 token tree 序列。empty fragment:分隔 token 的不可见 Rust 语法类别,即空白,或(在某些词法上下文中)空 token 序列。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) 是一个匹配器。整个匹配器是一个定界序列(具有开分隔符和闭分隔符 ( 和 )),$foo 和 $i 是简单 NT,其片段指示符分别为 expr 和 ident。
$(i:ident),* 也是一个 NT;它是一个复杂 NT,匹配以逗号分隔的标识符重复。, 是该复杂 NT 的分隔 token;它出现在已匹配片段的每对元素(如果有的话)之间。
复杂 NT 的另一个示例是 $(hi $e:expr ;)+,它匹配形式为 hi <expr>; hi <expr>; ... 的任何片段,其中 hi <expr>; 至少出现一次。请注意,此复杂 NT 没有专用的分隔 token。
(请注意,Rust 的解析器确保定界序列始终以适当的 token tree 结构嵌套和正确的开/闭分隔符匹配出现。)
我们通常使用变量 “M” 表示匹配器,变量 “t” 和 “u” 表示任意单独的 token,变量 “tt” 和 “uu” 表示任意 token tree。(使用 “tt” 确实可能与其作为片段指示符的附加角色产生歧义;但从上下文中可以清楚地看出哪种解释是预期的。)
“SEP” 范围在分隔 token 上,“OP” 范围在重复运算符 *、+ 和 ? 上,“OPEN”/“CLOSE” 范围在包围定界序列的匹配 token 对上(例如 [ 和 ])。
希腊字母 “α” “β” “γ” “δ” 表示可能为空的 token-tree 序列。(然而,希腊字母 “ε”(epsilon)在表述中具有特殊角色,不表示 token-tree 序列。)
- 此希腊字母约定通常仅在序列的存在是技术细节时使用;特别是,当我们希望强调正在对 token-tree 序列进行操作时,我们将对序列使用符号 “tt …”,而不是希腊字母。
请注意,匹配器仅仅是一个 token tree。如上所述的“简单 NT“是一个元变量 NT;因此它不是重复。例如,$foo:ty 是一个简单 NT,但 $($foo:ty)+ 是一个复杂 NT。
另请注意,在此形式规范的上下文中,术语“token“通常包括简单 NT。
最后,读者应记住,根据此形式规范的定义,没有简单 NT 匹配空片段,同样没有 token 匹配 Rust 语法的空片段。(因此,唯一可以匹配空片段的 NT 是复杂 NT。)这实际上并非完全正确,因为 vis 匹配器可以匹配空片段。因此,在此形式规范中,我们将 $v:vis 视为实际上是 $($v:vis)?,并要求匹配器匹配空片段。
匹配器不变式
要成为有效的,匹配器必须满足以下三个不变式。FIRST 和 FOLLOW 的定义稍后描述。
- 对于匹配器
M中的任何两个连续的 token tree 序列(即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(如果有的话)必须在预定的跟随集中。这确保了合法的宏定义将继续对 ... tt 的结束位置和 uu ... 的开始位置分配相同的判断,即使新的语法形式被添加到语言中。
第二个不变式表示,带分隔的复杂 NT 必须使用作为 NT 内部内容的预定跟随集一部分的分隔 token。这确保了合法的宏定义将继续将输入片段解析为相同的 tt ... 定界序列,即使新的语法形式被添加到语言中。
第三个不变式表示,当我们有一个可以匹配同一事物的两个或多个副本且中间没有分隔的复杂 NT 时,必须允许它们按照第一个不变式彼此相邻放置。此不变式还要求它们非空,这消除了一种可能的歧义。
注意:由于历史上的疏忽和对该行为的重大依赖,第三个不变式目前未强制执行。目前尚未决定如何处理此事。不遵守该行为的宏可能在 Rust 的未来版次中变为无效。请参见跟踪 issue。
FIRST 和 FOLLOW,非正式地
给定的匹配器 M 映射到三个集合:FIRST(M)、LAST(M) 和 FOLLOW(M)。
三个集合中的每一个都由 token 组成。FIRST(M) 和 LAST(M) 还可能包含一个特殊的非 token 元素 ε(“epsilon”),表示 M 可以匹配空片段。(但 FOLLOW(M) 始终只是 token 的集合。)
非正式地:
- FIRST(M):收集在将片段匹配到 M 时可能首先使用的 token。
- LAST(M):收集在将片段匹配到 M 时可能最后使用的 token。
-
FOLLOW(M):允许紧跟在由 M 匹配的某个片段之后的 token 集合。
换句话说:t ∈ FOLLOW(M) 当且仅当存在(可能为空的)token 序列 α, β, γ, δ,其中:
-
M 匹配 β,
-
t 匹配 γ,且
-
连接 α β γ δ 是一个可解析的 Rust 程序。
-
我们使用简写 ANYTOKEN 表示所有 token(包括简单 NT)的集合。例如,如果任何 token 在匹配器 M 之后都是合法的,那么 FOLLOW(M) = ANYTOKEN。
FIRST、LAST
以下是 FIRST 和 LAST 的形式归纳定义。
“A ∪ B” 表示集合并,“A ∩ B” 表示集合交,“A \ B” 表示集合差(即 A 中不在 B 中的所有元素)。
FIRST
FIRST(M) 通过对序列 M 及其第一个 token-tree(如果有)的结构进行案例分析来定义:
- 如果 M 是空序列,则 FIRST(M) = { ε },
-
如果 M 以 token t 开头,则 FIRST(M) = { t },
(注意:这涵盖了 M 以定界 token-tree 序列开头的情况,
M = OPEN tt ... CLOSE ...,其中t = OPEN,因此 FIRST(M) = {OPEN}。)(注意:这关键依赖于没有简单 NT 匹配空片段的性质。)
-
否则,M 是一个以复杂 NT 开头的 token-tree 序列:
M = $( tt ... ) OP α,或M = $( tt ... ) SEP OP α,(其中α是匹配器其余部分的(可能为空的)token tree 序列)。- 令 SEP_SET(M) = { SEP } 如果 SEP 存在且 ε ∈ FIRST(
tt ...);否则 SEP_SET(M) = {}。
- 令 SEP_SET(M) = { SEP } 如果 SEP 存在且 ε ∈ FIRST(
-
令 ALPHA_SET(M) = FIRST(
α) 如果 OP =*或?,且如果 OP =+则 ALPHA_SET(M) = {}。 -
FIRST(M) = (FIRST(
tt ...) \ {ε}) ∪ SEP_SET(M) ∪ ALPHA_SET(M)。
对复杂 NT 的定义值得一些论证。SEP_SET(M) 定义了分隔符可能是 M 的有效第一个 token 的可能性,这发生在定义了分隔符且重复的片段可能为空时。ALPHA_SET(M) 定义了复杂 NT 可能为空的可能性,这意味着 M 的有效第一个 token 是后续 token-tree 序列 α 的 token。这发生在使用 * 或 ? 时,此时可能有零次重复。理论上,如果使用 + 与可能为空的重复片段,这也可能发生,但第三个不变式禁止了这点。
由此,显然 FIRST(M) 可以包括来自 SEP_SET(M) 或 ALPHA_SET(M) 的任何 token,如果复杂 NT 匹配非空,那么任何开始 FIRST(tt ...) 的 token 也可以工作。最后要考虑的是 ε。SEP_SET(M) 和 FIRST(tt ...) \ {ε} 不能包含 ε,但 ALPHA_SET(M) 可以。因此,此定义允许 M 接受 ε 当且仅当 ε ∈ ALPHA_SET(M)。这是正确的,因为在复杂 NT 情况下,要让 M 接受 ε,复杂 NT 和 α 都必须接受它。如果 OP = +,意味着复杂 NT 不能为空,则根据定义 ε ∉ ALPHA_SET(M)。否则,复杂 NT 可以接受零次重复,然后 ALPHA_SET(M) = FOLLOW(α)。所以此定义相对于 ε 也是正确的。
LAST
LAST(M),通过案例分析 M 本身(一个 token-tree 序列)定义:
- 如果 M 是空序列,则 LAST(M) = { ε }
- 如果 M 是单个 token t,则 LAST(M) = { t }
-
如果 M 是重复零次或多次的单个复杂 NT,
M = $( tt ... ) *,或M = $( tt ... ) SEP *-
令 sep_set = { SEP } 如果 SEP 存在;否则 sep_set = {}。
-
如果 ε ∈ LAST(
tt ...),则 LAST(M) = LAST(tt ...) ∪ sep_set -
否则,序列
tt ...必须非空;LAST(M) = LAST(tt ...) ∪ {ε}。
-
-
如果 M 是重复一次或多次的单个复杂 NT,
M = $( tt ... ) +,或M = $( tt ... ) SEP +-
令 sep_set = { SEP } 如果 SEP 存在;否则 sep_set = {}。
-
如果 ε ∈ LAST(
tt ...),则 LAST(M) = LAST(tt ...) ∪ sep_set -
否则,序列
tt ...必须非空;LAST(M) = LAST(tt ...)
-
- 如果 M 是重复零次或一次的单个复杂 NT,
M = $( tt ...) ?,则 LAST(M) = LAST(tt ...) ∪ {ε}。
- 如果 M 是一个定界 token-tree 序列
OPEN tt ... CLOSE,则 LAST(M) = {CLOSE}。
-
如果 M 是非空 token-tree 序列
tt uu ...,-
如果 ε ∈ LAST(
uu ...),则 LAST(M) = LAST(tt) ∪ (LAST(uu ...) \ { ε })。 -
否则,序列
uu ...必须非空;则 LAST(M) = LAST(uu ...)。
-
FIRST 和 LAST 的示例
以下是一些 FIRST 和 LAST 的示例。(特别注意特殊元素 ε 如何基于输入的各个部分之间的交互被引入和消除。)
我们的第一个示例以树结构呈现,以详细说明匹配器的分析如何组合。
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 }
因此:
- FIRST(
$($d:ident $e:expr );* $( $(h)* );* $( f ;)+ g) = {$d:ident,h,;,f}
但请注意:
- FIRST(
$($d:ident $e:expr );* $( $(h)* );* $($( f ;)+ g)*) = {$d:ident,h,;,f, ε }
以下是类似的示例,但现在是 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)
最后,FOLLOW(M) 的定义如下。pat、expr 等表示具有给定片段指示符的简单非终结符。
- FOLLOW(pat) = {
=>,,,=,|,if,in}`。
- FOLLOW(expr) = FOLLOW(expr_2021) = FOLLOW(stmt) = {
=>,,,;}`。
- FOLLOW(ty) = FOLLOW(path) = {
{,[,,,=>,:,=,>,>>,;,|,as,where, 块非终结符}。
- FOLLOW(vis) = {
,l 除非原始priv之外的任何关键字或标识符;任何可以开始类型的 token;ident、ty 和 path 非终结符}。
- FOLLOW(t) = ANYTOKEN 对于任何其他简单 token,包括 block、ident、tt、item、lifetime、literal 和 meta 简单非终结符,以及所有终结符。
- FOLLOW(M),对于任何其他 M,定义为当 t 遍历 (LAST(M) \ {ε}) 时,FOLLOW(t) 的交集。
在撰写本文时,可以开始类型的 token 是 {(, [, !, *, &, &&, ?, 生命周期, >, >>, ::, 任何非关键字标识符, super, self, Self, extern, crate, $crate, _, for, impl, fn, unsafe, typeof, dyn},尽管此列表可能不完整,因为人们不总是记得在添加新 token 时更新附录。
复杂 M 的 FOLLOW 示例:
- 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
有效和无效匹配器的示例
有了上述规范,我们可以为为什么特定匹配器是合法的而其他是非法的提供论证。
-
($ty:ty < foo ,): 非法,因为 FIRST(< foo ,) = {<} ⊈ FOLLOW(ty) -
($ty:ty , foo <): 合法,因为 FIRST(, foo <) = {,} ⊆ FOLLOW(ty)。 -
($pa:pat $pb:pat $ty:ty ,): 非法,因为 FIRST($pb:pat $ty:ty ,) = {$pb:pat} ⊈ FOLLOW(pat),并且 FIRST($ty:ty ,) = {$ty:ty} ⊈ FOLLOW(pat)。 -
( $($a:tt $b:tt)* ; ): 合法,因为 FIRST($b:tt) = {$b:tt} ⊆ FOLLOW(tt) = ANYTOKEN,FIRST(;) = {;} 也是。 -
( $($t:tt),* , $(t:tt),* ): 合法(尽管实际尝试使用此宏将在展开期间发出局部歧义错误)。 -
($ty:ty $(; not sep)* -): 非法,因为 FIRST($(; not sep)* -) = {;,-} 不在 FOLLOW(ty) 中。 -
($($ty:ty)-+): 非法,因为分隔符-不在 FOLLOW(ty) 中。 -
($($e:expr)*): 非法,因为 expr NT 不在 FOLLOW(expr NT) 中。
影响
Rust 并非一门特别原创的语言,其设计元素来自广泛的来源。以下列出了其中一些(包括此后已移除的元素):
- SML、OCaml:代数数据类型、模式匹配、类型推断、分号语句分隔
- C++:引用、RAII、智能指针、移动语义、单态化、内存模型
- ML Kit、Cyclone:基于区域的内存管理
- Haskell (GHC):类型类、类型家族
- Newsqueak、Alef、Limbo:通道、并发
- Erlang:消息传递、线程崩溃、
链接线程崩溃、轻量级并发 - Swift:可选绑定
- Scheme:卫生宏
- C#:属性
- Ruby:闭包语法、
块语法 - NIL、Hermes:
类型状态 - Unicode Annex #31:标识符和模式语法
测试摘要
以下是引用中链接到各个规则标识符的全部测试的摘要。
{{summary-table}}
术语表
抽象语法树
“抽象语法树”(AST)是编译器在编译程序时对程序结构的一种中间表示。
对齐
值的对齐指定了值首选从哪些地址开始。始终是 2 的幂。对值的引用必须对齐。更多信息。
应用二进制接口(ABI)
应用二进制接口(ABI)定义了编译后的代码如何与其他编译后的代码交互。使用 extern 块和 extern fn 时,ABI 字符串影响:
- 调用约定:函数参数如何传递、值如何返回(例如在寄存器中还是在栈上)以及谁负责清理栈。
- 展开:是否允许栈展开。例如,
"C-unwind"ABI 允许跨 FFI 边界展开,而"C"ABI 不允许。
元数
元数指函数或运算符接受的参数数量。例如,f(2, 3) 和 g(4, 6) 的元数为 2,而 h(8, 2, 6) 的元数为 3。! 运算符的元数为 1。
数组
数组,有时也称为固定大小数组或内联数组,是一个描述元素集合的值,每个元素通过程序在运行时可以计算的索引来选择。它占用连续的内存区域。
关联项
关联项是与另一个项关联的项。关联项在实现中定义,在 trait 中声明。只有函数、常量和类型别名可以关联。与自由项对比。
毯式实现
任何在其中类型以未覆盖形式出现的实现。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。然而,impl<T> Bar<Vec<T>> for Vec<T> 不是毯式 impl,因为在此 impl 中出现的所有 T 实例都被 Vec 覆盖。
约束
约束是对类型或 trait 的限制。例如,如果对函数接受的参数设置了约束,则传递给该函数的类型必须遵守该约束。
组合子
组合子是高阶函数,仅应用函数和先前定义的组合子来提供其参数的结果。它们可用于以模块化方式管理控制流。
Crate
Crate 是编译和链接的单位。存在不同类型的 crate,例如库或可执行文件。Crate 可以链接和引用其他库 crate,称为外部 crate。一个 crate 有一个自包含的模块树,从称为 crate 根的无名根模块开始。程序项可以通过在 crate 根中标记为 public 使其对其他 crate 可见,包括通过 public 模块的路径。更多信息。
分发
分发是在涉及多态时确定实际运行哪个特定版本代码的机制。两种主要的分发形式是静态分发和动态分发。Rust 通过使用 trait 对象支持动态分发。
动态大小类型
动态大小类型(DST)是一种没有静态已知大小或对齐的类型。
实体
实体是一种语言构造,可以在源程序中以某种方式引用,通常通过路径。实体包括类型、程序项、泛型参数、变量绑定、循环标签、生命周期、字段、属性和 lint。
表达式
表达式是值、常量、变量、运算符和函数的组合,计算出单个值,可能带有副作用。
例如,2 + (3 * 4) 是一个返回值为 14 的表达式。
自由项
基础 trait
基础 trait 是这样一个 trait:为现有类型添加一个 impl 是一个破坏性更改。Fn trait 和 Sized 是基础 trait。
基础类型构造器
基础类型构造器是这样一个类型:在其上实现毯式实现是一个破坏性更改。&、&mut、Box 和 Pin 是基础类型构造器。
任何时候类型 T 被认为是局部的,&T、&mut T、Box<T> 和 Pin<T> 也被认为是局部的。基础类型构造器不能覆盖其他类型。任何时候使用术语“已覆盖类型“时,&T、&mut T、Box<T> 和 Pin<T> 中的 T 不被认为是被覆盖的。
有人居住的
如果类型有构造函数并因此可以被实例化,则该类型是有人居住的。有人居住的类型不是“空“的,因为可以存在该类型的值。与无人居住的相反。
固有实现
固有方法
已初始化
如果一个变量已被赋值且此后未被移动,则该变量是已初始化的。所有其他内存位置被假定为未初始化的。只有不安全 Rust 可以创建未初始化内存位置。
局部 trait
在当前 crate 中定义的 trait。一个 trait 定义是局部的或不取决于应用的类型参数。给定 trait Foo<T, U>,Foo 始终是局部的,无论替换 T 和 U 的类型是什么。
局部类型
在当前 crate 中定义的 struct、enum 或 union。这不受应用的类型参数影响。struct Foo 被认为是局部的,但 Vec<Foo> 不是。LocalType<ForeignType> 是局部的。类型别名不影响局部性。
模块
模块是包含零个或多个程序项的容器。模块组织成树,从根部的无名模块开始,称为 crate 根或根模块。路径 可用于引用其他模块的项,这可能受到可见性规则的限制。更多信息
名称
名称 是引用实体的标识符或生命周期或循环标签。名称绑定是当实体声明引入与该实体关联的标识符或标签时。路径、标识符和标签用于引用实体。
名称解析
名称解析 是将路径、标识符和标签绑定到实体声明的编译时过程。
命名空间
命名空间是基于名称所指实体的类型对已声明名称的逻辑分组。命名空间允许一个命名空间中的名称出现不会与另一个命名空间中的相同名称冲突。
在命名空间内,名称组织成层次结构,层次结构的每个级别都有自己的一组命名实体。
名义类型
可以直接通过路径引用的类型。具体指枚举、结构体、联合体和 trait 对象类型。
Dyn 兼容 trait
可以在 trait 对象类型(dyn Trait)中使用的 Trait。只有遵循特定规则的 trait 才是dyn 兼容的。
这些以前称为对象安全 trait。
路径
路径 是用于引用当前作用域或命名空间层次结构其他级别中的实体的一个或多个路径段的序列。
预导入
预导入,或称 Rust 预导入,是一小部分项——主要是 trait——被导入到每个 crate 的每个模块中。预导入中的 trait 是普遍存在的。
作用域
被匹配项
被匹配项是在 match 表达式和类似模式匹配构造中被匹配的表达式。例如,在 match x { A => 1, B => 2 } 中,表达式 x 是被匹配项。
大小
值的大小有两个定义。
第一个是存储该值必须分配多少内存。
第二个是该类型项的数组中连续元素之间的字节偏移量。
它是对齐的倍数,包括零。大小可能因编译器版本(随着新优化的实现)和目标平台而改变(类似于 usize 如何因平台而异)。
更多信息。
切片
切片是对连续序列的动态大小视图,写作 [T]。
它经常以其借用的形式出现,可以是可变或共享的。共享切片类型是 &[T],可变切片类型是 &mut [T],其中 T 表示元素类型。
语句
语句是编程语言中最小的独立元素,命令计算机执行某个操作。
字符串字面量
字符串字面量是直接存储在最终二进制文件中的字符串,因此将在 'static 持续时间内有效。
其类型是 'static 持续时间的借用字符串切片 &'static str。
字符串切片
字符串切片是 Rust 中最原始的字符串类型,写作 str。它经常以其借用的形式出现,可以是可变或共享的。共享字符串切片类型是 &str,可变字符串切片类型是 &mut str。
字符串切片始终是有效的 UTF-8。
Trait
Trait 是一种语言项,用于描述类型必须提供的功能。它允许类型对其行为做出某些承诺。
泛型函数和泛型结构体可以使用 trait 来约束它们接受的类型。
Turbofish
表达式中带有泛型参数的路径必须在开括号前加上 ::。与用于泛型的尖括号结合,这看起来像一条鱼 ::<>。因此,此语法俗称 turbofish 语法。
示例:
#![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 了解不加前缀会产生歧义的示例。
未覆盖类型
不作为其他类型参数出现的类型。例如,T 是未覆盖的,但 Vec<T> 中的 T 是已覆盖的。这仅与类型参数相关。
未定义行为
未指定的编译时或运行时行为。这可能导致但不限于:进程终止或损坏;不当、不正确或意外的计算;或特定于平台的结果。更多信息。
无人居住的
如果类型没有构造函数并因此永远不能被实例化,则该类型是无人居住的。无人居住的类型是“空“的,因为没有该类型的值。无人居住类型的经典示例是 never 类型 !,或没有变体的枚举 enum Never { }。与有人居住的相反。
零大小类型(ZST)
如果类型的大小为 0,则该类型是零大小的(ZST)。此类类型至多有一个可能的值。示例包括:
- 单元类型(见 layout.tuple.unit)。
- 函数项(见 type.fn-item.intro)。
- 元组结构体的构造函数(见 type.fn-item.intro)。
- 元组枚举变体的构造函数(见 type.fn-item.intro)。
- 没有字段或所有字段为零大小的
repr(C)结构体(见 layout.repr.c.struct.size-field-offset)。 - 没有字段或所有字段为零大小的
repr(transparent)结构体(见 layout.repr.transparent.layout-abi)。 - 零大小类型的数组(见 layout.array)。
- 长度为零的数组(见 layout.array)。
- 零大小类型的联合体(见 items.union.common-storage)。
#![allow(unused)]
fn main() {
use core::mem::{size_of, size_of_val};
fn f() {}
struct S(u8);
enum E { V(u8) }
#[repr(C)]
struct C1 {}
#[repr(C)]
struct C2 {
f1: (),
f2: [(); 10],
f3: [u8; 0],
f4: C1,
}
#[repr(transparent)]
struct T1 {}
#[repr(transparent)]
struct T2 {
f1: (),
f2: [(); 10],
f3: [u8; 0],
}
union U {
f1: (),
f2: [(); 10],
f3: [u8; 0],
}
assert_eq!(0, size_of::<()>());
assert_eq!(0, size_of_val(&f));
assert_eq!(0, size_of_val(&S));
assert_eq!(0, size_of_val(&E::V));
assert_eq!(0, size_of::<C1>());
assert_eq!(0, size_of::<C2>());
assert_eq!(0, size_of::<T1>());
assert_eq!(0, size_of::<T2>());
assert_eq!(0, size_of::<[(); 10]>());
assert_eq!(0, size_of::<[u8; 0]>());
assert_eq!(0, size_of::<U>());
}