介绍
本书是 Rust 编程语言的主要参考资料,所提供的内容为三类:
- 介绍语言构造及使用的章节。
- 介绍内存模型、并发模型、运行时服务、链接模型和调试功能的章节。
- 介绍相关语言设计基本原理和其他对设计有影响的参考资料的附录章节。
提醒: 本书还不完整,要记录所有内容还需要一段时间。 可以查看 GitHub issues 了解本书还未记录的内容。
Rust 发布
Rust 语言每六周发布一个新的版本。
第一个稳定版本是 Rust 1.0.0 ,随后是 Rust 1.1.0 等等。
rustc
、 cargo
等工具和 标准库 、本书等文档都随同语言版本一起发布。
本书原版新版本与最新的 Rust 版本匹配,可在 https://doc.rust-lang.org/reference/ 找到。之前版本可通过在 "reference" 前添加 Rust 版本号找到。 例如 https://doc.rust-lang.org/1.49.0/reference/ 。
【矢量工坊宝儿姐】注:当前的中文译本以2023年4月9日源 (Rust 1.69) 为基础,如有贡献、建议和问题,请至译本仓库https://github.com/VectorWorkshopBaoErJie/RustReferenceTranslate/
参考 不记录的内容
本书不是语言的简要介绍,如有需要,可以阅读 Rust 官方提供的 入门文档 。
本书中也没有语言发行版所包含 标准库 的参考内容。 标准库的参考是通过从源代码中提取文档属性,而单独记录。 许多 Rust 学习者所期望的语言特性可能包含于库中,因此可能要去标准库的参考中查找。
本书也通常不会记录 rustc
和 Cargo 工具的具体细节。
rustc
有其自己的 文档 。
Cargo 也有一本 文档 ,其中包含 参考。
但有些,如 链接 页仍然对 rustc
的工作机制有所描述。
本书仅作为 Rust 稳定版本的参考,对于正在开发中的特性,请参见 未稳定性文档。
Rust 编译器,包括 rustc
,会执行优化操作。
本书没有明确说明允许或禁止哪些优化。
对于编译后的程序,你需要通过运行程序、提供输入和观察输出的方式做 "黑盒" 测试,这样产生的效果应符合手册所阐述的内容。
当前,本书还没有达到完全规范和标准,还包含着一些依赖于 rustc
的特定细节,这些不应当成语言部分的规范。
我们打算在未来发布更加规范的参考。
如何使用本书
本书并不需要顺序阅读。 通常可以去单独阅读每个章节,其中未展开讨论的内容通常会交叉链接到其他章节。
建议阅读本书的方式主要有两种。
一、查找特定的问题的答案。
可以按下 s
键或点击顶部栏上的放大镜搜索与问题相关的关键词。
二、提高对语言某个方面的了解。 那么,只需浏览目录,展开阅读某一主题。
开卷有益,学而不怠,与君共勉。
约定
本书作为技术类书籍,信息表达方式上有一些约定。
-
定义术语的语言句子中,该术语会以 斜体 的形式出现。 在该章节之外的位置使用该术语时,通常以链接指向其定义部分。
如 某某术语 是指......这样的术语定义。
-
在以 粗体 标识的块中,使用 "版次差异:" 这样的字眼来说明不同版本中编译的差异。
版次差异: 在 2015 版中,这个语法是有效的,但在 2018 版中不允许。
-
"注意:" 为粗体的块,通常来注明关于书的状态,或者指出一些有用的而超出本书范围的信息。
注意: 这是一个标注的示例。
-
警告将显示为特殊的警告框的形式,用于标注语言中的非安全行为或可能引起混淆的语言特性。
警告: 这是一个警告的示例。
-
正文中的内联代码片段包含在
<code>
标签内。较长的代码示例会在一个语法高亮的框中展示,该框右上角有复制、执行和显示隐藏行的控制按钮。
// 这是隐藏行。 fn main() { println!("这是代码示例"); }
所有的示例都是针对 Rust 最新版本编写的,除非另有说明。
-
语法和词法结构包含在块中,第一行以 粗体上标 的形式标注 "词法" 或 "语法" 。
更多信息参见 符号约定 。
贡献
我们欢迎各种贡献。
你可以向 Rust 参考仓库 提交内容。对于发现的任何错误或不规范的问题,请 提交 issue 。
如果你的问题在本书中没有得到解答,你认为该问题有必要包含在本书中,请 提交 issue 或在 Zulip 的 t-lang/doc
频道提问。
通过这样的方式来了解本书最常使用的内容,有助于集中精力使这些部分变得更好。
符号约定
语法
以下符号约定在 "词法" 和 "语法" 的代码表示段中使用:
符号约定 | 示例 | 意义 |
---|---|---|
大写字母 | KW_IF,INTEGER_LITERAL | 表示词法分析器所生成的 token |
驼峰斜体 | LetStatement,Item | 表示语法产生式 |
string | x ,while ,* | 表示精确的字符 |
\x | \n,\r,\t,\0 | 表示此转义字符所代表的字符 |
x? | pub ? | 表示可选项 |
x* | 外部属性* | 表示 0 个或多个 x |
x+ | 宏匹配+ | 表示 1 个或多个 x |
xa..b | 十六进制数1..6 | 表示 x 重复 a 到 b 次 |
| | u8 | u16 ,块 | 条目 | 两个选项中的一个 |
[ ] | [b B ] | 列举出中括号内的任意一个字符 |
[ - ] | [a -z ] | 列举出指定范围内的任意一个字符 |
~[ ] | ~[b B ] | 列举除中括号内的任意一个字符之外的所有字符 |
~string | ~\n ,~*/ | 列举除指定序列之外的所有字符 |
( ) | (, 参数)? | 分组 |
译注:中文翻译无法表达驼峰形式,仅用斜体表示。无法表达大小写形式。如有必要可查看翻译仓库中的条目。
表中 string
产生式
在语法中,一些规则,尤其是 一元运算符 、 二元运算符 和 关键字 ,用简化表示:以易阅读的字符串清单的形式。 这些情况形成了有关 token 规则的子集,假定它们是由一个驱动着词法分析阶段的确定性有限自动机DFA操作生成的,则在所有此类表中字符串词条的分离分支上进行操作。
当在语法中看到用 monospace
等宽字体显示的字符串时,它隐式地指代表中 string
产生式的一个成员。有关更多信息,请参见 令牌 。
Lexical structure
输入格式
Rust 输入解析为 UTF-8 编码的 Unicode 代码序列。
关键字
Rust 关键字分为三类:
严格字
这些关键字只能用在上下文的正确的位置。 不能用作以下内容的名称:
词法:
KW_AS :as
KW_BREAK :break
KW_CONST :const
KW_CONTINUE :continue
KW_CRATE :crate
KW_ELSE :else
KW_ENUM :enum
KW_EXTERN :extern
KW_FALSE :false
KW_FN :fn
KW_FOR :for
KW_IF :if
KW_IMPL :impl
KW_IN :in
KW_LET :let
KW_LOOP :loop
KW_MATCH :match
KW_MOD :mod
KW_MOVE :move
KW_MUT :mut
KW_PUB :pub
KW_REF :ref
KW_RETURN :return
KW_SELFVALUE :self
KW_SELFTYPE :Self
KW_STATIC :static
KW_STRUCT :struct
KW_SUPER :super
KW_TRAIT :trait
KW_TRUE :true
KW_TYPE :type
KW_UNSAFE :unsafe
KW_USE :use
KW_WHERE :where
KW_WHILE :while
以下关键字从 2018 版开始添加。
词法 2018+
KW_ASYNC :async
KW_AWAIT :await
KW_DYN :dyn
保留字
以下关键字目前还未使用,但保留以供将来使用,使用规则与严格字相同。 这是为了使当前程序与未来版本向前兼容。
词法
KW_ABSTRACT :abstract
KW_BECOME :become
KW_BOX :box
KW_DO :do
KW_FINAL :final
KW_MACRO :macro
KW_OVERRIDE :override
KW_PRIV :priv
KW_TYPEOF :typeof
KW_UNSIZED :unsized
KW_VIRTUAL :virtual
KW_YIELD :yield
以下关键字从 2018 版本开始保留。
词法 2018+
KW_TRY :try
松散字
以下关键字只有在特定上下文中才具有关键字的含义。
因而,比如可以使用名称 union
声明变量或方法。
-
macro_rules
用于创建自定义 宏 。 -
union
用于声明 联合体 。 -
'static
用于静态生命周期,不能作为 泛型生命周期参数 或 循环标签 使用。// error[E0262]: invalid lifetime parameter name: `'static` fn invalid_lifetime_parameter<'static>(s: &'static str) -> &'static str { s }
-
在 2015 版中,
dyn
为松散字,当用在类型位置且随后的路径不以::
开头时,才解析为关键字。从 2018 版开始,
dyn
已提升为严格字。
词法
KW_UNION :union
KW_STATICLIFETIME :'static
词法 2015
KW_DYN :dyn
标识符
词法:
标识符或关键字 :
XID_起始 XID_延续*
|_
XID_延续+原始标识符 :
r#
标识符或关键字 不包括crate
,self
,super
,Self
非关键字标识符 : 标识符或关键字 不包括 strict 或 reserved 关键字
标识符 :
非关键字标识符 | 原始标识符
标识符遵循 Unicode 标准附录 #31 的规范,使用的是 Unicode 15.0 版本,此外还有下面的增强规则。 以下是一些标识符的例子:
foo
_identifier
r#true
Москва
東京
UAX #31 使用的编译设置是:
附加约束条件是,单个下划线字符不是标识符。
注意: 标识符以下划线开头通常用于指示该标识符有意未使用,并将消除
rustc
中的未使用警告。
标识符不能是 严格字 或 保留字 关键字,可以是下面所述的 r#
前缀的 原始标识符 。
零宽度不连字符 (ZWNJ U+200C) 和零宽度连字符 (ZWJ U+200D) 不能出现在标识符中。
在以下情况,标识符受到 ASCII 子集的 XID_起始
和 XID_延续
的限制:
规范化
标识符使用 Unicode 标准附录 #15 中定义的规范化形式 C (NFC) 进行规范化。如果两个标识符的 NFC 形式相同,那么它们就是相等的。
原始标识符
原始标识符类似于普通标识符,但带有 r#
前缀。
(注意,实际的标识符不包括 r#
。)
与普通标识符不同,原始标识符可以是任意严格字或保留字,除了词法中 原始标识符
不包括的关键字。
注释
词法
行注释 :
//
(~[/
!
\n
] |//
) ~\n
*
|//
块注释 :
/*
(~[*
!
] |**
| 块注释或文档) (块注释或文档 | ~*/
)**/
|/**/
|/***/
内部行文档 :
//!
~[\n
孤立CR]*内部块文档 :
/*!
( 块注释或文档 | ~[*/
孤立CR] )**/
外部行文档 :
///
(~/
~[\n
孤立CR]*)?外部块文档 :
/**
(~*
| 块注释或文档 ) (块注释或文档 | ~[*/
孤立CR])**/
块注释或文档 :
块注释
| 外部块文档
| 内部块文档孤立CR :
\r
不紧随\n
非文档注释
注释遵循 C++ 风格的行 (//
) 和块 (/* ... */
) 注释的一般形式。支持嵌套的块注释。
非文档注释被解释为空白的一种形式。
文档注释
以三个斜线开始的行文档注释 (///
),以及块文档注释 (/** ... */
),都是内部文档注释,被解释为 doc
属性 的特殊语法。
相当于在注释主体的周围写上 #[doc="..."]
,即 /// Foo
和 /** Bar */
会转换成 #[doc="Bar"]
。
以 //!
开头的行注释和 /*! ... */
块注释也是文档注释,其应用于父级,而不是之后条目。
相当于在注释位置写上 #![doc="..."]
。 //!
注释通常用于拥有源文件的模块。
在文档注释中不允许孤立的 CR (\r
) ,即后面没有 LF (\n
) 。
示例
#![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 {} } }
空白字符
空白字符是指非空字符串中包含的 Pattern_White_Space
Unicode 性质的字符,即:
U+0009
(水平制表符,'\t'
)U+000A
(换行符,'\n'
)U+000B
(垂直制表符)U+000C
(换页符)U+000D
(回车符,'\r'
)U+0020
(空格,' '
)U+0085
(下一行)U+200E
(从左到右标记)U+200F
(从右到左标记)U+2028
(行分割符)U+2029
(段落分隔符)
Rust 是一种 "形式自由" 的语言,所有形式的空白符号仅仅用于分隔语法中的 Token ,空白自身没有语义。
一个空白元素可以被替换成任何其他合规的空白元素, Rust 编译器将认为其具有相同的意义。
令牌
Token 是非递归的常规编程语言描述语法的原始制品。 Rust 源码解析时 Token 有以下几种 :
在本文档的语法表示部分,"简单" Token 以 string 产生式 的形式给出,并以 monospace
"等宽" 字体呈现。
译注: 中译本无法表达等宽字体,如有必要请至译本仓库查看对应的翻译词条。
字面值
字面值 Token 用于 字面值表达式 。
示例
字符和字符串
示例 | # 标记* | 字符集 | 转义 | |
---|---|---|---|---|
字符 | 'H' | 0 | All Unicode | 引号 & ASCII & Unicode |
字符串 | "hello" | 0 | All Unicode | 引号 & ASCII & Unicode |
原始字符串 | r#"hello"# | <256 | All Unicode | N/A |
字节 | b'H' | 0 | All ASCII | 引号 & Byte |
字节字符串 | b"hello" | 0 | All ASCII | 引号 & Byte |
原始字节字符串 | br#"hello"# | <256 | All ASCII | N/A |
* 同一字面值两侧 #
的数量必须相等。
ASCII 转义符
名称 | |
---|---|
\x41 | 7位字符编码(确切说是 2 位数字,最高为 0x7F ) |
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\\ | 反斜杠 |
\0 | 空 |
字节转义符
名称 | |
---|---|
\x7F | 8位字符编码 (确切地说是 2 位数字) |
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\\ | 反斜杠 |
\0 | 空字符 |
Unicode 转义符
名称 | |
---|---|
\u{7FFF} | 24 位 Unicode 字符编码 (最多 6 位) |
引号转义符
名称 | |
---|---|
\' | 单引号 |
\" | 双引号 |
数字
数字字面值* | 示例 | 指数运算 |
---|---|---|
十进制整数 | 98_222 | N/A |
十六进制整数 | 0xff | N/A |
八进制整数 | 0o77 | N/A |
二进制整数 | 0b1111_0000 | N/A |
浮点数 | 123.0E+77 | 可选的 |
*
所有的数字字面值允许 _
作为可视化分隔符: 1_234.0E+18f64
后缀
后缀是在字面值主体部分之后的一串字符(中间没有空白),其形式与非原始标识符或关键字相同。
词法
后缀 : 标识符或关键字
后缀非E : 后缀 不以e
或E
开始
任何种类的字面值字符串、整数等可带有任意后缀,将当作单个 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 |
字符和字符串字面值
字符字面值
词法
字符字面值 :
'
( ~['
\
\n \r \t] | 引号转义 | ASCII转义 | UNICODE转义 )'
后缀?引号转义 :
\'
|\"
ASCII转义 :
\x
八进制数 十六进制数
|\n
|\r
|\t
|\\
|\0
UNICODE转义 :
\u{
( 十六进制数_
* )1..6}
字符字面值 是指被两个 U+0027
(单引号) 字符包围的单个 Unicode 字符,
但 U+0027
本身除外,单引号必须在前面以 U+005C
字符 (\
) 转义 。
字符串字面值
词法
字符串字面值 :
"
(
~["
\
孤立CR]
| 引用转义
| ASCII转义
| UNICODE转义
| 字符串延续
)*"
后缀?字符串延续 :
\
随后 \n
字符串字面值 是由两个 U+0022
(双引号) 字符包围的任意 Unicode 字符序列,
但 U+0022
本身除外,双引号必须在前面以 U+005C
字符 (\
) 转义 。
字符串字面值中允许断行,换行符 (U+000A
) 或一对回车和换行符 (U+000D
, U+000A
) ,
这两个字节序列通常被转换成 U+000A
,但有一种例外,
当一个未转义的 U+005C
(\
) 字符出现在断行之前,那么忽略断行符和所有紧随其后的
(U+0020
) 、 \t
(U+0009
) 、 \n
(U+000A
) 、 \r
(U+0000D
) 字符。
因此 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
中) ,这有可能会让人感到意外。 在未来可能会调整这种行为。在做出决定之前,建议避免使用,也就是说,目前,会跳过多个连续的换行。 更多内容见这个 Issue 。
字符转义
可以在字符或非原始字符串字面值中使用一些额外的 转义 ,
转义以 U+005C
(\
) 开始,并以下列形式延续:
- 7 位编码转义 以
U+0078
(x
) 开始,后面正好有两个数值不超过0x7F
的 十六进制数字 。 它表示 ASCII 字符,其值等于提供的十六进制值。不允许更高的值,因为无法明确是指 Unicode 编码还是字节值。 - 24 位编码转义 以
U+0075
(u
) 开始,后面是最多六个 十六位数字 ,由大括号U+007B
({
) 和U+007D
(}
) 包围。它表示 Unicode 编码,等于所提供的十六进制值。 - 空白转义 是字符
U+006E
(n
) 、U+0072
(r
) 或U+0074
(t
) 之一,分别表示 Unicode 值U+000A
(LF) 、U+000D
(CR) 或U+0009
(HT) 。 - null 转义 是字符
U+0030
(0
) ,表示 Unicode 值U+0000
(NUL) 。 - 反斜线转义 是字符
U+005C
(\
) ,必须经过转义以表示自身。
原始字符串字面值
词法
原始字符串字面值 :
r
原始字符串上下文 后缀?原始字符串上下文 :
"
( ~ 孤立CR )* (非贪婪)"
|#
原始字符串上下文#
原始字符串字面值不处理任何转义。
以字符 U+0072
(r
) 开始,后面是少于 256 个的 U+0023
(#
) 和 U+0022
(双引号) 字符。
原始字符串主体 可以包含任何 Unicode 字符序列,并且只能由另一个 U+0022
(双引号) 字符结束,后面是 U+0022
(双引号) 字符与开头相同数量的 U+0023
(#
) 字符。
原始字符串主体中包含的所有 Unicode 字符都表示自己。
字符 U+0022
(双引号) 或 U+005C
(\
) 不具有任何特殊含义 (除非后面有至少相同数量的 U+0023
(#
) 字符用于表达原始字符串字面值)。
字符串字面值示例:
#![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 }
字节和字节字符串字面值
字节字面值
词法
字节字面值 :
b'
( ASCII字符 | 字节转义 )'
后缀?ASCII字符 :
任意ASCII (如 0x00 至 0x7F), 不包括'
,\
, \n, \r 或 \t字节转义 :
\x
十六进制数 十六进制数
|\n
|\r
|\t
|\\
|\0
|\'
|\"
字节字面值 表示是一个 ASCII 字符 (在 U+0000
到 U+007F
范围内) 或单一的 转义 ,前面是字符 U+0062
(b
) 和 U+0027
(单引号),后面是字符 U+0027
。
如果字面值有 U+0027
字符,必须由前置 U+005C
(\
) 字符来 转义 。其相当于一个 u8
无符号 8 位整数 数字字面值 。
字节字符串字面值
词法
字节字符串字面值 :
b"
( ASCII字符串 | 字节转义 | 字符串延续 )*"
后缀?ASCII字符串 :
任意ASCII (如 0x00 至 0x7F), 不包括"
,\
和 孤立CR
非原始的 字节字符串字面值 是一串 ASCII 字符和 转义 。
前面是字符 U+0062
(b
) 和 U+0022
(双引号) ,后面是字符 U+0022
。
如果字面值有 U+0022
字符,必须前置 U+005C
(\
) 字符来转义。
或者,字节字符串字面值是 原始字节字符串字面值 ,定义如下。
长度为 n
的字节字符串字面值的类型是 &'static [u8; n]
。
一些额外的 转义 在字节或非原始字节的字符串字面值中都是可用的。
转义以 U+005C
(\
) 开始,并以下列形式延续。
- 字节转义 以
U+0078
(x
) 开始,后面正好有两个 十六进制数字 。表示相当于所提供的十六进制值的字节。 - 空白转义 是字符
U+006E
(n
) 、U+0072
(r
) ,或U+0074
(t
) ,分别表示字节值0x0A
(ASCII LF) 、0x0D
(ASCII CR) 或0x09
(ASCII HT) 。 - null 转义 是字符
U+0030
(0
) ,表示字节值0x00
(ASCII NUL) 。 - 反斜线转义 是字符
U+005C
(\
) ,必须转义以表示其 ASCII 编码0x5C
。
原始字节字符串字面值
词法
原始字节字符串字面值 :
br
原始字节字符串正文 后缀?原始字节字符串正文 :
"
ASCII* (非贪婪)"
|#
原始字节字符串正文#
ASCII :
任意ASCII (如 0x00 至 0x7F)
原始字节字符串字面值不处理任何转义。
它们以字符 U+0062
(b
) 开始,然后是 U+0072
(r
) ,后面是少于 256 的字符 U+0023
(#
),以及一个 U+0022
(双引号) 字符。
原始字符串主体 可以包含任何 ASCII 字符序列,并且只能由另一个 U+0022
(双引号) 字符终止,后面是 U+0022
(双引号) 字符与开头数量相同 U+0023
(#
) 字符。
原始的字节字符串字面值不能包含任何非 ASCII 字节。
原始字符串主体中包含的所有字符表示其 ASCII 编码。
字符 U+0022
(双引号) 或 U+005C
(\
) 不具有任何特殊含义 (除非后面有至少相同数量的 U+0023
(#
) 字符表达原始字符串字面值) 。
字符串字面值示例:
#![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 }
数字字面值
数字字面值 是一个 整数字面值 或 浮点字面值 。语法上混合识别这两种字面值。
整数字面值
词法
整数字面值 :
( 十进制字面值 | 二进制字面值 | 八进制字面值 | 十六进制字面值 ) 非E后缀?十进制字面值 :
十进制数 (十进制数|_
)*二进制字面值 :
0b
(二进制数|_
)* 二进制数 (二进制数|_
)*八进制字面值 :
0o
(八进制数|_
)* 八进制数 (八进制数|_
)*十六进制字面值 :
0x
(十六进制数|_
)* 十六进制数 (十六进制数|_
)*十进制数 : [
0
-1
]二进制数 : [
0
-7
]八进制数 : [
0
-9
]十六进制数 : [
0
-9
a
-f
A
-F
]
整数字面值 的四种形式:
- 十进制字面值 以一个 十进制数字 开始,然后以任何 十进制数字 和 下划线 的混合。
- 十六进制字面值 以字符序列
U+0030
U+0078
(0x
) 开始,然后以任何十六进制数字和下划线混合(至少有一个数字)。 - 八进制字面值 以字符序列
U+0030
U+006F
(0o
) 开始,然后以任何八进制数字和下划线混合(至少有一个数字)。 - 二进制字面值 以字符序列
U+0030
U+0062
(0b
) 开始,然后以任何二进制数字和下划线混合(至少有一个数字)。
像其他字面值一样,整数字面值可以在后面加一个后缀(紧随而没有空格),如上所述。
后缀不能以 e
或 E
开头,因为这将被解释为浮点字面值的指数。
关于这些后缀的实现,见 字面值表达式 。
字面值表达式允许的整数字面值的示例:
#![allow(unused)] fn main() { #![allow(overflowing_literals)] 123; 123i32; 123u32; 123_u32; 0xff; 0xff_u8; 0x01_f32; // 整数 7986, 非浮点 1.0 0x01_e3; // 整数 483, 非浮点 1000.0 0o70; 0o70_i16; 0b1111_1111_1001_0000; 0b1111_1111_1001_0000i64; 0b________1; 0usize; // 这对于其类型来说值太大,但允许在字面值表达式。 128_i8; 256_u8; // 这是整数字面值,允许在浮点字面值表达式。 5f32; }
注意,比如 -1i8
将被解析为两个标记: -
和 1i8
。
字面值表达式不允许的整数字面值示例:
#![allow(unused)] fn main() { #[cfg(FALSE)] { 0invalidSuffix; 123AFB43; 0b010a; 0xAB_CD_EF_GH; 0b1111_f32; } }
元组索引
词法
元组索引:
整数字面值
元组索引用于指代 元组 、元组结构体 和 元组变体 的字段。
元组索引直接与字面值 Token 进行比对。元组索引从 0
开始,每一个连续的索引其值增加 1
,为十进制值。
因此,只有十进制的值才能匹配,其值不能有任何额外的 0
前缀字符。
#![allow(unused)] fn main() { let example = ("dog", "cat", "horse"); let dog = example.0; let cat = example.1; // 下面的例子是无效的。 let cat = example.01; // ERROR 字段名不能为 `01` let horse = example.0b10; // ERROR 字段名不能为 `0b10` }
注意: 元组索引可能包括某些后缀,但这并不意味着是有效的,在未来的版本中可能会删除。 更多内容见 https://github.com/rust-lang/rust/issues/60210 。
浮点字面值
词法
浮点字面值 :
十进制数.
(不是紧跟着.
,_
或 XID_起始 字符)
| 十进制字面值.
十进制字面值 后缀非E?
| 十进制字面值 (.
十进制字面值)? 浮点指数 后缀?浮点指数 :
(e
|E
) (+
|-
)? (十进制数|_
)* 十进制数 (十进制数|_
)*
浮点字面值 两种形式:
- 十进制字面值 后面一个句号字符
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
将被解析为两个符号: -
和 1.0
。
字面值表达式不允许的浮点字面值示例:
#![allow(unused)] fn main() { #[cfg(FALSE)] { 2.0f80; 2e5f80; 2e5e6; 2.0e5e6; 1.3e10u64; } }
类似于数字字面值的保留形式
词法
保留数 :
二进制字面值 [2
-9
&零空白;]
| 八进制字面值 [8
-9
&零空白;]
| ( 二进制字面值 | 八进制字面值 | 十六进制字面值 ).
(不是紧随着.
,_
或 XID_起始 字符)
| ( 二进制字面值 | 八进制字面值 ) (e
|E
)
|0b
_
* 输入结束或非二进制数
|0o
_
* 输入结束或非八进制数
|0x
_
* 输入结束或非十六进制数
| 十进制字面值 ( . 十进制字面值)? (e
|E
) (+
|-
)? 输入结束或非十进制数
以下类似于数字字面值的词法是 保留形式 。 由于这些形式可能引起歧义,编译器会拒绝它们,而不会解释为独立的 Token 。
-
一个无后缀的二进制或八进制字面值,中间没有空白,随后是一个超出其小数范围的十进制数字。
-
一个无后缀的二进制、八进制或十六进制字面值,中间没有空白,随后是一个点号(对点号后面的限制与浮点字面值相同)。
-
一个无后缀的二进制或八进制字面值,中间没有空白,随后是字符
e
或E
组成。 -
以一个小数点前缀开始的输入,但不是一个有效的二进制、八进制或十六进制字面值(因为它不包含数字)。
-
具有浮点字面值形式的输入,指数中没有数字。
保留形式的示例:
#![allow(unused)] fn main() { 0b0102; // 这不是 `0b010` 随后有 `2` 0o1279; // 这不是 `0o127` 随后有 `9` 0x80.0; // 这不是 `0x80` 随后有 `.` 和 `0` 0b101e; // 这不是有后缀的字面值或 `0b101` 随后有 `e` 0b; // 这不是一个整数字面值或 `0`随后有 `b` 0b_; // 这不是一个整数字面值或 `0` 随后有 `b_` 2e; // 这不是一个浮点字面值或 `2` 随后有 `e` 2.0e; // 这不是一个浮点字面值或 `2.0` 随后有 `e` 2em; // 这不是有后缀的字面值或 `2` 随后有 `em` 2.0em; // 这不是有后缀的字面值或 `2.0` 随后有 `em` }
生命周期和循环标签
词法
生命周期TOKEN :
'
标识符或关键字
|'_
生命周期或标签 :
'
非关键字标识符
生命周期参数和 循环标签 使用 '生命周期或标签' token 。词法分析器接收任何 '生命周期TOKEN' ,比如,能够在宏中使用。
标点符号
为了完整,这里列出了标点符号 Token 。它们各自的用途和含义定义在对应的链接页面中。
符号 | 名称 | 用法 |
---|---|---|
+ | 加号 | 加法,trait约束,宏重复匹配器 |
- | 减号 | 减法,否定 |
* | 星号 | 乘法,解引用,原始指针,宏重复匹配器,[用作通配符][wildcards] |
/ | 斜线 | 除法 |
% | 百分号 | 取余 |
^ | 插入符号 | 位运算异或和逻辑运算异或 |
! | 感叹号 | 位运算非和逻辑运算非,宏调用,内部属性,永不类型,否定的impl |
& | 与符号 | 位运算与和逻辑运算与,借用,引用,引用模式 |
| | 或符号 | 位运算或和逻辑运算或,闭包,模式匹配中的模式,if let和while let中的模式 |
&& | 与运算符 | 惰性与运算,借用,引用,引用模式 |
|| | 或运算符 | 惰性或运算,闭包 |
<< | 左移运算符 | 左移,嵌套泛型 |
>> | 右移运算符 | 右移,嵌套泛型 |
+= | 加等于运算符 | 加法赋值 |
-= | 减等于运算符 | 减法赋值 |
*= | 乘等于运算符 | 乘法赋值 |
/= | 除等于运算符 | 除法赋值 |
%= | 取余等于运算符 | 取余赋值 |
^= | 异或等于运算符 | 位异或赋值 |
&= | 与等于运算符 | 位与赋值 |
|= | 或等于运算符 | 位或赋值 |
<<= | 左移等于运算符 | 左移赋值 |
>>= | 右移等于运算符 | 右移赋值, 嵌套泛型 |
= | 等号 | 赋值,属性,各种类型定义 |
== | 双等号 | 等于 |
!= | 不等于号 | 不等于 |
> | 大于号 | 大于,泛型,路径 |
< | 小于号 | 小于,泛型,路径 |
>= | 大于等于号 | 大于等于,泛型 |
<= | 小于等于号 | 小于等于 |
@ | At | 子模式绑定 |
_ | 下划线 | 通配符模式,推断类型,常量、extern crates、use 声明和解构赋值中的匿名条目 |
. | 点号 | 字段访问,元组索引 |
.. | 双点号 | 区间,结构体表达式,模式,区间模式rangepat |
... | 三点号 | 可变参数函数,区间模式 |
..= | 双点等号 | 闭区间,区间模式 |
, | 逗号 | 各种分隔符 |
; | 分号 | 用于各种条目和语句的终止符,数组类型 |
: | 冒号 | 各种分隔符 |
:: | 路径分隔符 | 路径分隔符 |
-> | 箭头符 | 函数返回类型,闭包返回类型,函数指针类型 |
=> | 双箭头符 | 匹配分支,宏 |
# | 井号 | 属性 |
$ | 美元符 | 宏 |
? | 问号 | 问号运算符,大小可变,宏重复匹配器 |
~ | 波浪符 | Rust 1.0 之前就已经不再使用了,但其 token 仍可使用 |
定界符号
括号符号用在语法的各部分。一个左括号必须总是与一个右括号相配对。括号和其中的 Token 在 宏 中被称为 "token 树" 。三种类型的括号是:
括号 | 类型 |
---|---|
{ } | 大括号 |
[ ] | 方括号 |
( ) | 圆括号 |
保留前缀
词法 2021+
保留TOKEN双引号
: (标识符或关键字不包括b
或r
或br
|_
)"
保留TOKEN单引号
: (标识符或关键字不包括b
_ |_
)'
保留TOKEN井号
: (标识符或关键字不包括r
或br
|_
)#
一些被称为 保留前缀 的词法形式被保留下来,供将来使用。
如果源码输入在词法上被解析为非原始标识符(或关键字或 _
),紧随其后的是 #
、 '
或 "
字符(没有中间的空白) ,刚将其标识为保留前缀。
请注意,原始标识符、原始字符串字面值和原始字节字符串字面值可能包含 #
字符,但不会被解释为包含保留前缀。
同样,在原始字符串字面值、字节字面值、字节字符串字面值和原始字节字符串字面值中使用的 r
、 b
和 br
前缀也不被解释为保留前缀。
版次差异: 从 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 }
示例在 2021 版本之前是允许的,之后不允许:
#![allow(unused)] fn main() { macro_rules! lexes {($($_:tt)*) => {}} lexes!{a#foo} lexes!{continue'foo} lexes!{match"..." {}} }
宏
Rust 的功能和语法可以通过自定义宏来扩展。
这些宏被赋予名称,并通过一致的语法调用:some_extension!(...)
。
有两种定义新宏的方式:
宏调用
语法
宏调用 :
简单路径!
定界Token树定界Token树 :
(
Token树*)
|[
Token树*]
|{
Token树*}
Token树 :
Token不包括 定界符号 | 定界Token树宏调用语句 :
简单路径!
(
Token树*)
;
| 简单路径!
[
Token树*]
;
| 简单路径!
{
Token树*}
宏调用是在编译期发生,将展开宏,用宏的结果替换调用。 可以在以下语法调用宏:
当宏用作条目或语句时,使用 宏调用语句 语法,当未使用花括号时需要在末尾加上分号。
在宏调用或 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!(); }
实例宏
语法
宏规则定义 :
macro_rules
!
标识符 宏规则组定界宏规则组定界 :
(
宏规则组)
;
|[
宏规则组]
;
|{
宏规则组}
宏规则组 :
宏规则 (;
宏规则 )*;
?宏规则 :
宏匹配器=>
宏转录器宏匹配器 :
(
宏匹配*)
|[
宏匹配*]
|{
宏匹配*}
宏匹配 :
Token不包括$
和 定界符号 之外的任意 token
| 宏匹配器
|$
( 标识符或关键字 不包括crate
之外的任意标识符或关键字 | 原始标识符 |_
):
宏片段规格
|$
(
宏匹配+)
宏表示分割? 宏表示操作符宏片段规格 :
block
|expr
|ident
|item
|lifetime
|literal
|meta
|pat
|pat_param
|path
|stmt
|tt
|ty
|vis
宏表示分割 :
Token不包括 定界符号 和 宏表示操作符 之外的任意 token_宏表示操作符 :
*
|+
|?
宏转录器 :
定界Token树
允许用户以 macro_rules
声明的方式定义语法扩展。
这种语法扩展称为 "实例宏" ,或者简单的称为 "宏" 。
每个实例宏的定义都要有一个名称和一个以上 规则 。 每个规则都有两个部分:一个 匹配器 部分,用于描述匹配语法,另一个 转录器 部分,用于描述成功匹配并调用后将替换的语法。 匹配器和转录器都必须被包裹在定界符号中。 宏可以展开为表达式、语句、条目 (包括 trait 、实现和外部条目) 、类型或模式。
转录
当宏被调用时,宏展开器通过名称查找宏调用,并依次尝试匹配包含的宏规则。
在首个成功匹配处转录,如果导致错误,则不会尝试后续的规则。
在进行匹配时,不会对 Token 进行前瞻,如果编译器无法逐个明确如何解析,则会出错。
在下面的示例中,编译器不会前瞻标识符 $j ,不会前瞻是否已至 )
定界位置,虽然这能够更明确地解析:
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $j:ident) => { }; } ambiguity!(error); // Error: 局部歧义 }
在匹配器和转录器中, $
符号用于宏引擎实现调用的特定行为 (具体在 元变量 和 重复 中描述)。
非该符号标记的 Token 会按字面进行匹配和转录,特例是匹配器的外部定界符将匹配任意一对定界符,
例如,匹配器 (())
将匹配 {()}
,但是不匹配 {{}}
。字符 $
不能按字面匹配或转录。
转发匹配片段
在将匹配后的片段转发给内部的实例宏时,内部宏的匹配器将只能看到类型化的不透明 AST 片段。
内部宏无法简单通过字面 token 来匹配这个片段,只能使用相同类型规格的片段接收。
ident
、 lifetime
和 tt
片段类型例外,可以按照字面 token 的方式匹配。
以下是一个示例:
#![allow(unused)] fn main() { macro_rules! foo { // expr 表达式片段与 3 的字面值规则不匹配,虽然此表达式是 3 的字面值表达式。 ($l:expr) => { bar!($l); } // ERROR: ^^ 在宏调用中这个 token 没有预期的规则, } macro_rules! bar { (3) => {} } foo!(3); }
以下是在匹配 tt
Token 树片段后内部规则通过字面匹配 Token 的一个示例:
#![allow(unused)] fn main() { // 编译成功 macro_rules! foo { // `tt` Token 树片段直接作为字面 Token 与 3 的字面值表达式成功匹配。 ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3); }
元变量
在匹配器中, $
名称 :
片段规格 定义元变量,用于匹配指定类型的语法片段,并与语法片段绑定。
有效的片段规格包括:
译注:这里 '片段规格' 的概念实际上与 '片段类型' 等价,使用规格这个词是与 rust 中的类型系统有所区分。
item
: 条目block
: 块表达式stmt
: 语句 不含尾部分号 (即,不包括需要分号的条目语句)pat_param
: 模式非顶层项pat
: 至少是任意 模式非顶层项, 更多取决于版次expr
: 表达式ty
: 类型ident
: 一个 标识符或关键字 或 原始标识符path
: 类型路径tt
: Token树 (简单 token 或匹配在定界符号()
、[]
、{}
中 token)meta
: 一个 Attr, 属性的内容lifetime
: 生命周期TOKENvis
: 一个可空的 可见性 限定词literal
: 匹配的-
?字面值表达式
元变量的片段类型在匹配器中已被指定,在转录器中可以用 $
名称 来引用。关键字元变量 $crate
用于引用当前的 crate。
转录器中元变量将替换为匹配到的语法片段。
可以将元变量多次转录,或者并不进行转录。
参阅下面的 卫生性 。
出于向后兼容的原因,单独的 _
下划线表达式 不会被 expr
片段匹配。当 _
做为子表达式时可被 expr
片段匹配。
版次差异: 从 2021 版本开始,
pat
规格片段可以匹配顶层或模式 (即它们接受 模式) 。在 2021 版本之前,则完全匹配与
pat_param
相同的片段 (即它们接受 模式非顶层项 ) 。这与
macro_rules!
定义生效的版次相关联。
重复
在匹配器和转录器中,重复语法是通过将要重复的 token 放在 $(
…)
中,后跟重复运算符,可选地包含分隔符 token 。
分隔符 token 可以是除定界符或重复运算符外的任意 token ,但常用的是 ;
和 ,
。
例如, $( $i:ident ),*
表明由逗号分隔的任意数量的标识符。重复允许嵌套。
重复运算符有:
*
— 表示任意数量的重复。+
— 表示任意数字,但至少是 1 。?
— 表示有 0 或 1 个产生的可选片段。
由于 ?
表示最多出现一次,所以它不能与分隔符一起使用。
重复的片段同时匹配和转录成指定数量的该片段,并由分隔符隔开。
元变量将匹配到对应片段的每个重复项。例如,上面示例的 $( $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!{} // Error: m 不在作用域内。
macro_rules! m {
() => {};
}
m!{} // OK: 出现在 m 的声明之后。
mod uses_macro;
}
// m!{} // Error: 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); // Error: 没有规则可以匹配 '1' m!(2); macro_rules! m { (3) => {}; } m!(3); } m!(1); }
宏也可以在函数内部声明和使用,工作方式与模块中声明类似:
#![allow(unused)] fn main() { fn foo() { // m!(); // Error: m 不在作用域内。 macro_rules! m { () => {}; } m!(); } // m!(); // Error: m 不在作用域内。 }
macro_use
属性
macro_use
属性 有两个用法。一是,应用于模块后,使模块中宏的作用域在模块关闭后仍不结束:
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } // 这里使 m 的作用域延长到了模块之外 m!(); }
二是,应用于 crate 根模块中的 extern crate
声明,从而从另一个 crate 导入宏。
这种方式会将宏导入到 macro_use
预定义 中,而不是文本形式的导入,这表示可在任意位置隐藏此宏名称。
虽然 #[macro_use]
导入的宏可以在导入语句之前使用,但名称冲突时,最后导入的宏优先。
可选语法是,可以使用 [元标识符列表][MetaListIdents] 指定要导入的宏列表。当将 #[macro_use]
应用于模块时,不支持此功能。
#[macro_use(lazy_static)] // 仅 #[macro_use] 表示导入所有宏。
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // Error: lazy_static 在 `self` 未定义。
使用 #[macro_use]
导入的宏导出时必须使用 #[macro_export]
,下面将对此进行描述。
基于路径作用域
默认情况,宏没有基于路径的作用域。然而,如果赋予 #[macro_export]
属性,那么宏就被声明在 crate 根作用域内:
#![allow(unused)] fn main() { self::m!(); m!(); // OK: 基于路径方式查找,在当前模块中找到了 m 。 mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
#[macro_export]
标记的宏总是 pub
的,可以通过路径或以上面描述的 #[macro_use]
方式引入其他 crate 中。
卫生性
默认情况下,在宏中引用的所有标识符都会按原样展开,并从宏调用的位置查找。
如果宏引用的条目或宏,在调用位置不在作用域内,则可能会出现问题。
为了缓解这个问题,可以在路径中使用 $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() {} }
在 Rust 1.30 之前,不支持
$crate
和local_inner_macros
(下面会描述) 。 这两个语法与基于路径导入宏的语法一起添加,是为保证一些辅助宏不需要用户手动导入。 使用辅助宏的早期 Rust 版本编写的 crate 需要进行修改后,以使用$crate
或local_inner_macros
。
当一个宏被导出时,可以在 #[macro_export]
属性中添加 local_inner_macros
关键字,以自动在所有包含的宏调用中添加 $crate::
前缀。
这主要是为了迁移在 $crate
添加到语言之前编写的代码,使其能够与 Rust 2018 的基于路径的宏导入一起使用。
不建议在新代码中使用此功能。
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // 自动转换为 $crate::helper!() 。 } #[macro_export] macro_rules! helper { () => { () } } }
后继符冲突限制
宏系统使用的解析器相当强大,但出于防止当前或未来版本的语言产生歧义的考虑,对于宏有一些限制。 特别是,除了有关模糊展开的规则之外,由元变量匹配的非终端符号必须后跟一个 token ,此 token 已决定可以安全地在该类型的匹配之后使用。
举例来说,像 $i:expr [ , ]
这样的宏匹配器理论上是可以接受的,因为 [,]
不能成为合规的表达式的一部分,因此解析不会有歧义。
但是,由于 [
可以作为尾随的表达式的开始,因此 [
不是一个可以安全地在表达式之后排除的字符。
如果在以后的 Rust 版本中接受了 [,]
,这个匹配器就会变成有歧义或者解析错误,从而破坏工作中的代码。
对于,像 $i:expr,
或 $i:expr;
这样的匹配器是合规的,因为 ,
和 ;
是合规的表达式分隔符。
具体的规则如下:
expr
和stmt
后面只能跟着一个:=>
、,
、;
。pat_param
后面只能跟着一个:=>
、,
、=
、|
、if
、in
。pat
后面只能跟着一个:=>
、,
、=
、if
、in
。path
和ty
后面只能跟着一个:=>
、,
、=
、|
、;
、:
、>
、>>
、[
、{
、as
、where
, 或block
片段规格的元变量 。vis
后面只能跟着一个:,
, 一个非原始的priv
以外的标识符,任何可以开始一个类型的标记,或者一个带有ident
、ty
或path
片段规格的元变量。- 对所有其他片段规格没有限制。
版次差异: 在 2021 版之前,
pat
后面还可以跟着|
。
当涉及到重复时,规则适用于每次可能的展开,同时要考虑分隔符。这意味着:
- 如果重复包括一个分隔符,则该分隔符必须能够跟在重复内容之后。
- 如果重复可以重复多次 (
*
或+
) ,则其内容必须能够跟随它们自身。 - 重复的内容必须能够跟随之前的任何内容,而跟随重复内容的任何内容后面都必须能够跟随。
- 如果重复可以匹配零次 (
*
或?
) ,则后面必须能够跟随前面。
更多细节,见 形式说明 。
过程宏
过程宏 是以可执行函数的形式创建语法扩展。 过程宏有三种添加形式:
- [函数宏] -
custom!(...)
- [衍生宏] -
#[derive(CustomDerive)]
- [属性宏] -
#[CustomAttribute]
过程宏是在编译时运行代码,对 Rust 语法进行操作,输入语法,而产生新的语法。 你可以把过程宏想象成,把一段代码片段转换为另一段代码片段的函数。
过程宏必须定义在 crate 类型 为 proc-macro
的 crate 中。
注意: 在使用 Cargo 时,可用 crate 配置清单中的
proc-macro
键来定义。[lib] proc-macro = true
这个特殊的函数,要么返回语法,要么恐慌,要么死循环。 返回语法时,至于是替换语法,还是增加语法,取决于过程宏的类型。 编译器能够捕获过程宏的恐慌,并变成编译器错误。 编译器无法捕获死循环,将导致编译器挂起。
过程宏在编译过程中运行,类似于编译器的脚本,因而拥有与编译器相同的资源。 例如,标准输入输出及错误与编译器所能访问的相同。 同样地,文件系统的访问也是一样的。 因而,过程宏与 Cargo 构建脚本 有同样的安全问题。
过程宏有两种报告错误的方式。一是恐慌。
二是触发 compile_error
宏调用。
proc_macro
crate
过程宏 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
值,该值是不透明的,不能修改,但可以创建。
该值表示源码中一个作用域,作用主要是用于错误报告。
虽然不能直接修改,但 Token 可以改变与之 关联 的 Span
,比如从另一个 Token 获得一个 Span
。
过程宏卫生性
过程宏是 unhygienic "非卫生" 的,行为简单,像是直接把输出 Token 流写入到了其围绕的代码中一样。 意味着,会受到外部条目的影响,也会影响到外部导入部分。
因为有这个限制,宏作者需要非常小心,以保证宏能在更多情况下工作。
通常需要: 使用库中条目的绝对路径 (例如 ::std::option::Option
而不是 Option
) ,
或者确保生成的函数名称不太可能与其他函数冲突 (例如 __internal_foo
而不是 foo
) 。
函数式过程宏
函数式过程宏 使用宏调用操作符 (!
) 来调用。
这些宏是由一个具有 proc_macro
属性 和 (TokenStream) -> TokenStream
函数签名的 pub 函数 定义的。
函数输入 TokenStream
是宏调用定界符号内的内容,函数输出 TokenStream
是整个宏调用的内容。
例如,下面的定义的过程宏,忽略了输入内容,然后将一个 answer
函数输出到了所处作用域。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
然后可在 crate 中使用该宏后,打印 "42" 到标准输出。
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
函数式过程宏可以处在任意宏调用位置,其中包括 语句 、 表达式 、 模式 、 类型表达式 、 条目 ,
包括 extern
块 中的条目,内部及trait 实现 ,以及 trait 定义 。
衍生宏
衍生宏 为 derive
属性 定义了新的输入项。
可以为 结构体 、 枚举 或 联合体 创建新的 条目 。
也可以定义 衍生宏辅助属性 。
衍生宏可在 pub 函数 附加 proc_macro_derive
属性来定义,函数签名为 (TokenStream) -> TokenStream
。
函数输入 TokenStream
是具有 derive
属性条目的 token 流。
函数输出 TokenStream
必须是一组条目,然后添加到输入 TokenStream
的条目所在的 模块 或 块 中。
下面是一个衍生宏的例子,该宏没有处理输入,只是添加了一个函数 answer
。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
然后使用其衍生宏:
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
衍生宏辅助属性
衍生宏可以在其所在的 条目 的作用域内添加额外的 属性 。 这些属性被称为 衍生宏辅助属性 。 这些属性是 惰性的 ,其唯一目的是嵌入到定义它们的衍生宏中。 也就是说,让所有的宏可以看到这些属性。
通过 proc_macro_derive
宏内的 attributes
键定义辅助属性,用逗号分隔的标识符列表,即辅助属性的名称。
例如,下面衍生宏定义了一个辅助属性 helper
,但对于属性并没有做什么事情。
#![crate_type="proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
然后在结构体上使用衍生宏:
#[derive(HelperAttr)]
struct Struct {
#[helper] field: ()
}
属性宏
属性宏 定义了新的 外围属性 ,可以附加到 条目 上,
包括 extern
块 中的条目,内部及trait 的 实现 ,以及 trait 定义 。
属性宏是由附加 proc_macro_attribute
属性 的 pub 函数 来定义的,其函数签名为 (TokenStream, TokenStream) -> TokenStream
。
第一个 TokenStream
是属性名称后定界符号中的 token 树,不包括外部定界符号。
如果属性仅为名称,则该 TokenStream
为空。
第二个 TokenStream
是 条目 的其他部分,包括 条目 上的其他 属性 。
返回的 TokenStream
用任意数量的 条目 替换原 条目 。
例如,下面的属性宏接收输入流后按原样返回,实际上是一个空操作的属性。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
下面这个例子展示了,通过属性宏得到的字符串化的 TokenStream
s 。
运行后,其输出内容展示在以 "out:" 为前缀的函数后的注释中。
// my-macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{}\"", attr.to_string());
println!("item: \"{}\"", item.to_string());
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:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"
声明宏 Token 和过程宏 Token
声明式 macro_rules
宏和过程宏使用的 Token 规格类似,但也有所不同。
在 macro_rules
中的 Token 树 (对应于 tt
匹配器) 被定义为
- 定界符号的组 (
(...)
,{...}
, 等) 。 - 语言支持的所有运算符,包括单字符和多字符运算符 (
+
,+=
) 。- 请注意,这个集合不包括单引号
'
。
- 请注意,这个集合不包括单引号
- 字面值 (
"string"
,1
, 等) 。- 请注意,负 (如
-1
) 并不是这类字面值 Token 的一部分,而是单独的 Token 。
- 请注意,负 (如
- 标识符,包括关键字 (
ident
,r#ident
,fn
) - 生命周期 (
'ident
) - 在
macro_rules
中元变量将替换 (如$my_expr
在macro_rules! mac { ($my_expr: expr) => { $my_expr } }
在mac
的展开之后,无论传递的表达式是什么,都会被认为是单一的 Token 树 )
过程宏中的 Token 树被定义为
- 定界符号的组 (
(...)
,{...}
, 等) - 语言支持的运算符中使用的所有标点符号 (
+
,但不是+=
),还有单引号'
字符 (通常用在生命周期上,关于生命周期的分割和连接行为见下文) - 字面值 (
"string"
,1
, 等)- 支持负 (如
-1
) 作为整数和浮点数字面值的一部分。
- 支持负 (如
- 标识符,包括关键字 (
ident
,r#ident
,fn
)
当 Token 流被传递到过程宏时,会考虑这两个定义之间不匹配的情况。 请注意,下面的转换可能会产生惰性,所以如果没有实际检验 Token ,就可能不会发生。
传递给过程宏时
- 所有的多字符运算符都分解成单字符。
- 生命周期分解成一个
'
字符和一个标识符。 - 所有元变量的替换都表示为它们实际的 Token 流。
- 当需要保留语法分析的优先级时,这样的 Token 流可以被包装成带有隐式定界符号 ([Delimiter::None]) 的定界组 ([Group]) 。
tt
和ident
的替换从来不会被包装在这样的组中,而总是作为它们的实际的 Token 树来表示。
当从过程宏触发时
- 在适用的情况下,标点符号被粘接在多字符运算符中。
- 与标识符连接的单引号
'
被粘接在生命周期中。 - 负字面值被转换为两个标记 (
-
和字面值 ),当需要保留语法分析的优先级时,可能会被包裹在一个带定界符号的组 (Group
) 中,并带有隐式定界符号 (Delimiter::None
) 。
请注意,无论是声明性的还是过程性的宏都不支持 doc 注释标记 (例如 /// Doc
) ,当其传递给宏时,总是转换为表示其等价的 #[doc = r"str"]
属性的 Token 流。
Crate和源码文件
词法
UTF8BOM :\uFEFF
执行注解 :#!
~\n
+†
注意:尽管 Rust 和其他语言相似,可以实现解释器,但目前仅实现了编译器,并且设计理念也是编译型语言。
Rust 的语义同样遵循 '编译时' 和 '运行时' 两个可区分阶段。 1 编译时的 静态解释 规则将决定编译的成功或失败,运行时的 动态解释 规则将决定程序运行的行为。
编译器进行编译的模型主要围绕 crate 。 每个编译过程处理独立的源码形式的 crate ,如果编译成功,将生成单个二进制 crate : 可执行文件或某种类型的库。2
译注: crate 概念不仅限于源码,编译后的文件也是 crate 的一部分。
crate 是编译、链接、版本控制、分发和运行时加载的基本单位。 crate 包含了一个嵌套的 模块 树 。 该树的最高层是一个匿名模块。 条目都有一个规范的 模块路径 以查找它在的模块树中的位置。
Rust 编译器总是使用单个源码文件作为输入,并输出产生单个 crate 。
在编译该源码文件时会加载其他源码文件模块。
源码文件的扩展名为 .rs
。
一个源码文件就是指一个模块,通过源码文件中引用 模块 的条目从外部定义了模块的名称。 crate 的名称通过属性或外部定义。 一个模块文件中可以嵌套定义子模块。
每个源码文件 (即模块) 包含了零个或多个 条目 ,对于添加的模块 属性 往往会影响编译器的行为。 顶层模块可以添加应用于 crate 的属性。
#![allow(unused)] fn main() { // 以下是添加到顶层模块的属性 // 指定 crate 名称 #![crate_name = "projx"] // 指定输出 crate 制品的类型 #![crate_type = "lib"] // 这个属性可以添加到子模块中 #![warn(non_camel_case_types)] // 开启警告。 }
字节顺序标记
可选的 UTF8 字节顺序标记 (UTF8BOM 产生式) 表明该文件使用 UTF8 编码。 只能出现在源码文件的开头,是给解释器实现的预留,会被编译器忽略。
执行注解
源码文件可以包含 执行注解 ,表明操作系统该使用哪个程序来执行此文件,相当于以脚本方式执行。 执行注解只能出现在文件开头,在可选的 UTF8BOM 之后。 同样是给解释器的预留,会被编译器忽略。 例如:
#!/usr/bin/env rustx
fn main() {
println!("Hello!");
}
对于执行注解语法,存在限制以避免与 属性 混淆。
#!
字符忽略 空白 及注释后,不能紧跟 [
标记,否则,会被编译器当成属性。
预定义 和 no_std
本节已移至 预定义章节 。
Main 函数
如果要把 crate 编译成可执行文件,需要包含一个 main
函数 。
此 main
函数,不接受任何参数,不能声明任何 trait 或生命周期约束,也不能有任何 where
从句,且返回类型必须实现 Termination
trait。
fn main() {}
fn main() -> ! { std::process::exit(0); }
fn main() -> impl std::process::Termination { std::process::ExitCode::SUCCESS }
注意: 标准库中具有
Termination
实现的类型包括:
()
!
Infallible
ExitCode
Result<T, E> where T: Termination, E: Debug
no_main
属性
no_main
属性可以应用于 crate 顶层模块,从而可以禁用在编译为可执行二进制文件时 main
符号的输出。
从而已经定义了 main
函数的 carte 可以被其他 crate 链接。
crate_name
属性
crate_name
属性 可以应用于 crate 顶层模块,用来指定 crate 名称,语法为 元名称值字符串 。
#![allow(unused)] #![crate_name = "mycrate"] fn main() { }
该名称不能为空,只能包含 Unicode 字母数字 或 _
(U+005F) 字符。
在解释器中,同样有所区分。应该先作静态检查,例如语法分析、类型检查和代码分析,然后执行。
crate 编译模型类似于 ECMA-335 CLI 模型中的 assembly , SML/NJ 编译管理器中的 library ,Owens 和 Flatt 模块系统中的 unit,或 Mesa 中的 configuration 。
条件编译
语法
配置断言 :
配置选项
| 配置All
| 配置Any
| 配置Not配置选项 :
标识符 (=
(字符串字面值 | 原始字符串字面值))?配置All
all
(
配置断言列表?)
配置Any
any
(
配置断言列表?)
配置Not
not
(
配置断言)
配置断言列表
配置断言 (,
配置断言)*,
?
条件编译源码 是指根据某些条件来排除部分源码,或者包含部分源码。
条件编译,使用内置的 cfg
cfg_attr
属性 和 cfg
宏 来实现。
条件包括: 编译 crate 时的目标架构、传递给编译器的参数,其他一些可选项。
条件编译会进行 配置断言 ,断言的结果为 true 或 false 。 断言有以下几种形式:
- 配置选项: 如果该选项被设置则为 true ,否则为 false 。
all()
: 以逗号分隔的配置断言列表。如果至少有一项断言为 false ,则结果为 false 。如果没有断言,则为 true 。any()
: 以逗号分隔的配置断言列表。如果至少有一项断言为 true ,则结果为 true 。如果没有断言,则为 false 。not()
: 有一个配置断言。如果其断言为 false ,则为 true ,如果其断言为 true ,则为 false 。
配置选项 是被设置或未设置的名称与键值对。
名称是单个标识符,例如 unix
。
键值对是一个标识符,后跟一个字符串 。
例如, target_arch = "x86_64"
配置选项。
注意: 等号周围的空白将被忽略。
foo="bar"
和foo = "bar"
是等价的配置选项。
键在键值对配置选项的集合中不是唯一的。例如,可以同时设置 feature = "std"
和 feature = "serde"
。
设置配置选项
哪些配置选项被设置是在编译期确定。 某些配置选项是由编译器的状态来确定,称为编译器设置。 其他选项由用户设置,非源码中,而由外部输入给编译器。 正在编译的 crate 无法通过源码设置配置选项。
注意: 对于
rustc
,用户输入的配置选项可以使用--cfg
标志设置。
注意: 键为
feature
的配置选项通常通过 Cargo 配置清单设置,可参阅 Cargo 文档。
警告:输入的配置选项有可能与编译器的值相同。
例如,在 Windows 平台编译目标时输入 rustc --cfg "unix" program.rs
,这将同时设置 unix
和 windows
配置选项。
这不是一个良好的做法。
target_arch
目标 CPU 架构。 该值类似于 '目标平台三元组' 的第一个元素,但并不完全一致。
示例值:
"x86"
"x86_64"
"mips"
"powerpc"
"powerpc64"
"arm"
"aarch64"
target_feature
当前编译目标平台所提供的特性。
示例值:
"avx"
"avx2"
"crt-static"
"rdrand"
"sse"
"sse2"
"sse4.1"
详见 target_feature
属性 。
crt-static
表示可用的 静态 C 运行时库 。
target_os
是编译时目标平台操作系统,仅可设定一次。 该值类似于 '平台目标三元组' 的第二个和第三个元素。
示例值:
"windows"
"macos"
"ios"
"linux"
"android"
"freebsd"
"dragonfly"
"openbsd"
"netbsd"
target_family
是目标平台更一般的描述,目标平台所处的操作系统或体系结构。 该键可以设置多个。
示例值:
"unix"
"windows"
"wasm"
unix
和 windows
如果设置了 target_family = "unix"
,则意味着 unix
被设置。
如果设置了 target_family = "windows"
,则意味着 windows
被设置。
target_env
是目标平台进一步的区分信息,例如有关所使用的 ABI 或 libc
的信息。
由于历史原因,只有在实际需要区分时,此值才会被定义为非空字符串。
因而,比如在许多 GNU 平台上,此值将为空。此值类似于平台目标三元组的第四个元素。
有所不同的是,像 gnueabihf
这样的嵌入式 ABI 将简单地将 target_env
定义为 "gnu"
。
示例值:
""
"gnu"
"msvc"
"musl"
"sgx"
target_endian
用于描述目标平台的字节序,取值为 "little" 或 "big" ,即 '大端' 或 '小端' ,根据目标机器的 CPU 决定。
target_pointer_width
该值只设置一次,为目标平台的指针宽度 (以位为单位) 。
示例值:
"16"
"32"
"64"
target_vendor
该值只设置一次,其键是目标平台的供应商 (vendor) 。
示例值:
"apple"
"fortanix"
"pc"
"unknown"
target_has_atomic
目标平台支持每个比特宽度原子加载、存储和比较交换操作时,将设置该值。
当这个配置项存在时,所有适用于相关原子宽度的稳定的 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
属性
语法
CfgAttr属性 :
cfg
(
配置断言)
cfg
属性 根据 配置断言 决定是否排除所附着的条目。
语法为 cfg
(
配置断言 )
。
如果断言为 true,则将重新编写所附条目,移除 cfg
属性。
如果断言为 false,则排除对应条目源码。
一些示例:
#![allow(unused)] fn main() { // 仅为 macOS 平台编译时,包含该函数源码。 #[cfg(target_os = "macos")] fn macos_only() { // ... } // 只有在定义了 foo 或 bar 的时,包含该函数源码 #[cfg(any(foo, bar))] fn needs_foo_or_bar() { // ... } // 只有在 32 位架构的 unixish 操作系统编译时,包含该函数源码。 #[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
属性可在任何允许属性的位置使用。
cfg_attr
属性
语法
CfgAttr属性 :
cfg_attr
(
_配置断言,
CfgAttr组?)
cfg_attr
属性 可根据条件断言来选择性为所属条目添加其他属性。
当配置断言为真,将展开断言部分后面所列出的属性。
例如,以下模块将基于目标平台在 linux.rs
或 windows.rs
中找到:
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(windows, path = "windows.rs")]
mod os;
所列出的属性可以是 0 个、 1 个或多个。多个属性将会分别展开为单独的属性。比如:
#[cfg_attr(feature = "magic", sparkles, crackles)]
fn bewitched() {}
// 当 `magic` 特性标志启用时,上述源码将展开为:
#[sparkles]
#[crackles]
fn bewitched() {}
注意:
cfg_attr
可以去展开另一个cfg_attr
。 比如,#[cfg_attr(target_os = "linux", cfg_attr(feature = "multithreaded", some_other_attribute))]
是有效的。 这个例子等价于#[cfg_attr(all(target_os = "linux", feature ="multithreaded"), some_other_attribute)]
。
cfg_attr
属性可在任何允许属性的地方使用。
cfg
宏
内置的 cfg
宏经过配置断言,展开为 true
或 false
的字面值源码。
示例:
#![allow(unused)] fn main() { let machine_kind = if cfg!(unix) { "unix" } else if cfg!(windows) { "windows" } else { "unknown" }; println!("I'm running on a {} machine!", machine_kind); }
条目
语法:
条目:
外围属性*
可见性条目
| 宏条目可见性条目:
可见性?
(
模块
| 外部Crate
| Use声明
| 函数
| 类型别名
| 结构体
| 枚举
| 联合体
| 常量条目
| 静态条目
| Trait
| 实现
| 外部块
)
条目 是 crate 的具体组件。条目在 模块 中排列。 所有条目都有自己的 路径 以决定处在模块树的位置。
条目在编译时将完全确定,在运行时通常静态不变,且在内存中只读。
有以下几种类型的条目:
某些条目会形成隐式的作用域,从而包含子条目。 也就是说,在函数或模块内,条目的声明 (在许多情况下) 可以与语句、控制块等混合, 该条目的含义与在作用域外部声明相同 — 但条目仍然是静态的 — 有所不同的是,该条目在命名空间内的 路径 被所包围条目的名称限定,或者仅对于所包围条目是私有的 (在函数的情况下) 。 相关条目的语法解释了子条目声明可能出现的确切位置。
模块
模块是包含零个或多个 条目 的容器。
模块条目 是用一对花括号括起来的、以 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!(); } } }
模块和类型共享相同的命名空间。
在作用域中使用与模块同名的类型是被禁止的:也就是说,类型定义、trait、struct、enum、union、类型参数或 crate 不能隐藏作用域中模块的名称,反之亦然。使用 use
引入作用域的条目也有此限制。
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
的文件中。
上面的例子也可以使用名称为 util/mod.rs
的文件来表达 crate::util
的内容。
不允许同时存在 util.rs
和 util/mod.rs
。
注意: 注意:在 Rust 1.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 文件的目录,包括内联模块作为目录的组件。
对于非 mod-rs 文件,它是相同的,只是路径以非 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 和非 mod-rs 文件):
#[path = "thread_files"]
mod thread {
// 从 `thread_files/tls.rs` 中加载 `local_data` 模块,相对于这个源文件的目录。
#[path = "tls.rs"]
mod local_data;
}
模块的属性
模块和所有条目一样,可以接受外围属性,同时也可以接受内部属性,
内部属性可以出现在模块主体的左括号 {
之后,也可以出现在源文件的开头,在可选的 BOM 和执行注解之后。
内置的对模块有意义的属性包括 cfg
、 deprecated
、 doc
、 代码分析检查属性 、 path
和 no_implicit_prelude
。模块还接受宏属性。
外部crate声明
语法:
外部Crate :
extern
crate
Crate引用 As从句?;
Crate引用 :
标识符 |self
As从句 :
as
( 标识符 |_
)
一个 extern crate
声明 指定了对一个外部 crate 的依赖。
外部 crate 绑定到 extern crate
声明中提供的标识符。
此外,如果 extern crate
出现在 crate 根,则 crate 名称也会被添加到 extern 预定义 中,刚自动处于所有模块的作用域内。
可以使用 as
从句将导入的 crate 绑定到不同的名称。
编译时,外部 crate 会被解析为一个特定的 soname
,并且对于该 soname
的运行时链接需求会在编译时传递给链接器进行加载。
编译时,编译器会扫描其库路径,并根据外部 crate 编译时声明的 crate_name
属性 与可选的 crate_name
进行匹配,以解析出 soname
。
如果没有提供 crate_name
,则会假定一个默认的 name
属性,等于 extern crate
声明中给出的 标识符。
可以导入 self
作为当前 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
属性
可在 extern crate
条目上指定 no_link
属性 ,以阻止将 crate 链接到输出中。
这通常仅用于加载和访问 crate 中的宏。
Use 声明
语法:
Use声明 :
use
Use树;
Use树 :
(简单路径?::
)?*
| (简单路径?::
)?{
(Use树 (,
Use树 )*,
?)?}
| 简单路径 (as
( 标识符 |_
) )?
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
声明只在其所在的模块中可见。
如果在 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
Paths
注意: 本节内容不完整。
以下是一些关于 use
声明能够和不能够正常工作的例子:
#![allow(unused_imports)] use std::path::{self, Path, PathBuf}; // good: std 是 crate 名称 use crate::foo::baz::foobaz; // good: Foo 位于 crate 的根 mod foo { pub mod example { pub mod iter {} } use crate::foo::example::iter; // good: foo 位于 crate 的根 // use example::iter; // bad 在 2015 版次: 相对路径不允许没有 `self` ; 2018 版good use self::baz::foobaz; // good: Self 引用模块 'foo' use crate::foo::bar::foobar; // good: foo 位于 crate 的根 pub mod bar { pub fn foobar() { } } pub mod baz { use super::bar::foobar; // good: Super 指的是模块 foo pub fn foobaz() { } } } fn main() {}
版次差异: 在 2015 版本中,
use
路径还允许访问 crate 根中的条目。 使用上面的例子,以下use
路径在 2015 版中可以使用但在 2018 版中无法使用:mod foo { pub mod example { pub mod iter {} } pub mod baz { pub fn foobaz() {} } } use foo::example::iter; use ::foo::baz::foobaz; fn main() {}
2015 版不允许
use
声明引用 extern 预定义 。因此,在 2015 中,仍需要使用extern crate
声明来引用use
声明中的外部 crate。从 2018 版开始,use
声明可以像extern crate
一样指定一个外部 crate 依赖。在 2018 版本中,如果在作用域内存在与外部 crate 相同的名称,则使用该 crate 名称需要在前面加上
::
以明确选择 crate 名称。这是为了保留与未来潜在更改的兼容性。// use std::fs; // Error, 有歧义 use ::std::fs; // 从 std crate 导入,而不是下面的模块。 use self::std::fs as self_fs; // 导入下面的模块。 mod std { pub mod fs {} } fn main() {}
下划线导入
条目可以通过在路径前加一个下划线的形式 use path as _
导入,而不必绑定到一个名字。
这种方法特别适用于导入 trait ,以便可以使用 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 _; }
函数
语法
函数 :
函数修饰符组fn
标识符 泛型参数组?
(
函数参数组?)
函数返回类型? Where从句?
( 块表达式 |;
)函数修饰符组 :
const
?async
1?unsafe
? (extern
Abi?)?函数参数组 :
Self参数,
?
| (Self参数,
)? 函数参数 (,
函数参数)*,
?Self参数 :
外围属性* ( 简写Self | 类型化Self )简写Self :
(&
|&
生命周期)?mut
?self
类型化Self :
mut
?self
:
类型函数参数 :
外围属性* ( 函数参数模式 |...
| 模式 2 )函数参数模式 :
模式非顶层项:
( 类型 |...
)函数返回类型 :
->
类型1在 2015 版中不允许使用
async
限定。2在 2015 版中,只有在一个 trait 条目 的关联函数中才允许使用仅带有类型的函数参数。
函数包括一个 块 ,以及一个名称、一组参数和一个输出类型。
除名称外,其他是可选的。函数用关键字 fn
声明。
函数可以声明一组 输入 变量 作为参数,通过这些变量,调用者将参数传递给函数,并且函数的 输出 类型为函数完成时将返回给其调用者的 类型 。
如果未显式指定输出类型,则为 单元类型 。
当引用一个函数时,会产生一个大小为零的 函数条目类型 第一类 值 ,当通过引用调用时会直接调用该函数。
例如,以下是一个简单的函数:
#![allow(unused)] fn main() { fn answer_to_life_the_universe_and_everything() -> i32 { return 42; } }
函数参数
函数参数是不可拒绝的 模式 ,对于在无其他附加的 let
绑定中有效的模式在参数中也是有效的:
#![allow(unused)] fn main() { fn first((value, _): (i32, i32)) -> i32 { value } }
如果第一个参数是 Self参数 ,则表示该函数是一个 方法 。
带有 self
参数的函数只能作为 trait 或 实现 中的关联函数出现。
具有 ...
符号的参数表示是一个 可变参数函数 ,且只能作为 外部块函数 的最后一个参数。
可变参数可以有一个可选的标识符,例如 args: ...
。
函数体
函数的代码块在概念上被包装在一个块中,该块绑定了参数模式,然后 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 约束,以允许在该类型的值上调用该 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 { // details elided } foo(&[1, 2]); }
将类型参数 T
实例化为 i32
。
函数的类型参数还可以在函数名后跟一个后缀 路径 组件来明确指定。
如果没有足够的上下文来确定类型参数,则可能需要这样做。例如,mem::size_of::<u32>() == 4
。
外部函数修饰
extern
函数修饰符允许提供函数 定义组 ,这些函数可以使用特定的 ABI 进行调用:
extern "ABI" fn foo() { /* ... */ }
这些通常与 外部块 条目结合使用,可以提供用来调用函数的声明,而不必提供其函数 定义 :
extern "ABI" {
fn foo(); /* no body */
}
unsafe { foo() }
当在函数条目中省略 "extern" Abi?*
时,ABI 被分配 "Rust"
。例如:
#![allow(unused)] fn main() { fn foo() {} }
等价于:
#![allow(unused)] fn main() { extern "Rust" fn foo() {} }
ABI 使用非 "Rust" 的函数,允许其他编程语言调用该函数,比如 C 语言:
#![allow(unused)] fn main() { // 用 "C" ABI 声明函数 extern "C" fn new_i32() -> i32 { 0 } // 用 "stdcall" ABI 声明函数 #[cfg(target_arch = "x86_64")] extern "stdcall" fn new_i32_stdcall() -> i32 { 0 } }
与 外部块 类似,当使用 extern
关键字但省略了 "ABI"
时,使用的 ABI 默认为 "C"
。例如:
#![allow(unused)] fn main() { extern fn new_i32() -> i32 { 0 } let fptr: extern fn() -> i32 = new_i32; }
等价于:
#![allow(unused)] fn main() { extern "C" fn new_i32() -> i32 { 0 } let fptr: extern "C" fn() -> i32 = new_i32; }
非 "Rust" 函数的 ABI (应用程序二进制接口) 不能像 Rust 一样支持栈回退,因此尝试在这些 ABI 中回退超过函数末尾的部分会导致进程中止。
注意:
rustc
的 LLVM 后端通过执行非法指令来使进程中止。
Const 函数
被 const
修饰的函数是 常量函数 ,同样的构造函数 元组结构体 和 元组变体 也是。
常量函数 可以在 常量上下文 中被调用。
const
函数可以使用 extern
函数限定符,但是只能使用 "Rust"
和 "C"
ABI。
Const 函数不允许是 async 。
异步函数
函数可以被标记为 async "异步" 的,这也可以与 unsafe
关键字组合使用。
#![allow(unused)] fn main() { async fn regular_example() { } async unsafe fn unsafe_example() { } }
异步函数在调用时,不会立即执行,而是将其参数捕获为一个 "future" 。当该 future 被轮询时,会执行函数体。
一个 async 异步函数大致等同于一个返回 impl Future
的函数,并且具有一个 async move
块 作为函数体:
#![allow(unused)] fn main() { // Source 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
块 捕获了所有函数参数,包括那些未使用或绑定到_
模式的参数。 这确保了函数参数以与非异步函数相同的顺序被释放,除了释放操作在完全等待返回的 future 后发生。
有关 async
的更多信息,请参见 async
块。
版本差异: 异步函数仅在 Rust 2018 开始提供。
组合 async
和 unsafe
在 Rust 中,声明既是 async
又是 unsafe
的函数是合法的。
这样产生的函数是非安全的,不能安全地调用,但它 (像任何异步函数一样) 返回一个 future。
这个 future 是普通的 future,因此在 "await" 它的时候不需要使用 unsafe
上下文。
#![allow(unused)] fn main() { // 返回一个 future ,当被等待时,解除对 `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
关键字在异步函数中的使用方式与其在其他函数中的使用方式相同:它表示该函数对其调用者有一些额外的义务,以确保其安全性。
与任何其他非安全函数一样,这些条件可能会超出初始调用本身的范围。
例如,在上面的代码段中, unsafe_example
函数接受一个指针 x
作为参数,然后 (当等待时) 对该指针进行解引用。
这意味着 x
必须在 future 执行完成之前保持有效,并且调用者有责任确保这一点。
函数的属性
函数可以使用 外围属性 。 内部属性 可以直接在函数的 块 里的 {
后面使用。
以下示例展示了在函数中使用内部属性。该函数的文档注释只包含单词 "Example" 。
#![allow(unused)] fn main() { fn documented() { #![doc = "Example"] } }
注意: 除了用于代码分析 ,通常仅在函数条目上使用外围属性是惯用的。
函数可用的属性包括 cfg
、 cfg_attr
、 deprecated
、 doc
、 export_name
、 link_section
、 no_mangle
、代码分析 、 must_use
、 过程宏属性 、 测试 和 优化提示 。此外,函数还接受属性宏。
函数参数的属性
函数参数上允许使用 外围属性 ,其中允许使用的 内置属性 仅限于 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
过程宏负责检测其是否存在,并将其从输出令牌流中删除。
#[some_proc_macro_attribute]
fn foo_oof(#[some_inert_attribute] arg: u8) {
}
类型别名
语法
类型别名组 :
type
标识符 泛型参数组? (:
类型参数约束组 )? Where从句? (=
类型 Where从句?)?;
类型别名 为现有 类型 定义了一个新的名称。类型别名使用关键字 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); // Doesn't work }
当类型别名不作为关联类型使用时,必须包含一个 类型 ,不能包含 类型参数约束组 。
在 trait 中使用作为 关联类型 的类型别名,不应该包含类型本身的声明 类型 ,但是可以包含类型参数约束声明 类型参数约束组 。
类型别名在 trait impl 中作为 关联类型 使用时,必须包括 类型 ,但不能包括 类型参数约束组 。
在 trait impl 中,位于类型别名等号之前的 where 从句已被弃用 (例如 type TypeAlias<T> where T: Foo = Bar<T>
) 。
首选的是 where 从句位于等号之后 (例如 type TypeAlias<T> = Bar<T> where T: Foo
) 。
结构
语法
结构体 :
Struct结构体
| 元组结构体Struct结构体 :
struct
标识符 泛型参数组? Where从句? ({
结构体字段组?}
|;
)元组结构体 :
struct
标识符 泛型参数组?(
元组字段组?)
Where从句?;
结构体字段组 :
结构体字段 (,
结构体字段)*,
?元组字段组 :
元组字段 (,
元组字段)*,
?
结构体 是使用关键字 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
属性 指定特定的布局。
枚举
语法
枚举 :
enum
标识符 泛型参数组? Where从句?{
枚举条目组?}
枚举条目组 :
枚举条目 (,
枚举条目 )*,
?枚举条目 :
外围属性* 可见性?
标识符 ( 枚举条目元组 | 枚举条目结构体 )? 枚举条目判别值?枚举条目元组 :
(
元组字段组?)
枚举条目结构体 :
{
结构体字段组?}
枚举条目判别值 :
=
表达式
一个 枚举类型 , 简称枚举 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
则仅称为枚举变体。
没有构造器包含字段的枚举称为 field-less enum "无字段枚举" 。例如,以下是一个无字段枚举:
#![allow(unused)] fn main() { enum Fieldless { Tuple(), Struct{}, Unit, } }
如果一个不包含字段的枚举只包含单元枚举变体,则该枚举称为 unit-only enum "单元枚举"。例如:
#![allow(unused)] fn main() { enum Enum { Foo = 3, Bar = 2, Baz = 1, } }
判别值
每个枚举实例都有一个 判别值 :一个逻辑上与之关联的整数,用来确定它持有的变体。
在 默认表示 下,判别值解释为 isize
值。
然而,编译器允许在实际内存布局中使用较小的类型 (或其他区分变体的方式) 。
分配判别值
显式判别值
在两种情况下,可以通过在变体名称后跟 =
和 常量表达式 来明确设置变体的判别值 :
-
如果该枚举是 "单元枚举" 。
-
如果使用 原始表示 。例如:
#![allow(unused)] fn main() { #[repr(u8)] enum Enum { Unit = 3, Tuple(u16), Struct { a: u8, b: u16, } = 1, } }
隐式判别值
如果枚举变体的判别值没有指定,则它被设置为在声明中前一个变体的判别值加 1 。如果第一个变体的判别值未指定,则设置为零。
#![allow(unused)] fn main() { enum Foo { Bar, // 0 第一个未指定,则为0 Baz = 123, // 123 Quux, // 124 未指定,相比前一个加1 } 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 (collision with previous!) } }
如果先前枚举的判别值达到了其类型能够表示的最大值,那么下一个没有指定判别值的项将导致错误。
#![allow(unused)] fn main() { #[repr(u8)] enum OverflowingDiscriminantError { Max = 255, MaxPlusOne // 将是 256 ,但那会使枚举溢出。 } #[repr(u8)] enum OverflowingDiscriminantError2 { MaxMinusOne = 254, // 254 Max, // 255 MaxPlusOne // 将是 256 ,但那会使枚举溢出。 } }
访问判别值
通过 mem::discriminant
mem::discriminant
返回一个不透明的引用,指向枚举值的判别值,可以进行比较。但不能用于获取判别值。
转换
如果一个枚举类型是 单元枚举 ,那么它的判别值可以通过 数字强转 直接访问;比如:
#![allow(unused)] fn main() { enum Enum { Foo, Bar, Baz, } assert_eq!(0, Enum::Foo as isize); assert_eq!(1, Enum::Bar as isize); assert_eq!(2, Enum::Baz as isize); }
无成员的枚举 可以被强制类型转换,如果它们没有显式的判别值,或者只有单元变体是显式的。
#![allow(unused)] fn main() { enum Fieldless { Tuple(), Struct{}, Unit, } assert_eq!(0, Fieldless::Tuple() as isize); assert_eq!(1, Fieldless::Struct{} as isize); assert_eq!(2, Fieldless::Unit as isize); #[repr(u8)] enum FieldlessWithDiscrimants { First = 10, Tuple(), Second = 20, Struct{}, Unit, } assert_eq!(10, FieldlessWithDiscrimants::First as u8); assert_eq!(11, FieldlessWithDiscrimants::Tuple() as u8); assert_eq!(20, FieldlessWithDiscrimants::Second as u8); assert_eq!(21, FieldlessWithDiscrimants::Struct{} as u8); assert_eq!(22, FieldlessWithDiscrimants::Unit as u8); }
指针转换
如果枚举指定了 原始表示 ,那么可以通过非安全的指针转换来可靠地访问判别值:
#![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 {} }
零变体枚举被认为是 永不类型 的等效形式,但是它们不能被强制转换为其他类型。
#![allow(unused)] fn main() { enum ZeroVariants {} let x: ZeroVariants = panic!(); let y: u32 = x; // 类型不匹配的错误 }
变体可见性
枚举变体在语法上允许使用 可见性 注释,但在验证枚举时,会被拒绝。 这使得可以在使用它们的不同上下文中使用统一的语法来解析条目。
#![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 } } }
联合体
联合体与结构体声明使用相同的语法,只是将 struct
替换为 union
。
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
联合体的关键特性是,所有的字段共享相同的存储空间。 因此,对联合体的一个字段的写操作可能会覆盖其它字段的值,而联合体的大小则由其最大字段的大小决定。
联合体字段类型限制为以下类型的子集:
Copy
类型- 引用 (
&T
和&mut T
为任意的T
) ManuallyDrop<T>
(为任意的T
)- 元组和数组仅允许包含联合体字段类型
这个限制确保了联合体字段不需要被丢弃。
与结构体和枚举类似,可以为联合体实现 Drop
trait 来手动定义在释放时发生的行为。
初始化联合体
联合体类型的值可以使用与结构体类型相同的语法创建,只不过必须指定一个字段:
#![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表示法 写入然后读取联合体类似于从用于写入的类型到用于读取的类型的 形变
。
因此,所有对联合体字段的读取都必须放在 unsafe
块中:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; } }
通常,使用联合体的代码会在非安全的联合体字段访问外围提供安全的包装。
相比之下,写入联合体字段是安全的,因为它们只是覆盖任意数据,而不能导致未定义的行为。 (请注意,联合体字段类型永远不会具有粘联的丢弃 ,因此联合体字段写入永远不会隐式丢弃任何内容。)
联合体模式匹配
另一种访问联合体字段的方式是使用模式匹配。
对于联合体字段的模式匹配与结构体模式匹配使用相同的语法,不同的是模式必须指定一个且仅一个字段。
由于模式匹配类似于使用特定字段读取联合体,因此它也必须放在 unsafe
块中。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } fn f(u: MyUnion) { unsafe { match u { MyUnion { f1: 10 } => { println!("ten"); } MyUnion { f2 } => { println!("{}", f2); } } } } }
模式匹配可能将联合体作为较大结构的字段进行匹配。 特别地,当使用 Rust 联合体通过 FFI 实现 C 中的标记 union 时,得以同时匹配标记和相应的字段:
#![allow(unused)] fn main() { #[repr(u32)] enum Tag { I, F } #[repr(C)] union U { i: i32, f: f32, } #[repr(C)] struct Value { tag: Tag, u: U, } fn is_zero(v: Value) -> bool { unsafe { match v { Value { tag: Tag::I, u: U { i: 0 } } => true, Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true, _ => false, } } } }
联合体字段的引用
由于联合体字段共享存储空间,因此获得对联合体的一个字段的写访问权限可以给予对其所有其余字段的写访问权限。 借用检查规则必须调整以考虑这一点。 因此,如果联合体的一个字段被借用,则其所有其余字段在相同的生命周期内也被借用。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } // ERROR: 不能将 `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); } }
你所看到的是,在很多方面 (除了布局、安全性和所有权) 上,联合体和结构体表现得非常相似,这是由于从结构体继承了语法形式。 这在语言中的许多未提及方面也同样适用 (例如私有性、名称解析、类型推断、泛型、trait实现、内部实现、一致性、模式检查等等) 。
常量条目
常量条目 是一个可选名称的、不与程序中特定内存位置关联的 常量值 。
常量在使用时本质上是内联的,这意味着当它们在相关上下文中使用时,会被直接拷贝。
这包括使用来自外部 crate 的常量,以及 非-Copy
类型。对同一常量的引用不能保证引用同一内存地址。
常量必须显式指定类型。该类型必须具有 '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。 // prints "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); } }
静态条目
静态条目 与 常量 类似,不同之处在于它表示程序中精确的内存位置。
所有对静态条目的引用都指向同一内存位置。静态条目具有 static
生命周期,其生命周期超过 Rust 程序中的所有其他生命周期。静态条目在程序结束时不会调用 drop
函数。
静态初始化器是一个在编译时计算的 常量表达式 。 静态初始化器可能会引用其他的静态。
非 mut
的静态条目,如果包含的类型不是 内部可变类型 ,则可以被放置在只读内存中。
所有对静态的访问都是安全的,但是对静态有一些限制:
- 该类型必须有
Sync
trait ,以允许线程安全的访问。 - 常量不能引用静态。
在 外部块 中必须省略静态条目的初始化表达式,并且必须为自由静态条目提供初始化表达式。
静态 & 泛型
在泛型作用域中定义的静态条目 (例如在覆盖或默认实现中) 将恰好定义一个静态条目,就好像静态定义从当前作用域被提取到模块中一样。 不会为每个单态化生成一个静态条目。
代码:
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 的一个目标是难以出现并发错误,这显然是导致竞态条件或其他错误的一个很大源头。
因此,在读取或写入可变静态变量时,需要使用 unsafe
块。
应该注意确保对可变静态变量的修改,对与运行在同一进程中的其他线程方面是安全的。
可变静态变量仍然非常有用。它们可以与 C 语言库一起使用,并且还可以在 extern
块中绑定来自 C 语言的库。
#![allow(unused)] fn main() { fn atomic_add(_: &mut u32, _: u32) -> u32 { 2 } static mut LEVELS: u32 = 0; // 这违反了没有共享状态的理念,而且这在内部也不能防止竞争,所以这个函数是 `unsafe` 的。 unsafe fn bump_levels_unsafe1() -> u32 { let ret = LEVELS; LEVELS += 1; return ret; } // 假设我们有一个 atomic_add 函数,返回旧值, // 这个函数是 "safe" ,但是返回值的含义可能不是调用者所期望的,所以它仍然被标记为 `unsafe` 。 unsafe fn bump_levels_unsafe2() -> u32 { return atomic_add(&mut LEVELS, 1); } }
可变静态变量与普通静态变量具有相同的限制,只是类型不必实现 Sync
trait。
使用静态或常量
在选择使用常量条目或静态条目时可能会感到困惑。 通常情况下,应该优先使用常量而非静态条目,除非以下情况之一:
- 需要存储大量的数据
- 需要静态的单地址特性。
- 需要内部可变性。
Traits
语法
Trait :
unsafe
?trait
标识符 泛型参数组? (:
类型参数约束组? )? Where从句?{
内部属性*
关联条目*
}
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 约束
泛型条目可以使用 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); } }
对象安全
对象安全的 trait 可以作为 trait 对象 的基础 trait 。 如果具有以下特性 (在RFC 255中定义) ,则 trait 是 对象安全 的:
- 所有的 父级traits 也必须是对象安全的。
Sized
不能是 父级traits 。换句话说,必须不要求Self: Sized
。- 不能有任何关联常量。
- 不能有任何泛型关联类型。
- 所有关联函数必须是可从 trait 对象中可派发的,或者是明确不可派发的。
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::pin::Pin; // 对象安全方法的示例。 trait TraitMethods { fn by_ref(self: &Self) {} fn by_ref_mut(self: &mut Self) {} fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} fn with_lifetime<'a>(self: &'a Self) {} fn nested_pin(self: Pin<Arc<Self>>) {} } struct S; impl TraitMethods for S {} let t: Box<dyn TraitMethods> = Box::new(S); }
#![allow(unused)] fn main() { // 这个 trait 是对象安全的,但这些方法不能在 trait 对象上派发。 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(); // ERROR: 不能调用 Self 返回 obj.param(S); // ERROR: 不能调用 Self 参数 obj.typed(1); // ERROR: 不能调用泛型类型 }
#![allow(unused)] fn main() { use std::rc::Rc; // 非对象安全 trait 的示例。 trait NotObjectSafe { const CONST: i32 = 1; // ERROR: 不能有关联 const fn foo() {} // ERROR: 关联函数没有 Sized fn returns(&self) -> Self; // ERROR: Self 在返回类型 fn typed<T>(&self, x: T) {} // ERROR: 有泛型类型参数 fn nested(self: Rc<Box<Self>>) {} // ERROR: 尚不支持嵌套的接收者 } struct S; impl NotObjectSafe for S { fn returns(&self) -> Self { S } } let obj: Box<dyn NotObjectSafe> = Box::new(S); // ERROR }
#![allow(unused)] fn main() { // Self: Sized traits 不是 object-safe. trait TraitWithSize where Self: Sized {} struct S; impl TraitWithSize for S {} let obj: Box<dyn TraitWithSize> = Box::new(S); // ERROR }
#![allow(unused)] fn main() { // 如果 `Self` 是类型参数,则不是对象安全的。 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); // ERROR: 不能使用 `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` 的 supertrait `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 traits
Trait 中以 unsafe
关键字开头的条目表示实现该 trait 可能是非安全的。
使用正确实现的非安全 trait 是安全的。相应的 trait 实现 也必须以 unsafe
关键字开头。
Sync
和 Send
是 unsafe trait 的示例。
参数模式
未设置函数或方法的实现体只允许使用 标识符 或者 _
通配符模式。
目前允许使用 mut
标识符,但是这种方式已经被弃用,并且将来会变成一个严格的错误。
在 2015 版中,trait 函数或方法参数的模式是可选的。
#![allow(unused)] fn main() { // 2015 版 trait T { fn f(i32); // 不需要参数标识符。 } }
函数或方法的参数模式种类被限制为以下之一:
从 2018 版开始,函数或方法参数模式不再是可选的。 此外,只要有函数体,所有不可拒绝模式都是允许的。 如果没有函数体,则仍然受到上述限制。
#![allow(unused)] fn main() { trait T { fn f1((a, b): (i32, i32)) {} fn f2(_: (i32, i32)); // 没有主体不能使用元组模式。 } }
条目可见性
Trait 中的条目在语法上允许添加 可见性 注解,但是当验证该 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(); }
实现
语法
实现 :
内部实现 | Trait实现内部实现 :
impl
泛型参数组? 类型 Where从句?{
内部属性*
关联条目*
}
Trait实现 :
unsafe
?impl
泛型参数组?!
? 类型路径for
类型
Where从句?
{
内部属性*
关联条目*
}
实现 条目将 实现类型 与其中条目相关联。
实现使用关键字 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` 中没有使用 pub 。 // values::Color::red(); }
Trait 实现
trait实现 和内部实现类似,只是可选的泛型类型声明后面是 trait ,再跟着 for
关键字,最后是指向具名类型的路径。
该 trait 被称为 实现 trait ,由实现类型实现。
trait 所有非默认关联条目必须实现,可以重新定义其定义的默认关联条目,但无法定义任何其他条目。
与实现类型相关联的关联条目的路径是 <
,后跟到实现类型的路径,然后是 as
,后跟到 trait 的路径,之后 >
作为路径组件,然后是关联条目的路径组件。
Unsafe traits 指的是需要在 trait 实现中使用 unsafe
关键字的 trait 。
#![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 实现都可以实例化为同一类型。
唯一性规则
规定 impl<P1..=Pn> Trait<T1..=Tn> for T0
只有当以下至少一项为真时, impl
才是有效:
Trait
是 局部trait- 全部为
仅出现 未覆盖 类型参数是受限制的。需要注意的是,在一致性的目的下, 基本类型 是特殊的。
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 { /* 将整数视为比特序列 */ } }
如果泛型参数至少在一种类型的关联条目中出现,则该泛型参数 约束 了实现:
类型和常量参数必须始终约束实现。如果在关联类型中使用生命周期,则生命周期必须约束实现。
约束示例:
#![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 通过作为 GenericTrait 的一个参数来约束。 impl<T> Trait for GenericStruct<T> { /* ... */ } // 同样地, N 通过作为 ConstGenericStruct 的一个参数来约束 impl<const N: usize> Trait for ConstGenericStruct<N> { /* ... */ } // T 通过在类型 `U` 的约束中的关联类型来约束,该类型本身是约束 trait 的一个泛型参数。 impl<T, U> GenericTrait<U> for u32 where U: HasAssocType<Ty = T> { /* ... */ } // 和前面一样,除了类型是 `(U, isize)` 。 `U` 出现在包括 `T` 的类型里面,而不是类型本身。 impl<T, U> GenericStruct<U> where (U, isize): HasAssocType<Ty = T> { /* ... */ } }
非约束性的示例:
#![allow(unused)] fn main() { // 其余的都是错误,因为它们的类型或常量参数不受约束。 // T没有约束,因为它根本就没有出现。 impl<T> Struct { /* ... */ } // 由于同样的原因,N并没有受到约束。 impl<const N: usize> Struct { /* ... */ } // 在实现中使用 T 并不会约束 impl。 impl<T> Struct { fn uses_t(t: &T) { /* ... */ } } // 在 U 的绑定中, T 用作关联类型,但 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
和 代码分析 。
外部块
外部块提供了在当前 crate 中未定义的条目的 声明 ,是 Rust 实现外部函数接口 (FFI) 的基础。这类似于未经检查的导入。
在外部块中允许两种条目声明: 函数 和 静态 。
仅在 unsafe
上下文中才允许调用外部块中声明的函数或访问静态。
unsafe
关键字在 extern
关键字之前语法上是被允许的,但是在语义层面上会被拒绝。
因而宏可以从语法上使用 unsafe
关键字,然后从令牌流中移除。
函数
在外部块中声明的函数与其他 Rust 函数的声明方式相同,但是声明不能有函数体,仅以分号结束。
参数中不允许使用模式,只能使用 标识符 或 _
。不允许使用函数修饰符 (如 const
、 async
、 unsafe
和 extern
)。
外部块中的函数可以像 rust 中定义的函数一样被 rust 代码调用。 Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行转换。
在外部块中声明的函数隐式地被认为是 unsafe
的。
当将其强转为函数指针时,外部块中声明的函数类型为 unsafe extern "abi" for<'l1, ..., 'lm> fn(A1, ..., An) -> R
,
其中 'l1
, ... 'lm
为其生命周期参数, A1
, ... , An
为参数的声明类型,而 R
为返回值的声明类型。
Statics
在外部块中声明的静态的方式与 静态 在外部块之外声明的方式相同,但没有初始化表达式。 访问在外部块中声明的静态条目是非安全的,无论它是否可变,因为无法保证静态内存中的位模式是否有效,因为负责初始化该静态的代码是不确定的 (例如 C 语言) 。
就像 静态 在外部块之外声明一样,外部静态可以是不可变的也可以是可变的。 在执行任何 Rust 代码之前, 必须 先初始化不可变的静态。仅在 Rust 代码读取它之前,才将其初始化是不够的。
ABI
默认情况下,外部块假设它们正在调用的库在特定平台上使用标准 C ABI。
可以使用 abi
字符串指定其他 ABI,如下所示:
#![allow(unused)] fn main() { // 对于Windows API的接口 extern "stdcall" { } }
有三个跨平台的 ABI 字符串,以确保所有编译器支持:
extern "Rust"
-- 在 Rust 代码中编写普通fn foo()
时的默认 ABI。extern "C"
-- 与extern fn foo()
相同,使用你的 C 编译器支持的默认 ABI。extern "system"
-- 通常与extern "C"
相同,但在 Win32 上是"stdcall"
,在链接到 Windows API 本身时应使用它。
此外,还有一些特定于平台的 ABI 字符串:
extern "cdecl"
-- x86_32 C 代码的默认 ABI。extern "stdcall"
-- x86_32 上 Win32 API 的默认 ABI。extern "win64"
-- x86_64 Windows 上 C 代码的默认 ABI。extern "sysv64"
-- 非 Windows x86_64 上 C 代码的默认 ABI。extern "aapcs"
-- ARM 的默认 ABI。extern "fastcall"
--fastcall
ABI,对应于 MSVC 的__fastcall
和 GCC 和 clang 的__attribute__((fastcall))
。extern "vectorcall"
--vectorcall
ABI,对应于 MSVC 的__vectorcall
和 clang 的__attribute__((vectorcall))
。extern "efiapi"
-- 用于 UEFI 函数的 ABI。
可变函数
在外部块中的函数可以通过将 ...
指定最后一个参数为变参。
在变参之前必须至少有一个参数。可以使用标识符选择性地指定变量参数。
#![allow(unused)] fn main() { extern "C" { fn foo(x: i32, ...); fn with_name(format: *const u8, args: ...); } }
外部块的属性
以下 属性 控制外部块的行为。
link
属性
link
属性 指定编译器为 extern
块中的条目链接的本地库的名称。
它使用 元列表名称值字符串 语法来指定其输入。
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")]
extern {
// …
}
#[link(name = "CoreFoundation", kind = "framework")]
extern {
// …
}
#[link(wasm_import_module = "foo")]
extern {
// …
}
可以在空的外部块上添加 link
属性。
你可以使用它来满足代码中其他地方 (包括上游 crates) 的外部块的链接要求,而不是为每个外部块都添加属性。
链接修饰符: bundle
此修饰符仅与 static
链接类型兼容。使用任何其他类型都会导致编译器错误。
在构建 rlib 或 staticlib 时, +bundle
表示本地静态库将被打包到 rlib 或 staticlib 归档中,然后在链接最终二进制文件时从其中检索出来。
在构建 rlib 时, -bundle
表示本地静态库以名称的方式注册为该 rlib 的依赖项,并且其中的目标文件仅在链接最终二进制文件时包含,文件搜索也在最终链接期间按该名称执行。
在构建 staticlib 时, -bundle
表示本地静态库不会被包含在档案中,某些更高级别的构建系统需要在链接最终二进制文件时随后添加它。
当构建其他目标 (如可执行文件或动态库) 时,此修饰符不起作用。
修饰符的默认值是 +bundle
。
关于这个修饰符的更多实现细节可在 rustc 文档 bundle
找到 。
链接修饰符: whole-archive
此修饰符仅与 static
链接类型兼容。使用任何其他类型都会导致编译器错误。
+whole-archive
表示静态库作为整个归档进行链接,而不会丢弃任何对象文件。
这个修饰符的默认值是 -whole-archive
。
关于这个修改器的更多实现细节可在 rustc 文档 whole-archive
中找到。
链接修饰符: verbatim
此修饰符与所有链接类型兼容。
+verbatim
表示 rustc 本身不会为库名称添加任何目标指定的库前缀或后缀 (如 lib
或 .a
) ,并尝试尽可能要求链接器也是如此。
-verbatim
表示 rustc 将在将库名称传递给链接器之前添加特定目标的前缀和后缀,且不会阻止链接器隐式添加它。
这个修饰符的默认值是 -verbatim
。
关于这个修饰符的更多实现细节可在 rustc 文档 verbatim
中找到。
dylib
对比 raw-dylib
在 Windows 上,链接动态库需要向链接器提供导入库:这是一个特殊的静态库,以这样一种方式声明动态库导出的所有符号,使得链接器知道它们必须在运行时动态加载。
指定 kind = "dylib"
会指示 Rust 编译器基于 name
键链接导入库。
然后,链接器将使用其正常的库解析逻辑来找到该导入库。另外,指定 kind = "raw-dylib"
会指示编译器在编译期间生成一个导入库,并提供给链接器。
只有在 Windows 上才支持 raw-dylib
,不支持 32 位 x86 ( target_arch="x86"
) 。将其用于针对其他平台或 Windows 上的 x86 时将导致编译器错误。
link_name
属性
在 extern
块内的声明上可以指定 link_name
属性 ,以指示要为给定函数或静态导入的符号。
使用 元名称值字符串 语法来指定符号的名称。
#![allow(unused)] fn main() { extern { #[link_name = "actual_symbol_name"] fn name_in_rust(); } }
将此属性与 link_ordinal
属性一起使用会导致编译器错误。
link_ordinal
属性
可以在 extern
块内的声明上应用 link_ordinal
属性,以指示生成要链接的导入库时要使用的数字序数。
在 Windows 上,每个动态库导出的符号都有一个唯一的编号,可以在加载库时使用该编号查找该符号,而不必通过名称查找它。
警告:link_ordinal
只应在符号的序数已知为稳定时使用:如果没有在构建其所在二进制文件时显式设置符号的序数,则将自动分配一个序数,而该分配的序数可能会在二进制文件的不同构建之间发生变化。
#[link(name = "exporter", kind = "raw-dylib")]
extern "stdcall" {
#[link_ordinal(15)]
fn imported_function_stdcall(i: i32);
}
此属性仅与 raw-dylib
链接类型一起使用。使用任何其他类型都会导致编译器错误。
将此属性与 link_name
属性一起使用将导致编译器错误。
函数参数的属性
外部函数参数的属性遵循与 常规函数参数 相同的规则和限制。
泛型参数
语法
泛型参数组 :
<
>
|<
(泛型参数,
)* 泛型参数,
?>
泛型参数 :
外围属性* ( 生命周期参数 | 类型参数 | 常量参数 )生命周期参数 :
生命周期或标签 (:
生命周期约束组 )?
函数、 类型别名 、 结构体 、 枚举 、 联合体 、 traits 和 实现 可以通过类型、常量和生命周期参数 泛型化 。
这些参数在尖括号 (<...>
) 中列出,通常紧跟在条目名称之后,在其定义之前。
对于没有名称的实现,直接在 impl
之后。泛型参数的顺序仅限于首先为生命周期参数,然后交替出现类型和常量参数。
一些带有类型、常量和生命周期参数的条目的例子:
#![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); }
泛型参数在其声明的条目定义中是有效的。根据 条目声明 所述,不在函数体中声明的条目作用域内。
引用 、 原始指针 、 数组 、 切片 、 元组 , 和 函数指针 也有生命周期或类型参数,但不是用路径语法来引用。
常量泛型
常量泛型参数 指条目泛型为常量值。 const 标识符引入了常量参数的名称,并且必须使用给定类型的值来实例化条目实例。
仅允许的常量参数类型有 u8
、 u16
、 u32
、 u64
、 u128
、 usize
、i8
、 i16
、 i32
、 i64
、 i128
、 isize
、 char
、 bool
.
常量参数可以在任何 常量条目 可用的地方使用,但是当常量参数用于 类型 或 数组重复表达式 时,必须是独立的 (如下所述) 。也就是说,允许出现在以下位置:
- 作为相关条目签名一部分的类型应用 const 。
- 作为定义 关联常量 或作为 关联类型 参数的常量表达式的一部分。
- 作为条目中任何函数体中运行时表达式中的值。
- 作为条目中任何函数体中使用的任何类型的参数。
- 作为条目字段类型的一部分。
#![allow(unused)] fn main() { // 可以使用常量泛型参数的例子。 // 用于条目本身签名。 fn foo<const N: usize>(arr: [i32; N]) { // 用作函数体中的类型。 let x: [i32; N]; // 用作表达式。 println!("{}", N * 2); } // 用作结构体的字段。 struct Foo<const N: usize>([i32; N]); impl<const N: usize> Foo<N> { // 用作关联常量。 const CONST: usize = N * 4; } trait Trait { type Output; } impl<const N: usize> Trait for Foo<N> { // 用作关联类型。 type Output = [i32; N]; } }
#![allow(unused)] fn main() { // 不能使用常量泛型参数的例子。 fn foo<const N: usize>() { // 不能在函数体中的条目定义上使用。 const BAD_CONST: [usize; N] = [1; N]; static BAD_STATIC: [usize; N] = [1; N]; fn inner(bad_arg: [usize; N]) { let bad_value = N * 2; } type BadAlias = [usize; N]; struct BadStruct([usize; N]); } }
作为进一步的限制,常量参数只能出现在 类型 或 数组重复表达式 内部作为独立的参数。
在这些上下文中,它们只能用作单个段 路径表达式 ,可能包含在 块 中 (如 N
或 {N}
) 。也就是说,它们不能与其他表达式结合使用。
#![allow(unused)] fn main() { // 不可以使用常量参数的例子。 // 不允许在其他类型的表达式中组合,比如这里的返回类型中的算术表达式。 fn bad_function<const N: usize>() -> [u8; {N + 1}] { // 同样不允许用于数组重复表达式。 [1; {N + 1}] } }
路径 中的常量参数指定用于该条目的常量值。
参数必须是为常量参数指定的类型的 常量表达式 。
除非它是单个路径段 (标识符) 或 字面值 (可能以 -
令牌开头) ,否则常量表达式必须是 块表达式 (用大括号括起来) 。
注意: 这种句法限制是必要的,以避免在解析类型内的表达式时需要无限前瞻。
#![allow(unused)] fn main() { fn double<const N: i32>() { println!("doubled: {}", N * 2); } const SOME_CONST: i32 = 12; fn example() { // const 参数的使用示例。 double::<9>(); double::<-123>(); double::<{7 + 8}>(); double::<SOME_CONST>(); double::<{ SOME_CONST + 5 }>(); } }
当一个泛型参数可以同时被解析为类型或常量参数时,将总是被解析为类型参数。 将该参数放在块表达式中可以强制将其解释为常量参数。
#![allow(unused)] fn main() { type N = u32; struct Foo<const N: usize>; // 以下是一个错误,因为 `N` 被解释为类型别名 `N` 。 fn foo<const N: usize>() -> Foo<N> { todo!() } // ERROR // 可以用大括号包起来,强制解释为 `N` 常量参数。 fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok }
与类型和生命周期参数不同,常量参数可以在不在参数化条目内部使用的情况下声明,但在实现中除外,如 泛型实现 所述:
#![allow(unused)] fn main() { // 成功 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 约束职责时,在确定约束是否满足时,不考虑所有常量参数的实现是否穷尽。
例如,即使为 bool
类型实现了所有可能的常量值,但仍然会出错,因为 trait 约束未满足:
#![allow(unused)] fn main() { struct Foo<const B: bool>; trait Bar {} impl Bar for Foo<true> {} impl Bar for Foo<false> {} fn needs_bar(_: impl Bar) {} fn generic<const B: bool>() { let v = Foo::<B>; needs_bar(v); // ERROR: trait 约束 `Foo<B>: Bar` 不满足 } }
Where 从句
语法
Where从句 :
where
( Where从句条目,
)* Where从句条目 ?Where从句条目 :
生命周期Where从句条目
| 类型约束Where从句条目
where 从句 提供了另一种方法来指定类型和生命周期参数的约束,以及指定非类型参数的类型约束的方法。
for
关键字可用于引入 更高阶生命周期 ,仅允许 生命周期参数 。
#![allow(unused)] fn main() { struct A<T> where T: Iterator, // 可以用 A<T: Iterator> 代替 T::Item: Copy, // 绑定在一个关联类型上 String: PartialEq<T>, // 绑定在 `String` 上,使用类型参数 i32: Default, // 允许的,但没有用 { f: T, } }
属性
泛型生命周期和类型参数允许 属性 。在这个位置上没有内置属性起作用,尽管自定义派生属性可能会赋予其意义。
此示例展示使用自定义衍生属性来修改泛型参数的含义。
// 假设 MyFlexibleClone 衍生声明了 `my_flexible_clone` 作为它所理解的一个属性。
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
a: *const H
}
关联条目
关联条目 是 traits 及 实现 中类型定义的条目。 之所以称为关联条目,是因为是在关联类型中定义。 关联条目是可在模块中声明条目的子集,包括 关联函数 (包括方法)、 关联类型 和 关联常量。
关联条目通常与关联类型有逻辑关系。
例如, is_some
方法与 Option
类型本身有密切的关系,因此应该定义为关联条目。
关联条目有两种类型:定义和声明。定义是实际的实现,而声明只是签名。
trait 声明以及泛型类型上可用的内容组成了其约定。
关联函数和方法
关联函数 是与类型相关的 函数 。
关联函数声明 是签名,采用函数条目的书写形式,函数体替换为 ;
。
函数名称是标识符。 关联函数的泛型、参数列表、返回类型和 where 从句必须与其声明相同。
关联函数定义 定义了与类型关联的函数,写法与 函数条目 相同。
常见的关联函数一个例子是 new
函数,返回关联类型的值。
struct Struct { field: i32 } impl Struct { fn new() -> Struct { Struct { field: 0i32 } } } fn main () { let _struct = Struct::new(); }
当关联函数在 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 |
注意: 生命周期通常也可以用这种简写语法。
如果 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(); }
版本差异: 在 2015 版中,可以使用匿名参数 (例如
fn foo(u8)
) 声明 trait 方法。但在 2018 版中,已被弃用且会导致错误,所有参数必须要有参数名。
方法参数的属性
方法参数之上的属性遵循与 常规函数参数 相同的规则和限制。
关联类型
关联类型 是与另一个类型相关联的 类型别名 。 关联类型不能在 内部实现 中定义,不能在 trait 中给出默认实现。
关联类型声明 是签名。
用以下一种形式书写,其中 Assoc
是关联类型的名称,Params
是由逗号分隔的类型、生命周期或常量参数列表,
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
约束放宽。
关联类型定义 为类型别名。它们的书写方式与 关联类型声明 相似,而不能包含 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
中获取的关联类型 Assoc
,即 <Item as Trait>::Assoc
。此外,如果 Item
是类型参数,那么类型参数中可以使用 Item::Assoc
。
关联类型可能包含 泛型参数 和 where 约束 ;这些通常被称为 泛型关联类型 或 GAT。
如果类型 Thing
有一个来自 Trait
的关联类型 Item
,具有泛型 <'a>
,则可以像 <Thing as Trait> ::Item<'x>
这样命名该类型,其中 'x
是某个有效作用域内的生命周期。
此时,'x
将在实现中的关联类型定义中的 '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
从句是必需的,以便最大限度允许在 impls 中定义关联类型的范围。
为了做到这一点,在任何出现 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
函数中,我们可以证明 Self: 'a
,因为我们从 &'a mut self
推导出了这个隐含的约束。
因此,我们必须在 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
函数注释掉,那么 Checker
就需要一个 where T: 'x
的约束。
关联类型上的约束也会传播所需的 where 从句。
#![allow(unused)] fn main() { trait Iterable { type Item<'a> where Self: 'a; type Iterator<'a>: Iterator<Item = Self::Item<'a>> where Self: 'a; fn iter<'a>(&'a self) -> Self::Iterator<'a>; } }
在这里,由于 iter
, Item
上需要 where Self: 'a
。但是,由于 Iterator
的约束中使用了 Item
,因此也需要在那里写出 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` trait ConstantIdDefault { // 声明一个常量 `ID`,默认值为 `1` const ID: i32 = 1; } struct Struct; struct OtherStruct; // 为结构体 `Struct` 实现 `ConstantIdDefault` trait impl ConstantIdDefault for Struct {} // 为结构体 `OtherStruct` 实现 `ConstantIdDefault` trait,并重新定义 `ID` 常量的值为 `5` impl ConstantIdDefault for OtherStruct { const ID: i32 = 5; } fn main() { // 断言结构体 `Struct` 的 `ID` 常量值为 `1` assert_eq!(1, Struct::ID); // 断言结构体 `OtherStruct` 的 `ID` 常量值为 `5` assert_eq!(5, OtherStruct::ID); }
属性
语法
内部属性 :
#
!
[
属性]
外围属性 :
#
[
属性]
属性 :
简单路径 属性输入?
属性 语法形式更加通用和自由,具体的所表达的语义,根据名称、约定及编译器版本解释。 rust 属性以 ECMA-335 中的属性为模型,其语法来自 ECMA-334 (C#) 。
内部属性 ,以井号 (#
) 后跟一个叹号 (!
) 形式声明,应用到包含它的条目内部。
外围属性 ,在井号后不加叹号,应用于随后的条目。
属性由一个路径和一个可选的定界符号 token 树组成,该 token 树的具体含义由属性解释。
除属性宏之外的属性还允许输入是一个等号 (=
) 后跟一个表达式。
参阅下面的 元项语法 。
属性有 4 种:
属性可以应用于语言中的许多内容:
- 所有的 条目声明 都接受外围属性,而 外部块 、 函数 、 实现 和 模块 接受内部属性。
- 大多数 语句 都接受外围属性 (关于表达式语句的限制,见 表达式属性 ) 。
- 块表达式 可以接受外围和内部属性,但是只有是 表达式语句 的外部表达式或另一个块表达式的最终表达式时。
- 枚举 变体和 结构体 和 联合体 字段接受外围属性。
- 匹配表达式分支 接受外围属性。
- 泛型生命周期或类型参数 接受外围属性。
- 表达式在有限的情况下接受外围属性,详见 表达式属性 。
- 函数 、 闭包 和 函数指针 参数接受外围属性。这包括在函数指针和 外部块 中用
...
表示的变量参数的属性。
一些属性的例子:
#![allow(unused)] fn main() { // 应用于顶层模块或 crate 。 #![crate_type = "lib"] // 标记为单元测试的函数 #[test] fn test_foo() { /* ... */ } // 条件编译模块 #[cfg(target_os = "linux")] mod bar { /* ... */ } // 用于屏蔽警告/错误的代码分析 #[allow(non_camel_case_types)] type int8_t = i8; // 整个函数的内部属性 fn some_unused_variables() { #![allow(unused_variables)] let x = (); let y = (); let z = (); } }
元项属性语法
"元项" 是大多数 内置属性 规则的语法,具体如下:
语法
元项 :
简单路径
| 简单路径=
表达式
| 简单路径(
元?)
元 :
元项内部 (,
元项内部 )*,
?元项内部 :
元项
| 表达式
元项中的表达式必须能够被宏展开字面值表达式,不得包含整数或浮点类型后缀。 非字面值表达式将在语法分析后被拒绝,但过程宏没有这一限制。
请注意,另一个宏中的属性,在该外部宏展开之后再展开。
例如,以下代码将首先扩展 Serialize
过程宏,展开后如果保留了 include_str!
,那么其随后展开:
#[derive(Serialize)]
struct Foo {
#[doc = include_str!("x.md")]
x: u32
}
此外,条目的所有属性展开之后,属性所包含的宏才会展开:
#[macro_attr1] // 首先展开
#[doc = mac!()] // `mac!` 第4展开 。这里的 doc 属性不会主动展开
#[macro_attr2] // 第二展开
#[derive(MacroDerive1, MacroDerive2)] // 第三展开
fn foo() {}
各种内置属性所使用的元项语法会有所不同。 以下是一些常用形式:
语法
元字:
标识符元名称值字符串:
标识符=
(字符串字面值 | 原始字符串字面值)元列表路径:
标识符(
( 简单路径 (,
简单路径)*,
? )?)
元列表ID组:
标识符(
( 标识符 (,
标识符)*,
? )?)
元列表名称值字符串:
标识符(
( 元名称值字符串 (,
元名称值字符串)*,
? )?)
一些例子:
类型 | 示例 |
---|---|
元字 | no_std |
元名称值字符串 | doc = "example" |
元列表路径组 | allow(unused, clippy::inline_always) |
元列表ID组 | macro_use(foo, bar) |
元列表名称值字符串 | link(name = "CoreFoundation", kind = "framework") |
活动和惰性属性
属性会是活动的或惰性的。对属性进行解释后 活动属性 会被移除,而 惰性属性 会被保留。
cfg
和 cfg_attr
属性是活动属性。 test
属性在测试编译时是惰性的,而在其他情况下是活动的。
属性宏 是活动属性。所有其他属性都是惰性的。
工具属性
编译器也接收外部工具的属性,在 tool prelude 命名空间中。 属性路径的第一个部分是工具的名称,后面可能有一个或多个其他部分,其含义由工具解释。
当一个工具没有被使用时,允许工具属性且不会产生警告。 当该工具正在使用时,该工具需要负责处理和解释其属性。
工具属性在启用 no_implicit_prelude
属性后不可用。
#![allow(unused)] fn main() { // 告知 rustfmt 工具不要格式化以下元素。 #[rustfmt::skip] struct S { } // 控制 Clippy 工具的 "cyclomatic complexity" 阈值。 #[clippy::cyclomatic_complexity = "100"] pub fn f() {} }
注意:
rustc
目前可以识别 "clippy" 和 "rustfmt" 两个工具。
内置属性索引
以下是所有内置属性的索引。
- 条件编译
- 测试
test
— 标记为测试函数ignore
— 禁用测试函数should_panic
— 表示测试应产生恐慌
- 衍生
derive
— 自动 trait 实现automatically_derived
— 由derive
创建的实现的标记
- 宏
macro_export
— 导出一个macro_rules
宏,用于跨 crate 使用。macro_use
— 扩展宏的可见性,或从其他 crate 导入宏。proc_macro
— 定义函数式宏。proc_macro_derive
— 定义衍生宏。proc_macro_attribute
— 定义属性宏。
- 诊断
- ABI, linking, symbols, and 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
— 提示函数不太可能被调用。no_builtins
— 禁止使用某些内置函数。target_feature
— 配置特定平台的代码生成。track_caller
- 将父级调用位置传递给std::panic::Location::caller()
。instruction_set
- 指定用于生成函数代码的指令集
- 文档
doc
— 用于指定文档。请参见 Rustdoc 文档 获取更多信息。 文档注释 会被转换为doc
属性。
- 预导入
no_std
— 从预导入中删除 std 。no_implicit_prelude
— 禁用模块内的预导入查询。
- 模块
path
— 指定模块的文件名。
- 范围
recursion_limit
— 设置某些编译时操作的最大递归限制。type_length_limit
— 设置多态类型的最大规模。
- 运行时
panic_handler
— 设定处理恐慌的函数。global_allocator
— 设置全局内存分配器。windows_subsystem
— 指定要链接的 windows 子系统。
- 特性
feature
— 用于启用未稳定或实验性的编译器特性。有关rustc
实现的特性,请参见 未稳定性文档 。
- 类型系统
non_exhaustive
— 表示该类型在未来可能会添加更多的字段/变体。
测试属性
以下 属性 用于指定用于执行测试的函数。
将 crate 编译为 "test" 模式将启用构建测试函数以及用于执行测试的测试工具。
启用测试模式还启用 test
条件编译选项。
test
属性
test
属性 用于标记一个函数作为测试用例执行。这些函数只在测试模式下编译。
测试函数必须是无参、单态函数,并且返回类型必须实现 Termination
trait ,例如:
()
Result<T, E> where T: Termination, E: Debug
!
注意: 测试模式是通过向
rustc
传递--test
参数或使用cargo test
来启用的。
测试框架将调用返回值的 report
方法,并根据 ExitCode
代表的终止状态将测试分类为已通过或已失败。特别地:
- 返回
()
的测试只要终止且未发生恐慌即为通过。 - 返回
Result<(), E>
的测试只要返回Ok(())
即为通过。 - 返回
ExitCode::SUCCESS
的测试通过,返回ExitCode::FAILURE
的测试失败。 - 无法终止的测试既不通过也不失败。
#![allow(unused)] fn main() { use std::io; fn setup_the_thing() -> io::Result<i32> { Ok(1) } fn do_the_thing(s: &i32) -> io::Result<()> { Ok(()) } #[test] fn test_the_thing() -> io::Result<()> { let state = setup_the_thing()?; // expected to succeed do_the_thing(&state)?; // expected to succeed Ok(()) } }
ignore
属性
一个被标记为 test
的函数也可以被标记为 ignore
。
ignore
属性 告知测试框架不要将该函数作为测试执行,但在测试模式下它仍会被编译。
ignore
属性可以选择用 元名称值字符串 语法编写,以具体说明测试被忽略的原因。
#![allow(unused)] fn main() { #[test] #[ignore = "not yet implemented"] fn mytest() { // … } }
注意:
rustc
测试框架支持--include-ignored
标志,以强制运行被忽略的测试。
should_panic
属性
带有 test
属性的返回类型为 ()
的函数还可以带有 should_panic
属性。 should_panic
属性 表示仅当函数实际恐慌时,测试才会通过。
should_panic
属性可以选择性地接受一个输入字符串,该字符串必须出现在恐慌消息中。
如果在消息中未找到该字符串,则测试将失败。可以使用 元名称值字符串 语法或具有 expected
字段的 元列表名称值字符串 语法传递该字符串。
#![allow(unused)] fn main() { #[test] #[should_panic(expected = "values don't match")] fn mytest() { assert_eq!(1, 2, "values don't match"); } }
衍生
derive
属性 允许自动生成数据结构的新 条目 。
它使用 元列表路径组 语法来指定要实现的一组 trait 或要处理的 衍生宏 的路径。
例如,以下代码将为 Foo
创建一个 impl
条目 ,实现 PartialEq
和 Clone
两个 trait ,
而类型参数 T
将被赋予相应 impl
中的 PartialEq
或 Clone
约束。
#![allow(unused)] fn main() { #[derive(PartialEq, Clone)] struct Foo<T> { a: i32, b: T, } }
对于 PartialEq
所生成的 impl
等同于
#![allow(unused)] fn main() { struct Foo<T> { a: i32, b: T } impl<T: PartialEq> PartialEq for Foo<T> { fn eq(&self, other: &Foo<T>) -> bool { self.a == other.a && self.b == other.b } } }
你可以通过 过程宏 为你自己的 trait 实现 derive
。
automatically_derived
属性
自动生成的实现由 derive
属性自动添加了 automatically_derived
属性,它本身没有直接作用,但是可以被工具和代码分析诊断用来检测这些自动生成的实现。
诊断属性
下面的 属性 用于在编译期间控制或生成诊断信息。
代码分析检查属性
代码分析检查指的是一些可能存在不良编码模式的代码,例如不可到达的代码或省略的文档。
代码分析属性 allow
、 warn
、 deny
和 forbid
使用 元列表路径组 语法指定要更改为应用属性的实体的代码分析级别列表。
对于任何代码分析检查 C
:
allow(C)
会覆盖C
的检查,使违规不会被报告,warn(C)
警告有关C
的违规,但继续编译。deny(C)
在遇到C
的违规后发出错误信号,forbid(C)
与deny(C)
相同,但还禁止以后更改代码分析级别。
注意:
rustc
支持的代码分析检查可以通过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 } } }
代码分析属性可以覆盖先前属性中指定的级别,只要级别不会试图更改禁止的代码分析。 先前的属性来自语法树中更高级别的属性,或者是源代码中从左到右的先前属性。
以下示例展示了如何使用 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
禁止对某个代码分析检查使用 allow
:
#![allow(unused)] fn main() { #[forbid(missing_docs)] pub mod m3 { // 在这里尝试切换警告会导致错误 #[allow(missing_docs)] /// Returns 2. pub fn undocumented_too() -> i32 { 2 } } }
代码分析组
代码分析可以被组织成具有名称的组,以便可以一起调整相关代码分析的级别。使用命名组相当于列出该组中的代码分析。
#![allow(unused)] fn main() { // 这允许所有 "unused" 组中的代码分析。 #[allow(unused)] // 这将 "unused" 组中的 "unused_must_use" 代码分析改为了 "deny"。 #[deny(unused_must_use)] fn example() { // 这不会生成警告,因为 "unused_variables" 代码分析在 "unused" 组中。 let x = 1; // 这会生成一个错误,因为结果未使用且 "unused_must_use" 被标记为 "deny"。 std::fs::remove_file("some_file"); // ERROR: 必须使用的未使用的 `Result` } }
有一个特殊的名为 "warnings" 的分组,其中包括所有 "warn" 级别的代码分析。 "warnings" 分组忽略属性顺序,并适用于实体中所有会产生警告的代码分析。
#![allow(unused)] fn main() { unsafe fn an_unsafe_fn() {} // 这两个属性的顺序无关紧要。 #[deny(warnings)] // unsafe_code 代码分析通常默认为 "allow"。 #[warn(unsafe_code)] fn example_err() { // 这是一个错误,因为 `unsafe_code` 警告已经被提升到了 "deny"。 unsafe { an_unsafe_fn() } // ERROR: 使用 `unsafe` 块 } }
工具代码分析属性
'工具代码分析' 允许使用作用域 '代码分析' ,以允许、警告、禁止某些工具的代码分析。
只有在相关工具处于活动状态时才会检查工具代码分析。
如果像 allow
这样的代码分析属性引用了不存在的 '工具代码分析' ,则编译器在使用该工具之前不会警告不存在的代码分析。
否则,它们的工作方式与常规代码分析属性相同:
// 将整个 pedantic clippy 代码分析组设置为警告 #![warn(clippy::pedantic)] // 屏蔽 filter_map clippy 代码分析中的警告 #![allow(clippy::filter_map)] fn main() { // ... } // 仅针对该函数屏蔽 cmp_nan clippy 代码分析的警告 #[allow(clippy::cmp_nan)] fn foo() { // ... }
deprecated
属性
deprecated
属性 将一个条目标记为已弃用。rustc
在使用被 #[deprecated]
标记的条目时会发出警告。
rustdoc
将显示条目的弃用信息,包括 since
版本和 note
(如果有) 。
deprecated
属性有几种形式:
deprecated
— 发出一条通用信息。deprecated = "message"
— 在弃用消息中包含给定的字符串。- 无列表名称值字符串 语法有两个可选字段:
since
— 指定条目被弃用的版本号。rustc
目前不解释该字符串,但像 Clippy 这样的外部工具可能会检查值的有效性。note
— 指定应在弃用消息中包含的字符串。通常用于提供有关弃用和首选替代方法的解释。
deprecated
属性可应用于任何 条目 、 trait 条目 、 enum 变体 、 struct 字段 、 外部块条目 或 宏定义。
它不能应用于 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
属性 用于在值未被 "使用" 时发出诊断警告。它可以应用于用户定义的复合类型 ( struct
、 enum
和 union
) 、 functions
和 traits
。
must_use
属性可以通过使用 元名称值字符串 语法 (例如 #[must_use = "example message"]
) 包含一条消息。该消息将与警告一起给出。
当在用户定义的复合类型上使用时,如果 表达式 的 表达式语句 具有该类型,则违反了 unused_must_use
代码分析。
#![allow(unused)] fn main() { #[must_use] struct MustUse { // 一些字段 } impl MustUse { fn new() -> MustUse { MustUse {} } } // 违反了 `unused_must_use` lint. MustUse::new(); }
当应用于函数时,如果 表达式 是对该函数的 调用表达式,那么将违反 unused_must_use
警告。
#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 违反了 `unused_must_use` lint. five(); }
当在 trait 声明 上使用时,对该 trait 的 impl trait 或 dyn trait 返回的函数的 调用表达式 的 表达式语句 违反了 unused_must_use
代码代析。
#![allow(unused)] fn main() { #[must_use] trait Critical {} impl Critical for i32 {} fn get_critical() -> impl Critical { 4i32 } // 违反了 `unused_must_use` lint. get_critical(); }
当在 trait 声明中的函数上使用时,当调用表达式是 trait 实现的函数时,该行为也适用。
#![allow(unused)] fn main() { trait Trait { #[must_use] fn use_me(&self) -> i32; } impl Trait for i32 { fn use_me(&self) -> i32 { 0i32 } } // 违反了 `unused_must_use` 代码分析。 5i32.use_me(); }
当用于 trait 实现中的函数时,该属性无效。
注意:包含该值的无关紧要的无操作表达式不会违反代码分析。 例如,将该值包装在未实现
Drop
的类型中,然后不使用该类型,或者是块表达式的最终表达式,而不使用该表达式。
#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 这些都不违反 unused_must_use 代码分析。 (five(),); Some(five()); { five() }; if true { five() } else { 0i32 }; match true { _ => five() }; }
注意:当有一个有意舍弃的必须使用的值时,使用带有
_
模式的 let 语句是惯用的写法。#![allow(unused)] fn main() { #[must_use] fn five() -> i32 { 5i32 } // 不违反 unused_must_use 代码分析。 let _ = five(); }
代码生成属性
以下 属性 用于控制代码生成。
优化提示
cold
和 inline
属性 给出一些编译建议,期望生成更快的代码。
但仅是提示,可能会被编译器忽略。
这两个属性可应用于 函数 。 当应用于 trait 中的函数时,仅适用于作为 trait 实现的默认函数,而不是所有 trait 实现。 这些属性对于没有函数体的 trait 函数没有效果。
inline
属性
inline
属性* 建议将函数副本放置于调用者中,而不是在相应位置生成调用该函数的代码。
注意:
rustc
编译器基于内部启发式算法自动内联函数。不正确地内联函数可能会使程序变慢,因而此属性应谨慎使用。
有三种使用 inline
属性的方式:
#[inline]
建议 进行内联展开。#[inline(always)]
建议 总是进行内联展开。#[inline(never)]
建议 永远不进行内联展开。
注意: 无论形式如何,
#[inline]
都仅是提示,对于是否将带有该属性的函数副本放置在调用者中,并不作 强制 。
cold
属性
cold
属性 表示带有该属性的函数不太可能被调用。
no_builtins
属性
no_builtins
属性 可以用于禁用优化特定的代码模式以调用预期存在的库函数。
该属性可以在 crate 级别应用。
target_feature
属性
target_feature
属性 可以应用于函数,以启用为特定平台架构特性生成该函数的代码。
它使用带有单个 enable
键的 元列表名称值字符串 语法,其值是逗号分隔的要启用的特性名称字符串。
#![allow(unused)] fn main() { #[cfg(target_feature = "avx2")] #[target_feature(enable = "avx2")] unsafe fn foo_avx2() {} }
每个 目标架构 都有一组可启用的特性。 对于进行编译的 crate,如果指定了不受支持的目标架构特性,会出现错误。
对于使用不受当前平台支持的特性进行编译的函数,进行调用是 未定义行为 ,除非平台明确说明这是安全的。
使用 target_feature
标注的函数不会被内联到不支持所给定特性的上下文中。
#[inline(always)]
属性不能与 target_feature
属性一起使用。
可用特性
以下是可用的特性名称列表。
x86
或 x86_64
在此平台上执行不支持的特性是未定义的行为。
因此,该平台要求 #[target_feature]
仅应用于 unsafe
函数 。
特征 | 隐式启用 | 描述 |
---|---|---|
adx | ADX — 多精度加法进位指令扩展 | |
aes | sse2 | AES — 高级加密标准 |
avx | sse4.2 | AVX — 高级矢量扩展 |
avx2 | avx | AVX2 — 高级矢量扩展 2 |
bmi1 | BMI1 — 位操作指令集 | |
bmi2 | BMI2 — 位操作指令集 2 | |
cmpxchg16b | cmpxchg16b — 原子比较并交换 16 字节 (128 位) 数据 | |
fma | avx | FMA3 — 三操作数融合乘加 |
fxsr | fxsave 和 fxrstor — 保存和恢复 x87 FPU、MMX 技术和 SSE 状态 | |
lzcnt | lzcnt — 前导零计数 | |
movbe | movbe - 字节交换后移动数据 | |
pclmulqdq | sse2 | pclmulqdq — 打包无进位乘法 quadword |
popcnt | popcnt — 计算置 1 的位数 | |
rdrand | rdrand — 读取随机数 | |
rdseed | rdseed — 读取随机种子 | |
sha | sse2 | SHA — 安全哈希算法 |
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 |
ssse3 | sse3 | SSSE3 — 补充流式 SIMD 扩展 3 |
xsave | xsave — 保存处理器扩展状态 | |
xsavec | xsavec — 压缩保存处理器扩展状态 | |
xsaveopt | xsaveopt — 优化保存处理器扩展状态 | |
xsaves | xsaves — 管理模式保存处理器扩展状态 |
aarch64
这个平台要求 #[target_feature]
仅能应用于 unsafe
函数 。
更多关于这些特性的文档可以在 ARM 架构参考手册 或 developer.arm.com 中找到。
注意: 如果使用以下特性对,应该一起标记为已启用或已禁用:
paca
和pacg
,LLVM 当前将它们实现为一个特性。
特性 | 隐式启用 | 特性名称 |
---|---|---|
aes | neon | FEAT_AES - 高级 SIMD AES 指令 |
bf16 | FEAT_BF16 - BFloat16 指令 | |
bti | FEAT_BTI - 分支目标识别 | |
crc | FEAT_CRC - CRC32 校验和指令 | |
dit | FEAT_DIT - 数据独立定时指令 | |
dotprod | FEAT_DotProd - 高级 SIMD Int8 点乘指令 | |
dpb | FEAT_DPB - 数据缓存清除到持久点 | |
dpb2 | 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 转换指令 |
lse | FEAT_LSE - 大系统扩展 | |
lor | FEAT_LOR - 有限排序区域扩展 | |
mte | FEAT_MTE - 内存标记扩展 | |
neon | FEAT_FP 和 FEAT_AdvSIMD - 浮点数和高级 SIMD 扩展 | |
pan | FEAT_PAN - 特权访问不允许扩展 | |
paca | FEAT_PAuth - 指针认证(地址认证) | |
pacg | FEAT_PAuth - 指针认证(通用认证) | |
pmuv3 | FEAT_PMUv3 - 性能监视器扩展(v3) | |
rand | FEAT_RNG - 随机数生成器 | |
ras | FEAT_RAS - 可靠性、可用性和服务性扩展 | |
rcpc | FEAT_LRCPC - 一致性处理器ARMv8.2 可以支持多种架构,其中一些可用特性如下所示: | |
rdm | 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 - 推测存储旁路安全指令 | |
sve | fp16 | FEAT_SVE - 可扩展向量扩展指令 |
sve2 | sve | FEAT_SVE2 - 可扩展向量扩展2指令 |
sve2-aes | sve2 , aes | FEAT_SVE_AES - SVE AES指令 |
sve2-sm4 | sve2 , sm4 | FEAT_SVE_SM4 - SVE SM4指令 |
sve2-sha3 | sve2 , sha3 | FEAT_SVE_SHA3 - SVE SHA3指令 |
sve2-bitperm | sve2 | FEAT_SVE_BitPerm - SVE 位重排指令 |
tme | FEAT_TME - 事务内存扩展指令 | |
vh | FEAT_VHE - 虚拟化主机扩展指令 |
wasm32
或 wasm64
#[target_feature]
可以在 Wasm 平台上用于安全函数和 非安全函数
。
在 Wasm 引擎不支持的指令下,尝试使用 #[target_feature]
属性将在加载时失败,不会有与编译器预期不同的解释风险,因此不可能引起未定义行为。
特性 | 描述 |
---|---|
simd128 | WebAssembly simd proposal |
附加信息
target_feature
条件编译选项 可用于根据编译时设置选择性地启用或禁用代码的编译。
请注意,该选项不受 target_feature
属性的影响,仅受整个 crate 启用的特性的驱动。
标准库中的 is_x86_feature_detected
或 is_aarch64_feature_detected
宏可用于在这些平台上进行运行时特性检测。
注意:
rustc
对于每个目标和 CPU 都有一组默认启用的特性。 可以使用-C target-cpu
标志选择 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()); } }
注意:
core
提供core::panic::Location::caller
用于获取调用地址。它包装了rustc
实现的core::intrinsics::caller_location
内置函数。
注意: 由于产生的
Location
只是一个提示,实现可能会在遍历堆栈时提前停止。请参阅 限制 以获取重要的注意事项。
示例
当 f
直接被 calls_f
调用时,f
中的代码会观察到它在 calls_f
中被调用位置:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } fn calls_f() { f(); // <-- f() prints this location } }
当函数 f
被另一个带有 track_caller
属性的函数 g
直接调用,而 g
又被 calls_g
调用时, f
和 g
中的代码都可以观察到 calls_g
中调用 g
的位置:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } #[track_caller] fn g() { println!("{}", std::panic::Location::caller()); f(); } fn calls_g() { g(); // <-- g() prints this location twice, once itself and once from f() } }
当函数 g
被另一个有 track_caller
属性的函数 h
直接或间接调用时 (例如 h
被 calls_h
调用) ,则 f
、 g
和 h
中的代码都会观察到 calls_h
中 h
的调用位置。
也就是说,调用链中最深的有 track_caller
属性的函数的调用位置会传递到所有下层函数:
#![allow(unused)] fn main() { #[track_caller] fn f() { println!("{}", std::panic::Location::caller()); } #[track_caller] fn g() { println!("{}", std::panic::Location::caller()); f(); } #[track_caller] fn h() { println!("{}", std::panic::Location::caller()); g(); } fn calls_h() { h(); // <-- prints this location three times, once itself, once from g(), once from f() } }
以次类推。
限制
该信息是一个提示,实现不需要保留它。
特别地,将一个带有 #[track_caller]
的函数转换为函数指针会创建一个外壳函数,对于观察者来说,该外壳函数似乎是在函数定义的位置被调用的,从而在虚拟调用之间丢失了实际的调用者信息。
这种转换的常见例子是创建一个具有方法的 trait 对象,并将其进行标注。
注意: 函数指针的上述外壳函数是必要的,因为
rustc
在代码生成上实现track_caller
通过将一个隐式参数附加到函数 ABI 上,但是这对于间接调用是非安全的, 因为该参数不是函数类型的一部分,给定的函数指针类型可能引用一个没有该属性的函数或具有该属性的函数。创建外壳函数隐藏了函数指针调用者的隐式参数,从而保持了安全性。
instruction_set
属性
可以将 instruction_set
属性应用于函数,以启用针对目标体系结构支持的特定指令集的代码生成。
它使用 MetaListPath 语法和一个由架构和指令集组成的路径,指定如何为在单个程序中使用多个指令集的体系结构生成代码。
对于 ARMv4
和 ARMv5te
架构的目标,以下值可用:
arm::a32
- 使用 ARM 编码.arm::t32
- 使用 Thumb 编码.
#[instruction_set(arm::a32)]
fn foo_arm_code() {}
#[instruction_set(arm::t32)]
fn bar_thumb_code() {}
限制
下面的 属性 影响编译时的限制。
recursion_limit
属性
recursion_limit
属性 可以在 crate 级别上应用,以设置潜在的无限递归编译时操作 (如宏展开或自动解引用) 的最大深度。它使用 元名称值字符串 语法来指定递归深度。
注意:
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
属性限制了当进行单态化时,构建具体类型所做的最大类型替换数量。
它应用于 crate 级别,使用 元名称值字符串 语法设置限制,以基于类型替换数量设置限制。
注意:
rustc
中的默认值为 1048576。
#![allow(unused)] #![type_length_limit = "4"] fn main() { fn f<T>(x: T) {} // 编译失败,因为将 f::<((((i32,), i32), i32), i32)> 进行单态化需要超过 4 个类型元素。 f(((((1,), 2), 3), 4)); }
类型系统属性
以下属性用于更改类型的使用方式。
non_exhaustive
属性
non_exhaustive
属性 表示类型或变体将来可能会添加更多的字段或变体。
可以应用于 [struct
] 、 [enum
] 和 enum
变体。
non_exhaustive
属性使用 元字 语法,因此不需要输入任何内容。
在定义的 crate 中,non_exhaustive
没有任何作用。
#![allow(unused)] fn main() { #[non_exhaustive] pub struct Config { pub window_width: u16, pub window_height: u16, } #[non_exhaustive] pub 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 }; // 非穷尽结构体可以在定义的 crate 中进行完全匹配。 if let Config { window_width, window_height } = config { // ... } 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 => { }, } }
在定义之外,使用 non_exhaustive
注解的类型有限制,以保持向后兼容性,当添加新字段或变量时。
不能在定义之外构造非穷尽类型:
- 不能使用 结构体表达式 (包括 函数更新语法) 构造非穷尽变体 (
struct
或enum
variant ) 。 - 可以构造 [
enum
] 实例。
// `Config`,`Error` 和 `Message` 是在上游 crate 中定义并使用了 `#[non_exhaustive]` 标记的类型。
use upstream::{Config, Error, Message};
// 无法构造 `Config` 的实例,如果在新版本中添加了新的字段,则会导致编译失败,因此不允许。
let config = Config { window_width: 640, window_height: 480 };
// 可以构造 `Error` 的实例,如果新的变量被引入,也不会导致编译失败。
let error = Error::Message("foo".to_string());
// 无法构造 `Message::Send` 或 `Message::Reaction` 的实例,如果在新版本中添加了新的字段,则会导致编译失败,因此不允许。
let message = Message::Send { from: 0, to: 1, contents: "foo".to_string(), };
let message = Message::Reaction(0);
// 无法构造 `Message::Quit` 的实例,如果将其转换为元组变量 `upstream`,则会导致编译失败。
let message = Message::Quit;
在定义类型的 crate 之外,在匹配非穷尽类型时存在以下限制:
- 当匹配非穷尽变体 (
struct
或enum
variant) 时,必须使用带有..
的 结构体模式 ,元组变体构造函数的可见性降为min($vis, pub(crate))
。 - 当匹配非穷尽
enum
时,匹配一个变体不会导致分支的穷尽性。
// `Config` , `Error` 和 `Message` 是在上游 crate 中定义的类型,已经被注解为 `#[non_exhaustive]`。
use upstream::{Config, Error, Message};
// 无法在非穷尽枚举上进行匹配,除非使用通配符。
match error {
Error::Message(ref s) => {},
Error::Other => {},
// 可以编译: `_ => {},`
}
// 无法在非穷尽的结构体上进行匹配,除非使用通配符。
if let Ok(Config { window_width, window_height }) = config {
// 可以编译: `..`
}
match message {
// 无法在非穷尽的结构体上进行匹配,除非使用通配符。
Message::Send { from, to, contents } => { },
// 无法匹配非穷尽的元组或单元枚举变体。
Message::Reaction(type) => { },
Message::Quit => { },
}
在外部的 crate 中,也不允许将非穷尽类型进行强制类型转换。
use othercrate::NonExhaustiveEnum;
// 无法在其定义 crate 之外将非穷尽枚举转换为其他类型。
let _ = NonExhaustiveEnum::default() as u8;
非穷尽类型在下游的 crate 中总是被视为是有 inhabitant(指代值或实例) 。
语句和表达式
Rust 语言的 主要 语法是表达式。大多数产生值或引起副作用的计算都由表达式完成。 表达式通常可以嵌套其他表达式,按各自的规则顺序进行计算。
语句则主要是显式地序列化了表达式计算的顺序。
语句
语句 是 块 的组成部分,而块又是 表达式 或 函数 的组成部分。这同样形成了 '树' 的组织结构。
声明语句
声明语句 引入一个或多个 名称 到闭合语句块中。 声明的名称可能表示新变量或新 条目 。
声明语句有两种类型: let
语句和条目声明。
条目声明
条目声明语句 的语法形式与 模块 中的 条目声明 相同。 在语句块中声明一个条目,将被限制在包含该语句的块作用域中。 该条目不会被赋予 规范路径 ,也不会声明任何子条目。 唯一的例外是通过 实现 定义的关联条目,只要该条目和 (如果适用) trait 可访问,则可以在外部访问。 否则,与在模块中声明该条目的含义相同。
不会隐式捕获包含它的函数的泛型参数、参数和局部变量。
例如, inner
无法访问 outer_var
。
#![allow(unused)] fn main() { fn outer() { let outer_var = true; fn inner() { /* outer_var 不在作用域中。 */ } inner(); } }
let
语句
语法
Let语句 :
外围属性*let
模式非顶层项 (:
类型 )? (=
表达式 † (else
块表达式) ? ) ?;
† 当指定了
else
块时,表达式不能是 惰性布尔表达式 ,也不能以}
结尾。
let
语句引入了一组由 模式 给定的新 变量 。
模式可选地后跟一个类型注释,然后以初始化表达式结束,或者跟随可选的 else
块。
当没有给出类型注释时,编译器将推断类型,如果没有足够的类型信息进行明确推断,则会发出错误信号。
任何由变量声明引入的变量在声明处到包含块范围的结尾处之间可见,除非该变量被另一个同名变量声明隐藏。
如果不存在 else
块,则模式必须是不可拒绝的。
如果存在 else
块,则模式可以是可拒绝的。
如果模式不匹配 (这需要它是可拒绝的) ,则执行 else
块。
else
块必须始终发散 (求值为 永不类型 )。
译注:'拒绝' 和 '不可拒绝' 表示模式是否能够在任何情况下都成功匹配。 '发散' 指的是一个表达式是否无法正常终止并返回值。
#![allow(unused)] fn main() { let (mut v, w) = (vec![1, 2, 3], 42); // 绑定可以是可变或常量 let Some(t) = v.pop() else { // 可拒绝的模式需要一个 else 块 panic!(); // else 块必须发散 }; let [u, v] = [v[0], v[1]] else { // 这个模式是不可拒绝的,所以编译器会认为 else 块是冗余的进行代码分析检查 panic!(); }; }
表达式语句
表达式语句 是指仅执行表达式并忽略其结果的语句。 通常,此表达式语句的是为了触发其表达式的副作用。
如果一个表达式只包含一个块表达式或控制流表达式,并且在允许语句的上下文中,则可以省略分号。 这会有可能在解析时,是作为独立语句,还是作为另一个表达式的一部分,而产生歧义。这时,会被解析为语句。 当 块表达式 作为语句时,其类型必须是单元类型。
#![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]; // 是独立的表达式语句。 }
if 表达式语句省略了结尾的分号,其结果类型为 ()
。
#![allow(unused)] fn main() { // 错误: 块表达式类型是 i32, 不是 () // Error: expected `()` because of default return type // if true { // 1 // } // 成功: 块表达式类型是 i32 if true { 1 } else { 2 }; }
语句上的属性
语句可以接受 外围属性 。
在语句上具有意义的属性包括 cfg
和 代码分析检查属性 。
表达式
语法
表达式 :
无块表达式
| 块表达式无块表达式 :
外围属性*†
(
字面值表达式
| 路径表达式
| 操作符表达式
| 分组表达式
| 数组表达式
| Await表达式
| 索引表达式
| 元组表达式
| 元组索引表达式
| 结构体表达式
| 调用表达式
| 方法调用表达式
| 字段表达式
| 闭包表达式
| Async块表达式
| Continue表达式
| Break表达式
| 区间表达式
| Return表达式
| 下划线表达式
| 宏调用表达式
)块表达式 :
外围属性*†
(
块表达式
| Unsafe块表达式
| Loop表达式
| If表达式
| IfLet表达式
| Match表达式
)
表达式具有两个作用: 总是产生一个值,并可能产生其他效果 (也称为 "副作用" )。 副作用在表达式计算为一个值的期间产生。 许多表达式包含子表达式,也称为表达式的操作数。表达式的具体语义决定了以下行为:
- 在计算表达式时是否进一步计算操作数
- 计算操作数的顺序
- 如何将操作数的值组合以获得表达式的值
块仍是表达式的一种,因此块、语句、表达式和块可以递归嵌套。
注意: 给表达式的操作数命名可以便于讨论,但目前这些名称未稳定,可能会更改。
表达式优先级
Rust 运算符和表达式的优先级按照以下顺序进行排列,从强到弱。 在相同优先级的二元运算符中,根据其结合性得出分组顺序。
运算符/表达式 | 结合性 |
---|---|
路径 | |
方法调用 | |
字段表达式 | 从左到右 |
函数调用、数组索引 | |
? | |
一元 - * ! & &mut | |
as | 从左到右 |
* / % | 从左到右 |
+ - | 从左到右 |
<< >> | 从左到右 |
& | 从左到右 |
^ | 从左到右 |
| | 从左到右 |
== != < > <= >= | 需要括号 |
&& | 从左到右 |
|| | 从左到右 |
.. ..= | 需要括号 |
= += -= *= /= %= &= |= ^= <<= >>= | 从右到左 |
return break 闭包 |
操作数的求值顺序
以下表达式列表的所有表达式都以相同的方式对其操作数进行计算。 其他表达式要么不需要操作数,要么根据其各自的语法进行有条件的计算。
- 解引用表达式
- 错误传导表达式
- 取反表达式
- 算术和逻辑二元运算符
- 比较运算符
- 类型转换表达式
- 分组表达式
- 数组表达式
await
表达式- 索引表达式
- 元组表达式
- 元组索引表达式
- 结构体表达式
- 函数调用表达式
- 方法调用表达式
- 字段表达式
break
表达式- 范围表达式
return
表达式
这些表达式的操作数在产生表达式副作用之前进行求值。 取多个操作数的表达式按照源代码中从左到右的顺序进行求值。
注意: 其子表达式是否为一个表达式的操作数由前面的 "表达式优先级" 决定。
例如,两个 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()) ); }
注意: 由于这个规则是递归的,所以这些表达式从内到外依次求值,直到没有更深层次的子表达式。在这个过程中会暂时忽略同级的表达式。
占位表达式和值表达式
表达式从求值的特性上可区分为: 占位表达式和值表达式,以及次要的可赋值表达式。 在表达式中,操作数可以出现在占位上下文中或值上下文中。
占位表达式 是表示内存地址的表达式。这些表达式以 路径 引用局部变量、 静态变量 、 解引用 、 数组索引 表达式 ( expr[expr]
) 、 字段 引用 (expr.f
) 和带括号的占位表达式。其他表达式都是值表达式。
值表达式 是表示真实值的表达式。
以下为占位表达式上下文:
- 复合赋值 表达式的左操作数。
- 单目 借用 、 取地址 或 解引用 操作符的操作数。
- 字段表达式的操作数。
- 数组索引表达式的索引操作数。
- 任何 隐式借用 的操作数。
- let 语句 的初始化器。
if let
、match
或while let
表达式的 被匹配项 。- 结构体函数式更新 表达式的基础。
译注: 基础这个概念在多处被使用,类似于基类,但 rust 中没传统继承这概念,使用基类将可能引起读者混淆。
注意: 从历史上看,占位表达式称为左值,值表达式称为右值。
可赋值表达式 是指出现在 赋值 表达式的左操作数的表达式。 确切来说,可赋值表达式可以是:
- 占位表达式
- 下划线表达式。
- 包含可赋值表达式的 元组表达式。
- 包含可赋值表达式的 数组表达式。
- 包含可赋值表达式的 元组结构体表达式。
- 包含可赋值表达式的 结构体表达式 (可包含命名字段) 。
- 单元结构体。
在可赋值表达式中允许使用任意括号。
移动和复制类型
当在值表达式上下文中计算占位表达式,或者通过值在模式中绑定占位表达式时,它表示该内存位置所存储的值。
如果该值的类型实现了 Copy
,那么该值将被复制。
其它情况,如果该类型是 Sized
,那么可能移动该值。
只有以下占位表达式可以 '移出' :
在计算时从局部变量的占位表达式中移动出后,该占位将被取消初始化,不能再次读取,直到重新初始化。 在其他情况,尝试在值表达式上下文中使用占位表达式将产生错误。
Mutability
要将一个占位表达式赋值、进行可变借用、进行隐式可变借用或绑定到包含 ref mut
的模式中时,则必须是可变的。
这被称为可变占位表达式,其他称为不可变占位表达式。
下面的表达式可以是可变占位表达式上下文:
- 当前没有被借用的可变 变量 。
- 可变
static
条目 。 - 临时值 。
- 字段: 这将在可变占位表达式上下文中计算子表达式。
*mut T
指针的 解引用 。- 类型为
&mut T
的变量或变量字段的解引用。注意:这是对下一条规则的例外情况。 - 实现
DerefMut
的类型的解引用: 这要求被解引用的值在可变占位表达式上下文中求值。 - 实现
IndexMut
的类型的 数组索引: 这将在可变占位表达式上下文中计算被索引的值,但不包括索引。
临时值
在大多数占位表达式上下文中使用值表达式时,会创建一个临时的无名内存位置,并将其初始化为该值。
表达式计算为该位置,除非它被 提升 为 static
静态的。
临时值的 丢弃作用域 通常是封闭语句的末尾。
隐式借用
某些表达式将会通过隐式借用将一个表达式作为占位表达式。
比如,可以直接比较两个非固定大小的 切片 是否相等,因为 ==
运算符会隐式地借用它的操作数:
#![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); }
隐式借用可以出现在以下表达式中:
trait 重载
许多以下的运算符和表达式可以使用 std::ops
或 std::cmp
中的 trait 重载为其他类型可用。
这些 trait 在 core::ops
和 core::cmp
中也有相同的名称。
表达式属性
在表达式前面的 外围属性 只允许在以下几种情况下使用:
它们不允许在以下情况下使用:
字面值表达式
语法
字面值表达式 :
字符字面值
| 字符串字面值
| 原始字符串字面值
| 字节字面值
| 字节字符串字面值
| 原始字节字符串字面值
| 整数字面值
| 浮点数字面值
|true
|false
字面值表达式 仅由一个单独的 token 组成,其直接指示计算结果值,而不是通过名称或其他计算规则的间接引用。
字面值是 常量表达式 的其中一种形式,因此在编译时计算 (主要) 。
先前描述的词法 字面值 每种形式都可以形成字面值表达式,关键字 true
和 false
也可以。
#![allow(unused)] fn main() { "hello"; // 字符串类型 '5'; // 字符类型 5; // 整数类型 }
字符字面量表达式
字符字面量表达式由一个 字符字面值 token 构成。
注意: 此部分内容不完整。
字符串字面量表达式
字符串字面量表达式由一个 字符串字面值 或 原始字符串字面值 token 构成。
注意: 此部分内容不完整。
字节字面量表达式
字节字面量表达式由一个 字节字面值 token 牌构成。
注意: 此部分内容不完整。
字节字符串字面量表达式
字节字符串字面量表达式由一个 字节字符串字面值 或 原始字节字符串字面值 token 构成。
注意: 此部分内容不完整。
整型字面值表达式
整型字面值表达式由一个 整数字面值 token 组成。
如果 token 有 后缀 ,则后缀必须是 基本整数类型 中的一个名称: u8
、 i8
、 u16
、 i16
、 u32
、 i32
、 u64
、 i64
、 u128
、 i128
、 usize
或 isize
,且表达式具有该类型。
如果 token 没有后缀,则该表达式的类型由类型推断确定:
-
如果整型类型可以从周围的程序上下文中确定,则表达式具有该类型。
-
如果程序上下文中的整型类型不足以确定类型,则默认为有符号的 32 位整型
i32
。 -
如果程序上下文中的整型类型过多,则被视为静态类型错误。
整型字面量表达式的例子:
#![allow(unused)] fn main() { 123; // type i32 123i32; // type i32 123u32; // type u32 123_u32; // type u32 let a: u64 = 123; // type u64 0xff; // type i32 0xff_u8; // type u8 0o70; // type i32 0o70_i16; // type i16 0b1111_1111_1001_0000; // type i32 0b1111_1111_1001_0000i64; // type i64 0usize; // type usize }
该表达式的值根据 token 的字符串表示如下确定:
-
通过检查字符串的前两个字符,选择整数基数,如下:
0b
表示基数为 2。0o
表示基数为 8。0x
表示基数为 16。- 否则,基数为 10。
-
如果基数不是 10,则从字符串中删除前两个字符。
-
从字符串中删除任何后缀。
-
从字符串中删除任何下划线。
-
将字符串转换为
u128
值,就像通过u128::from_str_radix
选择的基数一样。如果值不适合u128
,则会出现编译器错误。 -
通过 数字转换 将
u128
值转换为表达式的类型。
注意: 如果该字面值的值超出了该类型的表示范围,最终转换将截断该字面值。
rustc
包括一个名为overflowing_literals
的 [代码分析检查] lint check ,默认为deny
,拒绝其中出现的表达式。
注意: 例如,
-1i8
是对字面值表达式1i8
应用了 取反运算符 ,而不是一个整数字面值表达式。有关表示有符号类型的最大负值的注解,请参见 溢出 。
浮点数字面值表达式
浮点数字面值表达式具有以下两种形式:
如果 token 具有 后缀 ,则后缀必须是 浮点类型 f32
或 f64
之一,且表达式具有该类型。
如果 token 没有后缀,则表达式的类型由类型推断确定:
-
如果可以从周围的程序上下文中 唯一 确定浮点类型,则表达式具有该类型。
-
如果程序上下文不足以确定类型,则默认为
f64
。 -
如果程序上下文类型约束冲突,则被视为静态类型错误。
浮点数字面值表达式的示例:
#![allow(unused)] fn main() { 123.0f64; // type f64 0.1f64; // type f64 0.1f32; // type f32 12E+99_f64; // type f64 5f32; // type f32 let x: f64 = 2.; // type f64 }
表达式的值根据 token 的字符串表示形式确定,具体如下:
- 从字符串中删除任何后缀。
- 从字符串中删除任何下划线。
- 将字符串转换为表达式的类型,如同通过
f32::from_str
或f64::from_str
处理。
注意: 例如,
-1.0
是对字面表达式1.0
应用 取反运算符 ,而不是单个浮点字面值表达式。
注意:
inf
和NaN
不是字面值 token 。 可以使用f32::INFINITY
、f64::INFINITY
、f32::NAN
和f64::NAN
常量来代替字面值表达式。 在rustc
中,被评估为无限大的字面值将触发overflowing_literals
代码分析检查。
布尔字面值表达式
布尔字面值表达式由 true
或 false
中的一个关键字组成。
该表达式的类型为原始的 布尔类型 ,其值是:
- 如果关键字是
true
,则为真 - 如果关键字是
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; }
块表达式
语法
块表达式 :
{
内部属性*
语句组?
}
块表达式 或 块 是控制流表达式和用于条目和变量声明的匿名命名空间作用域。
对于控制流表达式,块按顺序执行其组成的非条目声明语句,然后执行其最后的可选表达式。
在块匿名命名空间作用域内部的条目声明,对于块本身可用,而由 let
语句声明的变量从下一条语句开始到块结束之前可用。
块的语法是 {
,然后是 内部属性 ,以及任意数量的 语句 ,随后是一个可选的表达式,称为最终操作数,和 }
。
语句通常需要在分号后面,有两个例外:
- 条目声明语句不需要在分号后面。
- 表达式语句通常需要分号,除非其外围表达式是控制流表达式。
此外,语句之间允许有额外的分号,但不影响语义。
在评估块表达式时,除条目声明语句外,每个语句都按顺序执行。 然后,如果给定了最终操作数,则执行它。
块的类型是最终操作数的类型,如果省略,则为 ()
。
#![allow(unused)] fn main() { fn fn_call() {} let _: () = { fn_call(); }; let five: i32 = { fn_call(); 5 }; assert_eq!(5, five); }
注意: 对于控制流表达式,如果块表达式是表达式语句的外围表达式,则期望的类型是
()
,除非它紧跟着一个分号。
块始终是值表达式,并在值表达式上下文中计算最终操作数。
注意: 如果确实需要,这一特性可用于强制移动值。例如,以下示例在调用
consume_self
时失败,因为结构体已在块表达式中移动了s
。#![allow(unused)] fn main() { struct Struct; impl Struct { fn consume_self(self) {} fn borrow_self(&self) {} } fn move_by_block_expression() { let s = Struct; // 在块表达式中将值从 `s` 移出。 (&{ s }).borrow_self(); // 无法执行,因为 `s` 已经被移出了。 s.consume_self(); } }
async
块
语法
Async块表达式 :
async
move
? 块表达式
异步块是块表达式的一种变体,值求解为一个 future 。 块的最终表达式 (如果存在) 确定 future 的结果值。
执行异步块类似于执行闭包表达式:
它的即时效果是生成并返回一个匿名类型。
返回实现一个或多个 std::ops::Fn
trait 的类型,异步块返回的类型实现了 std::future::Future
trait。
这种类型的实际数据格式是未指明的。
注意: rustc 生成的 future 类型大致相当于具有每个
await
点一个变体的枚举类型,其中每个变体存储从其相应点恢复所需的数据。 版本差异: 异步块仅在 Rust 2018 及以后版本中可用。
捕获模式
异步块使用与闭包相同的 捕获模式 从环境中捕获变量。
与闭包一样,当写成 async { .. }
时,每个变量的捕获模式将从块的内容推断。
然而, async move { .. }
块将移动所有被引用的变量到生成的 future 中。
异步上下文
由于异步块构造了一个 future ,其定义了一个 异步上下文 ,这个上下文可以包含 await
表达式 。
异步上下文由异步块以及异步函数的函数体构建,异步函数的语义是以异步块为基础进行定义的。
控制流运算符
异步块有类似于函数的约束,就像闭包一样。
因此, ?
运算符和 return
表达式都会影响 future 的输出,而不是封闭函数或其他上下文。
也就是说,从异步块中的 return <expr>
将返回 <expr>
的结果作为 future 的输出。
同样,如果 <expr>?
传播错误,则该错误将作为 future 的结果传播。
最后, break
和 continue
关键字不能用于从异步块中分支跳出。
以下代码是非法的:
#![allow(unused)] fn main() { loop { async move { break; // error[E0267]: `break` inside of an `async` block } } }
unsafe
块
语法
Unsafe块表达式 :
unsafe
块表达式
在需要进行 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
、while let
和for
)。 - 作为 语句 使用的块表达式。
- 块表达式作为 数组表达式 、 元组表达式 、 调用表达式 和类似元组的 结构体 表达式的元素。
- 另一个块表达式的尾表达式是块表达式。
在块表达式上具有含义的外围属性包括 cfg
和 代码分析检查属性 。
例如,以下函数在 Unix 平台上返回 true
,在其他平台上返回 false
。
#![allow(unused)] fn main() { fn is_unix_platform() -> bool { #[cfg(unix)] { true } #[cfg(not(unix))] { false } } }
运算符表达式
语法
运算符表达式 :
借用表达式
| 解引用表达式
| 错误传导表达式
| 取反表达式
| 算术或逻辑表达式
| 比较表达式
| 惰性布尔表达式
| 类型转换表达式
| 赋值表达式
| 复合赋值表达式
在 Rust 语言中,内置类型的运算符已经被定义好了。
许多下面提到的运算符也可以通过 std::ops
或 std::cmp
中的 trait 进行重载。
溢出
调试模式下编译时,整数运算溢出会触发 panic。
编译器可以使用 -C debug-assertions
和 -C overflow-checks
标志控制溢出行为。
下列情况时将触发溢出:
- 当
+
、*
或二元-
创建的值大于可存储的最大值或小于最小值。 - 对任何有符号整数类型的最小值应用一元
-
,除非操作数是 字面值表达式 (或在一个或多个 分组表达式 中单独使用的字面值表达式) 。 /
或%
其左操作数是有符号整数类型的最小整数且右操作数是-1
时。 出于兼容性原因,即使禁用了-C overflow-checks
,此检查也会发生。<<
或>>
其右操作数大于或等于左操作数类型的位数或为负时。
注意: 对于一元
-
后面的字面值表达式,这意味着类似-128_i8
或let j: i8 = -(128)
的形式永远不会导致 panic,并且其期望值为 -128 。在这些情况下,字面值表达式已经具有其类型的最小值 (例如,
128_i8
的值为 -128) ,因为整数字面值按照 整数字面值表达式 中的描述被截断为其类型。这些最小值的否定不会改变值,因为采用了二进制补码的溢出约定。
在
rustc
中,这些最小值表达式也被overflowing_literals
代码分析检查忽略。
借用运算符
&
(共享借用) 和 &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; }
原始地址运算符
与借用运算符相关的是 原始地址运算符 ,它们没有一级语法,是以宏 ptr::addr_of!(expr)
和 ptr::addr_of_mut!(expr)
的方式公开。
表达式 expr
在占位表达式上下文中进行评估。
ptr::addr_of!(expr)
而后创建类型为 *const T
的常量原始指针指向给定的地址,而 ptr::addr_of_mut!(expr)
则创建类型为 *mut T
的可变原始指针。
每当占位表达式可以评估为未正确对齐或不存储有效值 (根据其类型确定) ,或者每当创建引用会引入不正确的别名假设时,必须使用原始地址运算而不是借用运算符。 在这些情况下,使用借用运算符将创建无效引用导致 未定义行为 ,但仍可以使用地址运算符构造原始指针。
以下是通过 packed
结构体创建指向未对齐地址的原始指针的示例:
#![allow(unused)] fn main() { use std::ptr; // 定义一个 packed 结构体 #[repr(packed)] struct Packed { f1: u8, f2: u16, } // 创建一个 packed 实例 let packed = Packed { f1: 1, f2: 2 }; // `&packed.f2` 会创建一个未对齐的引用,因此是未定义行为! // 使用 `ptr::addr_of!` 创建一个指向 `packed.f2` 的不可变原始指针 let raw_f2 = ptr::addr_of!(packed.f2); // 读取 `raw_f2` 指向的值,并断言其值等于2 assert_eq!(unsafe { raw_f2.read_unaligned() }, 2); }
以下是创建指向不包含有效值的地址的原始指针的示例:
#![allow(unused)] fn main() { use std::{ptr, mem::MaybeUninit}; struct Demo { field: bool, } let mut uninit = MaybeUninit::<Demo>::uninit(); // &uninit.as_mut().field 将创建对未初始化的 `bool` 的引用,因此为未定义行为! let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) }; // 将 'true' 写入先前未初始化的字段。 unsafe { f1_ptr.write(true); } // 因为 'uninit' 已在上面初始化,所以是安全的。 let init = unsafe { uninit.assume_init() }; }
解引用操作符
语法
解引用表达式 :
*
表达式
解引用运算符 *
也是一元前缀运算符。
当应用于 指针 时,表示指向的地址。
如果表达式的类型为 &mut T
或 *mut T
,并且是一个局部变量、局部变量的 (嵌套) 字段或可变的 占位表达式 ,则可以将结果的内存位置分配给其它变量。
对一个裸指针进行解引用需要使用 unsafe
。
对于非指针类型, *x
在不可变的 占位表达式上下文 中等同于 *std::ops::Deref::deref(&x)
,在可变的占位表达式上下文中等同于 *std::ops::DerefMut::deref_mut(&mut x)
。
#![allow(unused)] fn main() { let x = &7; assert_eq!(*x, 7); let y = &mut 9; *y = 11; assert_eq!(*y, 11); }
问号操作符
语法
错误传导表达式 :
表达式?
问号运算符 (?
) 可以展开有效的值或返回错误值,并将它们传播到调用函数中。
它是一元后缀运算符,只能应用于 Result<T, E>
和 Option<T>
类型。
当应用于 Result<T, E>
类型的值时,能够传播错误。
如果该值是 Err(e)
,从包含的函数或闭包返回 Err(From::from(e))
。应用于 Ok(x)
时,将展开该值求值为 x
。
#![allow(unused)] fn main() { use std::num::ParseIntError; fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // 立即返回一个 Err() Ok(x + y) // 不会被执行 } let res = try_to_parse(); println!("{:?}", res); assert!(res.is_err()) }
当应用于类型为 Option<T>
的值时,会传播 None
。如果值为 None
,则返回 None
。如果应用于 Some(x)
,将展开该值以求值为 x
。
#![allow(unused)] fn main() { fn try_option_some() -> Option<u8> { let val = Some(1)?; Some(val) } assert_eq!(try_option_some(), Some(1)); fn try_option_none() -> Option<u8> { let val = None?; Some(val) } assert_eq!(try_option_none(), None); }
?
不能被重载。
取反运算符
这是最后两个一元操作符。 这张表概括了它们在原始类型上的行为,以及用于重载其他类型的这些操作符的 trait 。 请记住,有符号整数总是使用二进制补码表示。所有这些操作符的操作数都在 值表达式上下文 中计算,因此它们会被移动或复制。
符号 | 整数 | 布尔 | 浮点数 | 重载 Trait |
---|---|---|---|---|
- | 取反* | 取反 | std::ops::Neg | |
! | 按位取反 | 逻辑取反 | std::ops::Not |
* 仅适用于有符号整数类型。
下面是这些运算符的一些示例
#![allow(unused)] fn main() { let x = 6; assert_eq!(-x, -6); assert_eq!(!x, -7); assert_eq!(true, !false); }
算术和逻辑二元操作符
语法
算术或逻辑表达式 :
表达式+
表达式
| 表达式-
表达式
| 表达式*
表达式
| 表达式/
表达式
| 表达式%
表达式
| 表达式&
表达式
| 表达式|
表达式
| 表达式^
表达式
| 表达式<<
表达式
| 表达式>>
表达式
二元运算符表达式均以中缀表示。此表总结了原始类型上算术和逻辑二元运算符的行为,以及用于重载其他类型的这些运算符的 trait 。 请记住,有符号整数始终使用二进制补码表示。所有这些运算符的操作数都在 值表达式上下文 中计算,因此会被移动或复制。
符号 | 整数 | 布尔 | 浮点数 | 重载 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 | |
& | 按位与 | 逻辑与 | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位或 | 逻辑或 | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位异或 | 逻辑异或 | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移*** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法向零取整。
** Rust 使用的余数采用 截断除法 定义。
给定 remainder = dividend % divisor
,余数将与被除数具有相同的符号。
*** 在有符号整数类型上进行算术右移,无符号整数类型上进行逻辑右移。
以下是使用这些运算符的示例:
#![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); }
比较运算符
语法
比较表达式 :
表达式==
表达式
| 表达式!=
表达式
| 表达式>
表达式
| 表达式<
表达式
| 表达式>=
表达式
| 表达式<=
表达式
在基本类型和标准库的许多类型上都定义了比较运算符。当比较运算符链接时,需要使用括号。
例如,表达式 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"); }
惰性布尔运算符
运算符 ||
和 &&
可以用于布尔类型的操作数。 ||
运算符表示逻辑或, &&
运算符表示逻辑与。
它们与 |
和 &
的区别在于,只有当左操作数不能确定表达式结果时才会对右操作数进行求值。
也就是说,只有当左操作数求值结果为 false
时 ||
才会对右操作数进行求值,只有当左操作数求值结果为 true
时 &&
才会对右操作数进行求值。
#![allow(unused)] fn main() { let x = false || true; // true let y = false && panic!(); // false 不会对右侧的操作数进行求值,因此 `panic!()` 表达式不会被执行。 }
类型转换表达式
类型转换表达式使用二元运算符 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 where V: Sized * | 指针类型到指针类型的转换 |
*T where T: Sized | 整型类型 | 指针类型到地址类型的转换 |
整型类型 | *V where V: Sized | 地址类型到指针类型的转换 |
&m₁ T 类型 | *m₂ T ** | 引用类型到指针类型的转换 |
&m₁ [T; n] 类型 | *m₂ T ** | 数组类型到指针类型的转换 |
函数条目 类型 | 函数指针 类型 | 函数条目到函数指针类型的转换 |
函数条目 类型 | *V where V: Sized | 函数条目到指针类型的转换 |
函数条目 类型 | 整型类型 | 函数条目到地址类型的转换 |
函数指针 类型 | *V where V: Sized | 函数指针到指针类型的转换 |
函数指针 类型 | 整型类型 | 函数指针到地址类型的转换 |
闭包类型 *** | 函数指针类型 | 闭包类型到函数指针类型的转换 |
* 当 T
和 V
都是不定大小类型但类型相同时,例如都是切片类型,它们之间是兼容的。
** 当且仅当 m₁
为 mut
或 m₂
为 const
时。允许将 mut
引用转换为 const
指针。
*** 仅适用于没有捕获 (关闭) 任何局部变量的闭包。
语义
数值转换
- 两个大小相同的整数之间的转换 (例如 i32 -> u32) 是无操作的 (Rust 对于固定整数的负值使用 2 的补码)
- 从较大的整数转换为较小的整数 (例如 u32 -> u8) 会截断
- 从较小的整数转换为较大的整数 (例如 u8 -> u32) 将:
- 如果源是无符号的,则用零填充
- 如果源是有符号的,则使用符号扩展
- 将浮点数转换为整数会将浮点数向零舍入
NaN
将返回0
- 大于最大整数值的值,包括
INFINITY
,将饱和到整数类型的最大值。 - 小于最小整数值的值,包括
NEG_INFINITY
,将饱和到整数类型的最小值。
- 将整数转换为浮点数将产生可能最接近的浮点数 *
- 如有必要,舍入按照
roundTiesToEven
模式进行 *** - 溢出时,将生成无限大 (与输入相同的符号)
- 注意:对于当前的数字类型集,只有在
u128 as f32
的值大于或等于f32::MAX + (0.5 ULP)
时才会发生溢出
- 如有必要,舍入按照
- 从 f32 转换为 f64 是完美的和无损的
- 从 f64 转换为 f32 将产生可能最接近的 f32 **
- 如有必要,舍入按照
roundTiesToEven
模式进行 *** - 溢出时,将生成无限大 (与输入相同的符号)
- 如有必要,舍入按照
* 如果硬件不支持此舍入模式和溢出行为的整数到浮点数的转换,则此类转换可能比预期的慢。
** 如果硬件不支持此舍入模式和溢出行为的 f64 到 f32 的转换,则此类转换可能比预期的慢。
*** 正如 IEEE 754-2008 §4.3.1: 中所定义的那样:选择最接近的浮点数,如果恰好介于两个浮点数之间,则优先选择带有偶数最低位数字的数字。
枚举转换
将枚举转换为其判别值,然后根据需要使用数值转换。转换仅限于以下种类的枚举:
原始类型到整数的转换
false
转换为0
,true
转换为1
char
转换为其码点值,然后如果需要则使用数字转换。
u8
到 char
的类型转换
将 u8
类型的值转换为相应的 Unicode 编码对应的字符类型 char
。
指针到地址转换
将裸指针转换为整数类型会产生指向内存的机器地址。
如果整数类型比指针类型小,地址可能会被截断;使用 usize
可以避免这种情况。
地址到指针的强制类型转换
将整数强制类型转换为原始指针会将该整数解释为内存地址,并产生一个指向该内存的指针。
警告: 这会与 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); }
赋值表达式
一个 赋值表达式 将一个值移动到一个指定的位置。
一个赋值表达式由一个 可变的 ,即 赋值运算符 ,后面跟一个等于号 (=
) 和一个 值表达式 ,即 被赋的值操作数 组成。
在最基本的形式中,一个赋值运算符是一个 占位表达式 ,我们首先讨论这种情况。
下面讨论更一般的解构赋值情况,但是这种情况总是分解为对占位表达式的顺序赋值,这可能被当成更为基本的情况。
基本赋值语句
对赋值表达式求值,需要先对操作数进行求值。先对被赋值操作数进行求值,然后是赋值操作数表达式。对于解构赋值,可赋值表达式的子表达式从左到右进行求值。
注意:与其他表达式不同的是,右操作数在左操作数之前进行求值。该表达式首先有一个效果:删除被赋值位置的值,除非被赋值的地方是未初始化的局部变量或局部变量的未初始化字段。然后将赋值的值复制或移动到被赋值的地方。
赋值表达式总是生成 单元值 。
Example:
#![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
进行解构声明不同,由于语法歧义,模式不能出现在赋值语句的左侧。
相反,一组对应模式的表达式被指定为 可赋值表达式 ,并允许出现在赋值语句的左侧。
然后,将赋值表达式展开为模式匹配,然后进行顺序赋值。
展开后的模式必须是不可拒绝的:特别地,这意味着仅允许具有在编译时已知长度的切片模式以及简单的切片 [..]
用于解构赋值。
展开的方法很简单,最好通过示例来说明。
#![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}; // desugars to: { 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; } }
在单个可赋值表达式中,标识符不禁止被多次使用。
下划线表达式 和空 区间表达式 可用于忽略某些值,而不将它们绑定到变量上。
注意,对于展开后的表达式,不适用默认绑定模式。
复合赋值表达式
语法
复合赋值表达式 :
表达式+=
表达式
| 表达式-=
表达式
| 表达式*=
表达式
| 表达式/=
表达式
| 表达式%=
表达式
| 表达式&=
表达式
| 表达式|=
表达式
| 表达式^=
表达式
| 表达式<<=
表达式
| 表达式>>=
表达式
复合赋值表达式 将算术和逻辑二元运算符与赋值表达式结合在一起。
例如:
#![allow(unused)] fn main() { let mut x = 5; x += 1; assert!(x == 6); }
复合赋值语法是一个 可变的 占位表达式,紧接着是 被赋值的操作数 ,然后是其中一个运算符,后面跟着一个 =
符号 (无空格) ,最后是一个 值表达式 ,即 修改操作数 。
与其他占位操作数不同,被赋值的占位操作数必须是一个占位表达式。尝试使用值表达式会导致编译错误,而不是将其升级为临时值。
复合赋值表达式的求值取决于操作符的类型。
如果两个类型都是原始类型,则将先计算修改操作数,然后是被赋值的操作数。 然后它将使用执行该运算符操作的被赋值操作数和修改操作数的值来设置被赋值操作数的占位的值。
注意: 这与其他表达式不同,因为右操作数在左操作数之前被评估。
否则,这个表达式是语法糖,用于调用运算符的重载复合赋值特性的函数 (参见本章前面的表格) 。 被赋值的操作数会自动被可变地借用。
例如,在 example
中,以下表达式语句是等价的:
#![allow(unused)] fn main() { struct Addable; use std::ops::AddAssign; impl AddAssign<Addable> for Addable { /* */ fn add_assign(&mut self, other: Addable) {} } fn example() { let (mut a1, a2) = (Addable, Addable); a1 += a2; let (mut a1, a2) = (Addable, Addable); AddAssign::add_assign(&mut a1, a2); } }
与赋值表达式一样,复合赋值表达式始终生成 单元值 。
警告:操作数的求值顺序根据操作数的类型而交换:对于原始类型,右操作数将首先得到评估,而对于非原始类型,左操作数将首先得到评估。 尽量不要编写依赖于复合赋值表达式中操作数的求值顺序的代码。参见 这个测试 ,了解使用此依赖性的示例。
分组表达式
语法
分组表达式 :
(
表达式)
括号表达式 将一个单独的表达式包裹起来,最终的结果是这个表达式的值。
括号表达式的语法是 (
,然后是一个表达式,称为 内嵌操作数 ,随后是一个 )
。
括号表达式的值等同于内嵌操作数的值。与其他表达式不同的是,括号表达式既是 占位表达式 也是值表达式。 当内嵌操作是占位表达式时,括号表达式也是占位表达式;当内嵌操作是值表达式时,括号表达式也是值表达式。
括号可以用于显式修改表达式中子表达式的优先级顺序。
以下是括号表达式的示例:
#![allow(unused)] fn main() { let x: i32 = 2 + 3 * 4; // 未使用括号的表达式 let y: i32 = (2 + 3) * 4; // 使用了括号的表达式 assert_eq!(x, 14); // 验证 x 的值是否为 14 assert_eq!(y, 20); // 验证 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"); }
数组和数组索引表达式
数组表达式
语法
数组表达式 :
[
数组元素组?]
数组表达式 构造 数组 。 数组表达式有两种形式。
第一种形式列出数组中的每个值。 这种形式的语法是将类型相同的表达式列表用逗号隔开并用方括号括起来。 这会产生一个包含按照书写顺序排列的这些值的数组。
第二种形式的语法是两个表达式之间用分号 (;
) 隔开并用方括号括起来。
分号前的表达式称为 重复操作数 。
分号后的表达式称为 长度操作数 。
它必须是类型为 usize 的 常量表达式 ,例如 字面值 或 常量条目 。
这种形式的数组表达式会创建一个长度为长度操作数的值的数组,每个元素都是重复操作数的副本。
也就是说,[a; b] 创建一个包含 b 个 a 值的副本的数组。
如果长度操作数的值大于 1 ,则要求重复操作数的类型是 Copy
,或者必须是指向常量条目的 路径 。
当重复操作数是常量条目时,它会被计算长度操作数的值的次数。 如果该值为 0 ,则不会对常量条目进行计算。 对于不是常量条目的表达式,则进行一次计算,然后将结果以长度操作数的次数复制。
#![allow(unused)] fn main() { [1, 2, 3, 4]; ["a", "b", "c", "d"]; [0; 128]; // 数组为 128 个 0 [0u8, 0u8, 0u8, 0u8,]; [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 二维数组 const EMPTY: Vec<i32> = Vec::new(); [EMPTY; 2]; }
数组和切片索引表达式
数组 和 切片 类型的值可以通过在它们后面写一个类型为 usize
的方括号括起来的表达式进行索引。
当数组是可变的,其结果的 内存位置 可以被赋值。
对于其他类型,索引表达式 a[b]
等价于 *std::ops::Index::index(&a, b)
,或者,在可变占位表达式的上下文中等价于 *std::ops::IndexMut::index_mut(&mut a, b)
。就像方法一样,会对 a
解引用以找到实现。
数组和切片的索引是从零开始。 数组访问是一个 常量表达式 ,因此如果索引值是常量,编译时可以进行边界检查。 否则,运行时会进行检查,如果检查失败,则线程将置于 恐慌状态。
#![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 来实现其他类型的索引。
元组和元组索引表达式
元组达式
语法
元组表达式 :
(
元组元素组?)
元组表达式 构造了 元组值 。
元组表达式的语法是括号包围的、用逗号分隔的表达式列表,称为 元组初始化操作数 。 一元组表达式需要在其元组初始化操作数后面加上逗号,以与 圆括号表达式 区分。
元组表达式是 值表达式 ,它的结果是一个新构造的元组类型的值。
元组初始化操作数的数量是构造的元组的元的数量。
没有元组初始化操作数的元组表达式会产生一个单元元组。
对于其他元组表达式,第一个写入的元组初始化操作数初始化字段 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) |
元组索引表达式
元组索引表达式的语法是一个表达式,即 元组操作数 ,后跟一个点号 ( .
) ,最后是一个元组索引。
元组索引的语法是一个 十进制字面值 ,没有前导零、下划线或后缀。
例如,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); }
注意: 与字段访问表达式不同,元组索引表达式可以作为 调用表达式 的函数操作,因为它不能与方法调用混淆,因为方法名称不能是数字。
注意: 尽管数组和切片也有元素,但必须使用 数组或切片索引表达式 或 切片模式 来访问它们的元素。
结构体表达式
语法
结构体表达式 :
结构体表达式结构
| 结构体表达式元组
| 结构体表达式单元结构体表达式结构 :
表达式中路径{
(结构体表达式字段组 | 结构省略)?}
结构体表达式字段组 :
结构体表达式字段 (,
结构体表达式字段)* (,
结构省略 |,
?)结构体表达式字段 :
外围属性 *
(
标识符
| (标识符 | 元组索引):
表达式
)结构省略 :
..
表达式结构体表达式元组 :
表达式中路径(
( 表达式 (,
表达式)*,
? )?
)
结构体表达式单元 : 表达式中路径
结构体表达式 用于创建结构体、枚举或联合体值。 它由指向 结构体 、 枚举变体 或 联合体 条目的路径,以及该条目字段的值组成。 结构体表达式有三种形式:结构体、元组和单元结构体。
以下是结构体表达式的示例:
#![allow(unused)] fn main() { struct Point { x: f64, y: f64 } struct NothingInMe { } struct TuplePoint(f64, f64); mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } } struct Cookie; fn some_fn<T>(t: T) {} Point {x: 10.0, y: 20.0}; NothingInMe {}; TuplePoint(10.0, 20.0); TuplePoint { 0: 10.0, 1: 20.0 }; // 得到的结果与上一行相同 let u = game::User {name: "Joe", age: 35, score: 100_000}; some_fn::<Cookie>(Cookie); }
字段结构体表达式
带有花括号括起来的字段结构体表达式允许你以任何顺序指定每个单独字段的值。字段名称用冒号与其值分开。
联合体 类型的值只能使用此语法创建,并且必须指定一个字段。
函数式更新语法
用于构造结构体类型值的结构体表达式可以用 ..
语法结尾,后跟表达式,表示函数式更新。
..
后面的表达式 (基表达式) 必须与新结构体类型相同。
整个表达式使用给定值填充指定的字段,并将其余字段从基表达式中移动或复制。 与所有结构体表达式一样,结构体的所有字段都必须是 可见的 ,即使未显式命名。
#![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}; // 成功, 仅 base.x 是被访问 drop(y_ref); }
带大括号的结构体表达式不能直接用于 loop 或 if 表达式头部,也不能用于 if let 或 match 表达式的 被匹配项。 但是,如果结构体表达式在另一个表达式中,例如在 括号 中,它们就可以在这些情况下使用。
字段名称可以是十进制整数值,用于指定用于构造元组结构体的索引。这可以与基础结构体一起使用,以填充未指定的其余索引:
#![allow(unused)] fn main() { struct Color(u8, u8, u8); // 定义一个名为 Color 的元组结构体 let c1 = Color(0, 0, 0); // 创建元组结构体实例的一般方式 let c2 = Color{0: 255, 1: 127, 2: 0}; // 按索引指定结构体字段的值 let c3 = Color{1: 0, ..c2}; // 使用基础结构体填充所有其他字段 }
结构体字段初始化的简写语法
在初始化具有命名 (但不是编号) 字段的数据结构 (struct、enum、union) 时,可以使用 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 }; }
元组结构体表达式
用括号括起来的字段的结构体表达式构造元组结构体。 虽然为了完整性而在此列出了它作为一个特定的表达式,但它等同于对元组结构体构造函数的 调用表达式 。 例如:
#![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` 值 }
单元结构体表达式
单元结构体表达式只是指向一个空结构体条目的路径。 这个路径指向该结构体的值的隐式常量。 单元结构体的值也可以用一个没有字段的结构体表达式构造。例如:
#![allow(unused)] fn main() { struct Gamma; let a = Gamma; // Gamma 单元值。 let b = Gamma{}; // 与 `a` 完全相同的值。 }
调用表达式
语法
调用语法 :
表达式(
调用参数组?)
调用表达式 调用函数。
调用表达式的语法是一个称为 函数操作数 表达式,后面跟着一个带括号的逗号分隔的表达式列表,称为 参数操作数 。
如果函数最终返回,则表达式完成。
对于 非函数类型 ,表达式 f(...)
使用 std::ops::Fn
、 std::ops::FnMut
或 std::ops::FnOnce
trait 中的方法,这些 trait 在是否通过引用、可变引用或取得所有权方面有所不同。
如果需要,将自动借用。函数操作数也将根据需要 自动解引用 。
以下是一些调用表达式的例子:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { 0 } let three: i32 = add(1i32, 2i32); let name: &'static str = (|| "Rust")(); }
消除函数调用的歧义
所有函数调用都是 完全限定语法 的语法糖。 根据调用的歧义性以及作用域内的条目,函数调用可能需要完全限定。
注意:在过去,术语 "无歧义函数调用语法" 、 "通用函数调用语法" 或 "UFCS" 已被用于文档、问题、 RFC 和其他社区资料。 但是,这些术语缺乏描述能力,可能会让问题变得更加混乱。 在这里提及是为了便于内容检索。
有几种常见情况可能导致方法或关联函数调用的接收者或引用产生歧义。 这些情况可能包括:
- 多个作用域内的 trait 为相同类型定义了同名方法
- 不希望自动解引用;例如,区分智能指针本身的方法和指针引用的方法
- 不带参数的方法,例如
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; // 可以这样做,因为只有一个叫做 `print` 的项适用于 `Foo` f.print(); // 更明确的,而且在 `Foo` 的情况下并不必要 Foo::print(&f); // 如果你不喜欢简略的语法 <Foo as Pretty>::print(&f); // b.print(); // 错误:发现多个 'print' // Bar::print(&b); // 仍然是错误:发现多个 `print` // 由于作用域内的项目定义了 `print` ,因此这是必要的 <Bar as Pretty>::print(&b); }
Refer to RFC 132 for further details and motivations.
方法调用表达式
方法调用 由一个表达式 (即 接收者 ) 加上一个点 ( .
) 、一个表达式路径段、和括号括起来的表达式列表组成。
方法调用会解析到特定 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 约束提供的方法,然后查找所有剩余的作用域内方法。
注意: 查找是按顺序对每种类型进行的,这可能偶尔会导致令人惊讶的结果。 下面的代码将打印 "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 版本之前,寻找可见方法时,如果候选接收者类型是 数组类型 ,则忽略标准库
IntoIterator
trait 提供的方法。此语法的版次由表示方法名称的 token 确定。
这种特殊情况可能会在将来被删除。
警告: 对于 trait 对象 ,如果存在与 trait 方法同名的内部方法,在方法调用表达式中尝试调用该方法会导致编译器错误。 可以使用 消除歧义的函数调用语法 来调用该方法,这样会调用 trait 方法而不是内部方法。 没有可以调用内部方法的形式。 不在具有与 trait 方法同名的内部方法的 trait 对象上定义内部方法,就不会出现问题。
字段访问表达式
字段表达式 是一个 占位表达式 ,它求值为 结构体 或 联合体 的一个字段的地址。
当操作数是 可变的 ,字段表达式也是可变的。
字段表达式的语法是一个表达式 (称为 容器操作数 ) ,然后是 .
,最后是一个 标识符 。
字段表达式不能后跟一个括号内的逗号分隔的表达式列表,因为那将被解析为 方法调用表达式 。
也就是说,它们不能作为 调用表达式 的函数操作数对象。
注意: 在调用表达式中使用字段表达式时,要将其包装在 括号表达式 中。
#![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 中移动值 }
闭包表达式
语法
闭包表达式 :
move
?
(||
||
闭包参数组?|
)
(表达式 |->
无约束类型组 块表达式)闭包参数组 :
闭包参数 (,
闭包参数)*,
?
闭包表达式 ,也称为 lambda 表达式或 lambda ,定义出 闭包类型 并求值为该类型的值。
闭包表达式的语法 move
关键字可选,随后是管道符号 (|
) 包裹的逗号分隔的 模式 列表,称为闭包参数,每个参数后面可选地跟随 :
和类型,然后是可选的 ->
和类型,称为 返回类型 ,随后的表达式,称为 闭包体操作数 。每个模式后面的可选类型是该模式的类型注解。如果存在返回类型,则闭包体必须是 块 。
闭包表达式即表示函数,该函数将一系列参数映射到跟随参数的表达式。
和 let
绑定 相同,闭包参数是无法拒绝的 模式 ,其类型注解可选,如果未给出类型,将从上下文中推断。
每个闭包表达式的类型是唯一的、匿名的。
值得注意的是,闭包表达式 捕获其环境 ,而普通的 函数定义 不会。
如果没有 move
关键字,闭包表达式 推断它如何从其环境中捕获每个变量 ,优先通过共享引用来捕获,借用闭包体内用到的所有外部变量。
如果需要,编译器将推断应该取代的是可变引用或者从环境中移动还是复制值 (取决于它们的类型) 。
通过在闭包前面加上 move
关键字,可以强制闭包通过复制或移动值来捕获其环境。
这通常用于确保闭包的生命周期为 'static
。
闭包类型实现的 trait
闭包类型实现哪些 trait 取决于如何捕获变量和捕获的变量的类型。
请参考 调用 trait 和强转 章节了解闭包实现 Fn
、 FnMut
和 FnOnce
的情况和时机。
如果每个捕获的变量的类型也实现了 trait ,则闭包类型实现 Send
和 Sync
。
示例
在这个例子中,我们定义了一个函数 ten_times
,它接受一个高阶函数参数,然后调用该函数,并传入一个闭包表达式作为参数,接着传入一个从环境中移动值的闭包表达式。
#![allow(unused)] fn main() { fn ten_times<F>(f: F) where F: Fn(i32) { // 遍历 0..10,对每个值调用 f 函数 for index in 0..10 { f(index); } } // 调用 ten_times 函数,并传入一个匿名函数作为参数,该匿名函数打印出 "hello" 和输入值 ten_times(|j| println!("hello, {}", j)); // 带有类型注解的调用方式,和上一行等价 ten_times(|j: i32| -> () { println!("hello, {}", j) }); let word = "konnichiwa".to_owned(); // 调用 ten_times 函数,并传入一个匿名函数作为参数,该匿名函数打印出 word 和输入值 // 由于匿名函数要使用外部变量 word,所以前面加上 move 关键字 ten_times(move |j| println!("{}, {}", word, j)); }
在闭包参数上的属性
在闭包参数上的属性遵循与 普通函数参数 相同的规则和限制。
循环和其他可中断的表达式
语法
循环表达式 :
循环标签? (
无限循环表达式
| 断言循环表达式
| 断言模式循环表达式
| 迭代循环表达式
| 标签块表达式
)
Rust 支持五种循环表达式:
loop
表达式 表示无限循环。while
表达式 循环直到断言为 false。while let
表达式 测试模式。for
表达式 从迭代器中提取值,循环直到迭代器为空。- 带标签的块表达式 只运行一次循环,但允许使用
break
提前退出循环。
这五种类型的循环都支持 break
表达式 和 标签 。
除了带标签的块表达式外,所有类型的循环都支持 continue
表达式 。
只有 loop
和带标签的块表达式支持 计算非平凡值 。
无限循环
语法
无限循环表达式 :
loop
块表达式
loop
表达式不断重复执行其主体:
loop { println!("I live."); }
。
没有相关的 break
表达式的 loop
表达式是发散的,并且具有 !
类型。
包含相关 break
表达式 的 loop
表达式可能会终止,并且必须具有与 break
表达式的值兼容的类型。
断言循环
while
循环首先会对布尔类型的循环条件进行求值。
如果循环条件为 true
,则会执行循环主体代码块,之后控制流会返回到循环条件,再次对其求值。
如果循环条件为 false
,则 while
循环终止执行。
一个例子:
#![allow(unused)] fn main() { let mut i = 0; while i < 10 { println!("hello"); i = i + 1; } }
断言模式循环
while let
循环在语义上类似于 while
循环,但是在条件表达式的位置上需要关键字 let
,后面跟着模式、等号、 被匹配项 和块表达式。
如果被匹配的表达式的值与模式匹配,那么循环体代码块就会被执行,然后控制流会返回到匹配模式的语句处。
否则, while
表达式会结束。
#![allow(unused)] fn main() { let mut x = vec![1, 2, 3]; while let Some(y) = x.pop() { println!("y = {}", y); } while let _ = 5 { println!("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() { // Prints 2, 2, then 1 println!("{}", v); } }
与 if let
表达式 一样,被匹配项不能是 惰性布尔运算符表达式 。
迭代器循环
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
仅用于说明,实际上用户无法键入这些名称。
注意:外部
match
用于确保在循环完成之前,iter_expr
中的任何 临时值 都不会被丢弃。next
在分配之前被声明,因为这样通常可以更正确地推断类型。
循环标签
语法
循环标签 :
生命周期或标签:
循环表达式可以选择性地使用标签。
标签在循环表达式之前写上一个生命周期,例如 '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
表达式
当遇到 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
表达式通常与包含它的最内层 loop
、 for
或 while
循环相关联,但可以使用 标签 来指定受影响的封闭循环。
例如:
#![allow(unused)] fn main() { 'outer: loop { while true { break 'outer; } } }
break
表达式仅允许在循环体中使用,并且有以下形式之一: break
、 break 'label
或者
(见下文) break EXPR
或 break 'label EXPR
。
标签块表达式
语法
标签块表达式 :
块表达式
带标签的块表达式与普通的块表达式非常相似,但是允许在块中使用 break
表达式。
与循环不同,标签块表达式中的 break
表达式必须有一个标签 (即标签是必须的)。
同样地,标签块表达式 必须 以标签开始。
#![allow(unused)] fn main() { fn do_thing() {} fn condition_not_met() -> bool { true } fn do_next_thing() {} fn do_last_thing() {} let result = 'block: { do_thing(); if condition_not_met() { break 'block 1; } do_next_thing(); if condition_not_met() { break 'block 2; } do_last_thing(); 3 }; }
continue
表达式
语法
Continue 表达式 :
continue
生命周期或标签?
当遇到 continue
关键字时,与其相关联的循环体的当前迭代会立即终止,控制权返回到循环 头部 。
在 while
循环中,头部是控制循环的条件表达式。在 for
循环中,头部是控制循环的调用表达式。
与 break
类似,continue
通常与最内层的封闭循环相关联,但可以使用 continue 'label
指定受影响的循环。
continue
表达式只允许出现在循环体中。
break
和循环值
在 Rust 中,当与 loop
关键字相关联时,可以使用 break
表达式通过 break EXPR
或 break 'label EXPR
的形式从循环中返回一个值,其中 EXPR
是返回给 loop
的表达式结果。
例如:
#![allow(unused)] fn main() { let (mut a, mut b) = (1, 1); // 初始化 a 和 b 为 1 let result = loop { // 进入循环 if b > 10 { // 如果 b 大于 10 break b; // 跳出循环并返回 b 的值 } let c = a + b; a = b; // 将 a 赋值为上一个 b 的值 b = c; // 将 b 赋值为上一个 c 的值 }; // 斐波那契数列中第一个大于 10 的数是 13 : assert_eq!(result, 13); }
在 loop
语句中,如果有关联的 break
语句,则不被视为发散,而且 loop
语句必须与每个 break
表达式兼容的类型。
未表明表达式的 break
语句的表达式被视为 ()
。
区间表达式
语法
区间表达式 :
区间式
| 区间From表达式
| 区间To表达式
| 区间Full表达式
| 区间包含表达式
| 区间To包含表达式区间From表达式 :
表达式..
区间To表达式 :
..
表达式区间Full表达式 :
..
区间To包含表达式 :
..=
表达式
..
和 ..=
运算符会根据下面的表格构造出一个 std::ops::Range
(或 core::ops::Range
) 的变体:
产生式 | 语法 | 类型 | 区间 |
---|---|---|---|
区间式 | 头.. 尾 | std::ops::Range | 头 ≤ x < 尾 |
区间From表达式 | 头.. | std::ops::RangeFrom | 头 ≤ x |
区间To表达式 | .. 尾 | std::ops::RangeTo | x < 尾 |
区间Full表达式 | .. | std::ops::RangeFull | - |
区间包含表达式 | 头..= 尾 | std::ops::RangeInclusive | 头 ≤ x ≤ 尾 |
区间To包含表达式 | ..= 尾 | std::ops::RangeToInclusive | x ≤ 尾 |
示例:
#![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
和 if let
表达式
if
表达式
语法
If表达式 :
if
表达式不包括结构体表达式 块表达式
(else
( 块表达式 | If表达式 | IfLet表达式 ) )?
if
表达式是程序控制流中的条件分支。
if
表达式的语法是一个条件操作数,后面跟着一个结果块,任意数量的 else if
条件和块,以及一个可选的末尾 else
块。条件操作数必须为 布尔类型 。
如果条件操作数计算结果为 true
,则执行结果块,跳过任何后续的 else if
或 else
块。
如果条件操作数计算结果为 false
,则跳过结果块,评估任何后续的 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"); } let y = if 12 * 15 > 150 { "Bigger" } else { "Smaller" }; assert_eq!(y, "Bigger"); }
if let
表达式
语法
IfLet表达式 :
if
let
模式=
被匹配项不包括惰性布尔运算符表达式 块表达式
(else
( 块表达式 | If表达式 | IfLet表达式 ) )?
if let
表达式在语义上类似于 if
表达式,但是在条件操作数的位置上,它期望关键字 let
后跟模式、等号和被匹配项 。如果被匹配项与模式匹配,则执行相应的代码块。
如果不匹配,存在 else
块时,则执行该块。
与 if
表达式类似, if let
表达式由执行的代码块的值确定。
#![allow(unused)] fn main() { let dish = ("Ham", "Eggs"); // 定义一个元组 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"); } }
if
和 if let
表达式可以混合使用:
#![allow(unused)] fn main() { let x = Some(3); let a = if let Some(1) = x { 1 } else if x == Some(2) { 2 } else if let Some(y) = x { y } else { -1 }; assert_eq!(a, 3); }
if let
表达式等价于如下所示的 match
表达式 :
if let PATS = EXPR {
/* body */
} else {
/*else */
}
等价于
match EXPR {
PATS => { /* body */ },
_ => { /* else */ }, // () if there is no else
}
可以使用 |
运算符指定多个模式。这与 match
表达式中的 |
运算符具有相同的语义:
#![allow(unused)] fn main() { enum E { X(u8), Y(u8), Z(u8), } let v = E::Y(12); if let E::X(n) | E::Y(n) = v { assert_eq!(n, 12); } }
该表达式不能是 惰性布尔运算符表达式。 使用惰性布尔运算符会与语言计划更改的特性产生歧义 (即 if-let 链的实现 - 请参见 eRFC 2947 )。 当需要惰性布尔运算符表达式时,可以通过如下方式使用括号来实现:
// 之前...
if let PAT = EXPR && EXPR { .. }
// 之后...
if let PAT = ( EXPR && EXPR ) { .. }
// 之前...
if let PAT = EXPR || EXPR { .. }
// 之后...
if let PAT = ( EXPR || EXPR ) { .. }
match
表达式
语法
Match表达式 :
match
被匹配项{
内部属性*
Match分支组?
}
被匹配项 :
表达式不包括结构体表达式Match分支组 :
( Match分支=>
( 无块表达式,
| 块表达式,
? ) )*
Match分支=>
表达式,
?Match分支守卫 :
if
表达式
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"), } }
在模式中绑定的变量的作用域限定在 match
守卫及分支表达式中。绑定模式 (移动、复制或引用) 取决于模式。
多个匹配模式可以用 |
运算符连接起来。每个模式将按照从左到右的顺序进行测试,直到找到成功匹配的模式为止。
#![allow(unused)] fn main() { let x = 9; let message = match x { 0 | 1 => "not many", // 如果 x 是 0 或 1,将 "not many" 赋值给 message 2 ..= 9 => "a few", // 如果 x 在 2 到 9 之间(包括 2 和 9),将 "a few" 赋值给 message _ => "lots" // 否则将 "lots" 赋值给 message }; assert_eq!(message, "a few"); // 演示模式匹配的顺序。 struct S(i32, i32); match S(1, 2) { // 如果 S 的第一个元素是 1 或者第二个元素是 2,则将 z 绑定为 1,并且通过断言来检查 z 的值。 S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1), // 否则发生错误 _ => panic!(), } }
每个 |
分隔的模式中的所有绑定都必须出现在分支的所有模式中。每个同名绑定必须类型相同,且绑定模式相同。
匹配守卫
match
分支可以接受 匹配守卫 来进一步细化匹配条件。
模式守卫出现在模式之后,由 if
关键字后面的 bool
类型表达式组成。
当模式成功匹配时,模式守卫表达式会被执行。如果表达式的结果为 true,则模式成功匹配。
否则,将测试下一个模式,包括同一分支中使用 |
运算符的其他匹配。
#![allow(unused)] fn main() { let maybe_digit = Some(0); fn process_digit(i: i32) { } fn process_other(i: i32) { } let message = match maybe_digit { Some(x) if x < 10 => process_digit(x), Some(x) => process_other(x), None => panic!(), }; }
注意:使用
|
操作符的多个匹配项可能导致模式守卫及其所产生的副作用执行多次。 例如:#![allow(unused)] fn main() { use std::cell::Cell; let i : Cell<i32> = Cell::new(0); match 1 { 1 | _ if { i.set(i.get() + 1); false } => {} _ => {} } assert_eq!(i.get(), 2); }
模式守卫可以引用其所跟随的模式中绑定的变量。 在评估模式守卫之前,会对匹配变量的部分获取共享引用。 在评估模式守卫时,这个共享引用被用于访问该变量。 只有当模式守卫计算结果为 true 时,才会将值从待匹配项移动或复制到该变量中。 这允许在守卫中使用共享借用,如果守卫不匹配,则不会移动待匹配项。 此外,通过在评估守卫时保持共享引用,还可以防止在守卫内进行修改。
匹配分支上的属性
可以在匹配分支上使用外围属性。
在匹配分支上具有意义的外围属性只有 cfg
和 代码分析检查属性 。
内部属性 可以直接放置在匹配表达式的左括号后面,在与 块表达式上的属性 相同的表达式上下文中。
return
表达式
语法
Return表达式 :
return
表达式?
return
表达式使用关键字 return
标识。
执行 return
表达式会将其参数移动到当前函数调用的指定输出位置,销毁当前函数激活帧,并将控制权转移到调用帧。
return
表达式的一个例子:
#![allow(unused)] fn main() { fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } }
Await 表达式
语法
Await表达式 :
表达式.
await
await
表达式是语法结构,用于暂停由实现了 std::future::IntoFuture
的计算,直到给定的 future 准备好产生值。
await
表达式的语法是一个实现了 IntoFuture
trait 的表达式,称为 future 操作数 ,后跟 .
和 await
关键字。
await
表达式只能在 async 上下文 中使用,比如 async fn
或 async
块 。更具体来说,await
表达式具有以下效果。
- 通过在 future 操作数上调用
IntoFuture::into_future
创建 future。 - 将 future 求解为 future
tmp
; - 使用
Pin::new_unchecked
固定tmp
; - 然后通过调用
Future::poll
方法并传递当前的 任务上下文 来对这个固定的 future 进行轮询; - 如果对
poll
的调用返回Poll::Pending
,那么 future 返回Poll::Pending
,暂停它的状态,以便当包围的 async 上下文重新被轮询时,执行返回到步骤 3 ; - 否则对
poll
的调用必须返回Poll::Ready
,在这种情况下,Poll::Ready
变体中包含的值将被用作await
表达式本身的结果。
版本差异:
await
表达式只从 Rust 2018 开始可用。
任务上下文
任务上下文指的是在当前 async 上下文 被轮询时提供给它的 Context
。
因为 await
表达式只能在 async 上下文中合法使用,所以必须有一些任务上下文可用。
近似解糖
实际上,一个 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
指的是从异步环境中获取的上下文。
_
表达式
语法
下划线表达式 :
_
下划线表达式,用符号 _
表示,在解构赋值中用作占位符。只能出现在赋值的左侧。
一个 _
表达式的例子:
#![allow(unused)] fn main() { let p = (1, 2); let mut a = 0; (_, a) = p; }
模式
语法
模式 :
|
? 模式非顶层项 (|
模式非顶层项 )*模式非顶层项 :
无区间模式
| 区间模式无区间模式 :
字面值模式
| 标识符模式
| 通配符模式
| 剩余模式
| 引用模式
| 结构体模式
| 元组结构体模式
| 元组模式
| 分组模式
| 切片模式
| 路径模式
| 宏调用
模式用于将值与结构进行匹配,并在这些结构内将变量绑定到值(可选)。 它们还用于函数和闭包的变量声明及参数。以下示例中的模式执行四个操作:
- 检查
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); } }; }
可拒绝性
当模式可能无法匹配其所匹配的值时,该模式被称为 可拒绝的 。相反, 不可拒绝 模式总是与其所匹配的值匹配。
译注: 可拒绝性主要区分了 "匹配" 的彻底性。需注意 "可拒绝的" 依然可以匹配,"不可拒绝" 则总是匹配。
例如:
#![allow(unused)] fn main() { let (x, y) = (1, 2); // "(x, y)" 是一个不可拒绝的模式 if let (a, 3) = (1, 2) { // "(a, 3)" 是可拒绝的,将不匹配 panic!("不应该到达这里"); } else if let (a, 4) = (3, 4) { // "(a, 4)" 是可拒绝的,并将匹配,且作了值绑定 println!("匹配到 ({}, 4)", a); } }
字面值模式
语法
字面值模式 :
true
|false
| 字符字面值
| 字节字面值
| 字符串字面值
| 原始字符串字面值
| 字节字符串字面值
| 原始字节字符串字面值
|-
? 整数字面值
|-
? 浮点数字面值
字面值模式 匹配与所创建字面值相同的值。 虽然负数不是字面值,但字面值模式接受字面值前面的可选负号。
目前接受浮点字面值,但由于行为比较复杂,未来的 Rust 版本中将禁止在字面值模式中使用它们 (参见问题 #41620)。
字面值模式总是可拒绝的。
例如:
#![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"), } } }
标识符模式
标识符模式将其匹配的值绑定到变量上。
标识符在模式中必须是唯一的。
该变量将隐藏作用域中的同名变量。
新绑定变量的作用域取决于模式所处的上下文 (例如 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 中,对同一内存位置的引用被绑定到变量值。
这种语法是必要的,因为在解构子模式中, &
操作符不能应用于值的字段。
例如,以下是无效的:
#![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
所表示的不是被匹配值的类型,而仅是明确绑定以引用的形式,而不是复制或移动值。
路径模式 优先于标识符模式。如果指定为 ref
或 ref mut
,并且标识符隐藏了一个常量,则会出现错误。
如果子模式是不可拒绝的或非特定子模式,则标识符模式是不可拒绝的。
绑定形式
为了使绑定更加友好,会尝试不同的 绑定形式 ,旨在更容易地将引用绑定到值。
当引用值被非引用模式匹配时,会自动被视为 ref
或 ref mut
绑定。
例如:
#![allow(unused)] fn main() { let x: &Option<i32> = &Some(3); // 创建一个类型为 &Option<i32> 的引用 x,指向 Some(3) if let Some(y) = x { // 对 x 进行模式匹配 // y 被转换成了 `ref y`,其类型为 &i32 } }
非引用模式 包括除绑定、 通配符模式 (_
)、引用类型的 const
模式 和 引用模式 之外的所有模式。
如果一个绑定的形式没有明确指定 ref
、 ref mut
或 mut
,则会使用 默认绑定形式 来确定变量如何被绑定。
默认绑定形式首先为 "移动" 形式,使用移动语义。
匹配模式时,编译器从外向内进行。
每次使用非引用模式匹配引用时,它将自动解引用该值并更新默认绑定形式。
引用将默认绑定形式设置为 ref
。
可变引用将形式设置为 ref mut
,除非形式已经是 ref
,否则它将保持为 ref
。
如果自动解引用的值仍然是引用,则解引用该值并重复此过程。
移动绑定和引用绑定可以混合在同一个模式中。 这样做将导致对象的部分移动绑定,并且之后无法再使用该对象。 如果类型不可复制,则适用此规则。
在下面的示例中, 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; }
通配符模式
语法
通配符模式 :
_
通配符模式 (下划线符号) 匹配任何值。其作用是忽略不重要的值。
在其他模式内部,它匹配单个数据字段 (与 ..
匹配多个剩余字段不同)。
与标识符模式不同,它不会复制、移动或借用它所匹配的值。
例如:
#![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 {} }
通配符模式始终是不可拒绝的。
剩余模式
语法
剩余模式 :
..
剩余模式 (..
符号) 作为一个可变长度的模式,用于匹配在之前和之后尚未被匹配到的零个或多个元素。
它只能在 元组模式 、 元组结构体模式 和 切片模式 中使用,并且只能出现一次作为这些模式中的一个元素。
在 切片模式 中,它也允许出现在 标识符模式 中。
剩余模式始终是不可拒绝的。
例如:
#![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 结尾 (.., 5) => println!("tail must be 5"), // 匹配所有其他情况 (..) => println!("matches everything else"), } }
区间模式
语法
区间模式 :
区间内部模式
| 区间From模式
| 区间To内部模式
| 废弃区间模式区间内部模式 :
区间模式约束..=
区间模式约束区间From模式 :
区间模式约束..
区间To内部模式 :
..=
区间模式约束废弃区间模式 :
区间模式约束...
区间模式约束
区间模式 匹配由其边界定义的区间内的标量值。它们由一个 符号 ( ..
、 ..=
或 ...
中的一个) 和一个或两个边界组成。在符号左边的边界是 下界 ,在右边的是 上界 。
具有下界和上界的区间模式将匹配其两个边界之间及其包括的所有值。它由其下界、后跟 ..=
,后跟其上界组成。区间模式的类型与其上界和下界的类型统一。
例如,模式 'm'..='p'
将仅匹配值 'm'
'n'
'o'
和 'p'
。
下界不能大于上界。也就是说,在 a..=b
中,必须满足 $a ≤ b$。例如,拥有区间模式 10..=0
是错误的。
只有下界的区间模式将匹配大于或等于下界的任何值。它由其下界后跟 ..
组成,与其下界具有相同的类型。例如, 1..
将匹配 1、9、9001 或 9007199254740991 (如果它是适当大小的) ,但不匹配 0 和有符号整数的负数。
只有上界的区间模式将匹配小于或等于上界的任何值。它由 ..=
后跟其上界组成,与其上界具有相同的类型。例如, ..=10
将匹配 10、1、0 和有符号整数类型的所有负值。
只有一个边界的区间模式不能用作 切片模式 的子模式的顶级模式。
边界值可以写作以下其中之一:
- 字符、字节、整数或浮点数字面值。
-
后跟一个整数或浮点数字面值。- 路径。
如果边界值为路径,在宏展开后,该路径必须解析为类型为 char
、整数类型或浮点类型的常量条目。
边界的类型和值取决于其书写方式。如果边界是一个路径,则模式具有路径解析后的常量的类型和值。
如果是字面值,则具有相应的字面值表达式的类型和值。
如果是以 -
开头的字面值,则具有与相应字面值表达式相同的类型,以及相应字面值表达式的值的 取反 后的值。
Examples:
#![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..=6 => "acid", 7 => "neutral", 8..=14 => "base", _ => unreachable!(), }); let uint: u32 = 5; match uint { 0 => "zero!", 1.. => "positive number!", }; // using paths to constants: 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; } // using qualified paths: 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
类型的区间模式在它们 span 类型的所有可能值时是不可拒绝的。
例如, 0u8..=255u8
是不可拒绝的。
整数类型的值区间是从其最小值到最大值的闭区间。
char
类型的值区间恰好包含所有 Unicode 标量值的区间: '\U{0000}'..='\U{D7FF}'
和 '\U{E000}'..='\U{10FFFF}'
。
浮点数区间模式已被弃用,可能会在未来的Rust版本中删除。 有关更多信息,请参见 issue #41620 。
版本差异: 在 2021 版之前,带有下限和上限的区间模式也可以使用
...
代替..=
进行编写,具有相同的含义。
注意: 尽管区间模式使用与 区间表达式 相同的语法,但不存在独占区间模式。 也就是说,
x .. y
和.. x
都不是有效的区间模式。
引用模式
语法
引用模式 :
(&
|&&
)mut
? 无区间模式
引用模式会对匹配值的指针进行解引用,并借用。
例如,以下两个对 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); }
引用模式的语法产生式决定了必须以标记 &&
来匹配引用到引用,该标记本身是单个标记,不是两个 &
标记。
添加 mut
关键字会对可变引用进行解引用。
可变性必须与引用的可变性相匹配。
引用模式始终是不可拒绝的。
结构体模式
语法
结构体模式 :
表达式中路径{
结构体模式组 ?
}
结构体模式组 :
结构体模式字段组 (,
|,
结构体模式附加)?
| 结构体模式附加结构体模式字段组 :
结构体模式字段 (,
结构体模式字段) *结构体模式字段 :
外围属性 *
(
元组索引:
模式
| 标识符:
模式
|ref
?mut
? 标识符
)结构体模式附加 :
外围属性 *
..
结构体模式匹配满足其子模式定义的所有条件的结构体值。 可用于 解构 结构体。
在结构体模式中,可以通过名称、索引 (对于元组结构体) 引用字段,或通过使用 ..
忽略字段:
#![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} => (), // order doesn't matter 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} => (), // order doesn't matter PointTuple {0: 10, ..} => (), PointTuple {..} => (), } }
如果没有使用 ..
,则需要匹配所有字段:
#![allow(unused)] fn main() { struct Struct { a: i32, b: char, c: bool, } let mut struct_value = Struct{a: 10, b: 'X', c: false}; match struct_value { Struct{a: 10, b: 'X', c: false} => (), Struct{a: 10, b: 'X', ref c} => (), Struct{a: 10, b: 'X', ref mut c} => (), Struct{a: 10, b: 'X', c: _} => (), Struct{a: _, b: _, c: _} => (), } }
ref
和/或 mut
标识符 语法匹配任何值,并将其绑定到与给定字段同名的变量。
#![allow(unused)] fn main() { struct Struct { a: i32, b: char, c: bool, } let struct_value = Struct{a: 10, b: 'X', c: false}; let Struct{a: x, b: y, c: z} = struct_value; // destructure all fields }
当结构体模式的任何子模式都是可拒绝的时,结构体模式就是可拒绝的。
元组结构模式
语法
元组结构模式 :
表达式中路径(
元组结构条目组?)
元组结构体模式匹配元组结构体和枚举值,匹配所有符合其子模式定义的条件。 可用于 解构 元组结构体或枚举值。
当子模式中有不可拒绝模式时,元组结构体模式就是可拒绝的。
元组模式
语法
元组模式 :
(
元组模式条目组?)
元组模式匹配满足其子模式定义的所有条件的元组值。可用于 解构 元组。
形式为 (..)
且具有单个 剩余模式 是一种特殊形式,不需要添加逗号,可以匹配任意大小的元组。
当其子模式中有一个可拒绝时,元组模式是可拒绝的。
以下是使用元组模式的示例:
#![allow(unused)] fn main() { let pair = (10, "ten"); let (a, b) = pair; assert_eq!(a, 10); assert_eq!(b, "ten"); }
分组模式
语法
分组模式 :
(
模式)
将一个模式括在括号中时,可以显式地控制复合模式的优先级。
例如,一个引用模式紧跟一个区间模式,如 &0..=5
将有歧义且不被允许,但可以用括号来表示。
#![allow(unused)] fn main() { let int_reference = &3; match int_reference { &(0..=5) => (), _ => (), } }
切片模式
语法
切片模式 :
[
切片模式条目组?]
切片模式可以匹配固定大小的数组和动态大小的切片。
#![allow(unused)] fn main() { // Fixed size 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
,不需要用括号括起来。
路径模式
语法
路径模式 :
路径表达式
路径模式 是指引用常量值或没有字段的结构体或枚举变体的模式。
未经修饰的路径模式可以引用:
- 枚举变体
- 结构体
- 常量
- 关联常量
修饰的路径模式只能引用关联常量。常量不能是联合体类型。结构体和枚举常量必须有 #[derive(PartialEq, Eq)]
。
当路径模式引用结构体或枚举变体并且枚举只有一个变体或类型为不可拒绝类型的常量时,路径模式是不可拒绝的。
当路径模式引用可拒绝的常量或具有多个变体的枚举变体时,是可拒绝的。
或模式
或模式 是指可以匹配两个或更多子模式的模式 (例如 A | B | C
)。可以任意嵌套。
从语法上讲,或模式可以在任何其他模式允许的地方使用 (由 模式 产生式表示),但有一些例外情况,例如 let
绑定和函数和闭包参数 (由 模式非顶层项 产生式表示) 。
静态语义
-
对于任意的模式
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
的模式的匹配分支a_i
,如果在其所处的深度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
处使用模式c(p | q, ..rest)
匹配被匹配表达式e_s
的动态语义,其中c
是某个构造函数,p
和q
是任意模式,rest
可选地包含c
中的其他因子,其定义与c(p, ..rest) | c(q, ..rest)
相同。
与其他未限定模式的优先级
正如本章的其他地方所示,有几种语法上未限定的模式,包括标识符模式、引用模式和或模式。或模式始终具有最低的优先级。
这使我们可以为未来可能的类型注解功能保留语法空间,并减少歧义。
例如,x @ A(..) | B(..)
将导致错误,即 x
在所有模式中都未绑定。&A(x) | B(x)
将导致在不同的子模式中 x
类型不匹配的错误。
Type system
类型
在 Rust 程序中,每个变量、条目和值都有一个类型。 一个 值 的 类型 定义了其存储在内存中的解释方式,以及该值可执行的相关操作。
内置类型与语言编译器紧密集成,实现方式与用户定义类型有所不同。 用户定义类型受到语法规则的限制,因而具有有限的功能。
Rust 类型包括:
- 原始类型:
- 序列类型:
- 用户定义类型:
- 函数类型:
- 指针类型:
- Trait 类型:
类型表达式
语法
类型 :
无约束类型组
| ImplTrait类型
| Trait对象类型无约束类型组 :
括号类型
| ImplTrait类型单约束
| Trait对象类型单约束
| 类型路径
| 元组类型
| 永不类型
| 原始指针类型
| 引用类型
| 数组类型
| 切片类型
| 推断类型
| 限定路径类型
| 裸函数类型
| 宏调用
在上面的 类型 语法规则中所定义的 类型表达式 描述的类型语法,可对应包含以下类型:
- 序列类型 ( 元组 数组 切片 ) 。
- 类型路径 可以引用:
- 指针类型 ( 引用 原始指针 函数指针 ) 。
- 推断类型 ,请求编译器确定类型。
- 用于消除歧义的 括号 。
- Trait 类型: Trait 对象 和 impl trait 。
- 永不类型 类型。
- 宏 ,可展开为类型表达式。
译注: rust 中相关语法和概念有很好的统一性,这里将类型归并到表达式概念下就是很好的体现。 这里要理解的一点是,之所以将类型归并到表达式,是强调了类型同样存在的求值机制, 即多个类型组合后的表达式会求值为一个更为复杂的类型结构,而通过这一类型去约束其他类型和值。
括号类型
括号类型 :
(
类型)
有时,类型组合可能不明确,需要添加括号以区分。
例如, 引用类型 中 类型约束 的 +
运算符对应的类型边界容易歧义,则需要使用括号。
这个消除歧义的语法使用 无约束类型组 语法规则,而不是 类型 。
#![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
。
布尔类型对象的其他任何位表示是 未定义行为 。
布尔类型是各种 表达式 中许多操作数的类型:
- if 表达式 和 while 表达式 中的条件操作数
- 惰性布尔运算符表达式 中的操作数
注意: 布尔类型类似于但不是 可枚举类型 。主要区别是构造函数未与类型关联 (比如
bool::true
) 。
与所有原始类型一样,布尔类型 p-impl 实现了 traits Clone
, Copy
, Sized
, Send
和 Sync
。
注意: 请参阅 标准库文档 以获取库操作。
布尔值的操作
当使用特定的运算符表达式作为布尔类型的操作数时,根据 [布尔逻辑][boolean logic] 规则进行计算。逻辑非
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
数字类型
整数类型
无符号整数类型包括:
类型 | 最小值 | 最大值 |
---|---|---|
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 位宽度。
注意:许多 Rust 代码可能假设指针、
usize
和isize
是 32 位或 64 位。 因此,对于 16 位指针的支持是有限的,可能需要库显式关注并确认其支持。
文本类型
类型 char
和 str
用于存储文本数据。
char
类型的值是一个 Unicode 标量值 (不是编码的替代品) ,为 32 位无符号整数,位于 0x0000 到 0xD7FF 或 0xE000 到 0x10FFFF 范围之间。
创建超出范围的 char
是 未定义行为 。 [char]
是实际长度为 1 的 UCS-4 / UTF-32 字符串。
str
类型的值与 [u8]
相同,是 8 位无符号字节的切片。
但是,Rust 标准库对 str
做了一些额外的评估: 在 str
上工作的方法评估并确保其中的数据是有效的 UTF-8 。
使用非 UTF-8 缓冲区调用 str
方法可能导致 未定义行为 ,会立即触发,也可能是之后。
由于 str
是 动态大小类型 ,因此只能通过指针类型 (例如 &str
) 来实例化。
永不类型
语法
永不类型 :!
永不类型 !
是一种没有值的类型,表示永远不会计算出确切结果。
类型为 !
的表达式可以强制转换为其他任何类型。
let x: ! = panic!();
// 可以强制转换为任何类型。
let y: u32 = x;
注意: 预计在 1.41 版本中稳定的永不类型,但由于最后时间发现了一些回归问题,因此稳定性被暂时撤销。
目前, !
类型只能出现在函数返回类型中。有关更多详细信息,请参见 问题跟踪 。
元组类型
元组类型 是一组由其他异构类型组成的结构体类型 1 。
元组类型的语法是由逗号分隔的类型列表,括在圆括号之中。 一元元组需要在其元素类型后面加上逗号,以便与 括号类型 区分。
元组类型的字段数量等于类型列表的长度。字段数量决定了元组的 元数 。拥有 n
个字段的元组被称为 n 元元组。
例如,具有 2 个字段的元组是一个 2 元元组。元组的字段使用递增的数字名称进行命名,匹配它们在类型列表中的位置。
第一个字段是 0
,第二个字段是 1
,以此类推。每个字段的类型是元组列表中相同位置的类型。
出于方便和历史原因,没有字段的元组类型 ()
通常被称为 单元 或 单元类型 。它的一个值也称为 单元 或 单元值。
元组类型的一些例子:
()
(单元类型)(f64,f64)
(String,i32)
(i32,String)
(与前一个示例的类型不同)(i32,f64,Vec<String>,Option<bool>)
可以使用 元组表达式 构造此类型的值。 另外,在如果不能求解为其他有意义的值时,则各类表达式将产生单元值。 元组字段可以通过 元组索引表达式 或 模式匹配 来访问。
如果它们的内部类型等价,则构造类型始终是等价的。有关元组结构体的具名版本,请参见 元组结构体 。
数组类型
数组是一种固定大小的类型为 T
的元素序列,数组类型的写法为 [T; N]
。
其大小是一个 常量表达式 ,值应该是 usize
类型。
例如:
#![allow(unused)] fn main() { // 一个栈上分配的数组 let array: [i32; 3] = [1, 2, 3]; // 一个堆上分配的数组,转换为一个切片类型 let boxed_array: Box<[i32]> = Box::new([1, 2, 3]); }
数组的所有元素都被初始化,且在安全方法和运算符中访问数组时始终进行边界检查。
注意: 标准库类型
Vec<T>
提供了一种在堆上分配的可调整大小的数组类型。
Slice types
语法
切片类型 :
[
类型]
切片是 动态大小类型 ,表示对类型为 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
类型类似于 ML 中的 data
构造声明,或者 Limbo 中的 pick ADT 。
联合类型
联合类型 是具名的、异构的类似 C 的联合体,由 union
条目 的名称来表示。
联合体没有 "活动字段" 的概念。每次访问联合都会将联合内容的一部分转换为访问字段的类型。
由于转换可能导致意外错误或未定义的行为,因此读取联合字段时,需要使用 unsafe
关键字。
联合体字段类型子集受到限制,以确保永远不需要丢弃。有关详细信息,请参见 条目 文档。
默认情况下, union
的内存布局未明确的 (特别是,字段不需要位于偏移量为 0 的地址) ,可以使用 #[repr(...)]
属性来固定布局。
函数条目类型
引用函数条目或类似元组结构或枚举变量的构造函数时,会生成其 函数条目类型 的大小为零的值。 该类型明确标识函数 - 其名称、类型参数和其提前绑定的生命周期参数 (但不包括其延迟绑定的生命周期参数,这些参数仅在调用函数时分配) - 因此该值不需要包含实际的函数指针,并且调用函数时不需要进行间接寻址。
没有直接引用函数条目类型的语法,但编译器在错误消息中将显示类型为类似于 fn(u32) -> i32 {fn_name}
的类型。
因为函数条目类型明确标识函数,不同函数的条目类型 - 不同的条目,或者相同条目的不同泛型 - 是不同的,混合它们会产生类型错误:
#![allow(unused)] fn main() { fn foo<T>() { } let x = &mut foo::<i32>; *x = foo::<u32>; //~ ERROR 类型不匹配 }
然而,从具有相同签名的函数条目到 函数指针 存在 强制转换 ,当期望直接使用函数指针时,会被触发。
当在同一 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> }; }
所有的函数条目都实现了 Fn
, FnMut
, FnOnce
, Copy
, Clone
, Send
和 Sync
。
闭包类型
闭包表达式 生成一个闭包值,具有独特匿名的类型,无法显式书写。 闭包类型类似于包含捕获变量的结构体。例如,以下闭包:
#![allow(unused)] fn main() { fn f<F : FnOnce() -> String> (g: F) { println!("{}", g()); } let mut s = String::from("foo"); let t = String::from("bar"); f(|| { s += &t; s }); // Prints "foobar". }
生成的闭包类型大致类似于以下结构体:
struct Closure<'a> {
s : String,
t : &'a String,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
fn call_once(self) -> String {
self.s += &*self.t;
self.s
}
}
因此,对 f
的调用将像以下代码一样工作:
f(Closure{s: s, t: &t});
捕获模式
编译器优先选择对闭合变量进行不可变借用,然后是唯一不可变借用 (参见下文) ,然后是可变借用,最后才是移动的方式。 编译器将选择与在闭包体内使用捕获变量的方式兼容的这些选项中的第一个。编译器不考虑周围的代码,比如涉及变量的生命周期或闭包本身的生命周期。
如果使用了 move
关键字,则所有的捕获都是通过移动,或者对于 Copy
类型是通过复制,而不管是否可以使用借用。
通常, move
关键字用于允许闭包超出捕获值的生命周期,例如,如果闭包正在被返回或用于生成新线程。
结构体、元组和枚举等复合类型始终被整体捕获,而不是逐个字段捕获。为了捕获单个字段,可能需要借用到一个局部变量中:
#![allow(unused)] fn main() { use std::collections::HashSet; struct SetVec { set: HashSet<u32>, vec: Vec<u32> } impl SetVec { fn populate(&mut self) { let vec = &mut self.vec; self.set.iter().for_each(|&n| { vec.push(n); }) } } }
如果闭包直接使用 self.vec
,那么它将尝试通过可变引用来捕获 self
。
但是,由于 self.set
已经被借用以进行迭代,代码将无法编译。
捕获中的唯一不可变借用
捕获可以通过一种称为唯一不可变借用的特殊借用方式发生,它无法在语言的任何其他地方使用,并且无法显式书写。 当修改可变引用的引用对象时,就会发生这种情况,例如以下示例:
#![allow(unused)] fn main() { let mut b = false; let x = &mut b; { let mut c = || { *x = true; }; // 下面这一行是错误的: // let y = &x; c(); } let z = &x; }
在这种情况下,无法将 x
借为可变引用,因为 x
不是可变的。
但与此同时,借用 x
为不可变引用将使赋值操作非法,因为 & &mut
引用可能不是唯一的,因此不能安全地用于修改值。
因此,使用了一种独特的不可变借用:它以不可变方式借用 x
,但像可变借用一样,必须是唯一的。
在上面的示例中,取消注释 y
的声明将导致错误,因为它将违反闭包对 x
的借用的唯一性; z
的声明是有效的,因为闭包的生命周期在块的末尾已过期,释放了借用。
调用trait和类型强制转换
闭包类型都实现了 FnOnce
,表示它们可以通过消耗闭包所有权来被调用一次。此外,一些闭包还实现了更具体的调用 trait:
注意:
move
闭包仍然可能实现Fn
或FnMut
,即使它们通过移动捕获变量。 这是因为闭包类型实现的trait是由闭包对捕获值的操作方式决定的,而不是它们如何捕获它们。
非捕获闭包 是不从其环境捕获任何内容的闭包。它们可以强制转换为与匹配签名的函数指针 (例如, 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); }
其他 traits
所有闭包类型都实现了 Sized
。此外,如果允许所存储的捕获类型这样做,则闭包类型会实现以下 traits:
Send
和 Sync
的规则与普通结构体类型相同,而 Clone
和 Copy
的行为则类似于 衍生 。
对于 Clone
,复制捕获变量的顺序未指定。
由于捕获通常是通过引用进行的,因此会出现以下一般规则:
- 如果所有捕获的变量都是
Sync
,则闭包是Sync
的。 - 如果所有非唯一不可变引用捕获的变量都是
Sync
,并且所有唯一不可变或可变引用、复制或移动捕获的值都是Send
,则闭包是Send
的。 - 如果闭包未通过唯一不可变或可变引用捕获任何值,并且它通过复制或移动捕获的所有值都是
Clone
或Copy
的,则闭包是Clone
或Copy
的。
指针类型
所有指针都是显式的第一类值。它们可以移动或复制,存储到数据结构中,并从函数返回。
引用 (&
和 &mut
)
共享引用 (&
)
共享引用指向由某个其他值拥有的内存。创建一个值的共享引用时,它会防止直接修改该值。 内部可变性 在某些情况下提供了一个例外。
顾名思义,可能存在对一个值的任意数量的共享引用。共享引用类型写作 &type
,或者在需要指定显式生命周期时写作 &'a type
。
复制引用是一个 "浅层" 操作:它只涉及到指针本身的复制,也就是说,指针是 Copy
的。
释放引用对它所指向的值没有影响,但引用 临时值 将在引用本身的作用域中保持其活动状态。
可变引用 (&mut
)
可变引用指向由某个其他值拥有的内存。可变引用类型写作 &mut type
或 &'a mut type
。
可变引用 (未被借用的) 是访问它所指向的值的唯一方式,因此不是 Copy
。
原始指针 (*const
and *mut
)
语法
原始指针类型 :
*
(mut
|const
) 无约束类型组
原始指针是非安全性或生存期保证的指针。原始指针写作 *const T
或 *mut T
。
例如, *const i32
意味着一个指向 32 位整数的原始指针。复制或丢弃原始指针不会对任何其他值的生命周期产生影响。
解引用原始指针是一个 unsafe
操作 。这也可以用于通过重新借用它 (&*
或 &mut *
) 将原始指针转换为引用。
通常不建议使用原始指针;其存在是为了支持与外部代码的互操作性,以及编写关键性能或低层的函数。
当比较原始指针时,是通过它们的地址进行比较,而不是通过它们所指向的内容进行比较。 当将原始指针与 动态大小类型 进行比较时,它们还会比较所附加的数据。
可以使用 core::ptr::addr_of!
创建 *const
指针和 core::ptr::addr_of_mut!
创建 *mut
指针来直接创建原始指针。
智能指针
标准库包含除了引用和原始指针之外的其他 '智能指针' 类型。
函数指针类型
语法
裸函数类型 :
对于生命周期组? 函数类型限定符组fn
(
函数参数可能命名为可变参数?)
裸函数返回类型?函数类型限定符组:
unsafe
? (extern
Abi?)?裸函数返回类型:
->
无约束类型组函数参数可能命名为可变参数 :
可能命名函数参数组 | 可能命名函数参数可变可能命名函数参数组 :
可能命名函数参数 (,
可能命名函数参数 )*,
?可能命名函数参数 :
外围属性* ( ( 标识符 |_
):
)? 类型可能命名函数参数可变 :
( 可能命名函数参数,
)* 可能命名函数参数,
外围属性*...
函数指针类型,用 fn
关键字表示,用来引用一个函数,该函数的ID不一定在编译时已知。
可以通过 函数条目 和无捕获的 闭包 进行强制转换来创建。
unsafe
修饰符表示该类型的值是一个 非安全的函数 ,而 extern
修饰符表示 外部函数 。
可变参数只能在使用了 "C"
或 "cdecl"
调用约定的 extern
函数类型中指定。
下面是一个例子,其中 Binop
被定义为函数指针类型:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { x + y } let mut x = add(5,7); type Binop = fn(i32, i32) -> i32; let bo: Binop = add; x = bo(5,7); }
函数指针参数上的属性
函数指针参数上的属性遵循与 常规函数参数 相同的规则和限制。
Trait 对象
语法
Trait对象类型 :
dyn
? [类型参数约束组]Trait对象类型单约束 :
dyn
? Trait约束
trait 对象 是另一个类型的不透明值,该类型实现了一组 trait 。trait 集由一个 对象安全 的 base trait 和任意数量的 [auto trait] 组成。
Trait 对象实现了 base trait ,它的 auto traits 以及 base trait 的任何 父级trait 。
Trait 对象写作关键字 dyn
后跟一组 trait 约束 ,但对 trait 约束有以下限制。
除了第一个 trait 之外的所有 trait 都必须是 auto trait ,不能有多个生命周期,并且不允许可选附加约束 (例如 ?Sized
)。
此外,路径到 traits 可以括在括号中。
例如,给定 Trait
,以下都是 trait 对象:
dyn Trait
dyn Trait + Send
dyn Trait + Send + Sync
dyn Trait + 'static
dyn Trait + Send + 'static
dyn Trait +
dyn 'static + Trait
.dyn (Trait)
版本差异: 在 2021 版之前,可以省略
dyn
关键字。注意: 为了清晰起见,建议在你的 trait 对象上始终使用
dyn
关键字,除非你的代码库支持使用 Rust 1.26 或更低版本进行编译。
版本差异: 在 2015 版中,如果 trait 对象的第一个限定是以
::
开头的路径,那么dyn
将被视为路径的一部分。你可以将第一个路径放在括号中来解决这个问题。 因此,如果你想要一个带有 trait::your_module::Trait
的 trait 对象,你应该将其写为dyn (::your_module::Trait)
。从 2018 版开始,
dyn
是正式的关键字,不允许在路径中使用,因此不需要括号。
如果 base trait 相互为别名, auto trait 集相同且生命周期约束相同,则两个 trait 对象类型为别名。例如, dyn Trait + Send + UnwindSafe
与 dyn Trait + UnwindSafe + Send
是相同的。
由于值的具体类型的不透明性,trait 对象是 动态大小类型 。与所有 DSTs 一样,trait 对象在某种类型的指针后面使用,例如 &dyn SomeTrait
或 Box<dyn SomeTrait>
。
指向 trait 对象的指针实例包括:
- 指向实现
SomeTrait
的类型T
的实例的指针 - 虚方法表 (通常简称为 vtable ) ,它包含对于
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
语法
ImplTrait类型 :impl
类型参数约束组ImplTrait类型1约束 :
impl
Trait约束
impl Trait
提供了一种方式来指定匿名但具体实现了特定 trait 的类型。
它可以出现在两种位置: 参数位置 (在这里它可以作为函数的匿名类型参数) ,以及返回位置 (在这里它可以作为抽象的返回类型) 。
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} // 参数位置: 匿名类型参数 fn foo(arg: impl Trait) { } // 返回值位置: 抽象返回类型 fn bar() -> impl Trait { } }
匿名类型参数
注意: 这经常被称为 "参数位置的impl Trait"。 (这里更准确的术语是 "参数",但 "参数位置的impl Trait" 是这个特性开发期间使用的措辞,而且它仍然在实现的某些部分中保留着。)
函数可以使用 impl
,后面跟一组 trait 约束,来声明一个参数为匿名类型。调用方必须提供一个满足匿名类型参数所声明的 trait 约束的类型,而函数只能使用匿名类型参数的 trait 约束可用的方法。
例如,以下这两种形式几乎是等价的:
trait Trait {}
// 泛型类型参数
fn foo<T: Trait>(arg: T) {
}
// impl Trait 作为函数参数的匿名类型参数
fn foo(arg: impl Trait) {
}
换句话说, impl Trait
作为函数参数是一种语法糖,用于泛型类型参数 <T: Trait>
,只不过该类型是匿名的并且不出现在 泛型参数组 列表中。
注意: 对于函数参数而言,泛型类型参数和
impl Trait
并不完全等效。 对于泛型参数如<T: Trait>
,调用方可以在调用时使用 泛型实参组 显式指定T
的泛型参数,例如foo::<usize>(1)
。 如果impl Trait
是 任何 函数参数的类型,那么调用方在调用该函数时永远不能提供任何泛型参数。 这包括返回类型或任何常量泛型参数。因此,从其中一个函数签名更改到另一个函数签名可能会对函数的调用方构成破坏性更改。
抽象返回类型
注意: 这通常被称为 "返回位置的 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 作为其返回类型的约束,而不是明确指定涉及的所有其他迭代器类型。
泛型和 impl Trait
在返回位置的差异
在参数位置上, impl Trait
在语义上与泛型类型参数非常相似。
然而,在返回位置上,两者之间存在显著的差异。
使用 impl Trait
,与泛型类型参数不同,函数选择返回类型,调用者无法选择返回类型。
该函数:
fn foo<T: Trait>() -> T {
允许调用者确定返回类型 T
,并且函数返回该类型。
该函数:
fn foo() -> impl Trait {
不允许调用者确定返回类型。相反,函数选择返回类型,但只承诺它将实现 Trait
。
限制
impl Trait
只能出现在自由函数或内部函数的参数或返回类型中。它不能出现在 trait 的实现内部,也不能是 let 绑定的类型或出现在类型别名中。
类型参数
在具有类型参数声明的条目的主体内部,其类型参数名称表示的类型:
#![allow(unused)] fn main() { // 定义了一个泛型函数 to_vec,其类型参数为 A,它要求 A 类型实现了 Clone trait fn to_vec<A: Clone>(xs: &[A]) -> Vec<A> { // 如果 xs 数组为空,则直接返回空的 Vec if xs.is_empty() { return vec![]; } // 定义变量 first,类型是 A,值是 xs 数组的第一个元素的克隆 let first: A = xs[0].clone(); // 定义变量 rest,它的类型是 Vec<A>,它的值是调用 to_vec 函数得到的结果, // 参数是 xs 数组的第 2 个元素到最后一个元素组成的切片。 let mut rest: Vec<A> = to_vec(&xs[1..]); // 在 rest 的开头插入 first 元素 rest.insert(0, first); // 返回更新后的 rest rest } }
这里, first
的类型为 A
,引用了 to_vec
的类型参数 A
; rest
的类型为 Vec<A>
,即元素类型为 A
的 vector 。
推断类型
语法
推断类型 :_
推断类型是通过周围可用信息让编译器尽可能地推断类型。 不能用于条目签名中。这通常在泛型参数中使用:
#![allow(unused)] fn main() { let x: Vec<_> = (0..10).collect(); }
动态大小类型
大多数类型在编译时具有固定的大小,且实现了 Sized
trait。大小仅在运行时已知的类型称为 动态大小类型 (DST) 或未确定大小类型 。
切片 和 trait 对象 是两个 DSTs 的例子。这样的类型只能在某些情况下使用:
- 指向 DSTs 的指针类型是有大小的,但大小是指向确定大小类型的指针的两倍。
- 指向切片的指针还会存储切片的元素数量。
- 指向 trait 对象的指针还会存储指向虚表的指针。
- 可以将 DSTs 作为类型参数提供给具有特殊
?Sized
约束的泛型类型参数。当相应的关联类型声明具有?Sized
约束时,它们还可用于关联类型定义。默认情况下,除非使用?Sized
约束,否则任何类型参数或关联类型都具有Sized
约束。 - trait可以为 DSTs 实现。与泛型类型参数有所不同,在 trait 定义中,默认情况下
Self: ?Sized
。 - 结构体可能包含一个 DST 作为最后一个字段;这使得结构体本身变成了 DST 。
译注: 这里的动态大小类型的概念,实际在叙述这样一种状态,比如 A 类型所定义的变量求值后存储占用了 4 个字节,经过计算后再去求值可能改变为 8 个字节,需注意这个变量本身始终固定为 2 个指针的大小,只是对这个指针求值后的间接存储大小可能会发生变化。
类型布局
类型布局包括其类型的字节大小、字节对齐方式以及类型字段的相对偏移量。 对于枚举类型,还包括判别值的布局及其解释方式。 类型布局并不是始终不变的,每次编译后很有可能因为优化等原因而改变。 这里只记录目前可以确保的内容。
大小和对齐
所有的值都有对齐和大小。
值的 对齐 决定存储这个值的地址结构,对齐值为 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 |
f32 | 4 |
f64 | 8 |
char | 4 |
usize
和 isize
类型大小大于等于目标平台上单个地址的大小。
例如,在 32 位目标平台上,其类型大小为 4 个字节,在 64 位目标平台上,类型大小为 8 个字节。
虽然系统平台可以去特定实现,但大多数基本数据类型的对齐方式通常是按照该类型的大小对齐。
特别是,在 x86 架构下, u64
和 f64
仅按照 32 位对齐。
指针和引用的布局
指针和引用具有相同的布局。指针或引用的可变性不会改变其布局。
指向有大小的类型的指针与 usize
具有相同的大小和对齐。
指向无大小的类型的指针是依然是有大小的。
其至少保证等于一个指针的大小和对齐。
注意: 虽然这个值不保证确定,但目前,所有指向动态大小类型的指针都是
usize
大小的两倍,并具有相同的对齐。
数组布局
一个 [T; N]
的数组大小为 size_of::<T>() * N
,并且具有与 T
相同的对齐方式。
数组的布局使得从零开始的第 n
个元素相对于数组的起始位置偏移了 n * size_of::<T>()
字节。
切片布局
切片的布局与其所裁切数组的部分相同。
注意: 这指的是原始
[T]
类型,而不是指向切片的指针 (&[T]
,Box<[T]>
等) 。
str
的布局
字符串切片是字符的 UTF-8 表示,其布局与类型为 [u8]
的切片相同。
元组布局
元组的布局遵循 默认表示 。
例外情况是空元组 (()
) ,它确保的类型的大小为 0 ,对齐为 1 。
Trait 对象布局
Trait 对象的布局与其包含的值相同。
注意: 这里是指原始的 Trait 对象类型,而不是指指针 (如
&dyn Trait
、Box<dyn Trait>
等) 指向 Trait 对象的类型。
闭包布局
闭包没有布局的保证。
表示形式
所有用户定义的复合类型 ( struct
、 enum
和 union
) 都有对应的 表示形式 ,用于指定类型的布局。
类型的可能表示形式包括:
可以通过在类型上应用 repr
属性来更改类型的表示形式。
以下示例展示具有 C
表示形式的结构体。
#![allow(unused)] fn main() { #[repr(C)] struct ThreeInts { first: i16, second: i8, third: i32 } }
通过 align
和 packed
修饰符可以分别提升或降低对齐值。
修饰符会修改属性所指定的表示形式,如果未指定表示形式属性,则修改默认的表示形式。
#![allow(unused)] fn main() { // 默认的表示,对齐方式被降低到 2。 #[repr(packed(2))] struct PackedStruct { first: i16, second: i8, third: i32 } // C 表示,对齐方式被提高到 8。 #[repr(C, align(8))] struct AlignedStruct { first: i16, second: i8, third: i32 } }
注意: 由于表示形式是条目的属性,代表了不依赖于泛型参数。 任何两个具有相同名称的类型都具有相同的表示形式。例如,
Foo<Bar>
和Foo<Baz>
的相同。
类型的表示形式可以改变字段之间的填充,但不会改变字段本身的布局。
例如,具有 C
表示形式的结构体中包含具有默认表示形式的结构体 Inner
,不会改变 Inner
的布局。
默认表示形式
没有 repr
属性的具名类型采用默认表示形式。
该表示形式也被称为 rust
表示形式,但这个概念并不正式。
此表示形式做了数据布局的基本保证,以期望保持数据的正确性。
意味着:
- 字段正确对齐。
- 字段不重叠。
- 类型的对齐至少是其字段的最大对齐。
严格地说,第一个保证意味着任何字段的偏移量都是该字段的对齐方式的倍数。 第二个保证不排除对字段进行排序,使得任何字段的偏移量加上其大小小于或等于排序中下一个字段的偏移量。排序时不保证与在类型声明中指定字段的顺序相同。
请注意,第二个保证并不保证字段具有不同的地址: 零大小类型可能与值结构中其他字段具有相同的地址。
此表示形式没有其他数据布局保证。
C
表示形式
C
表示形式旨在实现两个目的。一个目的是创建可与 C 语言互操作的类型,另一个目的是创建可以对其执行依赖于数据布局的操作 (比如将值重新解释为不同类型) 的类型。因些,此表示形式不一定仅是 C 编程语言的接口。
此表示形式可以应用于结构体、联合体和枚举。
但,空变体枚举 不支持 C
表示形式。
#[repr(C)]
结构体
结构体的对齐是其中对齐要求最高的字段的对齐。
字段的大小和偏移量由以下算法决定。
将当前偏移量初始化为 0 字节。
按照结构体中字段的声明顺序,确定字段的大小和对齐。 如果当前偏移量不是字段对齐的倍数,则添加填充字节,至到满足倍数。 字段的偏移量就是当前偏移量。 然后通过字段的大小增加当前偏移量。
最后,结构体的大小是将当前偏移量向上舍入到结构体对齐的最接近的倍数。
以下是这个算法的伪代码描述。
/// 返回在偏移 `offset` 之后需要的填充量,以确保以下地址将对齐到 `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;
}
// 结构体的大小为当前偏移量加上在此处添加的填充量,以确保结构体整体对齐到 `alignment` 的倍数
struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);
警告:此伪代码使用一种简单算法,为了清晰起见忽略了溢出问题。对于实际代码中执行内存布局计算,请使用 Layout
。
注意: 此算法可能会产生大小为零的结构体。在 C 中,像
struct Foo { }
这样的空结构体声明是不合法的。 但是,gcc 和 clang 都支持启用这样的结构体,并将其大小分配为零。 而 C++ 给空结构体大小分配为 1 ,如果是从其他结构体继承,或者是带有[[no_unique_address]]
属性的字段时,不会增加结构体的整体大小。
#[repr(C)]
联合体
使用 #[repr(C)]
声明的联合体将与目标平台上等效的 C 语言联合体声明具有相同的大小和对齐。
该联合体的大小将是其所有字段的最大大小舍入为其对齐,其对齐将是其所有字段的最大对齐。这些最大值可以来自不同的字段。
#![allow(unused)] fn main() { #[repr(C)] union Union { f1: u16, f2: [u8; 4], } assert_eq!(std::mem::size_of::<Union>(), 4); // 来自 f2 assert_eq!(std::mem::align_of::<Union>(), 2); // 来自 f1 #[repr(C)] union SizeRoundedUp { a: u32, b: [u16; 3], } assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 8); // 大小为 6,来自 b,舍入为 8,来自 a 的对齐 assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // 来自 a }
#[repr(C)]
无字段的枚举
对于 无字段的枚举 ,其 C
表示形式具有目标平台 C ABI 的默认 enum
大小和对齐。
注意:C 中的枚举表示形式是实现定义的,因此这只是一个 "最佳猜测" 。特别是,当 C 代码使用某些标志编译时,可能会不正确。
警告:C 语言中的 enum
和 Rust 的 无字段的枚举 在这种表示形式下存在关键差异。
在 C 中,enum
主要是 typedef
与一些命名常量组合,其 enum
类型对象可以容纳任意整数值。
比如,在 C
中,通常用于标志位。
相比之下,Rust 的 无字段的枚举 只能合法地容纳判别值,其他状况都是 未定义行为 。
因此,在 FFI 中使用无字段的枚举来模拟 C enum
通常是错误的。
#[repr(C)]
枚举类型的字段
使用 #[repr(C)]
声明的带有字段的枚举类型的表示形式是带有两个字段的 repr(C)
结构体,也称为 C 语言 "标签化联合体" ,即:
- 去掉所有字段的
repr(C)
版本的枚举类型 ("标签") repr(C)
联合体,其中包含每个有字段的变体的repr(C)
结构体 ("有效载荷")
注意: 对于
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; }
注意: 带有非
Copy
字段的union
是未稳定的,请参见 55149。
原始表示形式
原始表示形式 是与原始整数类型具有相同名称的表示。
即:u8
、u16
、u32
、u64
、u128
、usize
、i8
、i16
、i32
、i64
、i128
和isize
。
原始表示形式仅适用于枚举类型,并根据枚举是否有字段具有不同的行为。 对于 零变体枚举 来说,使用原始表示形式是错误的。 将两个原始表示形式结合在一起也是错误的。
无字段枚举的原始表示形式
对于 无字段枚举 ,原始表示形式将大小和对齐设置为与同名的原始类型相同。
例如,具有 u8
表示的无字段枚举只能具有 0 到 255 之间 (包括 0 和 255 ) 的判别值。
具有字段的枚举的基本表示形式
具有字段的基本表示形式枚举的表示形式是 repr(C)
联合体,其中包含每个变量的 repr(C)
结构体。
联合体中每个结构体的第一个字段是去除所有字段的枚举的基本表示形式版本 ("标签") ,剩余字段是该变量的字段。
注意:如果标签在联合体中有自己的成员,则该表示形式不会改变,这样可以更清晰地进行操作 (尽管为了遵循 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); }
注意: 具有非
Copy
字段的联合体是未稳定的,请参见 55149 。
将带字段的枚举的原始表示形式与 #[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)
之外使用原始表示形式可能会改变枚举的大小:
#![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 中判别值和值各占 1 个字节 assert_eq!(std::mem::size_of::<Enum8>(), 2); // Enum16::Variant0 中判别值和值各占 1 个字节,再加上 1 个字节的填充 assert_eq!(std::mem::size_of::<Enum16>(), 4); }
对齐修饰符
align
和 packed
修饰符可以用于分别提高或降低 struct
和 union
的对齐。
packed
也可以改变字段之间的填充 (但不会改变任何字段内部的填充) 。
对齐是以整数形式的参数指定的,形式为 #[repr(align(x))]
或 #[repr(packed(x))]
。
对齐值必须是从 1 到 229 的 2 的幂。对于 packed
,如果没有给出值,例如 #[repr(packed)]
,那么该值为 1。
对于 align
,如果指定的对齐小于没有 align
修饰符的类型的对齐,则对齐不受影响。
对于 packed
,如果指定的对齐大于没有 packed
修饰符的类型的对齐,则对齐和布局不受影响。
对于定位字段的目的,每个字段的对齐是指定的对齐和字段类型的对齐中较小的一个。
确保字段间的填充是最小的,以满足每个字段的 (可能已更改的) 对齐 (需注意,单独使用 packed
不能提供有关字段顺序的任何保证) 。
这些规则的一个重要后果是,具有 #[repr(packed(1))]
(或 #[repr(packed)]
) 的类型将没有字段间的填充。
align
和 packed
修饰符不能应用于同一类型,packed
类型不能以传递形式包含另一个 align
化的类型。
align
和 packed
只能应用于 [默认] 和 C
表示形式。
align
修饰符也可以应用于 enum
。
当这样做时,enum
的对齐的效果与如果将 enum
包装在具有相同 align
修饰符的新类型 struct
中相同。
警告: 对未对齐的指针进行解引用是 未定义行为 ,可以 安全地创建指向 packed
字段的未对齐指针。
这与 Rust 所有安全代码中所创建的未定义行为一样,是一个错误。
transparent
表示形式
transparent
表示形式只能用于具有以下特点的 struct
或 enum
中的单个变体:
- 具有非零大小的单个字段,且
- 具有大小为 0 且对齐为 1 的任意数量的字段 (例如
PhantomData<T>
) 。
具有此表示形式的结构体和枚举具有与单个非零大小字段相同的布局和 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
模块的相关类型,包装了只能通过原子操作来访问的值,允许其值在线程之间共享和改变。
子类型化和协变
子类型化是隐式的,可以在类型检查或推断阶段发生。 子类型化只限于两种情况: 类型和具有更高阶生命周期的类型之间的协变。 如果抹去类型的生命周期,则唯一的子类型是类型相等。
译注:子类型的这个概念与面向对象的特性相同,如果类型 A 是类型 B 的子类型,那么意味着 A 类型的值可以被 B 类型变量接收。 其主要的应用场景将值在不同类型间进行传递时,如果安全,不必手动转换,并且这种转换仅限于对类型生命周期的范围的调整,在这里被称为 '协变' 。
思考以下示例: 字符串字面量 "hi" 始终具有 '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<T>
是F<U>
的子类型 ( 子类型关系 "传递" )。 - 如果
T
是U
的子类型,则F<T>
是 逆变的 的,这意味着F<U>
是F<T>
的子类型。 - 否则,
F<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 和 lifetime 约束
语法
类型参数约束组 :
类型参数约束 (+
类型参数约束 )*+
?类型参数约束 :
生命周期 | Trait约束Trait约束 :
?
? For生命周期? 类型路径
|(
?
? For生命周期? 类型路径)
生命周期约束 :
( 生命周期+
)* 生命周期?生命周期 :
生命周期或标签
|'static
|'_
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
约束的泛型函数和类型用于T
使用Ty
。
#![allow(unused)] fn main() { type Surface = i32; trait Shape { fn draw(&self, surface: Surface); fn name() -> &'static str; } fn draw_twice<T: Shape>(surface: Surface, sh: T) { sh.draw(surface); // 可以调用方法,因为T: Shape sh.draw(surface); } fn copy_and_draw_twice<T: Copy>(surface: Surface, sh: T) where T: Shape { let shape_copy = sh; // 不移动 sh ,因为T: Copy draw_twice(surface, sh); // 可以使用泛型函数,因为T: Shape } struct Figure<S: Shape>(S, S); fn name_figure<U: Shape>( figure: Figure<U>, // 类型 Figure<U> 是 well-formed ,因为U: Shape ) { println!( "Figure of two {}", U::name(), // 可以使用关联函数 ); } }
当定义条目时,不使用条目参数或 高阶生命周期 的约束将被检查。 这样的约束如果为 false ,则会报错。
对于某些泛型类型,在使用该条目时还会检查 Copy
、 Clone
和 Sized
约束,即使使用时没有提供具体类型。
在可变引用、 trait 对象 或 切片 上将 Copy
或 Clone
作为约束是错误的。
在 trait 对象 或 切片 上使用 Sized
作为约束也是错误的。
#![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
一样长,因此当 &'b ()
有效时,引用 &'a ()
也是有效的。
#![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 约束
For生命周期 :
for
泛型参数组
Trait 约束可以对生命周期进行 提阶 ,该约束指示 对于所有 生命周期都成立的约束。
例如, 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); } }
类型强转
类型强转 指的是值的隐式操作,可以更改值的类型。
强转在特定情况自动发生,对于符合强转要求的类型,有高度的限制。
所有允许隐式强转的转换可以通过 类型转换运算符 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 } }
如果在这些类型转换位置之一的表达式是一个类型转换传播的表达式,那么该表达式中的相关子表达式也是类型转换位置。 传播从这些新的类型转换位置进行递归。 可传播的表达式及其相关子表达式包括:
-
数组字面值,数组类型为
[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_2
,T_2
转换为T_3
时,T_1
可以转换为T_3
(可传递的情况)注意,这种情况目前尚未完全支持。
-
&mut T
可以强转为&T
-
*mut T
可以强转为*const T
-
&T
可以强转为*const T
-
&mut T
可以强转为*mut T
-
如果
T
实现了Deref<Target = U>
,则&T
或&mut T
可以强转为&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 。 }
-
如果
T
实现了DerefMut<Target = U>
,则&mut T
可以强转为&mut U
。 -
TyCtor(
T
) 可以强转为 TyCtor(U
) ,其中 TyCtor(T
) 是以下之一:&T
&mut T
*const T
*mut T
Box<T>
而且
U
可以通过 不定大小强转 从T
中获得。 -
函数条目类型到
fn
指针 -
非捕获闭包到
fn
指针 -
!
到任何T
不定大小强转
下列类型强转被称为 不定大小强转
,因为它们涉及将有大小限制的类型转换为无大小限制的类型,并且在一些其他强转不允许的情况下被允许,如上所述。
这类强转可以在可强转的的任意位置。
Unsize
和 CoerceUnsized
两个辅助 trait ,可用实现此类强转并将其公开以供库使用。
下列强转是内置的,如果 T
可以通过其中之一转换为 U
,则将为 T
提供一个实现了 Unsize<U>
的实现:
-
[T; n]
转换为[T]
。 -
当
T
实现U + Sized
时,T
转换为dyn U
,而U
是 对象安全 的。 -
当满足以下条件时,
Foo<..., T, ...>
转换为Foo<..., U, ...>
:Foo
是一个结构体。T
实现了Unsize<U>
。Foo
的最后一个字段的类型涉及到了T
。- 如果该字段的类型为
Bar<T>
,则Bar<T>
实现了Unsized<Bar<U>>
。 T
不是任何其他字段类型的一部分。
此外,当 T
实现了 Unsize<U>
或 CoerceUnsized<Foo<U>>
时,类型 Foo<T>
可以实现 CoerceUnsized<Foo<U>>
。这使它可以提供到 Foo<U>
的不定大小的强转。
注意:尽管已经稳定化了不定大小的强转的定义及其实现,但这些特性本身尚未稳定,因此不能在稳定的 Rust 中直接使用它们。
最小上界强转
在某些情况下,编译器必须将多个类型强转在一起处理,会尝试找到最通用的类型。 这被称为 "最小上界强转" (Least Upper Bound coercion,简称LUB 强转) 。LUB 强转仅在以下情况下使用:
- 为一系列 if 分支查找公开类型。
- 为一系列 match 分支查找公开类型。
- 为数组元素查找公开类型。
- 为带有多个返回语句的闭包查找返回类型。
- 检查带有多个返回语句的函数的返回类型。
在每种情况下,存在一组要相互强转为某个目标类型 T_t
的类型 T0..Tn
,该目标类型最初是未知的。
计算 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]; // 用于多个返回语句的闭包 let clo = || { if true { a } else if false { b } else { c } }; let baz = clo(); // 用于多个返回语句的函数类型检查 fn foo() -> i32 { let (a, b, c) = (0, 1, 2); match 42 { 0 => a, 1 => b, _ => c, } } }
在这些示例中, ba*
的类型是通过最小上界 LUB 强转来确定的。
编译器在处理函数 foo
时检查 a
, b
, c
的 LUB 强转结果是否为 i32
。
注意事项
这个描述是非正式的,更准确的描述将作为规范 Rust 类型检查器的泛型尝试的一部分。
析构函数
当一个 已初始化过 的 变量 或 临时值 超出 作用域 时,会执行其 析构函数 会,或者说会被 销毁 。 赋值 时会运行其已初始化的左操作数的析构函数。 如果变量只有部分已初始化,那么只销毁已初始化字段。
类型 T 的析构函数包括:
- 如果
T: Drop
,则调用<T as std::ops::Drop>::drop
。 - 递归运行其所有字段的析构函数。
如果必须手动运行析构函数,比如在实现自己的智能指针时,可以使用 std::ptr::drop_in_place
。
一些例子:
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("{}", self.0); } } // 当左操作数被覆盖时会销毁 let mut overwritten = PrintOnDrop("drops when overwritten"); // 当作用域结束时会销毁 overwritten = PrintOnDrop("drops when scope ends"); // Tuple 的元素按顺序销毁 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` 的作用域结束时,只有第一个字段被销毁。 }
析构作用域
每个变量或临时变量都与一个 析构作用域 相关联。
当控制流离开析构作用域时,与该作用域相关联的所有变量按照声明 (对于变量) 或创建 (对于临时变量) 的相反顺序进行销毁。
在使用 match
表达式替换 for
、 if let
和 while let
表达式之后,确定析构作用域。
重载的操作符与内置的操作符没有区别,不考虑绑定模式。
对于函数或闭包,有以下析构作用域:
析构作用域按如下嵌套。当一次离开多个作用域时,例如从函数返回时,变量从内向外进行销毁。
- 整个函数的作用域是最外层的作用域。
- 函数体块包含在整个函数的作用域内。
- 表达式语句中表达式的父级是该语句的作用域。
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
语句的块的作用域相关联。
在 match
表达式中声明的局部变量与它们声明在的 match
分支的作用域相关联。
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } let declared_first = PrintOnDrop("Dropped last in outer scope"); { let declared_in_block = PrintOnDrop("Dropped in inner scope"); } let declared_last = PrintOnDrop("Dropped first in outer scope"); }
如果在 match
表达式的同一分支中使用了多个模式,则使用未指定的模式来确定丢弃的顺序。
临时变量的作用域
一个表达式的 临时作用域 是在 占位上下文 中使用该表达式时用于保存该表达式结果的临时变量的作用域,除非该表达式被 提升。
除了生命周期扩展之外,表达式的临时作用域是包含表达式的最小作用域,可以是以下之一:
- 整个函数体。
- 一个语句。
if
、while
或loop
表达式的主体。if
表达式的else
块。if
或while
表达式的条件表达式,或match
守卫。match
分支的主体表达式。- 惰性布尔表达式的第二个操作数。
注:
在函数体的最终表达式中创建的临时变量将在函数体绑定的任何命名变量之后释放,因为没有更小的封闭临时作用域。
match
表达式的 被匹配项 不是临时作用域,因此可以在match
表达式之后丢弃被匹配项中的临时变量。 例如,在match 1 { ref mut z => z };
中的1
的临时变量将一直存在到语句的结尾。
一些例子:
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } let local_var = PrintOnDrop("局部变量"); // 一旦条件被评估,将被丢弃 if PrintOnDrop("if条件").0 == "if条件" { // 在块的末尾被丢弃 PrintOnDrop("if块体").0 } else { unreachable!() }; // 在语句的末尾被丢弃 (PrintOnDrop("第一个操作数").0 == "" // 在 ) 处被丢弃 || PrintOnDrop("第二个操作数").0 == "") // 在表达式的末尾被丢弃 || PrintOnDrop("第三个操作数").0 == ""; // 在函数的末尾被丢弃,在局部变量之后。 // 将其更改为包含返回表达式的语句将使临时变量在局部变量之前被释放。 // 将其绑定到变量并返回该变量也会使临时变量首先被释放。 match PrintOnDrop("匹配的值,用于最终表达式") { // 一旦条件被评估,将被丢弃 _ if PrintOnDrop("守卫条件").0 == "" => (), _ => (), } }
操作数
在表达式中,为了保存操作数的结果,也会创建临时变量,而计算其他操作数。 这些临时变量与该操作数的表达式作用域相关联。 由于临时变量在计算表达式后被移动,除非表达式的操作数之一中断表达式、返回或抛出异常,否则将其丢弃不会产生副作用。
#![allow(unused)] fn main() { struct PrintOnDrop(&'static str); impl Drop for PrintOnDrop { fn drop(&mut self) { println!("drop({})", self.0); } } loop { // 元组表达式没有完成评估,因此操作数按相反的顺序丢弃 ( PrintOnDrop("外部元组第一个"), PrintOnDrop("外部元组第二个"), ( PrintOnDrop("内部元组第一个"), PrintOnDrop("内部元组第二个"), break, // 循环中断,后面的表达式不会执行 ), PrintOnDrop("永远不会创建"), ); } }
常量晋升
将值表达式晋升为 'static
发生在表达式写成常量并借用时,其借用可以在不改变运行时行为的情况下,在表达式原始写法的位置进行解引用。
也就是说,晋升的表达式可以在编译时计算,所得到的值不包含 内部可变性 或 析构函数
(这些属性在可能的情况下基于值来确定,例如, &None
始终具有类型 &'static Option<_>
,因为它不包含任何被禁止的内容)。
临时值生命周期延长
注意: 临时值生命周期延长的确切规则可能会发生变化。本文描述的是当前行为。
在 let
语句中的表达式的临时值作用域有时会被 延长 到包含 let
语句的块的作用域。
这是根据某些语法规则判断,当通常的临时值作用域太小时进行的。例如:
#![allow(unused)] fn main() { let x = &mut 0; // 通常此时一个临时变量已经被丢弃了,但是 `0` 的临时变量会一直存活到该块的结尾。 println!("{}", x); }
如果一个 借用表达式 、 解引用表达式 、 字段表达式 或 元组索引表达式 具有扩展临时作用域,则其操作数也具有扩展临时作用域。 如果一个 索引表达式 具有扩展临时作用域,则索引表达式也具有扩展临时作用域。
基于模式的扩展
一个 扩展模式 可以是:
- 一个通过引用或可变引用绑定的 标识符模式 。
- 一个 结构体模式 、 元组模式 、 元组结构体模式 或 切片模式 ,其中至少一个直接子模式是扩展模式。
因此,
ref x
、V(ref x)
和[ref x, y]
都是扩展模式,但x
、&ref x
和&(ref x,)
不是。如果let
语句中的模式是扩展模式,则初始化表达式的临时作用域会被扩展。
基于表达式的扩展
对于带有初始化器的 let
语句,一个 扩展表达式 是以下表达式之一:
因此, &mut 0
、 (&1, &mut 2)
和 Some {0: &mut 3}
中的借用表达式都是扩展表达式。 &0 + &1
和 Some(&mut 0)
中的借用表达式不是扩展表达式:后者在语法上是一个函数调用表达式。
任何扩展借用表达式的操作数都会扩展其临时作用域。
示例
以下是一些表达式具有扩展临时作用域的示例:
#![allow(unused)] fn main() { fn temp() {} trait Use { fn use_temp(&self) -> &Self { self } } impl Use for () {} // 存储 `temp()` 结果的临时变量在以下这些情况下与 x 具有相同的作用域。 let x = &temp(); let x = &temp() as &dyn Send; let x = (&*&temp(),); let x = { [Some { 0: &temp(), }] }; let ref x = temp(); let ref x = *&temp(); x; }
以下是一些表达式没有扩展临时作用域的示例:
#![allow(unused)] fn main() { fn temp() {} trait Use { fn use_temp(&self) -> &Self { self } } impl Use for () {} // 存储`temp()`结果的临时变量在以下这些情况下只能在 let 语句结束之前使用。 let x = Some(&temp()); // 错误 let x = (&temp()).use_temp(); // 错误 x; }
不运行析构函数
std::mem::forget
可以用来防止变量的析构函数被执行,而 std::mem::ManuallyDrop
提供了一个包装器来防止变量或字段被自动释放。
注意:通过
std::mem::forget
或其他方式防止析构函数被执行即使具有不是'static
类型的类型也是安全的。 除了在本文档中定义的保证运行析构函数的地方之外,类型不能依赖析构函数被运行来确保安全性。
生命周期省略
在编译器可以推断出一些合理的默认选择规则时,允许省略生命周期。
也可以用同样的方式推断占位符生命周期 '_
。
对于路径上的生命周期,使用 '_
很好的选择。
Trait 对象生命周期遵循的规则有所不同,将在下面讨论。
函数中的生命周期省略
为了让常见的模式更加人性化,生命周期参数可以在函数、函数指针和闭包 Trait 签名中被 省略 。 省略无法推断的生命周期参数将是错误。
- 在参数中每个被省略的生命周期都会成为单独的。
- 如果在参数中只有一个生命周期 (不论是否被省略) ,那么该生命周期将分配到 所有 被省略的输出生命周期。
在方法签名中还有一个规则:
- 如果接收者的类型是
&Self
或&mut Self
,那么指向Self
的这个引用的生命周期会被分配到所有省略的输出生命周期参数。
例子:
#![allow(unused)] fn main() { trait T {} trait ToCStr {} struct Thing<'a> {f: &'a i32} struct Command; trait Example { fn print1(s: &str); // 省略生命周期 fn print2(s: &'_ str); // 同样省略 fn print3<'a>(s: &'a str); // 展开 fn debug1(lvl: usize, s: &str); // 省略生命周期 fn debug2<'a>(lvl: usize, s: &'a str); // 展开 fn substr1(s: &str, until: usize) -> &str; // 省略生命周期 fn substr2<'a>(s: &'a str, until: usize) -> &'a str; // 展开 fn get_mut1(&mut self) -> &mut dyn T; // 省略生命周期 fn get_mut2<'a>(&'a mut self) -> &'a mut dyn T; // 展开 fn args1<T: ToCStr>(&mut self, args: &[T]) -> &mut Command; // 省略生命周期 fn args2<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // 展开 fn new1(buf: &mut [u8]) -> Thing<'_>; // 省略生命周期 - 推荐 fn new2(buf: &mut [u8]) -> Thing; // 省略生命周期 fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>; // 展开 } type FunPtr1 = fn(&str) -> &str; // 省略生命周期 type FunPtr2 = for<'a> fn(&'a str) -> &'a str; // 展开 type FunTrait1 = dyn Fn(&str) -> &str; // 省略生命周期 type FunTrait2 = dyn for<'a> Fn(&'a str) -> &'a str; // 展开 }
#![allow(unused)] fn main() { // 以下示例展示了无法省略生命周期参数的情况。 trait Example { // 无法推断,因为没有参数可推断。 fn get_str() -> &str; // ILLEGAL // 无法推断,不清楚是从第一个还是第二个参数中借用。 fn frob(s: &str, t: &str) -> &str; // ILLEGAL } }
默认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 {} }
'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} // 解析为 `fn<'a>(&'a str) -> &'a str`. const RESOLVED_SINGLE: fn(&str) -> &str = |x| x; // 解析为 `Fn<'a, 'b, 'c>(&'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
Rust 编译器已知某些 标准库 中存在的类型和 trait 。 本节介绍这些类型和 trait 的特殊特性。
Box<T>
Box<T>
具有 Rust 目前不允许用户定义类型拥有的一些特殊特性。
Box<T>
的 解引用运算符 产生一个可以被移动的地址。 表示*
运算符及Box<T>
的析构函数是内置到语言中的。- 方法 可以以
Box<Self>
作为接收者。 - 在与
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]auto traits 的角度来看,被视为拥有一个类型为 T
的实例。
运算符 trait
std::ops
和 std::cmp
中的 trait 用于重载 运算符 、 索引表达式 和 调用表达式 。
Deref
和 DerefMut
除了重载一元操作符 *
之外,Deref
和 DerefMut
还用于 方法解析 和 解引用强制转换。
Drop
Drop
trait 提供了一个 析构器 ,当该类型的值即将被销毁时运行。
Copy
Copy
trait 改变了实现它的类型的语义。实现 Copy
的类型的值在赋值时会被复制而不是移动。
只有不实现 Drop
trait 并且其所有字段都是 Copy
类型的类型才能实现 Copy
trait 。
对于枚举,意味着所有变体的所有字段都必须是 Copy
类型。
对于联合体,意味着所有变体都必须是 Copy
类型。
编译器为以下类型实现了 Copy
trait :
Clone
Clone
trait 是 Copy
的父级trait ,因此它也需要编译器生成的实现。编译器为以下类型实现了此 trait:
- 有内置
Copy
实现的类型 (见上文) Clone
类型的元组
Send
Send
trait 表明此类型的值可以安全地从一个线程发送到另一个线程。
Sync
Sync
trait 表明此类型的值可以安全地在多个线程之间共享。所有用于不可变 static
条目 的类型都必须实现此 trait 。
Termination
Termination
trait 指示 main 函数 和 test 函数 的可接受返回类型。
自动 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 ,而是根据需要的 trait 约束为没有达到 trait 约束的类型实现。例如,标准库为所有 T
是 Sync
的 &T
实现了 Send
;这意味着,如果 T
是 Send
但不是 Sync
,则编译器不会为 &T
实现 Send
。
自动 trait 也可以有否定实现,在标准库文档中表示为 impl !AutoTrait for T
,它们覆盖了自动实现。例如, *mut T
有一个 Send
的否定实现,因此即使 T
是 Send
, *mut T
也不是 Send
。
目前没有稳定的方法指定额外的否定实现;它们只存在于标准库中。自动 trait 可以作为 trait 对象 的附加约束添加到任何 trait 中,尽管通常只允许一个 trait 。
例如,Box<dyn Debug + Send + UnwindSafe>
是一个有效的类型。
Sized
Sized
trait 表示这个类型在编译时大小是已知的,即不是 动态大小类型 。
类型参数 (除了trait 中的 Self
) 默认都是 Sized
的,关联类型也是。
Sized
trait 总是由编译器自动实现,而不是由 实现条目 实现的。
这些隐式的 Sized
约束可以通过使用特殊的 ?Sized
约束进行放宽。
名称
实体 是一种语言构造,可以通过某种方式在源程序中引用,通常是通过 路径 。 实体包括 类型 、 条目 、 泛型参数 、 变量绑定 、 循环标签 、 生命周期 、 字段 、 属性 和 代码分析 。
声明 是一种语法构造,可以引入 名称 来引用一个实体。实体名称在 作用域 内是有效的 —— 源文本的一个区域,可以在其中引用该名称。 一些实体在源代码中 显式声明,一些实体则作为语言或编译器扩展的一部分 隐式声明。
路径 用于引用实体,可能在另一个作用域中。生命周期和循环标签使用 专用语法,使用前导引号。
名称被分为不同的 命名空间 ,允许不同命名空间中的实体共享相同的名称而不冲突。
名称解析 是将路径、标识符和标签与实体声明联系起来的编译时过程。
根据 可见性 ,访问某些名称可能会受到限制。
显式声明的实体
在源代码中显式引入名称的实体包括:
- 条目:
- 表达式:
- 泛型参数
- 高阶 trait 约束
let
语句 的模式绑定macro_use
属性 可以引入另一个 crate 中的宏名称macro_export
属性 可以将宏的别名引入 crate 根
此外, 宏调用 和 属性 可以通过扩展为上述项之一而引入名称。
隐式声明的实体
以下实体是由语言隐式定义的,或者是由编译器选项和扩展引入的:
- 语言预定义:
- 内置属性
- 标准库预定义 条目、属性和宏
- 根模块中的 标准库 crate
- 编译器链接的 外部 crate
- 工具属性
- 代码分析 和 工具 Lint 属性
- 衍生助手属性 在条目内有效,无需显式导入
'static
生命周期
此外,crate 根模块没有名称,但可以用某些 路径限定符 或别名来引用。
命名空间
命名空间 是指已声明的 名称 的逻辑分组。基于名称所指代实体的种类,将名称分隔到不同的命名空间中。 允许在不同的命名空间中有相同名称,且不发生冲突。
在一个命名空间内,名称按层次结构组织,每个层次结构都有其自己的命名实体集合。
有几个不同的命名空间,每个命名空间包含不同种类的实体。根据上下文,名称的使用将在不同的命名空间中查找该名称的声明,如 名称解析 章节所述。
以下是命名空间及其相应实体的列表:
- 类型命名空间
- 值命名空间
- 宏命名空间
- 生命周期命名空间
- 标签命名空间
不同命名空间中重复的名称如何可以无歧义地使用的示例:
#![allow(unused)] fn main() { // Foo 在类型命名空间中引入了一个类型和在值命名空间中引入了一个构造函数。 struct Foo(u32); // `Foo` 宏在宏命名空间中声明。 macro_rules! Foo { () => {}; } // `Foo` 在 `f` 参数类型中引用了类型命名空间中的 `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
宏 是宏命名空间中具有相同名称的两个不同实体,但它们仍可以在各自的上下文中使用。
无论子命名空间如何, use
导入 覆盖另一个宏仍将导致错误。
作用域
注意: 这是一个未来扩展的占位符。
预定义
预定义 是一组自动引入到一个 crate 中每个模块作用域的名称。
这些预定义名称不是模块本身的一部分: 它们在 命名解析 期间隐式查询。例如,即使像 Box
这样的内容在每个模块中都处于作用域中,但你无法将其称为 self::Box
,因为它不是当前模块的成员。
有几个不同的预定义:
标准库预定义
每个 crate 都有一个标准库预定义,其中包括来自单个标准库模块的名称。所使用的模块取决于 crate 的版本,以及是否应用了 no_std
属性 :
版本 | 未应用no_std | 应用no_std |
---|---|---|
2015 | std::prelude::rust_2015 | core::prelude::rust_2015 |
2018 | std::prelude::rust_2018 | core::prelude::rust_2018 |
2021 | std::prelude::rust_2021 | core::prelude::rust_2021 |
注意:
std::prelude::rust_2015
和std::prelude::rust_2018
的内容与std::prelude::v1
相同。
core::prelude::rust_2015
和core::prelude::rust_2018
的内容与core::prelude::v1
相同。
外部预定义
在根模块中使用 extern crate
导入的外部 crate 或提供给编译器的 crate (例如使用 rustc
的 --extern
标志) 将被添加到 外部定义 中。如果使用别名导入,例如 extern crate orig_name as new_name
,则符号 new_name
将添加到预定义中。
core
crate 始终会被添加到外部预定义中。只要在 crate 根中未指定 [no_std
属性] , std
crate 也会被添加到预定义中。
版本差异: 在 2015 版中,外部预定义中的 crate 不能通过 use 声明 引用,因此通常的做法是使用
extern crate
声明将它们引入作用域。从 2018 年版开始, use 声明 可以引用外部预定义中的 crate ,因此使用
extern crate
被认为不符合惯例。
注意: 随
rustc
一起提供的其他 crate ,例如alloc
和test
,在使用 Cargo 时不会自动包含在--extern
标志中。它们必须通过extern crate
声明引入作用域,即使在 2018 版中也是如此。#![allow(unused)] fn main() { extern crate alloc; use alloc::rc::Rc; }
对于 proc-macro crate , Cargo 会将
proc_macro
自动添加到外部预定义中。
no_std
属性
默认情况下,标准库会自动包含在 crate 的根模块中。std
crate 与一个隐式的 macro_use
属性 一同添加到根模块,将 std
导出的所有宏都添加到 macro_use
预定义模块 中。同时,core
和 std
也会添加到 extern 预定义 中。
可以在 crate 级别应用 no_std
属性 来阻止自动将 std
crate 添加到作用域中。会做三件事情:
- 防止
std
添加到 extern 预定义 中。 - 影响用于构建 标准库预定义 的模块 (如上所述) 。
- 将
core
crate 注入到 crate 根模块中,而不是std
,并将所有从core
导出的宏添加到macro_use
预定义模块 中。
注意: 当 crate 目标平台不支持标准库或有意不使用标准库的功能时,使用核心预定义而不是标准库预定义很有用。 这些功能主要包括动态内存分配 (例如
Box
和Vec
) 以及文件和网络功能 (例如std::fs
和std::io
) 。
警告: 使用 no_std
不会防止标准库被链接。在 crate 中放置 extern crate std;
是有效的,依赖项依然可以链接它。
语言预定义
语言预定义包括内置于语言中的类型和属性名称。语言预定义始终在作用域内。包括以下内容:
- 类型命名空间
- 布尔类型 —
bool
- 文本类型 —
char
和str
- 整数类型 —
i8
、i16
、i32
、i64
、i128
、u8
、u16
、u32
、u64
、u128
- 与机器相关的整数类型 —
usize
和isize
- 浮点数类型 —
f32
和f64
- 布尔类型 —
- 宏命名空间
macro_use
预定义
macro_use
预定义包括来自外部 crate 的宏,这些宏是通过 [macro_use
] 属性定义的。
工具预定义
工具预定义包括外部工具的工具名称,位于 类型命名空间 中。有关详细信息,请参见 工具属性 部分。
no_implicit_prelude
属性
no_implicit_prelude
attribute 可以应用于 crate 级别或模块上,表示它不应自动为该模块或其任何子级引入 标准库预定义、 extern 预定义 或 工具预定义 。
该属性不影响 语言预定义 。
版本差异:在 2015 版中,
no_implicit_prelude
属性不影响macro_use
预定义 ,并且标准库导出的所有宏仍包含在macro_use
预定义中。从 2018 版开始,将移除macro_use
预定义。
路径
路径 是由一个或多个路径段从 逻辑上 由命名空间 限定符(::
) 分隔的序列。
如果路径只包含一个段,则引用局部作用域中的 条目 或 变量 。如果路径有多个段,则总是引用一个条目。
以下是由标识符段组成的简单路径的两个示例:
x;
x::y::z;
路径的类型
简单路径
语法
简单路径 :
::
? 简单路径片段 (::
简单路径片段)*简单路径片段 :
标识符 |super
|self
|crate
|$crate
简单路径在 可见性 标记, 属性 , 宏 和 use
条目中使用。
例如:
#![allow(unused)] fn main() { use std::io::{self, Write}; mod m { #[clippy::cyclomatic_complexity = "0"] pub (in super) fn f1() {} } }
路径表达式
语法
表达式中路径 :
::
? 路径表达式片段 (::
路径表达式片段)*路径表达式片段 :
路径ID片段 (::
泛型参数组)?路径ID片段 :
标识符 |super
|self
|Self
|crate
|$crate
泛型参数组 :
<
>
|<
( 泛型参数,
)* 泛型参数,
?>
泛型参数 :
生命周期 | 类型 | 泛型参数组常量 | 泛型参数组绑定
路径允许指定带有泛型参数表达式的路径。可在表达式和模式的各个位置使用。
在泛型参数的左括号 <
前面需要添加 ::
标记以避免与小于号操作符产生歧义。这通常被称为 "鱼形" 语法。
#![allow(unused)] fn main() { (0..10).collect::<Vec<_>>(); Vec::<u8>::with_capacity(1024); }
泛型参数的顺序是受限的,首先是生命周期参数,然后是类型参数,然后是常量参数,最后是相等性约束。
常量参数必须用花括号括起来,除非是字面量或简单路径段。
对于 impl Trait
类型对应的合成类型参数是隐式的,不能显式地指定。
限定路径
语法
表达式中的限定路径 :
限定路径类型 (::
路径表达式片段)+限定路径类型 :
<
Type (as
类型路径)?>
类型中限定路径 :
限定路径类型 (::
类型路径片段)+
完全限定路径用于在 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 函数。 }
类型中路径
语法
类型路径 :
::
? 类型路径片段 (::
类型路径片段)*类型路径片段 :
类型ID片段::
? (泛型参数 | 类型路径Fn)?类型路径Fn :
(
类型路径Fn输入组?)
(->
类型)?
类型路径在类型定义、trait 约束、类型参数约束和限定路径中使用。
虽然 ::
token 在泛型参数之前是允许的,但不是必须的,因为与在 表达式中路径 的情况不同,这里没有歧义。
#![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>; }
路径限定符
路径可以用各种前导限定符来改变它解析的方式。
::
以 ::
开头的路径被认为是 全局路径 ,路径中的段从不同的起点开始解析,这个起点根据版本不同而有所不同。
路径中的每个标识符都必须解析为一个条目。
版本差异:在 2015 版本中,标识符从 "crate root" 开始解析 (在 2018 版本中是
crate::
) ,其中包括各种不同的条目,包括外部 crate、默认的 crate (如std
或core
) 以及 crate 顶层条目 (包括use
导入) 。从 2018 版本开始,以
::
开头的路径从 extern prelude 中的 crate 解析。也就是说,符号必须跟随 crate 的名称。
#![allow(unused)] fn main() { pub fn foo() { // 在 2018 版中,这通过 extern 预导模块访问 std。 // 在 2015 版中,这通过 crate root 访问 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
相对于当前模块,只能作为路径的第一个段,不能在前面加上 ::
。
fn foo() {} fn bar() { self::foo(); } fn main() {}
Self
Self
(注意大写) 用于在 traits 和 实现 中引用实现类型本身。
Self
只能作为第一个段,不能有前导 ::
。
#![allow(unused)] fn main() { trait T { type Item; const C: i32; // `Self` 将是任何实现 `T` 的类型。 fn new() -> Self; // `Self::Item` 将是实现中的类型别名。 fn f(&self) -> Self::Item; } struct S; impl T for S { type Item = i32; const C: i32 = 9; fn new() -> Self { // `Self` 是类型 `S`。 S } fn f(&self) -> Self::Item { // `Self::Item` 是类型 `i32`。 Self::C // `Self::C` 是常量值 `9`。 } } }
super
super
在路径中解析为父模块。只能在路径的前导段中使用,可在起始 self
段之后。
mod a { pub fn foo() {} } mod b { pub fn foo() { super::a::foo(); // call a's foo function } } fn main() {}
super
可以在第一个 super
或 self
之后重复使用,以引用祖先模块。
mod a { fn foo() {} mod b { mod c { fn foo() { super::super::foo(); // 调用 a's foo 函数 self::super::super::foo(); // 调用 a's 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; // None trait OtherTrait { // None fn g(&self); // None } impl OtherTrait for OtherStruct { fn g(&self) {} // None } impl OtherTrait for crate::a::Struct { fn g(&self) {} // None } impl crate::a::Trait for OtherStruct { fn f(&self) {} // None } } } fn main() {}
Name resolution
注意: 这是一个未来扩展的占位符。
可见性和私有性
语法
可见性 :
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)
除了 '公开' 和 '私有' 之外, Rust 还允许用户将一个条目声明为仅在给定范围内可见。 pub
限制的规则如下:
pub(in path)
使得一个条目在提供的path
中可见。path
必须是正在声明其可见性的条目的祖先模块。pub(crate)
使得一个条目在当前 crate 内可见。pub(super)
使得一个条目对其父模块可见。这等价于pub(in super)
。pub(self)
使得一个条目对当前模块可见。这等价于pub(in self)
或者不使用pub
。
版本差异: 从 2018 版开始,
pub(in path)
的路径必须以crate
、self
或super
开头。 2015 版也可以使用以::
开头的路径或来自 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() {} // 此函数在 `super` 中可见 pub(super) fn super_mod_visible_fn() { // 因为在同一 `mod` 中,所以此函数可见 inner_mod_visible_fn(); } // 此函数仅在 `inner_mod` 中可见, // 这等同于将其声明为 private。 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() }
注意: 这种语法只是增加了一个对条目可见性的限制,它并不保证该条目在指定范围内的所有部分都可见。 要访问一个条目,它的所有父级条目直到当前范围仍然必须可见。
重新导出和可见性
Rust 允许通过 pub use
公开且重新导出条目。因为这是一个使其公开的指令,从而允许通过上面的规则在当前模块中使用该条目。
这实际上允许了对重新导出的条目的公开访问。例如,这个程序是有效的:
pub use self::implementation::api; mod implementation { pub mod api { pub fn f() {} } } fn main() {}
这意味着,外部 crate 通过 implementation::api::f
引用将违反私有性,而允许以路径 api::f
引用。
当重新导出一个私有条目时,可以将其视为通过重新导出 "私有链条" 的快捷路径,而不是像通常一样通过命名空间层次结构传递。
内存模型
目前 Rust 还没有定义一个明确的内存模型。 正在研究各种提案,但目前来说,这是语言中一个未定义的领域。
内存分配及生命周期
程序中函数、模块和类型值等 条目 是静态的,在编译后计算确定,存储在已加载进程中时,将有确定的内存块占用。 条目不是动态分配,也不会被释放。
堆 是一个泛指的术语,用于描述堆箱。在堆中分配的内存的生命周期取决于指向它的堆箱值的生命周期。 由于堆箱值本身可能在帧之间传递,或者存储在堆中,因此堆分配可能会超出它们分配的所在帧的生命周期。 在堆中分配的内存保证在整个分配的生命周期中都驻留在堆的单个位置 - 它永远不会由于移动堆箱值而被移动。
译注: 这一语言特性不是 Rust 独有,编译性语言总是这样的,由源码编译形成确定的二进制文件,并且其二进制文件加载到内存中时结构也是确定的。运行时所需要的空间 '栈' 和 '堆' 根据需要向操作系统申请。那么值得深入思考的原则是 "凡是在编译时无法确定条目的大小和地址的语法都是错误!"
变量
变量 是栈帧的组成部分,包括函数的具名参数、匿名的 临时变量 或具名的局部变量。
局部变量 (或称为 栈局部变量 分配) 将直接持有一个值,该值在内存栈中分配。该值是栈帧的一部分。
除非另有声明,否则局部变量是不可变的。例如: 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: 使用可能未初始化 `uninit_after_if` } }
链接
注意: 这部分所描述的内容,主要是从编译器角度进行,而非语言。
编译器支持静态和动态地将不同的 crate 链接起来。 本节将探讨链接 crate 的各种方法,有关本地库的更多信息可以参阅《Rust 程序设计语言》中 FFI 章节 。
在一次编译会话中,编译器可以通过使用命令行标志或 crate_type
属性生成多个制品。
如果指定了一个或多个命令行标志,则忽略所有 crate_type
属性,仅构建由命令行所指定的制品。
-
--crate-type=bin
,#![crate_type = "bin"]
- 将生成可运行的可执行文件。 在 crate 中需要有一个main
函数,程序开始执行时,首先运行该函数。 并将链接所有的语言以及本地的依赖项,生成可分发的单个二进制文件。 这是 crate 的默认类型。 -
--crate-type=lib
,#![crate_type = "lib"]
- 将生成 "Rust 库" 。这是一个泛指的选项,因为库有多种形式。 其表示生成 "编译器推荐" 的库格式。目前生成的库,意味着对于 rustc 始终是可用的,但实际所生成库的类型可能会随时间而变化。 其余的输出类型都表示了单独的且不同的库类型,而lib
类型可以看作是其中一个的别名 (但实际的类型由编译器定义) 。 -
--crate-type=dylib
,#![crate_type = "dylib"]
- 将生成动态 Rust 库。这与lib
输出类型不同,此选项强制生成动态库。 所生成的动态库可以用作其他库和/或可执行文件的依赖项。 此输出类型将在 Linux 上创建*.so
文件,在 macOS 上创建*.dylib
文件,在 Windows 上创建*.dll
文件。 -
--crate-type=staticlib
,#![crate_type = "staticlib"]
- 将生成静态系统库。 这与其他库输出不同,因为 Rust 编译器决不会尝试链接staticlib
格式的库。 此输出类型的可以创建一个包含所有本地 crate 代码以及所有上游依赖项的静态库, 将在 Linux、macOS 和 Windows (MinGW) 上创建*.a
文件,在 Windows (MSVC) 上创建*.lib
文件。 此格式推荐在将 Rust 代码链接到现有的非 Rust 应用程序时使用,因为这一格式的库不会对其他 Rust 代码产生动态依赖。 -
--crate-type=cdylib
,#![crate_type = "cdylib"]
- 将生成一个动态系统库。 这用于编译可从另一种语言中加载的动态库。 此输出类型将在 Linux 上创建*.so
文件,在 macOS 上创建*.dylib
文件,在 Windows 上创建*.dll
文件。 -
--crate-type=rlib
,#![crate_type = "rlib"]
- 将生成 "Rust库" 文件。这用作中间制品,可以看作是 "静态Rust库" 。 这些rlib
文件不像staticlib
文件一样被编译器静态链接。 编译器在将来的链接中将解释这些rlib
文件,这基本上意味着rustc
将像在动态库中查找元数据一样查找rlib
文件中的元数据。 这种输出形式用于生成静态链接的可执行文件以及staticlib
输出。 -
--crate-type=proc-macro
,#![crate_type = "proc-macro"]
- 产生的输出未指定,但如果提供了-L
路径,则编译器将识别输出制品为宏,它可以加载到程序中。 使用此 crate 类型编译的 crate 必须仅导出 过程宏 。编译器将自动设置proc_macro
配置选项 。这些crate总是使用编译器自身构建的相同目标进行编译。 例如,如果您从 Linux 执行编译器,并且使用x86_64
CPU,则目标将是x86_64-unknown-linux-gnu
,即使 crate 是正在为不同目标构建的另一个 crate 的依赖项。
请注意,这些输出是可叠加的,这意味着如果指定多个,则编译器将生成每种形式的输出而无需重新编译。
但是,这仅适用于由同一方法指定的输出。如果仅指定了 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-musleabi
arm-unknown-linux-musleabihf
armv7-unknown-linux-musleabihf
i686-unknown-linux-musl
x86_64-unknown-linux-musl
C 运行时的链接是配置为遵守 crt-static
目标特性的。这些目标特性通常是通过编译器本身的标志从命令行配置的。例如,要启用静态运行时,你将执行:
rustc -C target-feature=+crt-static foo.rs
而要动态链接到 C 运行时,则会执行:
rustc -C target-feature=-crt-static foo.rs
不支持在 C 运行时链接方式之间切换的目标将忽略此标志。建议在编译器成功后检查生成的二进制文件,确保它被链接为你所期望的方式。
对于 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
内联汇编
通过 asm!
和 global_asm!
宏提供对内联汇编的支持。可以使用它来嵌入手写汇编到编译器生成的汇编输出中。
以下体系结构上的内联汇编支持已经稳定:
- x86 和 x86-64
- ARM
- AArch64
- RISC-V
如果在不支持的目标上使用 asm!
,编译器会发出错误。
示例
#![allow(unused)] fn main() { 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); }
语法
以下是常规语法的 ABNF 规范:
format_string := STRING_LITERAL / RAW_STRING_LITERAL
dir_spec := "in" / "out" / "lateout" / "inout" / "inlateout"
reg_spec := <register class> / "\"" <explicit register> "\""
operand_expr := expr / "_" / expr "=>" expr / expr "=>" "_"
reg_operand := [ident "="] dir_spec "(" reg_spec ")" operand_expr
clobber_abi := "clobber_abi(" <abi> *("," <abi>) [","] ")"
option := "pure" / "nomem" / "readonly" / "preserves_flags" / "noreturn" / "nostack" / "att_syntax" / "raw"
options := "options(" option *("," option) [","] ")"
operand := reg_operand / clobber_abi / options
asm := "asm!(" format_string *("," format_string) *("," operand) [","] ")"
global_asm := "global_asm!(" format_string *("," format_string) *("," operand) [","] ")"
作用域
内联汇编有两种使用方式。
使用 asm!
宏时,汇编代码在函数作用域中生成,并集成到函数的编译器生成的汇编代码中。这些汇编代码必须遵守 严格规则 以避免未定义行为。注意,在某些情况下,编译器可能会选择将汇编代码生成为一个单独的函数并生成对它的调用。
使用 global_asm!
宏时,汇编代码在全局作用域中生成,不在函数内。这可以用于使用汇编代码手写整个函数,并且通常提供更多使用任意寄存器和汇编指令的自由。
模板字符串参数
汇编模板使用与 格式化字符串 相同的语法 (即由大括号指定占位符) 。 相应的参数按顺序、索引或名称访问。但是,不支持隐式命名参数 (由 RFC #2795 引入) 。
asm!
调用可能有一个或多个模板字符串参数;具有多个模板字符串参数的 asm!
被视为所有字符串在它们之间用 \n
连接。
预期的用法是,每个模板字符串参数对应于一个汇编代码行。所有模板字符串参数必须出现在任何其他参数之前。
与格式化字符串一样,位置参数必须出现在命名参数和显式 寄存器操作数 之前。
模板字符串中的占位符不能使用显式寄存器操作数。所有其他命名和位置操作数必须至少在模板字符串中出现一次,否则会生成编译器错误。
除了用于将操作数替换为传递给汇编器的代码的方式之外,确切的汇编代码语法是特定于目标的,并且对编译器不透明。
目前,所有受支持的目标都遵循LLVM内部汇编器使用的汇编代码语法,这通常对应于GNU汇编器 (GAS)的语法。
在x86上,默认情况下使用 GAS 的 .intel_syntax noprefix
模式。
在ARM上,使用 .syntax unified
模式。
这些目标对汇编代码施加了一个额外的限制: 必须在 asm 字符串的末尾将任何汇编器状态 (例如可以使用 .section
更改的当前部分) 恢复为其原始值。
不符合 GAS 语法的汇编代码将导致汇编器特定的行为。关于内联汇编使用的指令的进一步约束在 指令支持 中指示。
操作数类型
支持几种操作数类型:
in(<reg>) <expr>
<reg>
可以是寄存器类或显式寄存器。 分配的寄存器名称将替换为 asm 模板字符串中的名称。- 分配的寄存器将在 asm 代码开始时包含
<expr>
的值。 - 在 asm 代码结束时,分配的寄存器必须包含相同的值 (除非另一个
lateout
被分配到相同的寄存器) 。
out(<reg>) <expr>
<reg>
可以是寄存器类或显式寄存器。 分配的寄存器名称将替换为 asm 模板字符串中的名称。- 分配的寄存器在 asm 代码开始时包含未定义的值。
<expr>
必须是一个 (可能未初始化的) place 表达式,分配的寄存器的内容在 asm 代码结束时写入该表达式。- 可以使用下划线 (
_
) 代替表达式,在 asm 代码结束时会丢弃寄存器的内容 (有效地作为 clobber) 。
lateout(<reg>) <expr>
- 与
out
相同,但是寄存器分配器可以重新使用分配给in
的寄存器。 - 应在读取所有输入之后再写入寄存器,否则可能破坏输入。
- 与
inout(<reg>) <expr>
<reg>
可以引用寄存器类或显式寄存器。 分配的寄存器名称将替换为汇编模板字符串中。- 分配的寄存器将在汇编代码开始时包含
<expr>
的值。 <expr>
必须是一个可变的初始化地点表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
inout(<reg>) <in expr> => <out expr>
- 与
inout
相同,但是寄存器的初始值取自<in expr>
的值。 <out expr>
必须是一个 (可能未初始化的) 地点表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。- 可以为
<out expr>
指定下划线 (_
) ,这将导致在汇编代码结束时丢弃寄存器的内容 (实际上起到破坏作用) 。 <in expr>
和<out expr>
可能具有不同的类型。
- 与
inlateout(<reg>) <expr>
/inlateout(<reg>) <in expr> => <out expr>
- 与
inout
相同,但是寄存器分配器可以重用分配给in
的寄存器 (如果编译器知道in
具有与inlateout
相同的初始值,则可能会发生这种情况) 。 - 只有在读取所有输入后才应写入寄存器,否则可能会破坏输入。
- 与
sym <path>
<path>
必须引用fn
或static
。- 指向该条目的重命名符号名称将替换为汇编模板字符串中。
- 替换的字符串不包括任何修改器 (例如 GOT 、 PLT 、重定位等) 。
<path>
允许指向#[thread_local]
静态变量,在这种情况下,汇编代码可以将符号与重定位 (例如@plt
、@TPOFF
) 组合以从线程本地数据读取。
操作数表达式从左到右依次评估,就像函数调用参数一样。
在 asm!
执行完成后,输出按照从左到右的顺序进行写入。
如果两个输出指向同一个位置,则该位置将包含最右边输出的值。
由于 global_asm!
存在于函数外部,因此它只能使用 sym
操作数。
寄存器操作数
输入和输出操作数可以被指定为显式寄存器或者是寄存器类,从中寄存器分配器可以选择一个寄存器。
显式寄存器以字符串字面量表示 (例如 "eax"
) ,而寄存器类则以标识符表示 (例如 reg
) 。
需要注意的是,显式寄存器将寄存器别名 (例如 ARM 上的 r14
vs lr
) 和寄存器的较小视图 (例如eax
vs rax
) 视为等效于基础寄存器。
在两个输入操作数或两个输出操作数中使用相同的显式寄存器是编译时错误。
此外,在输入操作数或输出操作数中使用重叠的寄存器 (例如 ARM VFP) 也是编译时错误。
只有以下类型的操作数被允许用于内联汇编:
- 整数 (有符号和无符号)
- 浮点数
- 指针 (仅限thin)
- 函数指针
- SIMD 向量 (用
#[repr(simd)]
定义的结构体,并且实现了Copy
) 。其中包括在std::arch
中定义的架构特定的向量类型,例如__m128
(x86) 或int8x16_t
(ARM) 。
以下是当前支持的寄存器类的列表:
Architecture | Register class | Registers | LLVM constraint code |
---|---|---|---|
x86 | reg | ax , bx , cx , dx , si , di , bp , r[8-15] (x86-64 only) | 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 | Only clobbers |
x86 | x87_reg | st([0-7]) | Only clobbers |
x86 | mmx_reg | mm[0-7] | Only clobbers |
x86-64 | tmm_reg | tmm[0-7] | Only clobbers |
AArch64 | reg | x[0-30] | r |
AArch64 | vreg | v[0-31] | w |
AArch64 | vreg_low16 | v[0-15] | x |
AArch64 | preg | p[0-15] , ffr | Only clobbers |
ARM (ARM/Thumb2) | reg | r[0-12] , r14 | r |
ARM (Thumb1) | reg | r[0-7] | r |
ARM | sreg | s[0-31] | t |
ARM | sreg_low16 | s[0-15] | x |
ARM | dreg | d[0-31] | w |
ARM | dreg_low16 | d[0-15] | t |
ARM | dreg_low8 | d[0-8] | x |
ARM | qreg | q[0-15] | w |
ARM | qreg_low8 | q[0-7] | t |
ARM | qreg_low4 | q[0-3] | x |
RISC-V | reg | x1 , x[5-7] , x[9-15] , x[16-31] (non-RV32E) | r |
RISC-V | freg | f[0-31] | f |
RISC-V | vreg | v[0-31] | Only clobbers |
注:
在 x86 上,我们将
reg_byte
与reg
区别对待,因为编译器可以分别分配al
和ah
,而reg
保留整个寄存器。在 x86-64 上,高字节寄存器 (例如
ah
) 不可用于reg_byte
寄存器类。一些寄存器类被标记为 "仅占用" ,这意味着这些类中的寄存器不能用作输入或输出,只能用作类似
out(<explicit register>) _
或lateout(<explicit register>) _
的占用。
每个寄存器类对可以与其一起使用的值类型有约束。
这是必要的,因为将值加载到寄存器中的方式取决于其类型。
例如,在大端系统上,将 i32x4
和 i8x16
加载到 SIMD 寄存器中可能会导致不同的寄存器内容,即使这两个值的按字节的内存表示相同。
支持特定寄存器类的类型的可用性可能取决于当前启用的目标特性。
Architecture | Register class | Target feature | Allowed types |
---|---|---|---|
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 | Only clobbers |
x86 | x87_reg | N/A | Only clobbers |
x86 | tmm_reg | N/A | Only clobbers |
AArch64 | reg | None | i8 , i16 , i32 , f32 , i64 , f64 |
AArch64 | vreg | neon | i8 , i16 , i32 , f32 , i64 , f64 , i8x8 , i16x4 , i32x2 , i64x1 , f32x2 , f64x1 , i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
AArch64 | preg | N/A | Only clobbers |
ARM | reg | None | i8 , i16 , i32 , f32 |
ARM | sreg | vfp2 | i32 , f32 |
ARM | dreg | vfp2 | i64 , f64 , i8x8 , i16x4 , i32x2 , i64x1 , f32x2 |
ARM | qreg | neon | i8x16 , i16x8 , i32x4 , i64x2 , f32x4 |
RISC-V32 | reg | None | i8 , i16 , i32 , f32 |
RISC-V64 | reg | None | i8 , i16 , i32 , f32 , i64 , f64 |
RISC-V | freg | f | f32 |
RISC-V | freg | d | f64 |
RISC-V | vreg | N/A | Only clobbers |
注意:为了上表的目的,指针、函数指针和
isize
/usize
被视为相应整数类型的等效类型 (取决于目标的i16
/i32
/i64
) 。
如果一个值的大小比分配给它的寄存器小,那么对于输入,该寄存器的高位将具有未定义的值,对于输出,将被忽略。
唯一的例外是 RISC-V 上的 freg
寄存器类,其中 f32
值作为 RISC-V 架构所需的 NaN 被封装在 f64
中。
当为 inout
操作数指定分开的输入和输出表达式时,两个表达式必须具有相同的类型。
唯一的例外是如果两个操作数都是指针或整数,则它们只需要具有相同的大小。
这个限制存在是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的绑定操作数。
寄存器名称
一些寄存器有多个名称,编译器会将这些名称都视为与基础寄存器名称相同。以下是所有支持的寄存器别名列表:
Architecture | Base register | Aliases |
---|---|---|
x86 | ax | eax , rax |
x86 | bx | ebx , rbx |
x86 | cx | ecx , rcx |
x86 | dx | edx , rdx |
x86 | si | esi , rsi |
x86 | di | edi , rdi |
x86 | bp | bpl , ebp , rbp |
x86 | sp | spl , esp , rsp |
x86 | ip | eip , rip |
x86 | st(0) | st |
x86 | r[8-15] | r[8-15]b , r[8-15]w , r[8-15]d |
x86 | xmm[0-31] | ymm[0-31] , zmm[0-31] |
AArch64 | x[0-30] | w[0-30] |
AArch64 | x29 | fp |
AArch64 | x30 | lr |
AArch64 | sp | wsp |
AArch64 | xzr | wzr |
AArch64 | v[0-31] | b[0-31] , h[0-31] , s[0-31] , d[0-31] , q[0-31] |
ARM | r[0-3] | a[1-4] |
ARM | r[4-9] | v[1-6] |
ARM | r9 | rfp |
ARM | r10 | sl |
ARM | r11 | fp |
ARM | r12 | ip |
ARM | r13 | sp |
ARM | r14 | lr |
ARM | r15 | pc |
RISC-V | x0 | zero |
RISC-V | x1 | ra |
RISC-V | x2 | sp |
RISC-V | x3 | gp |
RISC-V | x4 | tp |
RISC-V | x[5-7] | t[0-2] |
RISC-V | x8 | fp , s0 |
RISC-V | x9 | s1 |
RISC-V | x[10-17] | a[0-7] |
RISC-V | x[18-27] | s[2-11] |
RISC-V | x[28-31] | t[3-6] |
RISC-V | f[0-7] | ft[0-7] |
RISC-V | f[8-9] | fs[0-1] |
RISC-V | f[10-17] | fa[0-7] |
RISC-V | f[18-27] | fs[2-11] |
RISC-V | f[28-31] | ft[8-11] |
有些寄存器不能用于输入或输出操作数:
架构 | 不支持的寄存器 | 原因 |
---|---|---|
All | sp | 栈指针必须在 asm 代码块结束时恢复为其原始值。 |
All | bp (x86), x29 (AArch64), x8 (RISC-V) | 帧指针不能用作输入或输出。 |
ARM | r7 或 r11 | 在 ARM 上,帧指针可以是 r7 或 r11 ,具体取决于目标。帧指针不能用作输入或输出。 |
All | si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64), x9 (RISC-V) | LLVM 在函数具有复杂堆栈帧的情况下将其用作“基指针”。 |
x86 | ip | 这是程序计数器,不是实际的寄存器。 |
AArch64 | xzr | 这是一个无法修改的常量零寄存器。 |
AArch64 | x18 | 这是某些 AArch64 目标上保留的操作系统寄存器。 |
ARM | pc | 这是程序计数器,不是实际的寄存器。 |
ARM | r9 | 这是某些 ARM 目标上保留的操作系统寄存器。 |
RISC-V | x0 | 这是一个无法修改的常量零寄存器。 |
RISC-V | gp ,tp | 这些寄存器是保留的,不能用作输入或输出。 |
帧指针和基指针寄存器被 LLVM 保留作为内部使用。虽然 asm!
语句不能显式地指定保留寄存器的使用,但在某些情况下,LLVM 将为 reg
操作数分配其中一个保留寄存器。
使用保留寄存器的汇编代码应该小心,因为 reg
操作数可能会使用相同的寄存器。
模板修改器
在花括号中的 :
后可以加上修改器,来改变插入模板字符串时操作数的格式,这些修改器不影响寄存器分配。每个模板占位符只能有一个修改器。
这些修改器是 LLVM 和 GCC 的汇编模板参数修改器的子集,但使用的字母代码不同。
Architecture | Register class | Modifier | Example output | LLVM modifier |
---|---|---|---|---|
x86-32 | reg | None | eax | k |
x86-64 | reg | None | rax | q |
x86-32 | reg_abcd | l | al | b |
x86-64 | reg | l | al | b |
x86 | reg_abcd | h | ah | h |
x86 | reg | x | ax | w |
x86 | reg | e | eax | k |
x86-64 | reg | r | rax | q |
x86 | reg_byte | None | al / ah | None |
x86 | xmm_reg | None | xmm0 | x |
x86 | ymm_reg | None | ymm0 | t |
x86 | zmm_reg | None | zmm0 | g |
x86 | *mm_reg | x | xmm0 | x |
x86 | *mm_reg | y | ymm0 | t |
x86 | *mm_reg | z | zmm0 | g |
x86 | kreg | None | k1 | None |
AArch64 | reg | None | x0 | x |
AArch64 | reg | w | w0 | w |
AArch64 | reg | x | x0 | x |
AArch64 | vreg | None | v0 | None |
AArch64 | vreg | v | v0 | None |
AArch64 | vreg | b | b0 | b |
AArch64 | vreg | h | h0 | h |
AArch64 | vreg | s | s0 | s |
AArch64 | vreg | d | d0 | d |
AArch64 | vreg | q | q0 | q |
ARM | reg | None | r0 | None |
ARM | sreg | None | s0 | None |
ARM | dreg | None | d0 | P |
ARM | qreg | None | q0 | q |
ARM | qreg | e / f | d0 / d1 | e / f |
RISC-V | reg | None | x1 | None |
RISC-V | freg | None | f0 | None |
注:
- 在 ARM 上,
e
/f
:打印 NEON 四重 (128 位) 寄存器的低位或高位双字寄存器名称。- 在 x86 上:我们对于没有修饰符的
reg
的行为与 GCC 不同。 GCC 将根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。- 在 x86 上,
xmm_reg
:LLVM 修饰符x
、t
和g
尚未在 LLVM 中实现 (仅由 GCC 支持) ,但这应该是一个简单的更改。
如前一节所述,如果内联汇编传递的输入值比寄存器宽度小,则寄存器的上位比特将包含未定义的值。
如果内联汇编只访问寄存器的低位,这不是一个问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称 (例如,使用 ax
代替 rax
) 来实现。
由于这是一个简单的错误,编译器将根据输入类型在适当的地方建议使用模板修饰符。
如果对一个操作数的所有引用已经有修饰符,则该警告将对该操作数进行抑制。
ABI破坏标记
clobber_abi
关键字可用于将默认一组破坏标记应用于 asm!
块。这将根据特定调用约定自动插入必要的破坏标记:如果调用约定在函数调用时不能完全保留寄存器的值,则会在操作数列表中隐式添加 lateout("...") _
(其中 ...
替换为寄存器名称) 。
clobber_abi
可以指定任意次数。它将为所有指定的调用约定的并集中的所有唯一寄存器插入一个破坏标记。
当使用 clobber_abi
时,编译器禁止使用通用寄存器类输出:所有输出都必须指定显式寄存器。显式寄存器输出优先于由 clobber_abi
插入的隐式破坏标记:仅当该寄存器未用作输出时,才会为该寄存器插入破坏标记。
以下 ABIs 可与 clobber_abi
一起使用:
Architecture | ABI name | Clobbered registers |
---|---|---|
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" (on 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" (on non-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 |
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] |
注意:
- 在 AArch64 上,仅当
x18
在目标上不被视为保留寄存器时,才将其包括在破坏列表中。
每个 ABI 的受破坏寄存器列表会随着架构获取新寄存器而在 rustc 中更新:这确保了当 LLVM 在其生成的代码中开始使用这些新寄存器时, asm!
破坏项将继续正确。
选项
标志用于进一步影响内联汇编块的行为。目前定义了以下选项:
pure
:asm!
块没有副作用,其输出仅取决于其直接输入 (即值本身,而不是它们所指向的内容) 或从内存读取的值 (除非也设置了nomem
选项) 。这使得编译器可以执行比程序中指定的更少的asm!
块次数 (例如将其提升出循环) 或甚至完全消除它 (如果输出没有使用) 。nomem
:asm!
块不读取或写入任何内存。这使得编译器可以在asm!
块之间缓存修改后的全局变量的值,因为它知道它们不会被asm!
读取或写入。readonly
:asm!
块不写入任何内存。这使得编译器可以在asm!
块之间缓存未修改的全局变量的值,因为它知道它们不会被asm!
写入。preserves_flags
:asm!
块不修改标志寄存器 (在下面的规则中定义) 。这使得编译器可以在asm!
块之后避免重新计算条件标志。noreturn
:asm!
块永远不会返回,其返回类型定义为!
(永远不会) 。如果执行超过 asm 代码的结尾,行为是未定义的。noreturn
asm 块的行为就像不返回的函数一样;特别是,作用域中的局部变量在调用之前不会被丢弃。nostack
:asm!
块不将数据推入栈,也不写入堆栈保护区域 (如果目标支持) 。如果未使用此选项,则栈指针保证根据目标 ABI 适当对齐,以用于函数调用。att_syntax
: 此选项仅在 x86 上有效,导致汇编程序使用 GNU 汇编程序的.att_syntax
前缀模式。寄存器操作数将替换为带有前导%
的操作数。raw
: 这将导致模板字符串被解析为原始汇编字符串,没有对{
和}
进行特殊处理。这在使用include_str!
包含来自外部文件的原始汇编代码时非常有用。
编译器对选项进行了一些额外的检查:
nomem
和readonly
选项是互斥的:指定两者都会导致编译时错误。pure
选项必须与nomem
或readonly
选项结合使用,否则会发出编译时错误。- 在没有输出或只有被丢弃的输出 (
_
) 的asm!
块上指定pure
是编译时错误。 - 在带有输出的
asm!
块上指定noreturn
是编译时错误。
global_asm!
只支持 att_syntax
和 raw
选项。
其他选项在全局作用域内的内联汇编中没有意义。
函数内联汇编规则
为了避免未定义的行为,在使用函数内联汇编 (asm!
) 时必须遵守以下规则:
- 任何未在输入中指定的寄存器,在进入汇编块时将包含未定义的值。
- 在内联汇编的上下文中, "未定义的值" 意味着该寄存器可以 (非确定性地) 具有体系结构允许的任何可能值。
需要注意的是,它与 LLVM 中的
undef
不同,后者每次读取时都可以具有不同的值 (因为在汇编代码中不存在这样的概念) 。
- 在内联汇编的上下文中, "未定义的值" 意味着该寄存器可以 (非确定性地) 具有体系结构允许的任何可能值。
需要注意的是,它与 LLVM 中的
- 任何未指定为输出的寄存器,在离开汇编块时必须具有与进入时相同的值,否则行为是未定义的。
- 这仅适用于可以指定为输入或输出的寄存器。 其他寄存器遵循特定于目标的规则。
- 需要注意的是,
lateout
可能会分配到与in
相同的寄存器中,此时此规则不适用。 不过代码不应该依赖此规则,因为它取决于寄存器分配的结果。
- 如果执行从汇编块中退出,则行为是未定义的。
- 如果汇编代码调用一个函数,然后函数抛出异常也同样适用。
- 汇编代码被允许读取和写入的内存位置集合与 FFI 函数允许的位置集合相同。
- 参考非安全代码指南了解确切的规则。
- 如果设置了
readonly
选项,则只允许内存读取。 - 如果设置了
nomem
选项,则不允许读取或写入内存。 - 这些规则不适用于汇编代码私有的内存,例如在汇编块中分配的栈空间。
- 编译器不能假设汇编中的指令实际上就是将要执行的指令。
- 这实际上意味着编译器必须将
asm!
视为黑匣子,并仅考虑接口规范,而不考虑指令本身。 - 可以通过特定于目标的机制进行运行时代码修补。
- 这实际上意味着编译器必须将
- 除非设置了
nostack
选项,汇编代码可以使用堆栈指针下面的堆栈空间。- 进入汇编块时,堆栈指针保证对于函数调用具有适当的对齐方式 (根据目标 ABI) 。
- 你负责确保不会溢出堆栈 (例如,使用堆栈探测以确保触发警戒页) 。
- 分配堆栈内存时,应根据目标 ABI 调整堆栈指针。
- 在离开 asm 块之前,堆栈指针必须恢复到其原始值。
- 如果设置了
noreturn
选项,则执行控制流落到asm
块末尾时的行为未定义。 - 如果设置了
pure
选项,则执行除直接输出之外的副作用的asm!
的行为是未定义的。 如果相同输入的两次asm!
执行产生不同的输出,则行为也是未定义的。- 在与
nomem
选项一起使用时,"输入" 只是asm!
的直接输入。 - 在与
readonly
选项一起使用时,"输入" 包括asm!
的直接输入以及asm!
块允许读取的任何内存。
- 在与
- 如果设置了
preserves_flags
选项,则必须在退出asm
块时还原这些标志寄存器:- x86
EFLAGS
中的状态标志位 (CF、PF、AF、ZF、SF、OF) 。- 浮点状态字 (全部) 。
MXCSR
中的浮点异常标志 (PE、UE、OE、ZE、DE、IE) 。
- ARM
CPSR
中的条件标志 (N、Z、C、V) 。CPSR
中的饱和标志 (Q) 。CPSR
中的大于或等于标志 (GE) 。FPSCR
中的条件标志 (N、Z、C、V) 。FPSCR
中的饱和标志 (QC) 。FPSCR
中的浮点异常标志 (IDC、IXC、UFC、OFC、DZC、IOC) 。
- AArch64
- 条件标志 (
NZCV
寄存器) 。 - 浮点状态 (
FPSR
寄存器) 。
- 条件标志 (
- RISC-V
fcsr
中的浮点异常标志 (fflags
) 。- 向量扩展状态 (
vtype
、vl
、vcsr
) 。
- x86
- 在 x86 上,进入 asm 块时方向标志 (
EFLAGS
中的 DF) 被清除,并且在退出时必须保持清除状态。- 如果在退出 asm 块时设置了方向标志,则行为未定义。
- 在 x86 上,除非所有的
st([0-7])
寄存器都被标记为被污染的,否则 x87 浮点寄存器堆栈必须保持不变,方法是使用out("st(0)") _,out("st(1)") _,...
。- 如果所有 x87 寄存器都被污染,则在进入
asm
块时保证 x87 寄存器堆栈为空。汇编代码必须确保在退出 asm 块时 x87 寄存器堆栈也为空。
- 如果所有 x87 寄存器都被污染,则在进入
- 将栈指针和非输出寄存器恢复到其原始值的要求仅适用于退出
asm!
块时。- 这意味着从不返回的
asm!
块 (即使没有标记为noreturn
) 不需要保留这些寄存器。 - 当返回到与你进入的
asm!
块不同的asm!
块时 (例如用于上下文切换) ,这些寄存器必须包含你正在 退出 的asm!
块进入时的值。- 你不能退出未进入的
asm!
块。也不能退出已经退出的asm!
块 (除非首先再次进入它) 。 - 你负责切换任何特定于目标的状态 (例如线程本地存储,堆栈边界) 。
- 你不能从一个
asm!
块中的地址跳转到另一个asm!
块中的地址,即使在同一个函数或块中,也必须将它们的上下文视为可能不同并要求进行上下文切换。你不能假设这些上下文中的任何特定值 (例如当前堆栈指针或堆栈指针下方的临时值) 在两个asm!
块之间保持不变。 - 你可以访问的内存位置集是你输入和退出的
asm!
块所允许的位置集的交集。
- 你不能退出未进入的
- 这意味着从不返回的
- 你不能假设两个在源代码中相邻的
asm!
块,即使它们之间没有其他代码,也会在二进制代码中以连续的地址出现,而没有其他指令在它们之间。 - 你不能假设
asm!
块将在输出二进制代码中出现一次。 编译器可以实例化多个asm!
块,例如当包含它的函数在多个位置内联时。 - 在 x86 平台上,内联汇编不得以指令前缀 (如
LOCK
) 作为结尾,因为这将应用于编译器生成的指令。- 目前,编译器无法检测到这种情况,但将来可能会检测并拒绝此类代码。
注意: 通常情况下,
preserves_flags
覆盖的标志位是在函数调用时 不会 保留的标志位。
正确性和有效性
除了之前的所有规则之外, asm!
的字符串参数在所有其他参数求值、格式化和操作数翻译后,必须最终成为对于目标架构既具有语法正确性又具有语义有效性的汇编代码。
格式化规则允许编译器生成正确语法的汇编代码。有关操作数的规则允许 Rust 操作数与 asm!
中的操作数之间有效的转换。
遵循这些规则是必要的,但不足以保证最终扩展的汇编代码是正确且有效的。例如:
- 参数可能被放置在格式化后语法上不正确的位置。
- 一个指令可能被正确书写,但给定不符合架构的操作数。
- 一个架构未指定的指令可能会被汇编成未指定的代码。
- 一组指令,每个指令都正确且有效,如果放置在紧接着的位置,可能会导致未定义行为。
因此,这些规则是 "非穷尽" 的。编译器不需要检查最终汇编代码的正确性和有效性。汇编器可以检查正确性和有效性,但并不是必需的。
当使用 asm!
时,一个排版错误就足以使程序非安全,并且汇编规则可能包括数千页的架构参考手册。
程序员应该谨慎使用,因为使用这种 "非安全" 功能意味着承担不违反编译器或架构规则的责任。
指令支持
内联汇编支持一些 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
.lcomm
.long
.octa
.option
.p2align
.popsection
.private_extern
.pushsection
.quad
.scl
.section
.set
.short
.size
.skip
.sleb128
.space
.string
.text
.type
.uleb128
.word
目标特定指令支持
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 的安全子集中使用:
unsafe
关键字
unsafe
关键字可以出现在几种不同的上下文: 非安全函数 (unsafe fn
) ,非安全块 (unsafe {}
) ,非安全 trait (unsafe trait
) 和非安全 trait 实现 (unsafe impl
) 中。
该关键字在不同的使用语境表达了不同的含义,以及是否启用 unsafe_op_in_unsafe_fn
代码分析:
- 用于标记 定义 附加安全条件的代码 (
unsafe fn
,unsafe trait
) 。 - 用于标记需要 满足 附加安全条件的代码 (
unsafe {}
,unsafe impl
,没有unsafe_op_in_unsafe_fn
的unsafe fn
) 。
以下讨论这些情况的具体案例。 参见 关键字文档 以获取一些相关信息。
非安全函数 (unsafe fn
)
非安全函数是在所有上下文和/或所有可能的输入上都非安全的函数。所表述的它们具有 附加安全条件 ,是指编译器不会检查所有调用者必须满足的要求。
例如, get_unchecked
具有附加的安全条件,即索引必须在边界内。非安全函数应该配有说明文档,以进行解释说明。
这样的函数必须以关键字 unsafe
为前缀,并且只能从 unsafe
块内部调用,或者在没有 unsafe_op_in_unsafe_fn
代码分析的情况下在 unsafe fn
中调用。
非安全块 (unsafe {}
)
代码块可以加上 unsafe
关键字,以允许调用 unsafe
函数或对原始指针进行引用。
默认情况下,非安全函数的函数体也被视为非安全块;这可以通过启用 unsafe_op_in_unsafe_fn
限制来改变。
通过将语句放入非安全块中,程序员声明他们已经满足了该块内所有语句的附加安全条件。
非安全块是非安全函数的逻辑对偶:非安全函数定义调用者必须满足的证明义务,而非安全块则声明所有相关证明义务已得到满足。
有许多方法可以满足证明义务;例如,可能存在运行时检查或数据结构不变,保证某些属性肯定为真,或者非安全块可以位于 unsafe fn
中,并使用其自己的证明义务来满足其调用方的证明义务。
非安全块用于包装外部库、直接使用硬件或实现语言中未直接出现的特性。 例如,Rust 提供了实现内存安全并发所需的语言特性,但标准库中的线程和消息传递的实现使用了非安全块。
Rust 的类型系统的动态安全要求有些保守,因此在某些情况下,使用安全代码会带来性能成本。
例如,双向链表不是树结构,只能在安全代码中使用引用计数指针来表示。
通过使用 unsafe
块来将反向链接表示为原始指针,可以在不使用引用计数的情况下实现。
(有关此特定示例的更深入探索,请参见 "使用多链表学习 Rust" )
非安全 trait (unsafe trait
)
非安全 trait 是一种需要实现附加的安全条件的 trait,实现该 trait 必须遵守这些附加的安全条件。该非安全 trait 应该附带有说明这些附加安全条件的文档。
这样的 trait 必须以关键字 unsafe
为前缀,并且只能由 unsafe impl
块实现。
非安全 trait 实现 (unsafe impl
)
在实现非安全 trait 时,实现必须以 unsafe
关键字为前缀。通过编写 unsafe impl
,程序员表明他们已经注意到了 trait 所需的附加安全条件。
非安全 trait 实现是非安全 trait 的逻辑对偶:非安全 trait 定义了实现必须遵守的证明义务,而非安全实现则声明已经履行了所有相关的证明义务。
未定义的行为
如果 Rust 代码表现出以下列表中的行为,则代码不正确。包括 unsafe
块和 unsafe
函数中的代码。
unsafe
仅表示避免未定义行为产生是由程序员负责,但无法保证不会导致未定义行为。
在编写 unsafe
代码时,程序员的责任是确保与 unsafe
代码交互的安全代码不会触发这些行为。
如果安全代码始终不会触发 unsafe
代码的未定义行为则称为 健壮的 ,
反之,如果安全代码可以触发 unsafe
代码的未定义行为,就是不健壮的。
警告: 以下是一个非穷尽的列表。 Rust 中不允许哪些操作和不允许的行为没有正式的模型,因此可能还有更多被视为非安全的行为。 以下列表仅列出了已知的确定的未定义行为。请在编写非安全代码之前阅读 Rustonomicon。
-
数据竞争。
-
在使用 解引用表达式 (
*expr
) 对一个 悬垂指针 或非对齐指针进行求值时,甚至在 位置表达式上下文 中 (例如addr_of!(*expr)
) 。 -
违反 指针别名规则 。
Box<T>
,&mut T
和&T
遵循 LLVM 的有界 无别名 模型,除非&T
包含UnsafeCell<U>
。 引用和 box 在其存在期间不能是 悬垂 的。确切的存活期未被指定,但存在某些限制:- 对于引用,存活期上限由借用检查器分配的语法生命周期确定;它的生存期不能比那个生命周期更长。
- 每次引用或 box 传递给或从函数返回时,都被认为是活动的。
- 当引用 (但不是
Box
! ) 传递给函数时,它的生存期至少与该函数调用相同,除非&T
包含一个UnsafeCell<U>
。
以下内容也适用于这些类型的值作为复合类型的 (嵌套) 字段传递,但不适用于指针间接引用的情况。
-
修改不可变数据。
const
中的所有数据都是不可变的。此外,通过共享引用访问的数据或由不可变绑定拥有的数据也是不可变的,除非该数据包含在UnsafeCell<U>
中。 -
通过编译器内部函数调用产生未定义行为。
-
执行使用当前平台不支持的平台特性编译的代码 (请参阅
target_feature
) , 除非 该平台明确记录此操作是安全的。 -
使用错误的调用 ABI 调用函数或从错误的取消 ABI 函数中回溯。
-
在私有字段和局部变量中生成无效值。 "生成" 值是指将值分配给或从地址读取值、将值传递给函数/原始操作或从函数/原始操作返回值。 以下值是无效的 (在其各自的类型中) :
-
在
bool
中除false
(0
) 或true
(1
) 之外的值。 -
枚举中的判别值不在类型定义中。
-
空的
fn
指针。 -
char
中的值是代理或大于char::MAX
。 -
!
(所有值对于此类型都是无效的) 。 -
从 未初始化的内存 中获取的整数 (
i*
/u*
) 、浮点数 (f*
) 或裸指针,或在str
中使用未初始化的内存。 -
悬垂引用或
Box<T>
,非对齐或指向无效值。 -
宽指针、
Box<T>
或裸指针中的无效元数据:- 如果
dyn Trait
的元数据不是指向与指针或引用指向的实际动态特性相匹配的Trait
的虚表的指针,则该元数据是无效的。 - 如果长度不是有效的
usize
(即不能从未初始化的内存中读取) ,则切片元数据无效。
- 如果
-
自定义无效值类型的无效值。在标准库中,这会影响
NonNull<T>
和NonZero*
。注意:
rustc
使用未稳定的rustc_layout_scalar_valid_range_*
属性来实现此功能。
-
-
不正确使用内联汇编。有关更多详细信息,请参阅使用内联汇编编写代码时要遵循的 规则 。
注意: 对于任何具有受限制的有效值集的类型,未初始化的内存也是隐式无效的。换句话说,读取未初始化的内存仅在 union
内部和在类型的字段/元素之间 "填充" (间隙)中是允许的。
注意: 未定义行为影响整个程序。例如,在 C 中调用表现出未定义行为的函数意味着你的整个程序包含未定义行为,这也可能影响 Rust 代码。反之,在 Rust 中发生未定义行为也会对调用其他语言的 FFI 调用执行的代码产生不利影响。
悬垂指针
如果引用/指针为 null ,或者它所指向的字节不全属于同一个处于活动状态的分配 (因此特别注意它们必须全部属于 某个 分配) ,则该引用/指针是 "悬垂" 的。
它所指向的字节跨度由指针值和指向类型的大小确定 (使用 size_of_val
) 。
如果大小为 0 ,则指针必须指向处于活动状态分配的内部 (包括指向分配的最后一个字节的后面) ,或者直接从非零整数字面值构建而来。
请注意,动态大小的类型 (例如切片和字符串) 指向其整个范围,因而表示的长度元数据不要太大。
特别指出的是, Rust 值的动态大小不能超过 isize::MAX
(使用 size_of_val
确定) 。
不被视为 unsafe
的行为
Rust 编译器并不认为以下行为是非安全的,尽管程序员可能 (应该) 认为它们是不可取的、不符合预期或错误的。
死锁
内存和其他资源泄漏
退出时未调用析构函数
通过指针泄漏公开随机化的基址
整数溢出
如果程序出现算术溢出,那么程序员就犯了一个错误。 在下面的讨论中,算术溢出和算术环绕之间有的区别,前者是错误的,而后者是有意的。
当程序员启用了 debug_assert!
断言 (例如,启用了非优化构建) 时,实现必须插入动态检查以在溢出时引发 panic
。其他类型的构建可能会在溢出时导致 panics
或静默地环绕值,由实现自行决定。
在隐式环绕的情况下,实现必须使用二进制补码溢出约定提供明确定义的 (即使仍然被认为是错误的) 结果。
整数类型提供了固有方法,允许程序员显式地执行算术环绕。例如, i32::wrapping_add
提供了二进制补码的环绕加法。
标准库还提供了一个 Wrapping<T>
的新类型,它确保 T
的所有标准算术操作都具有环绕语义。
有关整数溢出的错误条件、原理和更多细节,请参见 RFC 560 。
逻辑错误
安全的代码可能会具有附加的逻辑约束,这些约束既无法在编译时检查,也无法在运行时检查。 如果程序违反了这些约束,则其行为可能是逻辑错误,但不会导致未定义行为。 这可能会包括恐慌、错误的结果、终止和非终端。 这种行为可能也会因为运行次数、构建方式或编译方式而不同。
例如,实现 Hash
和 Eq
都要求被认为相等的值具有相等的哈希值。
其一个例子是数据结构中,比如 BinaryHeap
、 BTreeMap
、 BTreeSet
、 HashMap
和 HashSet
,修改这些数据结构的键时有其约束。
如果违反约束则认为不是非安全的,但这时程序将产生错误,其行为不可预测。
常量求值
常量求值是在编译期间计算 表达式 结果的过程。 其条件是所有表达式的子集能够在编译时进行求值。
常量表达式
常量表达式可以在编译时求值。在 常量上下文 中,唯一允许的表达式,并且始终在编译时求值。 在其他位置,如 let 语句 中,常量表达式可能会被求值,但不能保证在编译时求值。 如果必须在编译时求值 (即在常量上下文中) ,则越界的 数组索引 或 溢出 等行为会导致编译器错误。 否则,这些行为会导致警告,但可能会在运行时崩溃。
以下表达式是常量表达式,只要任何操作数也是常量表达式。并且不会导致 Drop::drop
调用。
- 字面值。
- 常量参数。
- 函数 和 常量 的 路径 。不允许递归定义常量。
- 静态变量 的路径。这些只允许在静态变量的初始化器中使用。
- 元组表达式 。
- 数组表达式 。
- 结构体 表达式。
- 块表达式 ,包括
unsafe
块。 - 字段 表达式。
- 索引表达式,使用
usize
的 数组索引 或 切片 。 - 区间表达式 。
- 不捕获环境变量的 闭包表达式 。
- 在整数和浮点数类型、
bool
和char
上使用的内置 否定 、 算术 、 逻辑 、 比较 或 惰性布尔 运算符。 - 共享 借用 ,除非应用于具有 内部可变性 的类型。
- 解引用运算符 ,除了原始指针。
- 分组 表达式。
- 转换 表达式,除了
- 指针到地址转换和
- 函数指针到地址转换。
- const函数 和 const 方法的调用。
- loop 、 while 和
while let
表达式。 - if 、
if let
和 match 表达式。
常量上下文
常量上下文 是以下情况之一:
常量函数
常量函数是指允许在常量上下文中调用的函数。
将函数声明为 const
不会影响函数现有的用途,只限制了参数和返回类型可以使用的类型和表达式。
可以像普通函数一样自由地使用常量函数进行相同的操作。
在常量上下文中调用函数时,编译器会在编译时解释该函数。解释环境为编译目标,而不是主机环境。
因此,如果你正在针对 32 位系统进行编译,则无论是在 64 位系统还是 32 位系统上构建, usize
都是 32 位。
常量函数有一些限制,以确保可以在编译时求值。例如,不可能将随机数生成器编写为常量函数。 在编译时调用常量函数将始终产生与在运行时调用它相同的结果,即使调用多次也是如此。 有一个例外:如果在极端情况下进行复杂的浮点数运算,则可能会得到(极微小的)不同结果。 建议不要使数组长度和枚举判别值依赖于浮点数计算。
允许在常量上下文中使用但不允许在常量函数中使用的显著特性包括:
- 浮点运算
- 浮点值与除了
Copy
之外的 trait 约束的泛型参数一样处理。因此,你无法对它们进行任何操作,只能复制/移动它们。
- 浮点值与除了
在常量函数中可以使用以下内容,但在常量上下文中不行:
- 使用泛型类型和生命周期参数。
- 常量上下文允许有所限制的使用 常量泛型参数 。
应用程序二进制接口 (ABI)
本节记录的特性,影响 crate 编译输出的 ABI 。
有关指定导出函数的 ABI 的信息,请参阅 外部函数 。 有关指定链接外部库的 ABI 的信息,请参阅 外部块 。
used
属性
used
属性 仅适用于 static
条目 。
此 属性 强制编译器将变量保留在输出对象文件 (.o .rlib 等,不包括最终二进制文件) 中,
即使该变量没有被 crate 中的其他条目使用或引用。
但是,链接器仍然可以删除这样的条目。
下面的例子,展示了编译器在什么条件下保留一个 static
条目到输出对象文件中。
#![allow(unused)] fn main() { // foo.rs // 因为 `#[used]` 而保留: #[used] static FOO: u32 = 0; // 因为未使用而可移除: #[allow(dead_code)] static BAR: u32 = 0; // 因为可以被公开访问而保留: pub static BAZ: u32 = 0; // 因为被公开可访问的函数所引用而保留: static QUUX: u32 = 0; pub fn quux() -> &'static u32 { &QUUX } // 因为被私有未使用的函数所引用而可移除: static CORGE: u32 = 0; #[allow(dead_code)] fn corge() -> &'static u32 { &CORGE } }
$ rustc -O --emit=obj --crate-type=rlib foo.rs
$ nm -C foo.o
0000000000000000 R foo::BAZ
0000000000000000 r foo::FOO
0000000000000000 R foo::QUUX
0000000000000000 T foo::quux
no_mangle
属性
no_mangle
属性 可以用于任何 条目 ,以禁用标准符号名称编码。该条目的符号将是该条目名称的标识符。
此外,该条目将从生成的库或对象文件中公开导出,类似于 used
属性 。
link_section
属性
link_section
属性 指定将 函数 或 静态 内容放置到对象文件节。
使用 元名称值字符串 语法来指定节名称。
#![allow(unused)] fn main() { #[no_mangle] #[link_section = ".example_section"] pub static VAR1: u32 = 1; }
export_name
属性
export_name
属性 指定将在 函数 或 静态 上导出的符号名称。
使用 元名称值字符串 语法来指定符号名称。
#![allow(unused)] fn main() { #[export_name = "exported_symbol_name"] pub fn name_in_rust() { } }
Rust 运行时
本节所记录的特性,定义了 Rust 运行时的某些方面。
panic_handler
属性
panic_handler
属性 只能应用于具有签名 fn(&PanicInfo) -> !
的函数。
使用此 属性 标记的函数定义了 panic 的行为。
PanicInfo
结构包含有关 panic 位置的信息。
在二进制、 dylib 或 cdylib crate 的依赖图中,必须有一个单独的 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();
// logs "panicked at '$reason', src/main.rs:27:4" to some `sink`
let _ = writeln!(sink, "{}", info);
loop {}
}
标准行为
标准库提供了一个默认情况下会展开堆栈的 panic_handler
实现,但可以 更改为终止进程。
可以使用 set_hook 函数在运行时修改标准库的 panic 行为。
global_allocator
属性
global_allocator
属性 用于设置实现 GlobalAlloc
trait 的 静态条目 的全局分配器。
windows_subsystem
属性
windows_subsystem
属性 可以在 Windows 目标上的链接时应用于 crate 级别,以设置 subsystem。
使用 元名称值字符串 语法来指定子系统,值为 console
或 windows
。
在非 Windows 目标上,此属性将被忽略,对于非 bin
crate types 也是如此。
"console" 子系统是默认值。如果从现有控制台运行控制台进程,将附加到该控制台,否则将创建一个新的控制台窗口。
"windows" 子系统通常用于不希望在启动时显示控制台窗口的 GUI 应用程序。将在现有控制台之外运行。
#![allow(unused)] #![windows_subsystem = "windows"] fn main() { }
Appendices
附录: 宏的参照集歧义性形式化规范
该规范是为了帮助理解 Rust 宏展开期间的参照集歧义性而设计的。 参照集是指给定上下文环境下宏可能展开的所有可能性集合。 在某些情况下,这些集合可能会有歧义,这会导致宏展开出现问题。
本页面记录了 实例宏 的正式规范,其中包括以下规则。 这些规则最初在 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 树的有限序列形成的树结构。delimiter token
: 用于分隔一个片段的结尾和下一个片段的开头的 token 。separator token
: 复杂 NT 中的可选分隔符 token ,用于分隔匹配重复中每对元素。separated complex NT
: 具有自己的分隔符 token 的复杂 NT 。delimited sequence
: 带有适当的开/闭定界符的 token 树序列。empty fragment
: Rust 语言中分隔 token 的无形语法类,即空格或 (在某些词法上下文中) 空 token 序列。fragment specifier
: 简单 NT 中指定 NT 接受的片段的标识符。language
: 形式自由的语言。
Example:
#![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
是具有 expr
和 ident
作为它们各自的片段规格的简单 NT 。
$(i:ident),*
也是一个 NT ; 它是一个复杂 NT ,用于匹配逗号分隔的标识符的重复。
,
是复杂 NT 的分隔符 token ;它出现在匹配片段的每一对元素 (如果有的话) 之间。
$(hi $e:expr ;)+
是一个复杂的非终端符(NT)的例子,它匹配形式为 hi <expr>; hi <expr>; ...
的片段,其中至少包含一次 hi <expr>;
。请注意,这个复杂的非终端符并没有专用的分隔符令牌。
(注意, Rust 的解析器确保定界序列始终具有正确嵌套的 token 树结构和正确匹配的开/闭定界符。)
我们通常使用变量 "M" 代表一个匹配器,变量 "t" 和 "u" 代表任意单个 token ,变量 "tt" 和 "uu" 代表任意 token 树。 ( "tt" 的使用可能存在潜在的歧义,因为它还作为片段规格的额外角色;但从上下文中可以清楚地知道意思。)
"SEP" 将遍历分隔符 token , "OP" 将遍历重复运算符 *
、 +
和 ?
, "OPEN"/"CLOSE" 将遍历匹配定界序列的 token 对 (例如 [
和 ]
)。
希腊字母 "α" "β" "γ" "δ" 表示可能为空的 token 树序列。 (然而,希腊字母 "ε" (epsilon) 在表述中具有特殊作用,不代表 token 树序列。)
- 这种希腊字母约定通常只在序列的存在是技术细节时使用;特别地,当我们希望 强调 我们正在操作 token 树序列时,将使用符号 "tt ..." 表示序列,而不是希腊字母。
请注意,匹配器仅仅是一个 token 树。如上所述,"简单 NT" 是一个元变量 NT;因此它是一个非重复项。
例如, $foo:ty
是一个简单的 NT ,但 $($foo:ty)+
是一个复杂的 NT。
还请注意,在这种形式化语境下,术语 "token" 通常包括简单的 NT。
最后,读者应该记住,根据这个形式化的定义,没有简单的 NT 匹配空片段,同样,没有 token 匹配 Rust 语法的空片段。
(因此,唯一可以匹配空片段的 NT 是复杂的 NT。) 这实际上并不正确,因为 vis
匹配器可以匹配空片段。
因此,为了形式化的目的,我们将把 $v:vis
实际上视为 $($v:vis)?
,并要求匹配器匹配一个空片段。
匹配器的不变性
要有效,匹配器必须符合以下三个不变量。FIRST 和 FOLLOW 的定义将在后面描述。
- 对于匹配器
M
中的任意两个连续的 token 树序列 (即M = ... tt uu ...
,其中uu ...
非空) ,我们必须有 FOLLOW(... tt
) ∪ {ε} ⊇ FIRST(uu ...
)。 - 对于匹配器中的任何分隔的复杂 NT,
M = ... $(tt ...) SEP OP ...
,我们必须有SEP
∈ FOLLOW(tt ...
)。 - 对于匹配器中的未分隔的复杂 NT,
M = ... $(tt ...) OP ...
,如果 OP =*
或+
,则我们必须有 FOLLOW(tt ...
) ⊇ FIRST(tt ...
)。
第一个不变量表示,无论匹配器后面有什么实际 token (如果有的话) ,它都必须在预定的 FOLLOW 集中的某个位置。
这确保了一个合法的宏定义将继续分配相同的决定,即 ... tt
结束并且 uu ...
开始的位置,即使语言中添加了新的语法形式。
第二个不变量表示,分隔的复杂 NT 必须使用作为 NT 内部内容预定 FOLLOW 集的分隔符 token 。
这确保了一个合法的宏定义将继续将输入片段解析为相同的 tt ...
的定界序列,即使语言中添加了新的语法形式。
第三个不变量表示,当我们有一个复杂的 NT 可以匹配两个或多个连续的相同元素而不需要分隔符时,根据第一个不变量,这些元素必须可以被放在一起。 该不变量还要求它们必须是非空的,这消除了可能的歧义。
注意: 由于历史遗漏和对该行为的重要依赖,目前未执行第三个不变量。 目前尚未决定如何继续处理此问题。不遵守此行为的宏在 Rust 的未来版本中可能会变得无效。 请参见 问题跟踪。
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 集合。
换句话说:当且仅当存在 (可能为空的) token 序列 α β γ δ 时,t ∈ FOLLOW(M),其中:
-
M 匹配 β,
-
t 匹配 γ ,以及
-
连接 α β γ δ 是一个可解析的 Rust 程序。
我们使用简写 ANYTOKEN 来表示所有 token (包括简单的 NT )。例如,如果在匹配器 M 之后任何 token 都是合法的,则 FOLLOW(M) = ANYTOKEN 。
(为了回顾对上述非正式描述的理解,读者此时可能希望跳转到 FIRST/LAST的示例 ,然后再阅读它们的正式定义。)
FIRST, LAST
以下是 FIRST 和 LAST 的正式归纳定义。
"A ∪ B" 表示并集, "A ∩ B" 表示交集, "A \ B" 表示差集 (即 A 中所有在 B 中不存在的元素)。
FIRST
对于序列 M 及其第一个 token 树的结构 (如果有) ,对 FIRST(M) 进行情况分析定义:
-
如果 M 是空序列,则 FIRST(M) = { ε },
-
如果 M 以一个 token t 开头,则 FIRST(M) = { t } ,
(注意:这涵盖了 M 以定界的 token 树序列开头的情况,
M = OPEN tt ... CLOSE ...
,在这种情况下,t = OPEN
,因此 FIRST(M) = {OPEN
}。)(注意:这主要依赖于一个属性,即没有简单的 NT 可以匹配空片段。)
-
否则, M 是以复杂的 NT 开头的 token 树序列:
M = $( tt ... ) OP α
或M = $( tt ... ) SEP OP α
, (其中α
是匹配器的其余部分的 (可能为空的) token 树序列)。- 如果 SEP 存在且 ε ∈ FIRST(
tt ...
) ,则让 SEP_SET(M) = { SEP }; 否则,SEP_SET(M) = {}。
- 如果 SEP 存在且 ε ∈ FIRST(
-
如果 OP =
*
或?
,则让 ALPHA_SET(M) = FIRST(α
) ,如果 OP =+
,则 ALPHA_SET(M) = {}。 -
FIRST(M) = (FIRST(
tt ...
) \ {ε}) ∪ SEP_SET(M) ∪ ALPHA_SET(M) 。
复杂非终端符的定义需要一些解释。 SEP_SET (M) 定义了分隔符可能是 M 的有效首个符号的可能性,当定义了分隔符并且重复片段可能为空时会发生这种情况。
ALPHA_SET (M) 定义了复杂非终端符可以为空的可能性,这意味着 M 的有效首个符号是以下 token-tree 序列 α
中的符号。
当使用 *
或 ?
时会发生这种情况,此时可能没有重复。理论上,如果使用 +
并带有潜在为空的重复片段,则也可能发生这种情况,但这被第三个不变量禁止。
从这里开始,显然 FIRST(M) 可以包括来自 SEP_SET(M) 或 ALPHA_SET(M) 的任何令牌,如果复合 NT 匹配非空,则任何以 FIRST(tt ...
) 开头的令牌也可以使用。
考虑的最后一个部分是 ε 。 SEP_SET(M) 和 FIRST(tt ...
) \ {ε}不能包含 ε,但 ALPHA_SET(M) 可能会包含。
因此,这个定义允许 M 接受 ε ,当且仅当 ε∈ALPHA_SET(M) 。这是正确的,因为对于 M 在复合 NT 情况下接受 ε ,复合 NT 和 α 都必须接受它。
如果 OP = +
,意味着复合 NT 不能为空,根据定义, ε∉ALPHA_SET(M) 。
否则,复合 NT 可以接受零次重复,然后 ALPHA_SET(M)=FOLLOW(α
) 。因此,这个定义在 ε 方面是正确的。
LAST
LAST(M) 是根据 M 本身 (一个令牌树序列) 的情况分析定义的。
-
如果 M 是空序列,则 LAST(M) = { ε } 。
-
如果 M 是单个令牌 t ,则 LAST(M) = { t } 。
-
如果 M 是单一的复合 NT ,重复出现零次或多次,如
M = $( tt ... ) *
或M = $( tt ... ) SEP *
,则 LAST(M)={LAST('tt ...')}。- 如果存在 SEP ,则令 sep_set = { SEP } ;否则,令 sep_set = {} 。
- 如果 ε ∈ LAST(
tt ...
) ,则 LAST(M) = LAST(tt ...
) ∪ sep_set 。 - 否则,序列'tt ...'必须是非空的;LAST(M) = LAST(
tt ...
) ∪ {ε} 。
-
如果 M 是重复出现一次或多次的单一复合 NT ,如
M = $( tt ... ) +
或M = $( tt ... ) SEP +
,- 如果存在 SEP ,则令 sep_set = { SEP } ;否则,令s ep_set = {} 。
- 如果 ε ∈ LAST(
tt ...
) ,则 LAST(M) = LAST(tt ...
) ∪ sep_set 。 - 否则,序列
tt ...
必须是非空的;LAST(M) = LAST('tt ...')。
-
如果 M 是重复出现零次或一次的单一复合 NT ,如
M = $( tt ...) ?
,则 LAST(M) = LAST(tt ...
) ∪ {ε} 。 -
如果 M 是一个定界的 token 树序列
OPEN tt ... CLOSE
,则 LAST(M) = {CLOSE
}。 -
如果 M 是一个非空的令牌树的序列
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(stmt) = {
=>
,,
,;
}`. -
FOLLOW(ty) = FOLLOW(path) = {
{
,[
,,
,=>
,:
,=
,>
,>>
,;
,|
,as
,where
, 块非终端符}。 -
FOLLOW(vis) = {
,
l 除非是非原始的priv
,任何关键字或标识符; 可以开始类型的任何令牌; ident , ty 和 path 非终端符}。 -
对于任何其他简单的 token ,包括 block 、 ident 、 tt 、 item 、 lifetime 、 literal 和 meta 的简单非终端符以及所有终端符, FOLLOW(t) = ANYTOKEN 。
-
对于任何其他 M , FOLLOW(M) 的定义是对 (LAST(M){ε}) 的所有 t 的 FOLLOW(t) 的交集,其中 t 在终端符中范围。
可以开始类型的令牌是,截至本写作时,为 {(
、[
、!
、*
、&
、&&
、?
、lifetime、>
、>>
、::
、任何非关键字标识符、super
、self
、Self
、extern
、crate
、$crate
、_
、for
、impl
、fn
、unsafe
、typeof
、dyn
},尽管此列表可能不完整,因为人们不会总是记得在添加新令牌时更新附录。
复杂 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 <
) = {,
} is ⊆ 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
} is ⊆ FOLLOW(tt
) = ANYTOKEN, as is FIRST(;
) = {;
}. -
( $($t:tt),* , $(t:tt),* )
: 合法, (尽管任何尝试实际使用此宏都将在扩展期间发出局部歧义错误的信号。). -
($ty:ty $(; not sep)* -)
: 不合法,因为 FIRST($(; not sep)* -
) = {;
,-
} 不在 FOLLOW(ty
) 中。 -
($($ty:ty)-+)
:不合法,因为分隔符-
不在 FOLLOW(ty
) 中。 -
($($e: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 附录 #31: 标识符和模式语法
术语表
抽象语法树
"抽象语法树" (AST) 是编译器在编译时,表示程序结构的一种中间形式。
对齐
值的对齐以值的首个地址指定。该值始终是 2 的幂次方。值的引用必须对齐。 更多。
元数
元数是指函数或操作符接受的操作数的数量。例如, f(2, 3)
和 g(4, 6)
的元数为 2 ,而 h(8, 2, 6)
的元数为 3 。!
运算符的元数为 1 。
数组
数组有时也称为固定大小数组或内联数组,该值描述了由多个元素组成的集合,每个元素在程序运行时可由索引选择。 数组占用了一段连续的内存区域。
关联条目
关联条目指与另一个条目相关联的条目。关联条目在 实现 中定义,可在 traits 中声明。 只有函数、常量和类型别名可以是关联的。相对的是 自由条目 。
覆盖实现
类型实现包含 未覆盖 的类型是泛型实现。
impl<T> Foo for T
, impl<T> Bar<T> for T
, impl<T> Bar<Vec<T>> for T
和 impl<T> Bar<T> for Vec<T>
都为泛型实现。
但 impl<T> Bar<Vec<T>> for Vec<T>
不是泛型实现,因为此 impl
中出现的所有 T
的实例都被 Vec
覆盖了。
约束
约束是对类型或 trait 的限制。例如,如果对函数的参数加了约束,则传递给该函数的类型必须遵守该约束。
组合
组合是只应用函数和之前定义的组合,并以从其参数提供结果的高阶函数。组合以模块化的方式管理控制流。
Crate
Crate (箱体) 是编译和链接的基本单位。有库或可执行等不同的类型。 Crate 可以链接和引用 '外部 Crate' 。 Crate 具有内部的模块树,顶层是一个匿名模块,称为 Crate 根。 在 Crate 根中将条目标记为公开的,可使这些条目对其他 Crate 可见。 请参考 Rust文档 以了解更多信息。
调度
调度是在涉及多态性时确定实际运行的特定代码版本的机制。 静态调度和动态调度是两种主要的调度形式。 虽然 Rust 更侧重静态调度,但也通过称为 "特征对象" 的机制支持动态调度。
动态大小类型
动态大小类型 (DST) 是一种没有静态已知大小或对齐的类型。
实体
实体 是一种语言结构,在源程序中可以通过某种方式进行引用,通常是通过 路径 。 实体包括 类型 、条目 、泛型参数 、 变量绑定 、 循环标签 、 生命周期 、 字段、 属性 和 代码分析 。
表达式
表达式是值、常量、变量、运算符和函数的组合,可以求值为单个值,可以具有或不具有副作用。
例如,2 + (3 * 4)
是一个值为 14 的表达式。
自由条目
指的是不属于 实现 的 条目 ,例如 自由函数 或 自由常量 。与 关联条目 相对。
基本 trait
基本 trait 是指为现有类型添加一个 impl 会导致代码不兼容的 trait 。 Fn
trait 和 Sized
trait 是基本 trait 。
基本类型构造器
基本类型构造器是指实现 泛型实现 会破坏其结构的类型。 &
、 &mut
、 Box
和 Pin
是基本类型构造器。
任何时候,只要类型 T
被视为 局部类型, &T
、 &mut T
、 Box<T>
和 Pin<T>
也会被视为局部类型。基本类型构造器不能覆盖其他类型。每当使用术语 "覆盖类型" 时, &T
、 &mut T
、 Box<T>
和 Pin<T>
中的 T
不被视为覆盖的类型。
有实例
如果一个类型具有构造函数并且因此可以被实例化,则称其为 "有实例" 。有实例的类型不是 "空的" ,因为它可以有值。与 无实例 相对。
内部实现
适用于具名类型而不是 trait-类型对的 实现 。 更多 。
内部方法
在内部实现中定义的方法,而不是在 trait 实现中定义的方法。
初始化
如果变量被赋值且未被移动,那么表示被初始化了。对应的其他内存位置被认为是未初始化的。只有使用非安全 Rust 才能创建未初始化的内存位置。
局部 trait
在当前 crate 中定义的 trait
。一个 trait 定义是否局部与应用的类型参数无关。
例如对于 trait Foo<T, U>
,不管 T
和 U
被替换为哪些类型, Foo
都是局部的。
局部类型
在当前 crate 中定义的 struct
、 enum
或 union
类型。应用的类型参数不会影响此定义。
例如, struct Foo
被认为是局部类型,但 Vec<Foo>
不是。LocalType<ForeignType>
也是局部类型。类型别名不影响类型的局部性。
模块
模块是容纳零个或多个 条目 的容器。模块被组织成一棵树,从一个匿名根模块开始,称为 crate 根或根模块。 可以使用 路径 引用其他模块中的条目,这些引用可能受 可见性规则 的限制。 参阅模块
名称
名称 是指引用一个 实体 的 标识符 或 生命周期或循环标签 。 当一个实体声明引入与该实体相关联的标识符或标签时,称之为 名称绑定 。可以使用 路径 、标识符和标签来引用实体。
名称解析
名称解析 是将 路径 、 标识符 和 标签 与 实体 声明绑定的编译时过程。
命名空间
命名空间 是基于名称所引用的 实体 的种类而声明的名称的逻辑分组。 命名空间允许一个命名空间中的名称与另一个命名空间中有相同的名称而不冲突。
在命名空间内,名称按层级组织,每个层级都有自己的命名实体集合。
具名类型
可直接通过路径引用的类型。具体而言,包括 枚举 、 结构体 、 联合体 和 trait 对象 。
对象安全trait
可以用作 trait 对象 的 Traits 。只有符合特定 规则 的 Trait 才是对象安全的。
路径
Path 是一个由一个或多个路径段组成的序列,用于引用当前作用域或其他层级的 命名空间 中的 实体 。
预导入
预导入或称为 Rust 预导入,是条目的集合,主要用于将 Trait 导入到 crate 的每个模块中。 预导入库中的 Trait 在任意位置可用。
作用域
作用域 是源代码文本中的一个区域,在此区域内的命名实体 entity 可以使用其名称引用。
被匹配项
被匹配项是在 match
表达式和类似的模式匹配结构中被匹配的表达式。例如,在 match x { A => 1, B => 2 }
中,表达式 x
是被匹配项。
值大小
一个值的大小有两个定义:
第一个定义是该值所需的内存占用。
第二个定义具有该类型的数组中,相邻元素之间的偏移量 (以字节为单位) 。
其大小是对齐的倍数,包括零。大小可以根据编译器版本 (随着新的优化) 和目标平台而变化 (类似于usize
因平台而异) 。
请参考 Rust文档 以了解更多信息。
切片
切片是对连续序列的动态大小的 '视图' ,写作 [T]
。
通常以其借用形式出现,可以是可变的或共享的。
共享切片类型是 &[T]
,而可变切片类型是 &mut [T]
,其中 T
表示元素类型。
语句
语句是编程语言中最小的独立元素,它命令计算机执行操作。
字符串字面值
字符串字面值是直接存储在最终二进制文件中的字符串,因此在 'static
时间中总是有效的。
类型是 'static
时间借用的字符串切片, &'static str
。
字符串切片
字符串切片是 Rust 中最原始的字符串类型,写作 str
。通常以其借用形式出现,可以是可变的或共享的。
共享字符串切片类型是 &str
,而可变字符串切片类型是 &mut str
。
字符串切片始终是有效的 UTF-8。
Trait
[Trait] 是一种语言条目,用于描述类型必须提供的功能。允许类型对其行为做出某些承诺。
泛型函数和泛型结构可以使用 trait 来约束或限制其允许接受的类型。
鱼形符号
在表达式中具有通用参数的路径必须在开括号前加上 ::
。与通用角括号结合使用,看起来像鱼形符号 ::<>
。因此,这种语法俗称为鱼形语法。
例如:
#![allow(unused)] fn main() { let ok_num = Ok::<_, ()>(5); let vec = [1, 2, 3].iter().map(|n| n * 2).collect::<Vec<_>>(); }
这个 ::
前缀是为了消除在逗号分隔列表中有多个类型参数的泛型路径的歧义。
请参见 鱼形符号测试 ,其中有一个在没有前缀时产生歧义的例子。
未覆盖的类型
不作为另一类型的参数出现的类型。例如, T
是未覆盖的类型,而 Vec<T>
中的 T
是覆盖的。这仅与类型参数有关。
未定义行为
编译时或运行时未定义行为。可能导致但不限于:进程终止或破坏;不适当、不正确或意外的计算;或平台特定的结果。更多信息。
无法驻留的
如果一个类型没有构造函数,因此永远无法被实例化,则该类型是无法驻留的。
一个无法驻留的类型在某种意义上是 "空的" ,因为该类型没有值。
无法驻留类型的典型示例是 永不类型 !
,或者没有变体的枚举 enum Never { }
。与 驻留 相对。