Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

引言

本书是 Rust 编程语言的主要参考资料。

注意

有关本书中已知错误和遗漏,请参阅我们的 问题单。如果您发现编译器行为与本文描述不符的情况,请提交问题,以便我们讨论哪个是正确的。

Rust 发布版本

Rust 每六周发布一个新的语言版本。 该语言的第一个稳定版本是 Rust 1.0.0,随后是 Rust 1.1.0 等。 工具(rustccargo 等)和文档(标准库、本书等)随语言版本一起发布。

本书的最新版本,与最新的 Rust 版本相匹配,始终可在 https://doc.rust-lang.org/reference/ 找到。 可以通过在 “reference” 目录前添加 Rust 版本号来查找以前的版本。 例如,Rust 1.49.0 的参考资料位于 https://doc.rust-lang.org/1.49.0/reference/

《参考手册》不是什么

本书并非语言入门指南。 阅读本书需假定您已具备语言基础知识。 另有一本 书籍 可帮助您获取此类基础知识。

本书也不是语言发行版中包含的 标准库 的参考资料。 这些库通过从其源代码中提取文档属性来单独记录。 许多您可能期望是语言特性的功能在 Rust 中是库特性,因此您要查找的内容可能在那里,而不是在这里。

同样,本书通常不记录 rustc 作为工具或 Cargo 的具体细节。 rustc 有自己的 书籍。 Cargo 有一本 书籍,其中包含一份 参考手册。 还有一些页面,例如 链接,也是描述 rustc 的工作方式。

本书也仅作为稳定版 Rust 中可用功能的参考。 对于正在开发的不稳定功能,请参阅 不稳定手册

Rust 编译器,包括 rustc,会执行优化。 本参考手册没有指定允许或不允许哪些优化。 相反,请将编译后的程序视为一个黑盒子。 您只能通过运行它,提供输入并观察其输出来进行探测。 所有以这种方式发生的事情都必须符合参考手册的规定。

如何使用本书

本书不假设您是按顺序阅读本书的。 每个章节通常可以独立阅读,但会交叉链接到其他章节,以提及它们所涉及但不讨论的语言方面。

阅读本文档主要有两种方式。

第一种是回答一个具体问题。 如果您知道哪个章节回答了该问题,您可以直接跳转到目录中的该章节。 否则,您可以按 s 键或点击顶部栏的放大镜图标来搜索与您问题相关的关键词。 例如,假设您想知道在 let 语句中创建的临时值何时被丢弃。 如果您不知道 临时值的生命周期 定义在 表达式章节 中,您可以搜索“临时 let”,第一个搜索结果将带您到该部分。

第二种是普遍提高您对语言某方面的知识。 在这种情况下,只需浏览目录,直到看到您想了解更多内容的部分,然后开始阅读即可。 如果某个链接看起来很有趣,请点击它,然后阅读该部分。

话虽如此,阅读本书没有错误的方式。请以您认为最有帮助的方式阅读。

约定

像所有技术书籍一样,本书在信息展示方面也有某些约定。 这些约定在此处记录。

  • 定义术语的语句包含 斜体 的该术语。 每当该术语在该章节之外使用时,它通常是链接到包含此定义的章节。

    示例术语 是一个正在定义的术语的示例。

  • 主要文本描述最新的稳定版本。与以前版本的差异在版本块中分离:

    2018 版次差异

    在 2018 版次之前,行为是这样的。从 2018 版次开始,行为是那样的。

  • 包含有关本书状态的有用信息或指出有用但大多超出范围的信息的注释位于注释块中。

    注意

    这是一个示例注释。

  • 示例块显示一个示例,演示某个规则或指出某个有趣的方面。一些示例可能有隐藏行,可以通过将鼠标悬停或点击示例时出现的眼睛图标来查看。

    例子

    这是一个代码示例。

    #![allow(unused)]
    fn main() {
    println!("hello world");
    }
  • 显示语言中不健全行为或语言功能之间可能令人困惑的交互的警告位于特殊的警告框中。

    警告

    这是一个示例警告。

  • 文本中的内联代码片段位于 <code> 标签内。

    较长的代码示例位于一个带有语法高亮显示的框中,其右上角有复制、执行和显示隐藏行的控件。

    // 这是一行隐藏行。
    fn main() {
        println!("这是一个代码示例");
    }

    除非另有说明,所有示例均针对最新版本编写。

  • 语法和词法产生式在 符号 章节中描述。

  • 规则标识符出现在每个语言规则之前,用方括号括起来。这些标识符提供了一种引用和链接到语言中特定规则的方法(例如,示例规则)。规则标识符使用句点将部分从最一般到最具体地分开(例如,解构器.作用域.嵌套.函数体)。在窄屏幕上,规则名称将折叠显示 [*]

    可以点击规则名称以链接到该规则。

    警告

    规则的组织目前处于变动中。暂时而言,这些标识符名称在不同版本之间不稳定,如果它们发生更改,指向这些规则的链接可能会失效。我们打算在组织稳定后将其稳定化,以便指向规则名称的链接在不同版本之间不会中断。

  • 具有相关测试的规则将在其下方包含一个“测试”链接(在窄屏幕上,该链接为 [T])。单击该链接将弹出一个测试列表,可以单击以查看测试。例如,请参见 输入.编码.utf8

    将规则链接到测试是一项持续进行的工作。有关概述,请参阅 测试摘要 章节。

贡献

我们欢迎各种形式的贡献。

您可以通过在 Rust语言参考仓库 中提出问题或发送拉取请求来为本书做出贡献。 如果本书没有回答您的问题,并且您认为它的答案属于其范围,请不要犹豫 提交问题 或在 Zulip 上的 t-lang/doc 流中提问。 了解人们最常使用本书做什么有助于我们将注意力集中在使这些部分尽善尽美上。 当然,如果您发现任何错误或非规范性但未明确指出的内容,也请 提交问题

记法

语法

以下记法用于词法分析器语法格式文法片段:

记法示例含义译者注
CAPITALKW_IF, INTEGER_LITERAL词法分析器生成的词法单元大写
ItalicCamelCaseLetStatement, Item一个语法产生式斜体、驼峰方式大小写(单词首字母大写)
stringx, while, *确切的字符(或字符串)
x?pub?一个可选的项
x*OuterAttribute*0个或多个x
x+MacroMatch+1个或多个x
xa..bHEX_DIGIT1..6a到b次重复x
Rule1 Rule2fn Name Parameters规则的顺序序列
|u8 | u16, Block | Item二者之一
[ ][b B]列出的任意字符
[ - ][a-z]范围内的任意字符
~[ ]~[b B]任意字符,除了列出的
~string~\n, ~*/任意字符,除了此序列
( )(, Parameter)?分组项
U+xxxxU+0060一个Unicode字符
<text><any ASCII char except CR>一个匹配内容的英文描述
Rule suffixIDENTIFIER_OR_KEYWORD except crate对前一个规则的修改
// Comment.// Single line comment.延伸到行尾的注释。

序列的优先级高于|选项。

字符串表产生式

语法中的某些规则 —— 特别是一元运算符二元运算符关键字 —— 以简化形式给出:作为可打印字符串的列表。这些情况构成了关于词法单元规则的一个子集,并被认为是词法分析阶段的结果,该阶段由一个DFA驱动,作用于所有此类字符串表条目的析取,从而为解析器提供输入。

当语法中出现等宽字体的字符串时,它隐式引用了此类字符串表产生式的一个成员。有关更多信息,请参阅词法单元

语法可视化

在每个语法块下方都有一个按钮,用于切换语法格式图的显示。一个方形元素是一个非终结规则,一个圆角矩形是一个终结符。

词法结构

输入格式

Syntax
CHAR<a Unicode scalar value>

NUL → U+0000

CHAR a Unicode scalar value
NUL U+0000

本章描述了源文件如何被解释为词法单元序列。

有关程序如何组织成文件的描述,请参阅crate和源文件

源文件编码

每个源文件都被解释为以UTF-8编码的Unicode字符序列。

如果文件不是有效的UTF-8,则会报错。

字节序标记移除

如果序列中的第一个字符是U+FEFF字节序标记),则将其移除。

CRLF规范化

每对紧跟着U+000A (LF) 的U+000D (CR) 字符会被单个U+000A (LF) 替换。此操作只执行一次,不会重复,因此在规范化之后,输入中仍然可能存在紧跟着U+000A (LF) 的U+000D (CR)(例如,如果原始输入包含 “CR CR LF LF”)。

字符U+000D (CR) 的其他出现位置则保留不变(它们被视为空白符)。

Shebang移除

如果剩余序列以字符#!开头,则从序列中移除直到(并包括)第一个U+000A (LF) 的所有字符。

例如,以下文件的第一行将被忽略:

#!/usr/bin/env rustx

fn main() {
    println!("Hello!");
}

作为例外,如果#!字符后面紧跟着(忽略中间的注释空白符)一个[词法单元,则不进行任何移除。这可以防止源文件开头的内部属性被移除。

注意

标准库的include!宏会对其读取的文件进行字节序标记移除、CRLF规范化和Shebang移除。include_str!include_bytes!宏则不会。

词法单元化

然后,所得的字符序列将转换为词法单元,具体描述见本章的其余部分。

关键字

Rust 将关键字分为三类:

严格关键字

这些关键字只能在其正确的上下文中使用。它们不能用作以下内容的名称:

以下关键字在所有版次中都存在:

  • _
  • as
  • async
  • await
  • break
  • const
  • continue
  • crate
  • dyn
  • else
  • enum
  • extern
  • false
  • fn
  • for
  • if
  • impl
  • in
  • let
  • loop
  • match
  • mod
  • move
  • mut
  • pub
  • ref
  • return
  • self
  • Self
  • static
  • struct
  • super
  • trait
  • true
  • type
  • unsafe
  • use
  • where
  • while

2018 版次差异

以下关键字在 2018 版次中添加:

  • async
  • await
  • dyn

保留关键字

这些关键字尚未投入使用,但已为将来保留。它们与严格关键字具有相同的限制。这样做的理由是通过禁止当前程序使用这些关键字,使其与 Rust 的未来版本向前兼容。

  • abstract
  • become
  • box
  • do
  • final
  • gen
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

2018 版次差异

try关键字在 2018 版次中作为保留关键字添加。

2024 版次差异

gen关键字在 2024 版次中作为保留关键字添加。

弱关键字

这些关键字仅在特定上下文中有特殊含义。例如,可以使用union这个名字声明一个变量或方法。

  • 'static
  • macro_rules
  • raw
  • safe
  • union
  • macro_rules用于创建自定义
  • union用于声明一个联合体,并且仅在联合体声明中使用时才作为关键字。
  • 'static用于静态生命周期,不能用作泛型生命周期参数循环标签

    // error[E0262]: invalid lifetime parameter name: `'static`
    fn invalid_lifetime_parameter<'static>(s: &'static str) -> &'static str { s }
    
  • safe用于函数和静态量,在外部块中有特殊含义。
  • raw用于原始借用运算符,并且仅在匹配原始借用运算符形式(例如&raw const expr&raw mut expr)时才作为关键字。

2018 版次差异

在 2015 版次中,dyn在类型位置后跟不以::<开头的路径、生命周期、问号、for关键字或左括号时是一个关键字。

从 2018 版次开始,dyn已被提升为严格关键字。

标识符

Syntax
IDENTIFIER_OR_KEYWORD → ( XID_Start | _ ) XID_Continue*

XID_Start<XID_Start defined by Unicode>

XID_Continue<XID_Continue defined by Unicode>

RAW_IDENTIFIERr# IDENTIFIER_OR_KEYWORD

NON_KEYWORD_IDENTIFIERIDENTIFIER_OR_KEYWORDexcept a strict or reserved keyword

IDENTIFIERNON_KEYWORD_IDENTIFIER | RAW_IDENTIFIER

RESERVED_RAW_IDENTIFIER
    r# ( _ | crate | self | Self | super )not immediately followed by XID_Continue

XID_Start `XID_Start` defined by Unicode
XID_Continue `XID_Continue` defined by Unicode
NON_KEYWORD_IDENTIFIER except a strict or reserved keyword IDENTIFIER_OR_KEYWORD
RESERVED_RAW_IDENTIFIER r# not immediately followed by XID_Continue _ crate self Self super

标识符遵循Unicode标准附件#31中针对Unicode 16.0版的规范,并增加了下述内容。一些标识符的例子:

  • foo
  • _identifier
  • r#true
  • Москва
  • 東京

UAX #31中使用的概要是:

注意

_开头的标识符通常用于表明一个有意不使用的标识符,并且会消除rustc中的未使用警告。

标识符不能是严格保留关键字,除非带上下述原始标识符中描述的r#前缀。

零宽不连接符(ZWNJ U+200C)和零宽连接符(ZWJ U+200D)字符不允许出现在标识符中。

在以下情况,标识符被限制为XID_StartXID_Continue的ASCII子集:

规范化

标识符使用规范化形式 C (NFC) 进行规范化,如Unicode标准附件#15中所定义。两个标识符在它们的 NFC 形式相等时才相等。

过程宏和声明式宏在它们的输入中接收规范化的标识符。

原始标识符

原始标识符类似于普通标识符,但带有r#前缀。(请注意,r#前缀不作为实际标识符的一部分。)

与普通标识符不同,原始标识符可以是任何严格保留关键字,除了上面为RAW_IDENTIFIER列出的那些。

使用RESERVED_RAW_IDENTIFIER词法单元是错误的。

注释

Syntax
LINE_COMMENT
      // ( ~[/ ! LF] | // ) ~LF*
    | //

BLOCK_COMMENT
      /*
        ( ~[* !] | ** | BLOCK_COMMENT_OR_DOC )
        ( BLOCK_COMMENT_OR_DOC | ~*/ )*
      */
    | /**/
    | /***/

INNER_LINE_DOC
    //! ~[LF CR]*

INNER_BLOCK_DOC
    /*! ( BLOCK_COMMENT_OR_DOC | ~[*/ CR] )* */

OUTER_LINE_DOC
    /// ( ~/ ~[LF CR]* )?

OUTER_BLOCK_DOC
    /**
      ( ~* | BLOCK_COMMENT_OR_DOC )
      ( BLOCK_COMMENT_OR_DOC | ~[*/ CR] )*
    */

BLOCK_COMMENT_OR_DOC
      BLOCK_COMMENT
    | OUTER_BLOCK_DOC
    | INNER_BLOCK_DOC

LINE_COMMENT // ⚠️ with the exception of / ! LF CHAR // ⚠️ with the exception of LF CHAR //
BLOCK_COMMENT /* ⚠️ with the exception of * ! CHAR ** BLOCK_COMMENT_OR_DOC BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CHAR */ /**/ /***/
INNER_LINE_DOC //! ⚠️ with the exception of LF CR CHAR
INNER_BLOCK_DOC /*! BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CR CHAR */
OUTER_LINE_DOC /// ⚠️ with the exception of / CHAR ⚠️ with the exception of LF CR CHAR
OUTER_BLOCK_DOC /** ⚠️ with the exception of * CHAR BLOCK_COMMENT_OR_DOC BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CR CHAR */

非文档注释

注释遵循C++风格的单行注释(//)和块注释(/* ... */)形式。支持嵌套的块注释。

非文档注释被解释为一种空白字符。

文档注释

以恰好_三_个斜杠(///)开头的行文档注释,以及块文档注释(/** ... */),这两种都是外部文档注释,被解释为doc属性的一种特殊语法格式。

也就是说,它们等价于在注释主体周围编写#[doc="..."],例如,/// Foo 变为#[doc="Foo"]/** Bar */ 变为#[doc="Bar"]。因此,它们必须出现在接受外部属性的项之前。

//!开头的行注释和/*! ... */块注释是文档注释,它们应用于注释的父级,而不是紧随其后的项。

也就是说,它们等价于在注释主体周围编写#![doc="..."]//!注释通常用于文档化占据一个源文件的模块。

字符U+000D(CR)不允许出现在文档注释中。

注意

按照惯例,文档注释包含Markdown,这是rustdoc所期望的。然而,注释语法格式不识别任何内部Markdown。/** \glob = “/.rs”;` / 会在第一个/`处终止注释,其余代码将导致语法格式错误。与行文档注释相比,这稍微限制了块文档注释的内容。

注意

序列U+000D(CR)紧跟U+000A(LF)在之前会被转换为单个U+000A(LF)。

示例

#![allow(unused)]
fn main() {
//! 适用于此crate的隐式匿名模块的文档注释

pub mod outer_module {

    //!  - 内部行级文档注释
    //!! - 仍然是内部行级文档注释(但开头带感叹号)

    /*!  - 内部块级文档注释 */
    /*!! - 仍然是内部块级文档注释(但开头带感叹号) */

    //   - 仅是普通注释
    ///  - 外部行级文档注释(恰好三个斜杠)
    //// - 仅是普通注释

    /*   - 仅是普通注释 */
    /**  - 外部块级文档注释(恰好两个星号) */
    /*** - 仅是普通注释 */

    pub mod inner_module {}

    pub mod nested_comments {
        /* 在 Rust 中 /* 我们可以 /* 嵌套注释 */ */ */

        // 所有三种块级注释都可以包含或嵌套在
        // 任何其他类型的注释中:

        /*   /* */  /** */  /*! */  */
        /*!  /* */  /** */  /*! */  */
        /**  /* */  /** */  /*! */  */
        pub mod dummy_item {}
    }

    pub mod degenerate_cases {
        // 空的内部行级文档注释
        //!

        // 空的内部块级文档注释
        /*!*/

        // 空的行级注释
        //

        // 空的外部行级文档注释
        ///

        // 空的块级注释
        /**/

        pub mod dummy_item {}

        // 空的两个星号的块注释不是文档块,它是普通的块注释
        /***/

    }

    /* 下一个是不允许的,因为外部文档注释
       需要一个接收文档的条目 */

    /// 我的条目在哪里?
  mod boo {}
}
}

空白字符

Syntax
WHITESPACE
      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 // 段落分隔符

TAB → U+0009 // 水平制表符,'\t'

LF → U+000A // 换行符,'\n'

CR → U+000D // 回车符,'\r'

WHITESPACE U+0009 U+000A U+000B U+000C U+000D U+0020 U+0085 U+200E U+200F U+2028 U+2029
TAB U+0009
LF U+000A
CR U+000D

空白字符是指任何非空字符串,它只包含具有Pattern_White_Space Unicode属性的字符。

Rust是一种自由格式语言,这意味着所有形式的空白字符仅用于在语法格式中分隔_词法单元_,并且没有语义上的意义。

如果Rust程序中的每个空白字符元素被任何其他合法的空白字符元素(例如一个空格字符)替换,其含义保持不变。

词法单元

Syntax
Token
      RESERVED_TOKEN
    | RAW_IDENTIFIER
    | CHAR_LITERAL
    | STRING_LITERAL
    | RAW_STRING_LITERAL
    | BYTE_LITERAL
    | BYTE_STRING_LITERAL
    | RAW_BYTE_STRING_LITERAL
    | C_STRING_LITERAL
    | RAW_C_STRING_LITERAL
    | FLOAT_LITERAL
    | INTEGER_LITERAL
    | LIFETIME_TOKEN
    | PUNCTUATION
    | IDENTIFIER_OR_KEYWORD

词法单元是语法格式中由正则(非递归)语言定义的原始生成式。Rust源输入可以分解为以下几种词法单元:

在此文档的语法中,“简单”词法单元以字符串表生成式形式给出,并以等宽字体显示。

字面量

字面量是字面量表达式中使用的词法单元。

示例

字符和字符串

示例# 数量1字符转义
字符字面量'H'0所有 Unicode引号 & ASCII & Unicode
字符串字面量"hello"0所有 Unicode引号 & ASCII & Unicode
原始字符串字面量r#"hello"#<256所有UnicodeN/A
字节字面量b'H'0所有 ASCII引号 & 字节
字节字符串字面量b"hello"0所有 ASCII引号 & 字节
原始字节字符串字面量br#"hello"#<256所有 ASCIIN/A
C字符串字面量c"hello"0所有 Unicode引号 & 字节 & Unicode
原始C字符串字面量cr#"hello"#<256所有 UnicodeN/A

ASCII转义

名称
\x417位字符码(恰好2个十六进制数字,最大0x7F)
\n换行
\r回车
\tTab键
\\反斜杠
\0空字符

字节转义

名称
\x7F8位字符码(恰好2个十六进制数字)
\n换行
\r回车
\tTab键
\\反斜杠
\0空字符

Unicode转义

名称
\u{7FFF}24位Unicode字符码(最多6个十六进制数字)

引号转义

名称
\'单引号
\"双引号

数字

数字字面量2示例幂运算
十进制整数98_222N/A
十六进制整数0xffN/A
八进制整数0o77N/A
二进制整数0b1111_0000N/A
浮点数123.0E+77可选

后缀

后缀是紧跟在字面量主要部分(无中间空白)后面的一串字符,其形式与非原始标识符关键字相同。

Syntax
SUFFIXIDENTIFIER_OR_KEYWORDexcept _

SUFFIX_NO_ESUFFIXnot beginning with e or E

SUFFIX_NO_E not beginning with `e` or `E` SUFFIX

任何类型的字面量(字符串、整数等)与任何后缀组合,都可作为有效的词法单元。

带有任何后缀的字面量词法单元可以传递给宏而不会产生错误。宏本身将决定如何解释此类词法单元以及是否产生错误。特别是,声明宏的literal fragment specifier匹配带有任意后缀的字面量词法单元。

#![allow(unused)]
fn main() {
macro_rules! blackhole { ($tt:tt) => () }
macro_rules! blackhole_lit { ($l:literal) => () }

blackhole!("string"suffix); // OK
blackhole_lit!(1suffix); // OK
}

然而,在被解释为字面量表达式或模式的字面量词法单元上,后缀是受限制的。非数字字面量词法单元的任何后缀都会被拒绝,而数字字面量词法单元只接受以下列表中的后缀。

整数浮点数
u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isizef32, f64

字符和字符串字面量

字符字面量

Syntax
CHAR_LITERAL
    '
        ( ~[' \ LF CR TAB] | QUOTE_ESCAPE | ASCII_ESCAPE | UNICODE_ESCAPE )
    ' SUFFIX?

QUOTE_ESCAPE\' | \"

ASCII_ESCAPE
      \x OCT_DIGIT HEX_DIGIT
    | \n | \r | \t | \\ | \0

UNICODE_ESCAPE
    \u{ ( HEX_DIGIT _* )1..6valid hex char value }3

字符字面量是单个Unicode字符,用两个U+0027(单引号)字符括起来,但U+0027本身除外,它必须通过前面加一个U+005C字符(\)进行转义。

字符串字面量

Syntax
STRING_LITERAL
    " (
        ~[" \ CR]
      | QUOTE_ESCAPE
      | ASCII_ESCAPE
      | UNICODE_ESCAPE
      | STRING_CONTINUE
    )* " SUFFIX?

STRING_CONTINUE\ LF

字符串字面量是任何Unicode字符序列,用两个U+0022(双引号)字符括起来,但U+0022本身除外,它必须通过前面加一个U+005C字符(\)进行转义。

换行符(由字符U+000A (LF)表示)在字符串字面量中是允许的。字符U+000D (CR)不得出现在字符串字面量中。当未转义的U+005C字符(\)紧接在换行符之前时,换行符不会出现在词法单元所表示的字符串中。有关详细信息,请参阅字符串续行转义

字符转义

在字符或非原始字符串字面量中,还有一些额外的_转义_可用。转义以U+005C\)开头,并接以下形式之一:

  • 7位码点转义以U+0078x)开头,后跟恰好两个十六进制数字,值最大为0x7F。它表示ASCII字符,其值等于提供的十六进制值。不允许更高的值,因为它们是Unicode码点还是字节值存在歧义。
  • 24位码点转义以U+0075u)开头,后跟最多六个十六进制数字,并用花括号U+007B{)和U+007D})括起来。它表示等于所提供十六进制值的Unicode码点。该值必须是有效的Unicode标量值。
  • 空白转义是字符U+006En)、U+0072r)或U+0074t)之一,分别表示Unicode值U+000A (LF)、U+000D (CR)或U+0009 (HT)。
  • 空转义是字符U+00300),表示Unicode值U+0000 (NUL)。
  • 反斜杠转义是字符U+005C\),它必须被转义才能表示自身。

原始字符串字面量

Syntax
RAW_STRING_LITERALr RAW_STRING_CONTENT SUFFIX?

RAW_STRING_CONTENT
      " ( ~CR )* (non-greedy) "
    | # RAW_STRING_CONTENT #

原始字符串字面量不处理任何转义。它们以字符U+0072r)开头,后跟少于256个U+0023#)字符和一个U+0022(双引号)字符。

原始字符串体可以包含除U+000D (CR)以外的任何Unicode字符序列。它只由另一个U+0022(双引号)字符终止,后跟与起始U+0022(双引号)字符前相同数量的U+0023#)字符。

原始字符串体中包含的所有Unicode字符都表示其本身,字符U+0022(双引号)(除非后面紧跟的U+0023#)字符数量与用于开始原始字符串字面量的字符数量相同或更多)或U+005C\)不具有任何特殊含义。

字符串字面量示例:

#![allow(unused)]
fn main() {
"foo"; r"foo";                     // foo
"\"foo\""; r#""foo""#;             // "foo"

"foo #\"# bar";
r##"foo #"# bar"##;                // foo #"# bar

"\x52"; "R"; r"R";                 // R
"\\x52"; r"\x52";                  // \x52
}

字节和字节字符串字面量

字节字面量

Syntax
BYTE_LITERAL
    b' ( ASCII_FOR_CHAR | BYTE_ESCAPE ) ' SUFFIX?

ASCII_FOR_CHAR
    <any ASCII (i.e. 0x00 to 0x7F) except ', \, LF, CR, or TAB>

BYTE_ESCAPE
      \x HEX_DIGIT HEX_DIGIT
    | \n | \r | \t | \\ | \0 | \' | \"

ASCII_FOR_CHAR any ASCII (i.e. 0x00 to 0x7F) except `'`, `\`, LF, CR, or TAB

字节字面量是一个单个ASCII字符(在U+0000U+007F范围内)或一个单个_转义_字符,前面带有字符U+0062b)和U+0027(单引号),后面带有字符U+0027。如果U+0027字符出现在字面量中,它必须通过前面加一个U+005C\)字符进行转义。它等同于一个u8无符号8位整数数字字面量。

字节字符串字面量

Syntax
BYTE_STRING_LITERAL
    b" ( ASCII_FOR_STRING | BYTE_ESCAPE | STRING_CONTINUE )* " SUFFIX?

ASCII_FOR_STRING
    <any ASCII (i.e 0x00 to 0x7F) except ", \, or CR>

非原始字节字符串字面量是一个ASCII字符和_转义_序列,前面带有字符U+0062b)和U+0022(双引号),后面带有字符U+0022。如果U+0022字符出现在字面量中,它必须通过前面加一个U+005C\)字符进行转义。另外,字节字符串字面量可以是下面定义的原始字节字符串字面量。

换行符(由字符U+000A (LF)表示)在字节字符串字面量中是允许的。字符U+000D (CR)不得出现在字节字符串字面量中。当未转义的U+005C字符(\)紧接在换行符之前时,换行符不会出现在词法单元所表示的字符串中。有关详细信息,请参阅字符串续行转义

在字节或非原始字节字符串字面量中,还有一些额外的_转义_可用。转义以U+005C\)开头,并接以下形式之一:

  • 字节转义以U+0078x)开头,后跟恰好两个十六进制数字。它表示等于所提供十六进制值的字节。
  • 空白转义是字符U+006En)、U+0072r)或U+0074t)之一,分别表示字节值0x0A (ASCII LF)、0x0D (ASCII CR)或0x09 (ASCII HT)。
  • 空转义是字符U+00300),表示字节值0x00 (ASCII NUL)。
  • 反斜杠转义是字符U+005C\),它必须被转义才能表示其ASCII编码0x5C

原始字节字符串字面量

Syntax
RAW_BYTE_STRING_LITERAL
    br RAW_BYTE_STRING_CONTENT SUFFIX?

RAW_BYTE_STRING_CONTENT
      " ASCII_FOR_RAW* (non-greedy) "
    | # RAW_BYTE_STRING_CONTENT #

ASCII_FOR_RAW
    <any ASCII (i.e. 0x00 to 0x7F) except CR>

原始字节字符串字面量不处理任何转义。它们以字符U+0062b)开头,后跟U+0072r),再后跟少于256个U+0023#)字符和一个U+0022(双引号)字符。

原始字符串体可以包含除U+000D (CR)以外的任何ASCII字符序列。它只由另一个U+0022(双引号)字符终止,后跟与起始U+0022(双引号)字符前相同数量的U+0023#)字符。原始字节字符串字面量不能包含任何非ASCII字节。

原始字符串体中包含的所有字符都表示其ASCII编码,字符U+0022(双引号)(除非后面紧跟的U+0023#)字符数量与用于开始原始字符串字面量的字符数量相同或更多)或U+005C\)不具有任何特殊含义。

字节字符串字面量示例:

#![allow(unused)]
fn main() {
b"foo"; br"foo";                     // foo
b"\"foo\""; br#""foo""#;             // "foo"

b"foo #\"# bar";
br##"foo #"# bar"##;                 // foo #"# bar

b"\x52"; b"R"; br"R";                // R
b"\\x52"; br"\x52";                  // \x52
}

C字符串和原始C字符串字面量

C字符串字面量

Syntax
C_STRING_LITERAL
    c" (
        ~[" \ CR NUL]
      | BYTE_ESCAPEexcept \0 or \x00
      | UNICODE_ESCAPEexcept \u{0}, \u{00}, …, \u{000000}
      | STRING_CONTINUE
    )* " SUFFIX?

C_STRING_LITERAL c" ⚠️ with the exception of " \ CR NUL CHAR except `\0` or `\x00` BYTE_ESCAPE except `\u{0}`, `\u{00}`, …, `\u{000000}` UNICODE_ESCAPE STRING_CONTINUE " SUFFIX

C字符串字面量是一个Unicode字符和_转义_序列,前面带有字符U+0063c)和U+0022(双引号),后面带有字符U+0022。如果U+0022字符出现在字面量中,它必须通过前面加一个U+005C\)字符进行转义。另外,C字符串字面量可以是下面定义的原始C字符串字面量。

C字符串隐式以字节0x00终止,因此C字符串字面量c""等同于手动从字节字符串字面量b"\x00"构造一个&CStr。除了隐式终止符,字节0x00不允许出现在C字符串中。

换行符(由字符U+000A (LF)表示)在C字符串字面量中是允许的。字符U+000D (CR)不得出现在C字符串字面量中。当未转义的U+005C字符(\)紧接在换行符之前时,换行符不会出现在词法单元所表示的字符串中。有关详细信息,请参阅字符串续行转义

在非原始C字符串字面量中,还有一些额外的_转义_可用。转义以U+005C\)开头,并接以下形式之一:

  • 字节转义以U+0078x)开头,后跟恰好两个十六进制数字。它表示等于所提供十六进制值的字节。
  • 24位码点转义以U+0075u)开头,后跟最多六个十六进制数字,并用花括号U+007B{)和U+007D})括起来。它表示等于所提供十六进制值的Unicode码点,以UTF-8编码。
  • 空白转义是字符U+006En)、U+0072r)或U+0074t)之一,分别表示字节值0x0A (ASCII LF)、0x0D (ASCII CR)或0x09 (ASCII HT)。
  • 反斜杠转义是字符U+005C\),它必须被转义才能表示其ASCII编码0x5C

C字符串表示没有定义编码的字节,但C字符串字面量可以包含高于U+007F的Unicode字符。这些字符将被替换为该字符的UTF-8表示字节。

以下C字符串字面量是等效的:

#![allow(unused)]
fn main() {
c"æ";        // LATIN SMALL LETTER AE (U+00E6)
c"\u{00E6}";
c"\xC3\xA6";
}

2021 版次差异

C字符串字面量在2021或更高版次中被接受。在更早的版次中,词法单元c""被解析为c ""

原始C字符串字面量

Syntax
RAW_C_STRING_LITERAL
    cr RAW_C_STRING_CONTENT SUFFIX?

RAW_C_STRING_CONTENT
      " ( ~[CR NUL] )* (non-greedy) "
    | # RAW_C_STRING_CONTENT #

原始C字符串字面量不处理任何转义。它们以字符U+0063c)开头,后跟U+0072r),再后跟少于256个U+0023#)字符和一个U+0022(双引号)字符。

原始C字符串体可以包含除U+0000 (NUL)和U+000D (CR)以外的任何Unicode字符序列。它只由另一个U+0022(双引号)字符终止,后跟与起始U+0022(双引号)字符前相同数量的U+0023#)字符。

原始C字符串体中包含的所有字符都以UTF-8编码表示其本身。字符U+0022(双引号)(除非后面紧跟的U+0023#)字符数量与用于开始原始C字符串字面量的字符数量相同或更多)或U+005C\)不具有任何特殊含义。

2021 版次差异

原始C字符串字面量在2021或更高版次中被接受。在更早的版次中,词法单元cr""被解析为cr "",而cr#""#被解析为cr #""#(这不符合语法格式)。

C字符串和原始C字符串字面量示例

#![allow(unused)]
fn main() {
c"foo"; cr"foo";                     // foo
c"\"foo\""; cr#""foo""#;             // "foo"

c"foo #\"# bar";
cr##"foo #"# bar"##;                 // foo #"# bar

c"\x52"; c"R"; cr"R";                // R
c"\\x52"; cr"\x52";                  // \x52
}

数字字面量

数字字面量可以是整数字面量,也可以是浮点数字面量。识别这两种字面量的语法格式是混合的。

整数字面量

Syntax
INTEGER_LITERAL
    ( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL | DEC_LITERAL ) SUFFIX_NO_E?

DEC_LITERALDEC_DIGIT ( DEC_DIGIT | _ )*

BIN_LITERAL0b _* BIN_DIGIT ( BIN_DIGIT | _ )*

OCT_LITERAL0o _* OCT_DIGIT ( OCT_DIGIT | _ )*

HEX_LITERAL0x _* HEX_DIGIT ( HEX_DIGIT | _ )*

BIN_DIGIT → [0-1]

OCT_DIGIT → [0-7]

DEC_DIGIT → [0-9]

HEX_DIGIT → [0-9 a-f A-F]

整数字面量有以下四种形式:

  • 十进制字面量以十进制数字开头,并可包含十进制数字和下划线的任意组合。
  • 十六进制字面量以字符序列U+0030 U+00780x)开头,并可包含十六进制数字和下划线的任意组合(至少包含一个数字)。
  • 八进制字面量以字符序列U+0030 U+006F0o)开头,并可包含八进制数字和下划线的任意组合(至少包含一个数字)。
  • 二进制字面量以字符序列U+0030 U+00620b)开头,并可包含二进制数字和下划线的任意组合(至少包含一个数字)。

像任何字面量一样,整数字面量可以紧跟(不带任何空格)一个如上所述的后缀。后缀不能以eE开头,因为这将被解释为浮点字面量的指数。有关这些后缀的效果,请参阅整数字面量表达式

被接受为字面量表达式的整数字面量示例:

#![allow(unused)]
fn main() {
#![allow(overflowing_literals)]
123;
123i32;
123u32;
123_u32;

0xff;
0xff_u8;
0x01_f32; // integer 7986, not floating-point 1.0
0x01_e3;  // integer 483, not floating-point 1000.0

0o70;
0o70_i16;

0b1111_1111_1001_0000;
0b1111_1111_1001_0000i64;
0b________1;

0usize;

// These are too big for their type, but are accepted as literal expressions.
128_i8;
256_u8;

// This is an integer literal, accepted as a floating-point literal expression.
5f32;
}

请注意,例如-1i8被解析为两个词法单元:-后跟1i8

不被接受为字面量表达式的整数字面量示例:

#![allow(unused)]
fn main() {
#[cfg(false)] {
0invalidSuffix;
123AFB43;
0b010a;
0xAB_CD_EF_GH;
0b1111_f32;
}
}

元组索引

Syntax
TUPLE_INDEXDEC_LITERAL | BIN_LITERAL | OCT_LITERAL | HEX_LITERAL

元组索引用于引用元组元组结构体元组枚举变体的字段。

元组索引与字面量词法单元直接比较。元组索引从0开始,每个后续索引的十进制值递增1。因此,只有十进制值会匹配,并且该值不能有任何额外的0前缀字符。

元组索引不能包含任何后缀(例如usize)。

#![allow(unused)]
fn main() {
let example = ("dog", "cat", "horse");
let dog = example.0;
let cat = example.1;
// The following examples are invalid.
let cat = example.01;  // ERROR no field named `01`
let horse = example.0b10;  // ERROR no field named `0b10`
let unicorn = example.0usize; // ERROR suffixes on a tuple index are invalid
let underscore = example.0_0; // ERROR no field `0_0` on type `(&str, &str, &str)`
}

浮点数字面量

Syntax
FLOAT_LITERAL
      DEC_LITERAL ( . DEC_LITERAL )? FLOAT_EXPONENT SUFFIX?
    | DEC_LITERAL . DEC_LITERAL SUFFIX_NO_E?
    | DEC_LITERAL .not immediately followed by ., _ or an XID_Start character

FLOAT_EXPONENT
    ( e | E ) ( + | - )? _* DEC_DIGIT ( DEC_DIGIT | _ )*

浮点数字面量有两种形式:

  • 十进制字面量,后跟句点字符U+002E.)。这后面可选地跟另一个十进制字面量,带有可选的指数。
  • 单个十进制字面量,后跟指数。

像整数字面量一样,浮点数字面量可以后跟一个后缀,只要后缀前的部分不以U+002E.)结尾。如果字面量不包含指数,则后缀不能以eE开头。有关这些后缀的效果,请参阅浮点数字面量表达式

被接受为字面量表达式的浮点数字面量示例:

#![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;
}
}

类似于数字字面量的保留形式

Syntax
RESERVED_NUMBER
      BIN_LITERAL [2-9]
    | OCT_LITERAL [8-9]
    | ( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL ) .not immediately followed by ., _ or an XID_Start character
    | ( BIN_LITERAL | OCT_LITERAL ) ( e | E )
    | 0b _* <end of input or not BIN_DIGIT>
    | 0o _* <end of input or not OCT_DIGIT>
    | 0x _* <end of input or not HEX_DIGIT>
    | DEC_LITERAL ( . DEC_LITERAL )? ( e | E ) ( + | - )? <end of input or not DEC_DIGIT>

RESERVED_NUMBER BIN_LITERAL 2-9 OCT_LITERAL 8-9 BIN_LITERAL OCT_LITERAL HEX_LITERAL not immediately followed by `.`, `_` or an XID_Start character . BIN_LITERAL OCT_LITERAL e E 0b _ end of input or not BIN_DIGIT 0o _ end of input or not OCT_DIGIT 0x _ end of input or not HEX_DIGIT DEC_LITERAL . DEC_LITERAL e E + - end of input or not DEC_DIGIT

以下类似于数字字面量的词法形式是_保留形式_。由于可能存在的歧义,词法分析器会拒绝这些形式,而不是将其解释为单独的词法单元。

  • 未加后缀的二进制或八进制字面量,紧接着(无中间空白)一个超出其基数范围的十进制数字。
  • 未加后缀的二进制、八进制或十六进制字面量,紧接着(无中间空白)一个句点字符(对句点后面的内容有与浮点数字面量相同的限制)。
  • 未加后缀的二进制或八进制字面量,紧接着(无中间空白)字符eE
  • 以基数前缀之一开头但不是有效的二进制、八进制或十六进制字面量(因为它不包含任何数字)的输入。
  • 具有浮点数字面量形式,但指数中没有数字的输入。

保留形式的示例:

#![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`
}

生命周期和循环标签

Syntax
LIFETIME_TOKEN
      RAW_LIFETIME
    | ' IDENTIFIER_OR_KEYWORDnot immediately followed by '

LIFETIME_OR_LABEL
      RAW_LIFETIME
    | ' NON_KEYWORD_IDENTIFIERnot immediately followed by '

RAW_LIFETIME
    'r# IDENTIFIER_OR_KEYWORDnot immediately followed by '

RESERVED_RAW_LIFETIME'r# ( _ | crate | self | Self | super )not immediately followed by '

RAW_LIFETIME 'r# not immediately followed by `'` IDENTIFIER_OR_KEYWORD
RESERVED_RAW_LIFETIME 'r# not immediately followed by `'` _ crate self Self super

生命周期参数和循环标签使用LIFETIME_OR_LABEL词法单元。任何LIFETIME_TOKEN都会被词法分析器接受,例如,可以在宏中使用。

原始生命周期类似于普通生命周期,但其标识符带有r#前缀。(请注意,r#前缀不作为实际生命周期的一部分。)

与普通生命周期不同,原始生命周期可以是除上述RAW_LIFETIME列出的关键字之外的任何严格或保留关键字

使用RESERVED_RAW_LIFETIME词法单元是错误的。

2021 版次差异

原始生命周期在2021或更高版次中被接受。在更早的版次中,词法单元'r#lt被解析为'r # lt

标点符号

标点符号词法单元用作运算符、分隔符和语法格式的其他部分。

Syntax
PUNCTUATION
      ...
    | ..=
    | <<=
    | >>=
    | !=
    | %=
    | &&
    | &=
    | *=
    | +=
    | -=
    | ->
    | ..
    | /=
    | ::
    | <-
    | <<
    | <=
    | ==
    | =>
    | >=
    | >>
    | >
    | ^=
    | |=
    | ||
    | !
    | #
    | $
    | %
    | &
    | (
    | )
    | *
    | +
    | ,
    | -
    | .
    | /
    | :
    | ;
    | <
    | =
    | ?
    | @
    | [
    | ]
    | ^
    | {
    | |
    | }
    | ~

PUNCTUATION ... ..= <<= >>= != %= && &= *= += -= -> .. /= :: <- << <= == => >= >> > ^= |= || ! # $ % & ( ) * + , - . / : ; < = ? @ [ ] ^ { | } ~

注意

有关标点符号字符如何使用的链接,请参阅语法格式索引

分隔符

括号标点符号用于语法格式的各个部分。开括号必须始终与闭括号配对。括号及其内部的词法单元在中被称为“词法单元树”。括号有三种类型:

括号类型
{ }花括号
[ ]方括号
( )小括号

保留词法单元

几种词法单元形式被保留以备将来使用或避免混淆。源输入匹配其中一种形式是错误的。

保留前缀

Syntax
RESERVED_TOKEN_DOUBLE_QUOTE
    IDENTIFIER_OR_KEYWORDexcept b or c or r or br or cr "

RESERVED_TOKEN_SINGLE_QUOTE
    IDENTIFIER_OR_KEYWORDexcept b '

RESERVED_TOKEN_POUND
    IDENTIFIER_OR_KEYWORDexcept r or br or cr #

RESERVED_TOKEN_LIFETIME
    ' IDENTIFIER_OR_KEYWORDexcept r #

一些被称为_保留前缀_的词法形式被保留以备将来使用。

源输入如果被词法解释为非原始标识符(或关键字),且紧跟#'"字符(无中间空白),则被识别为保留前缀。

请注意,原始标识符、原始字符串字面量和原始字节字符串字面量可能包含#字符,但不会被解释为包含保留前缀。

同样,用于原始字符串字面量、字节字面量、字节字符串字面量、原始字节字符串字面量、C字符串字面量和原始C字符串字面量的rbbrccr前缀不被解释为保留前缀。

源输入如果被词法解释为非原始生命周期(或关键字),且紧跟#字符(无中间空白),则被识别为保留生命周期前缀。

2021 版次差异

从2021版次开始,词法分析器会将保留前缀报告为错误(特别是,它们不能传递给宏)。

在2021版次之前,词法分析器会接受保留前缀,并将其解释为多个词法单元(例如,标识符关键字的一个词法单元,后跟一个#词法单元)。

所有版次都接受的示例:

#![allow(unused)]
fn main() {
macro_rules! lexes {($($_:tt)*) => {}}
lexes!{a #foo}
lexes!{continue 'foo}
lexes!{match "..." {}}
lexes!{r#let#foo}         // 三个词法单元: r#let # foo
lexes!{'prefix #lt}
}

在2021版次之前接受但之后被拒绝的示例:

#![allow(unused)]
fn main() {
macro_rules! lexes {($($_:tt)*) => {}}
lexes!{a#foo}
lexes!{continue'foo}
lexes!{match"..." {}}
lexes!{'prefix#lt}
}

保留的守卫

Syntax
RESERVED_GUARDED_STRING_LITERAL#+ STRING_LITERAL

RESERVED_POUNDS#2..

保留的守卫是为将来使用而保留的语法格式,如果使用,将生成编译错误。

保留的带守卫字符串字面量是一个词法单元,由一个或多个U+0023#)紧接着一个STRING_LITERAL组成。

保留的井号是一个由两个或更多U+0023#)组成的词法单元。

2024 版次差异

在2024版次之前,词法分析器会接受保留的守卫,并将其解释为多个词法单元。例如,#"foo"#形式被解释为三个词法单元。##被解释为两个词法单元。


  1. 相同字面量两侧#的数量必须相等。

  2. 所有数字字面量都允许使用_作为视觉分隔符:1_234.0E+18f64

  3. 参阅lex.token.literal.char-escape.unicode

Rust的功能和语法可以通过称为宏的自定义定义进行扩展。它们被赋予名称,并通过一致的语法some_extension!(...)来调用。

有两种方法可以定义新宏:

  • 声明宏 以更高级的声明性方式定义新语法。
  • 过程宏 使用操作输入词法单元的函数来定义函数式宏、自定义派生和自定义属性。

宏调用

Syntax
MacroInvocation
    SimplePath ! DelimTokenTree

DelimTokenTree
      ( TokenTree* )
    | [ TokenTree* ]
    | { TokenTree* }

TokenTree
    Tokenexcept delimiters | DelimTokenTree

MacroInvocationSemi
      SimplePath ! ( TokenTree* ) ;
    | SimplePath ! [ TokenTree* ] ;
    | SimplePath ! { TokenTree* }

宏调用在编译时展开宏,并用宏的结果替换该调用。宏可以在以下情况中调用:

当用作项或语句时,使用MacroInvocationSemi形式,如果未使用大括号,则末尾需要分号。可见性限定符绝不允许出现在宏调用或macro_rules定义之前。

#![allow(unused)]
fn main() {
// 用作表达式。
let x = vec![1,2,3];

// 用作语句。
println!("Hello!");

// 用在模式中。
macro_rules! pat {
    ($i:ident) => (Some($i))
}

if let pat!(x) = Some(1) {
    assert_eq!(x, 1);
}

// 用在类型中。
macro_rules! Tuple {
    { $A:ty, $B:ty } => { ($A, $B) };
}

type N2 = Tuple!(i32, i32);

// 用作项。
use std::cell::RefCell;
thread_local!(static FOO: RefCell<u32> = RefCell::new(1));

// 用作关联项。
macro_rules! const_maker {
    ($t:ty, $v:tt) => { const CONST: $t = $v; };
}
trait T {
    const_maker!{i32, 7}
}

// 宏内的宏调用。
macro_rules! example {
    () => { println!("Macro call in a macro!") };
}
// 外部宏`example`展开后,内部宏`println`也会展开。
example!();
}

宏调用可以通过两种作用域进行解析:

声明宏

Syntax
MacroRulesDefinition
    macro_rules ! IDENTIFIER MacroRulesDef

MacroRulesDef
      ( MacroRules ) ;
    | [ MacroRules ] ;
    | { MacroRules }

MacroRules
    MacroRule ( ; MacroRule )* ;?

MacroRule
    MacroMatcher => MacroTranscriber

MacroMatcher
      ( MacroMatch* )
    | [ MacroMatch* ]
    | { MacroMatch* }

MacroMatch
      Tokenexcept $ and delimiters
    | MacroMatcher
    | $ ( IDENTIFIER_OR_KEYWORDexcept crate | RAW_IDENTIFIER ) : MacroFragSpec
    | $ ( MacroMatch+ ) MacroRepSep? MacroRepOp

MacroFragSpec
      block | expr | expr_2021 | ident | item | lifetime | literal
    | meta | pat | pat_param | path | stmt | tt | ty | vis

MacroRepSepTokenexcept delimiters and MacroRepOp

MacroRepOp* | + | ?

MacroTranscriberDelimTokenTree

macro_rules 允许用户以声明式的方式定义语法扩展。我们称这种扩展为“声明宏”或简称“宏”。

每个声明宏都有一个名称和一条或多条 规则 。每条规则包含两部分:一个 匹配器 ,描述它匹配的语法;一个 转录器 ,描述成功匹配的调用将替换成的语法。匹配器和转录器都必须由分隔符包围。宏可以展开为表达式、语句、项(包括特型、impl 和外部项)、类型或模式。

转录

当宏被调用时,宏扩展器会按名称查找宏调用,并依次尝试每条宏规则。它转录第一个成功匹配的规则;如果此操作导致错误,则不会尝试后续匹配。

匹配时,不执行前瞻(lookahead);如果编译器无法逐个词法单元明确地确定如何解析宏调用,则会报错。在下面的示例中,编译器不会在前瞻到标识符之后,去检查后面的词法单元是否是 ),即使那样可以使其明确地解析调用:

#![allow(unused)]
fn main() {
macro_rules! ambiguity {
    ($($i:ident)* $j:ident) => { };
}

ambiguity!(error); // 错误:局部歧义
}

在匹配器和转录器中,$ 词法单元用于调用宏引擎的特殊行为(在下面的元变量重复中描述)。不属于此类调用的词法单元会被字面匹配和转录,但有一个例外。这个例外是,匹配器的外部分隔符将匹配任意一对分隔符。因此,例如,匹配器 (()) 将匹配 {()} 但不匹配 {{}}。字符 $ 不能被字面匹配或转录。

转发匹配到的片段

当将一个匹配到的片段转发给另一个声明宏时,第二个宏中的匹配器将看到该片段类型的不透明 AST。第二个宏不能使用字面词法单元来匹配匹配器中的片段,只能使用相同类型的片段说明符。identlifetimett 片段类型是一个例外,它们可以通过字面词法单元进行匹配。下面示例说明了此限制:

#![allow(unused)]
fn main() {
macro_rules! foo {
    ($l:expr) => { bar!($l); }
// 错误:               ^^ 宏调用中不期望此词法单元
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

下面示例说明了在匹配 tt 片段后如何直接匹配词法单元:

#![allow(unused)]
fn main() {
// 编译通过
macro_rules! foo {
    ($l:tt) => { bar!($l); }
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

元变量

在匹配器中,$ 名称 : 片段说明符 匹配指定种类的Rust语法片段,并将其绑定到元变量 $名称

有效的片段说明符有:

在转录器中,元变量仅通过 $名称 引用,因为片段种类已在匹配器中指定。元变量会被替换为匹配它们的语法元素。元变量可以被转录多次或根本不转录。

关键字元变量 $crate 可用于指代当前 crate。

2021 版次差异

从2021版次开始,pat 片段说明符匹配顶层或模式(即,它们接受模式)。

在2021版次之前,它们匹配的片段与 pat_param 完全相同(即,它们接受PatternNoTopAlt)。

相关的版次是 macro_rules! 定义生效的版次。

2024 版次差异

在2024版次之前,expr片段说明符不匹配顶层的下划线表达式常量块表达式。它们在子表达式中是允许的。

存在expr_2021片段说明符是为了保持与2024之前的版次的向后兼容性。

重复

在匹配器和转录器中,重复是通过将要重复的词法单元放在 $() 内部,然后跟随一个重复操作符来表示的,可选地在两者之间放置一个分隔词法单元。

分隔词法单元可以是除分隔符或重复操作符之外的任何词法单元,但 ;, 最为常见。例如,$( $i:ident ),* 表示任意数量的用逗号分隔的标识符。允许嵌套重复。

重复操作符有:

  • * — 表示任意数量的重复。
  • + — 表示任意数量,但至少一个。
  • ? — 表示一个可选片段,出现零次或一次。

由于 ? 最多表示一次出现,它不能与分隔符一起使用。

重复片段既匹配又转录为指定数量的片段,并由分隔词法单元分隔。元变量匹配其相应片段的每次重复。例如,上面 $( $i:ident ),* 示例将 $i 匹配到列表中的所有标识符。

在转录期间,对重复施加了额外的限制,以便编译器知道如何正确地展开它们:

  1. 元变量在转录器中出现的重复次数、种类和嵌套顺序必须与在匹配器中完全相同。因此,对于匹配器 $( $i:ident ),*,转录器 => { $i }=> { $( $( $i )* )* }=> { $( $i )+ } 都是非法的,但 => { $( $i );* } 是正确的,它将逗号分隔的标识符列表替换为分号分隔的列表。
  2. 转录器中的每个重复必须至少包含一个元变量,以决定要展开多少次。如果在同一个重复中出现多个元变量,它们必须绑定到相同数量的片段。例如,( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* )) 必须绑定与 $j 片段相同数量的 $i 片段。这意味着用 (a, b, c; d, e, f) 调用宏是合法的,并展开为 ((a,d), (b,e), (c,f)),但 (a, b, c; d, e) 是非法的,因为它没有相同数量的片段。此要求适用于每个嵌套重复层。

作用域、导出和导入

由于历史原因,声明宏的作用域不完全像项那样工作。宏有两种形式的作用域:文本作用域和基于路径的作用域。文本作用域基于事物在源文件中出现的顺序,甚至可以跨多个文件,它是默认的作用域。这将在下面进一步解释。基于路径的作用域与项作用域的工作方式完全相同。宏的作用域、导出和导入主要由属性控制。

当宏通过非限定标识符(不属于多部分路径)调用时,它首先在文本作用域中查找。如果没有结果,则在基于路径的作用域中查找。如果宏的名称使用路径进行限定,则它只在基于路径的作用域中查找。

use lazy_static::lazy_static; // 基于路径的导入。

macro_rules! lazy_static { // 文本定义。
    (lazy) => {};
}

lazy_static!{lazy} // 文本查找首先找到我们的宏。
self::lazy_static!{} // 基于路径的查找忽略我们的宏,找到导入的宏。

文本作用域

文本作用域主要基于事物在源文件中出现的顺序,其工作方式类似于用 let 声明的局部变量的作用域,只不过它也适用于模块级别。当使用 macro_rules! 定义宏时,宏在定义之后(请注意,它仍然可以递归使用,因为名称是从调用站点查找的)进入作用域,直到其周围的作用域(通常是模块)关闭。这可以进入子模块,甚至跨越多个文件:

//// src/lib.rs
mod has_macro {
    // m!{} // 错误:m 不在作用域中。

    macro_rules! m {
        () => {};
    }
    m!{} // 正常:出现在 m 的声明之后。

    mod uses_macro;
}

// m!{} // 错误:m 不在作用域中。

//// src/has_macro/uses_macro.rs

m!{} // 正常:出现在 src/lib.rs 中 m 的声明之后。

多次定义宏不是错误;除非前一个声明已超出作用域,否则最近的声明将遮蔽前一个。

#![allow(unused)]
fn main() {
macro_rules! m {
    (1) => {};
}

m!(1);

mod inner {
    m!(1);

    macro_rules! m {
        (2) => {};
    }
    // m!(1); // 错误:没有规则匹配 '1'
    m!(2);

    macro_rules! m {
        (3) => {};
    }
    m!(3);
}

m!(1);
}

宏也可以在函数内部局部声明和使用,工作方式类似:

#![allow(unused)]
fn main() {
fn foo() {
    // m!(); // 错误:m 不在作用域中。
    macro_rules! m {
        () => {};
    }
    m!();
}

// m!(); // 错误:m 不在作用域中。
}

宏的文本作用域名称绑定会遮蔽宏的基于路径的作用域绑定。

#![allow(unused)]
fn main() {
macro_rules! m2 {
    () => {
        println!("m2");
    };
}

// 解析为下方 use 声明中的基于路径的候选。
m!(); // 打印 "m2\n"

// 引入第二个具有文本作用域的 `m` 候选。
//
// 这将遮蔽本示例其余部分中来自下方的基于路径的候选。
macro_rules! m {
    () => {
        println!("m");
    };
}

// 引入 `m2` 宏作为基于路径的候选。
//
// 此项在此整个示例中均在作用域内,而不仅仅在 use 声明下方。
use m2 as m;

// 解析为 use 声明上方具有文本作用域的宏候选。
m!(); // 打印 "m\n"
}

注意

有关不允许遮蔽的区域,请参见名称解析歧义

基于路径的作用域

默认情况下,宏没有基于路径的作用域。宏可以通过两种方式获得基于路径的作用域:

宏可以被重导出,以使其获得来自除 crate 根模块之外的模块的基于路径的作用域。

#![allow(unused)]
fn main() {
mac::m!(); // 正常:基于路径的查找在 mac 模块中找到 `m`。

mod mac {
    // 引入具有文本作用域的宏 `m`。
    macro_rules! m {
        () => {};
    }

    // 从 `m` 的文本作用域内重导出具有基于路径的作用域。
    pub(crate) use m;
}
}

宏具有隐式的 pub(crate) 可见性。#[macro_export] 将隐式可见性更改为 pub

#![allow(unused)]
fn main() {
// 隐式可见性为 `pub(crate)`。
macro_rules! private_m {
    () => {};
}

// 隐式可见性为 `pub`。
#[macro_export]
macro_rules! pub_m {
    () => {};
}

pub(crate) use private_m as private_macro; // 正常。
pub use pub_m as pub_macro; // 正常。
}
#![allow(unused)]
fn main() {
// 隐式可见性为 `pub(crate)`。
macro_rules! private_m {
    () => {};
}

// 隐式可见性为 `pub`。
#[macro_export]
macro_rules! pub_m {
    () => {};
}

pub(crate) use private_m as private_macro; // 正常。
pub use pub_m as pub_macro; // 正常。

pub use private_m; // 错误:`private_m` 仅在该 crate 内可见
                   // 且不能在外部重导出。
}

macro_use属性

macro_use 属性 有两个目的:它可以用于模块,以扩展其中定义的宏的作用域;它也可以用于 extern crate,将其他 crate 中的宏导入到macro_use预导入中。

例子

#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
    macro_rules! m {
        () => {};
    }
}
m!(); // 正常
}
#[macro_use]
extern crate log;

当用于模块时,macro_use 属性使用 MetaWord 语法。

当用于 extern crate 时,它使用 MetaWordMetaListIdents 语法。有关这些语法如何使用的更多信息,请参见macro.decl.scope.macro_use.prelude

macro_use 属性可以应用于模块或 extern crate

注意

rustc 会忽略在其他位置的使用,但会对其进行 lint。这在将来可能会成为错误。

macro_use 属性不能用于extern crate self

macro_use 属性可以在一个形式上使用任意次数。

可以指定 MetaListIdents 语法中的 macro_use 多个实例。所有指定的宏的并集将被导入。

注意

在模块上,rustc 会对第一个 MetaWord macro_use 属性之后的任何 macro_use 属性进行 lint。

extern crate 上,rustc 会对任何由于未导入已被另一个 macro_use 属性导入的宏而没有效果的 macro_use 属性进行 lint。如果两个或更多 MetaListIdents macro_use 属性导入相同的宏,则会对第一个进行 lint。如果存在任何 MetaWord macro_use 属性,则会对所有 MetaListIdents macro_use 属性进行 lint。如果存在两个或更多 MetaWord macro_use 属性,则会对第一个之后的属性进行 lint。

macro_use 用于模块时,模块的宏作用域会超出模块的词法作用域。

例子

#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
    macro_rules! m {
        () => {};
    }
}
m!(); // 正常
}

在 crate 根目录中的 extern crate 声明上指定 macro_use 会从该 crate 导入导出的宏。

以这种方式导入的宏被导入到macro_use预导入中,而不是文本式地,这意味着它们可以被任何其他名称遮蔽。macro_use 导入的宏可以在 import 语句之前使用。

注意

rustc 目前在冲突时会优先选择最后导入的宏。请不要依赖此行为。这种行为很不寻常,因为 Rust 中的导入通常与顺序无关。macro_use 的这种行为在将来可能会改变。

有关详细信息,请参见 Rust问题单#148025

当使用 MetaWord 语法时,所有导出的宏都会被导入。当使用 MetaListIdents 语法时,只有指定的宏会被导入。

例子

#[macro_use(lazy_static)] // 或者 `#[macro_use]` 导入所有宏。
extern crate lazy_static;

lazy_static!{}
// self::lazy_static!{} // 错误:`lazy_static` 未在 `self` 中定义。

要使用 macro_use 导入的宏必须使用 macro_export 导出。

macro_export属性

macro_export 属性 将宏从 crate 导出,并使其在 crate 根目录中可用,以进行基于路径的解析。

例子

#![allow(unused)]
fn main() {
self::m!();
//  ^^^^ 正常:基于路径的查找在当前模块中找到 `m`。
m!(); // 同上。

mod inner {
    super::m!();
    crate::m!();
}

mod mac {
    #[macro_export]
    macro_rules! m {
        () => {};
    }
}
}

macro_export 属性使用 MetaWordMetaListIdents 语法。使用 MetaListIdents 语法时,它接受单个 local_inner_macros 值。

macro_export 属性可以应用于 macro_rules 定义。

注意

rustc 会忽略在其他位置的使用,但会对其进行 lint。这在将来可能会成为错误。

宏上只有第一次使用 macro_export 有效。

注意

rustc 会对第一次使用之后的任何使用进行 lint。

默认情况下,宏只有文本作用域,不能通过路径解析。当使用 macro_export 属性时,该宏会在 crate 根目录中可用,并可以通过其路径引用。

例子

没有 macro_export 时,宏只有文本作用域,因此宏的基于路径的解析会失败。

macro_rules! m {
    () => {};
}
self::m!(); // 错误
crate::m!(); // 错误
fn main() {}

有了 macro_export,基于路径的解析就有效了。

#[macro_export]
macro_rules! m {
    () => {};
}
self::m!(); // 正常
crate::m!(); // 正常
fn main() {}

macro_export 属性导致宏从 crate 根目录导出,以便其他 crate 可以通过路径引用它。

例子

给定 log crate 中的以下内容:

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! warn {
    ($message:expr) => { eprintln!("WARN: {}", $message) };
}
}

从另一个crate中,你可以通过路径引用宏:

fn main() {
    log::warn!("example warning");
}

macro_export 允许在 extern crate 上使用 macro_use,将宏导入到 macro_use预导入中。

例子

给定 log crate 中的以下内容:

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! warn {
    ($message:expr) => { eprintln!("WARN: {}", $message) };
}
}

在依赖 crate 中使用 macro_use 允许你从预导入中使用宏:

#[macro_use]
extern crate log;

pub mod util {
    pub fn do_thing() {
        // 通过宏预导入解析。
        warn!("example warning");
    }
}

local_inner_macros 添加到 macro_export 属性会使宏定义中的所有单段宏调用具有隐式的 $crate:: 前缀。

注意

这主要旨在作为一种工具,用于迁移在语言中添加 $crate 之前编写的代码,以使其与 Rust 2018 基于路径的宏导入协同工作。不建议在新代码中使用它。

例子

#![allow(unused)]
fn main() {
#[macro_export(local_inner_macros)]
macro_rules! helped {
    () => { helper!() } // 自动转换为 $crate::helper!()。
}

#[macro_export]
macro_rules! helper {
    () => { () }
}
}

卫生

声明宏具有 混合站点卫生 。这意味着循环标签块标签和局部变量在宏定义站点查找,而其他符号在宏调用站点查找。例如:

#![allow(unused)]
fn main() {
let x = 1;
fn func() {
    unreachable!("this is never called")
}

macro_rules! check {
    () => {
        assert_eq!(x, 1); // 使用定义站点的 `x`。
        func();           // 使用调用站点的 `func`。
    };
}

{
    let x = 2;
    fn func() { /* 不会恐慌 */ }
    check!();
}
}

在宏展开中定义的标签和局部变量不共享于不同的调用之间,因此此代码无法编译:

#![allow(unused)]
fn main() {
macro_rules! m {
    (define) => {
        let x = 1;
    };
    (refer) => {
        dbg!(x);
    };
}

m!(define);
m!(refer);
}

一个特殊情况是 $crate 元变量。它指代定义宏的 crate,可以用于路径的开头,以查找在调用站点不在作用域内的项或宏。

//// `helper_macro` crate 中的定义。
#[macro_export]
macro_rules! helped {
    // () => { helper!() } // 这可能会因 'helper' 不在作用域中而导致错误。
    () => { $crate::helper!() }
}

#[macro_export]
macro_rules! helper {
    () => { () }
}

//// 在另一个 crate 中的使用。
// 请注意,`helper_macro::helper` 并未导入!
use helper_macro::helped;

fn unit() {
    helped!();
}

请注意,由于 $crate 指代当前 crate,因此在引用非宏项时必须与完全限定的模块路径一起使用:

#![allow(unused)]
fn main() {
pub mod inner {
    #[macro_export]
    macro_rules! call_foo {
        () => { $crate::inner::foo() };
    }

    pub fn foo() {}
}
}

此外,即使 $crate 允许宏在展开时引用其自身 crate 内的项,其使用对可见性也没有影响。被引用的项或宏仍必须从调用站点可见。在下面的示例中,任何尝试从其 crate 外部调用 call_foo!() 的操作都会失败,因为 foo() 不是公共的。

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! call_foo {
    () => { $crate::foo() };
}

fn foo() {}
}

注意

在 Rust 1.30 之前,不支持 $cratelocal_inner_macros。它们是与基于路径的宏导入一起添加的,以确保辅助宏不需要由导出宏的 crate 用户手动导入。为早期 Rust 版本编写的、使用辅助宏的 crate 需要修改以使用 $cratelocal_inner_macros,才能与基于路径的导入良好协作。

后续集歧义限制

宏系统使用的解析器功能相当强大,但它受到限制,以防止在语言的当前或未来版本中出现歧义。

具体来说,除了关于歧义展开的规则之外,元变量匹配的非终结符之后必须跟随一个已被确定可以安全地用于该类匹配的词法单元。

例如,像 $i:expr [ , ] 这样的宏匹配器理论上今天可以在 Rust 中接受,因为 [,] 不能是合法表达式的一部分,因此解析将始终是明确的。然而,由于 [ 可以开始尾随表达式,[ 不是一个可以安全地排除在表达式之后出现的字符。如果在后续的 Rust 版本中接受 [,],这个匹配器将变得模糊或解析错误,从而破坏现有代码。然而,像 $i:expr,$i:expr; 这样的匹配器是合法的,因为 ,; 是合法的表达式分隔符。具体规则是:

  • exprstmt 只能跟随以下之一:=>,;
  • pat_param 只能跟随以下之一:=>,=|ifin
  • pat 只能跟随以下之一:=>,=ifin
  • pathty 只能跟随以下之一:=>,=|;:>>>[{aswhereblock 片段说明符的宏变量。
  • vis 只能跟随以下之一:,,非原始 priv 之外的标识符,任何可以开始类型的词法单元,或具有 identtypath 片段说明符的元变量。
  • 所有其他片段说明符都没有限制。

2021 版次差异

在 2021 版次之前,pat 后面也可以跟随 |

当涉及重复时,规则适用于所有可能的展开次数,并考虑分隔符。这意味着:

  • 如果重复包含分隔符,则该分隔符必须能够跟随重复的内容。
  • 如果重复可以多次重复(*+),则内容必须能够跟随自身。
  • 重复的内容必须能够跟随其之前的内容,并且其之后的内容必须能够跟随重复的内容。
  • 如果重复可以匹配零次(*?),则其之后的内容必须能够跟随其之前的内容。

有关更多详细信息,请参见形式化规范

过程宏

过程宏允许通过执行函数来创建语法扩展。 过程宏分为三种类型:

过程宏允许你在编译时运行操作Rust语法的代码,既可以消费Rust语法,也可以生成Rust语法。你可以将过程宏视为从一个AST到另一个AST的函数。

过程宏必须定义在crate类型proc-macro的crate根目录中。 宏不能在其定义的crate中使用,只能在另一个crate中导入后才能使用。

注意

当使用Cargo时,过程宏crate通过清单中的proc-macro键定义:

[lib]
proc-macro = true

作为函数,它们必须返回语法、恐慌,或无限循环。返回的语法会根据过程宏的种类替换或添加语法。编译器会捕获恐慌,并将其转换为编译器错误。无限循环不会被编译器捕获,这会导致编译器挂起。

过程宏在编译期间运行,因此拥有与编译器相同的资源。例如,标准输入、错误和输出与编译器可访问的相同。同样,文件访问也相同。因此,过程宏具有与Cargo的构建脚本相同的安全问题。

过程宏有两种报告错误的方式。第一种是恐慌。第二种是发出一个compile_error宏调用。

proc_macro crate

过程宏crate几乎总是会链接到编译器提供的proc_macro crateproc_macro crate提供了编写过程宏所需的类型和使其更方便的工具。

这个crate主要包含词法单元流类型。过程宏操作词法单元流而不是AST节点,这对于编译器和过程宏来说都是一个更稳定的长期接口。词法单元流大致等同于Vec<TokenTree>,其中TokenTree大致可以被认为是词法单元。例如,foo是一个Ident词法单元,.是一个Punct词法单元,1.2是一个Literal词法单元。词法单元流类型与Vec<TokenTree>不同,克隆开销很小。

所有词法单元都带有一个关联的SpanSpan是一个不透明的值,不能被修改但可以被创建。Span代表程序中源代码的范围,主要用于错误报告。虽然你不能修改Span本身,但你总是可以更改与任何词法单元关联的Span,例如通过从另一个词法单元获取Span

过程宏卫生

过程宏是不卫生的。这意味着它们的行为就如同输出词法单元流被直接内联写入其旁边的代码一样。这意味着它受外部项影响,也影响外部导入。

宏作者需要小心,以确保他们的宏在给定此限制的情况下,尽可能在更多上下文中工作。这通常包括使用库中项的绝对路径(例如,::std::option::Option而不是Option),或者通过确保生成的函数具有不太可能与其他函数冲突的名称(如__internal_foo而不是foo)。

proc_macro属性

proc_macro属性 定义一个函数式过程宏。

例子

此宏定义忽略其输入,并在其作用域中发出一个answer函数

#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

我们可以在一个二进制crate中使用它来打印“42”到标准输出。

extern crate proc_macro_examples;
use proc_macro_examples::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

proc_macro属性使用MetaWord语法。

proc_macro属性只能应用于fn(TokenStream) -> TokenStream类型的pub函数,其中词法单元流来自proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于crate的根目录中。

proc_macro属性在一个函数上只能指定一次。

proc_macro属性在crate根目录中的宏命名空间中公开定义宏,其名称与函数同名。

一个函数式过程宏的函数式宏调用会将宏调用分隔符内的内容作为输入词法单元流参数传递,并用函数的输出词法单元流替换整个宏调用。

函数式过程宏可以在任何宏调用位置被调用,包括:

proc_macro_derive属性

proc_macro_derive属性 应用于函数定义一个 派生宏 ,它可以通过 derive属性 调用。这些宏会获得一个结构体枚举联合体定义的词法单元流,并可以在其后发出新的。它们还可以声明和使用派生宏辅助属性

例子

这个派生宏忽略其输入,并添加定义一个函数的词法单元。

#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

要使用它,我们可以这样写:

extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;

#[derive(AnswerFn)]
struct Struct;

fn main() {
    assert_eq!(42, answer());
}

proc_macro_derive属性的语法格式是:

Syntax
ProcMacroDeriveAttribute
    proc_macro_derive ( DeriveMacroName ( , DeriveMacroAttributes )? ,? )

DeriveMacroNameIDENTIFIER

DeriveMacroAttributes
    attributes ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )

派生宏的名称由DeriveMacroName给出。可选的attributes参数在macro.proc.derive.attributes中描述。

proc_macro_derive属性只能应用于定义在crate根目录中,并带有Rust ABIpub函数,其类型fn(TokenStream) -> TokenStream,其中词法单元流来自proc_macro crate。该函数可以是const,并且可以使用extern显式指定 Rust ABI,但不能使用任何其他限定符(例如,它不能是asyncunsafe)。

proc_macro_derive属性在一个函数上只能使用一次。

proc_macro_derive属性在crate的根目录中的宏命名空间中公开定义派生宏。

输入词法单元流是应用derive属性的的词法单元流。输出词法单元流必须是(可能为空的)一组。这些会在输入之后,在同一模块内被追加。

派生宏辅助属性

派生宏可以声明 派生宏辅助属性 ,用于派生宏所应用的的作用域内。这些属性惰性的。虽然它们的目的被声明它们的宏使用,但它们可以被任何宏看到。

派生宏的辅助属性通过将其标识符添加到proc_macro_derive属性的attributes列表中来声明。

例子

这声明了一个辅助属性然后忽略了它。

#![crate_type="proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_derive(WithHelperAttr, attributes(helper))]
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}

要使用它,我们可以这样写:

extern crate proc_macro_examples;
use proc_macro_examples::WithHelperAttr;

#[derive(WithHelperAttr)]
struct Struct {
    #[helper] field: (),
}

当一个派生宏调用应用于一个时,该派生宏引入的辅助属性会在以下情况下生效:1) 用于应用于该并在派生宏调用后词法应用的属性,以及 2) 用于应用于内部的字段和变体的属性

注意

rustc目前允许在引入它们的宏之前使用派生辅助。这种乱序使用的派生辅助可能不会遮蔽其他属性宏。此行为已被弃用,并计划移除。

#[helper] // 已弃用,未来将成为硬错误。
#[derive(WithHelperAttr)]
struct Struct {
    field: (),
}

更多详情,请参见Rust问题单#79202

proc_macro_attribute属性

proc_macro_attribute属性 定义一个 属性宏 ,它可以作为外部属性使用。

例子

此属性宏接受输入流并原样发出,实际上是一个空操作属性。

#![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
}

例子

这显示了在编译器输出中,属性宏所看到的字符串化词法单元流

// my-macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    println!("attr: \"{attr}\"");
    println!("item: \"{item}\"");
    item
}
// src/lib.rs
extern crate my_macro;

use my_macro::show_streams;

// 示例:基本函数。
#[show_streams]
fn invoke1() {}
// 输出:attr: ""
// 输出:item: "fn invoke1() {}"

// 示例:带输入的属性。
#[show_streams(bar)]
fn invoke2() {}
// 输出:attr: "bar"
// 输出:item: "fn invoke2() {}"

// 示例:输入中有多个词法单元。
#[show_streams(multiple => tokens)]
fn invoke3() {}
// 输出:attr: "multiple => tokens"
// 输出:item: "fn invoke3() {}"

// 示例:输入中有分隔符。
#[show_streams { delimiters }]
fn invoke4() {}
// 输出:attr: "delimiters"
// 输出:item: "fn invoke4() {}"

proc_macro_attribute属性使用MetaWord语法。

proc_macro_attribute属性只能应用于fn(TokenStream, TokenStream) -> TokenStream类型的pub函数,其中词法单元流来自 proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于crate的根目录中。

proc_macro_attribute属性在一个函数上只能指定一次。

proc_macro_attribute属性在crate的根目录中的宏命名空间中定义属性,其名称与函数同名。

属性宏只能用于:

第一个词法单元流参数是属性名称后的带分隔符词法单元树,但不包括外部分隔符。如果应用的属性只包含属性名称,或属性名称后跟空分隔符,则词法单元流为空。

第二个词法单元流的其余部分,包括上的其他属性

应用属性的会被返回的词法单元流中的零个或多个替换。

声明宏词法单元和过程宏词法单元

声明式macro_rules宏和过程宏对词法单元(或更确切地说,TokenTrees)使用相似但不同的定义。

macro_rules中的词法单元树(对应tt匹配器)定义如下:

  • 带分隔符的组((...){...}等)
  • 语言支持的所有运算符,包括单字符和多字符的(++=)。
    • 请注意,此集合不包括单引号'
  • 字面量("string"1等)
    • 请注意,负号(例如-1)永远不会是此类字面量词法单元的一部分,而是一个单独的运算符词法单元。
  • 标识符,包括关键字(identr#identfn
  • 生命周期('ident
  • macro_rules中的元变量替换(例如macro_rules! mac { ($my_expr: expr) => { $my_expr } }mac展开后的$my_expr,无论传递的表达式如何,都将被视为单个词法单元树)

过程宏中的词法单元树定义如下:

  • 带分隔符的组((...){...}等)
  • 语言支持的运算符中使用的所有标点符号(+,但不包括+=),以及单引号'字符(通常用于生命周期,有关生命周期拆分和合并行为,请参见下文)
  • 字面量("string"1等)
    • 负号(例如-1)作为整数和浮点字面量的一部分受支持。
  • 标识符,包括关键字(identr#identfn

当词法单元流在过程宏之间传递时,会考虑这两种定义之间的不匹配。 请注意,下面的转换可能以惰性方式发生,因此如果词法单元未被实际检查,则可能不会发生转换。

当传递给过程宏时

  • 所有多字符运算符都会被拆分为单字符。
  • 生命周期会被拆分为一个'字符和一个标识符。
  • 关键字元变量 $crate 作为单个标识符传递。
  • 所有其他元变量替换都表示为其底层词法单元流。
    • 此类词法单元流可能被包装到带分隔符的组(Group)中,使用隐式分隔符(Delimiter::None),当需要保留解析优先级时。
    • ttident替换永远不会被包装到此类组中,并且始终表示为其底层词法单元树。

当从过程宏发出时

  • 标点符号在适用时会被组合成多字符运算符。
  • 与标识符连接的单引号'会被组合成生命周期。
  • 负字面量会被转换为两个词法单元(-和字面量),可能被包装到带分隔符的组(Group)中,使用隐式分隔符(Delimiter::None),当需要保留解析优先级时。

请注意,声明宏和过程宏都不支持文档注释词法单元(例如/// Doc),因此当它们被传递给宏时,总是会被转换为表示其等效#[doc = r"str"]属性的词法单元流。

Crate与源文件

Syntax
Crate
    InnerAttribute*
    Item*

注意

尽管Rust与其他任何语言一样,既可以由解释器实现,也可以由编译器实现,但目前唯一的实现是编译器,并且该语言始终被设计为可编译的。因此,本节假定存在一个编译器。

Rust的语义遵循编译期与运行期之间的 阶段区别1。具有 静态解释 的语义规则决定编译的成功或失败,而具有 动态解释 的语义规则决定程序在运行时的行为。

编译模型围绕称为 crate 的制品展开。每次编译都会处理一个源代码形式的单个crate,如果成功,则生成一个二进制形式的单个crate:一个可执行文件或某种库。2

一个 crate 是编译和链接的单元,也是版本控制、分发和运行时加载的单元。一个crate包含一个嵌套的模块作用域_树_。这棵树的顶层是一个匿名模块(从模块内部路径的角度来看),并且crate内的任何都有一个规范的模块路径,表示其在crate的模块树中的位置。

Rust编译器总是以单个源文件作为输入被调用,并总是生成单个输出crate。该源文件的处理可能会导致其他源文件作为模块被加载。源文件的扩展名为.rs

一个Rust源文件描述了一个模块,该模块的名称和位置——在当前crate的模块树中——是从源文件外部定义的:可以通过引用源文件中的显式Module项,或者通过crate本身的名称来定义。

每个源文件都是一个模块,但并非每个模块都需要自己的源文件:模块定义可以嵌套在一个文件中。

每个源文件包含零个或多个定义序列,并且可以选择以任意数量应用于包含模块的属性开头,其中大部分会影响编译器的行为。

匿名crate模块可以拥有适用于整个crate的额外属性。

注意

文件内容可以由shebang开头。

#![allow(unused)]
fn main() {
// 指定crate名称。
#![crate_name = "projx"]

// 指定输出制品类型。
#![crate_type = "lib"]

// 开启一个警告。
// 这可以在任何模块中完成,而不仅仅是匿名crate模块。
#![warn(non_camel_case_types)]
}

main函数

包含main函数的crate可以编译成可执行文件。

如果存在main函数,它必须不接受任何参数,不得声明任何特型或生命周期边界,不得有任何where子句,并且其返回类型必须实现Termination特型。

fn main() {}
fn main() -> ! {
    std::process::exit(0);
}
fn main() -> impl std::process::Termination {
    std::process::ExitCode::SUCCESS
}

main函数可以是一个导入,例如来自外部crate或当前crate。

#![allow(unused)]
fn main() {
mod foo {
    pub fn bar() {
        println!("Hello, world!");
    }
}
use foo::bar as main;
}

注意

标准库中实现了Termination的类型包括:

未捕获的外部unwinding

当“外部“unwind(例如从C++代码抛出的异常,或使用不同恐慌处理器的Rust代码中的panic!)传播超出main函数时,进程将安全终止。这可能以中止的形式发生,在这种情况下,不保证会执行任何Drop调用,并且错误输出可能不如运行时被“原生“Rust panic终止时那么详细。

欲了解更多信息,请参阅恐慌文档

no_main属性

no_main属性 可以应用于crate级别,以禁用为可执行二进制文件发出main符号。当链接到某个其他对象定义了main时,这会很有用。

crate_name属性

crate_name属性 可以应用于crate级别,使用MetaNameValueStr语法格式指定crate的名称。

#![allow(unused)]
#![crate_name = "mycrate"]
fn main() {
}

crate名称不能为空,并且只能包含Unicode字母数字字符或_(U+005F)字符。


  1. 这种区别也存在于解释器中。静态检查,如语法分析、类型检查和linting,无论程序何时执行,都应该在程序执行之前进行。

  2. 一个crate在某种程度上类似于ECMA-335 CLI模型中的assembly,SML/NJ Compilation Manager中的 库(library),Owens和Flatt模块系统中的 单元(unit),或Mesa中的 配置(configuration)

条件编译

条件编译的源代码 是指仅在特定条件下才会被编译的源代码。

可以使用cfgcfg_attr 属性以及内置的cfg!宏来使源代码条件编译。

是否进行编译可以取决于被编译的crate的目标架构、传递给编译器的任意值,以及下面进一步描述的其他因素。

每种形式的条件编译都接受一个 配置断言 ,该断言求值为真或假。该断言是以下形式之一:

  • 一个配置选项。如果该选项已设置,则断言为真;如果未设置,则为假。
  • all(),带有一个逗号分隔的配置断言列表。如果所有给定的断言都为真,或者列表为空,则它为真。
  • any(),带有一个逗号分隔的配置断言列表。如果至少有一个给定的断言为真,则它为真。如果没有断言,则它为假。
  • not(),带有一个配置断言。如果其断言为假,则它为真;如果其断言为真,则它为假。
  • truefalse字面量,分别总是为真或假。

配置选项 可以是名称,也可以是键值对,它们要么已设置,要么未设置。

名称写作单个标识符,例如unix

键值对写作一个标识符、=,然后是一个字符串,例如target_arch = "x86_64"

注意

=周围的空白符会被忽略,所以foo="bar"foo = "bar"是等效的。

键不需要是唯一的。例如,feature = "std"feature = "serde"可以同时设置。

已设置的配置选项

哪些配置选项被设置是在crate编译期间静态确定的。

一些选项是 编译器设置的,基于编译的相关数据。

其他选项是 任意设置的,基于代码外部传递给编译器的输入。

无法在被编译的crate的源代码内部设置配置选项。

注意

对于rustc,任意设置的配置选项使用--cfg标志设置。指定目标的配置值可以使用rustc --print cfg --target $TARGET显示。

注意

键为feature的配置选项是Cargo用于指定编译时选项和可选依赖项的约定。

target_arch

键值选项,设置为目标CPU架构一次。该值类似于平台目标三元组的第一个元素,但不完全相同。

示例值:

  • "x86"
  • "x86_64"
  • "mips"
  • "powerpc"
  • "powerpc64"
  • "arm"
  • "aarch64"

target_feature

键值选项,为当前编译目标可用的每个平台特性设置。

示例值:

  • "avx"
  • "avx2"
  • "crt-static"
  • "rdrand"
  • "sse"
  • "sse2"
  • "sse4.1"

有关可用特性的更多详细信息,请参阅target_feature属性

target_feature选项还提供了一个额外的crt-static特性,用于指示静态 C 运行时是否可用。

target_os

键值选项,设置为目标的操作系统一次。该值类似于平台目标三元组的第二个和第三个元素。

示例值:

  • "windows"
  • "macos"
  • "ios"
  • "linux"
  • "android"
  • "freebsd"
  • "dragonfly"
  • "openbsd"
  • "netbsd"
  • "none"(通常用于嵌入式目标)

target_family

键值选项,提供目标的更通用描述,例如目标通常所属的操作系统或架构家族。可以设置任意数量的target_family键值对。

示例值:

  • "unix"
  • "windows"
  • "wasm"
  • "unix""wasm"两者

unixwindows

如果设置了target_family = "unix",则unix被设置。

如果设置了target_family = "windows",则windows被设置。

target_env

键值选项,包含关于目标平台ABI或libc的进一步区分信息。出于历史原因,此值仅在实际需要区分时才定义为非空字符串。因此,例如,在许多GNU平台上,此值将为空。此值类似于平台目标三元组的第四个元素。一个区别是,像gnueabihf这样的嵌入式ABI将简单地将target_env定义为"gnu"

示例值:

  • ""
  • "gnu"
  • "msvc"
  • "musl"
  • "sgx"
  • "sim"
  • "macabi"

target_abi

键值选项,包含关于目标ABI的进一步区分信息。

出于历史原因,此值仅在实际需要区分时才定义为非空字符串。因此,例如,在许多GNU平台上,此值将为空。

示例值:

  • ""
  • "llvm"
  • "eabihf"
  • "abi64"

target_endian

键值选项,根据目标CPU的字节序设置为"little""big"

target_pointer_width

键值选项,设置为目标指针宽度(以位为单位)一次。

示例值:

  • "16"
  • "32"
  • "64"

target_vendor

键值选项,设置为目标供应商一次。

示例值:

  • "apple"
  • "fortanix"
  • "pc"
  • "unknown"

target_has_atomic

键值选项,为目标支持原子加载、存储和比较并交换操作的每个位宽设置。

当此cfg存在时,所有稳定的core::sync::atomicAPI都可用于相关的原子位宽。

可能的值:

  • "8"
  • "16"
  • "32"
  • "64"
  • "128"
  • "ptr"

test

在编译测试harness时启用。通过rustc使用--test标志来完成。有关测试支持的更多信息,请参见测试

debug_assertions

默认情况下在没有优化的情况下编译时启用。这可用于在开发中启用额外的调试代码,但在生产中不启用。例如,它控制标准库的debug_assert!宏的行为。

proc_macro

当被编译的crate使用proc_macro crate类型编译时设置。

panic

键值选项,根据恐慌策略设置。请注意,将来可能会添加更多值。

示例值:

  • "abort"
  • "unwind"

条件编译的形式

cfg属性

cfg属性 根据配置断言有条件地包含其所附加的形式。

例子

#![allow(unused)]
fn main() {
// 该函数只在为 macOS 编译时才会被包含在构建中
#[cfg(target_os = "macos")]
fn macos_only() {
  // ...
}

// 该函数只在 foo 或 bar 被定义时才会被包含
#[cfg(any(foo, bar))]
fn needs_foo_or_bar() {
  // ...
}

// 该函数只在为 32 位架构的类 Unix 操作系统编译时才会被包含
#[cfg(all(unix, target_pointer_width = "32"))]
fn on_32bit_unix() {
  // ...
}

// 该函数只在 foo 未定义时才会被包含
#[cfg(not(foo))]
fn needs_not_foo() {
  // ...
}

// 该函数只在恐慌策略设置为 unwound 时才会被包含
#[cfg(panic = "unwind")]
fn when_unwinding() {
  // ...
}
}

cfg属性的语法是:

Syntax
CfgAttributecfg ( ConfigurationPredicate )

cfg属性可以在允许属性的任何地方使用。

cfg属性可以用于一个形式任意多次。如果任何cfg断言为假,则附加了这些属性的形式将不会被包含,cfg.attr.crate-level-attrs中描述的情况除外。

如果断言为真,则该形式将被重写为不带cfg属性。如果任何断言为假,则该形式将从源代码中删除。

当crate级别的cfg有一个假断言时,crate本身仍然存在。任何在cfg之前的crate属性会被保留,而任何在cfg之后的crate属性以及所有后续的crate内容都会被移除。

例子

不移除前面属性的行为允许您执行诸如包含#![no_std]以避免链接std之类的操作,即使#![cfg(...)]已经移除了crate的内容。例如:

// 即使 crate 级别的 `cfg` 属性为 false,此 `no_std` 属性也会被保留。
#![no_std]
#![cfg(false)]

// 该函数未被包含。
pub fn example() {}

cfg_attr属性

cfg_attr属性 根据配置断言有条件地包含属性。

例子

以下模块将根据目标平台,位于linux.rswindows.rs

#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(windows, path = "windows.rs")]
mod os;

cfg_attr属性的语法是:

Syntax
CfgAttrAttributecfg_attr ( ConfigurationPredicate , CfgAttrs? )

CfgAttrsAttr ( , Attr )* ,?

cfg_attr属性可以在允许属性的任何地方使用。

cfg_attr属性可以用于一个形式任意多次。

crate_typecrate_name属性不能与cfg_attr一起使用。

当配置断言为真时,cfg_attr会展开为断言后列出的属性。

可以列出零个、一个或多个属性。多个属性将分别展开为单独的属性。

例子

#[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!宏

内置的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);
}

Syntax
Item
    OuterAttribute* ( VisItem | MacroItem )

VisItem
    Visibility?
    (
        Module
      | ExternCrate
      | UseDeclaration
      | Function
      | TypeAlias
      | Struct
      | Enumeration
      | Union
      | ConstantItem
      | StaticItem
      | Trait
      | Implementation
      | ExternBlock
    )

MacroItem
      MacroInvocationSemi
    | MacroRulesDefinition

一个 是 crate 的一个组件。 项 在 crate 中通过一组嵌套的 模块 来组织。每个 crate 都有一个唯一的“最外层”匿名模块;crate 内的所有后续 项 在 crate 的模块树中都有 路径

项 完全在编译时确定,通常在执行期间保持固定,并可能驻留在只读内存中。

有几种类型的 项 :

项 可以在 crate 根部模块块表达式 中声明。

项 的一个子集,称为 关联项,可以在 特型实现 中声明。

项 的一个子集,称为外部项,可以在 extern 中声明。

项 可以按任何顺序定义,唯一的例外是 macro_rules,它有自己的作用域行为。

项 名称的 名称解析 允许在模块或块中引用该 项 的位置之前或之后定义它。

有关 项 的作用域规则,请参见 项作用域

模块

Syntax
Module
      unsafe? mod IDENTIFIER ;
    | unsafe? mod IDENTIFIER {
        InnerAttribute*
        Item*
      }

模块是零个或多个 的容器。

一个 模块项 (module item) 是一个模块,由大括号包围、命名,并以前缀关键字 mod 开头。 模块项 向构成 crate 的模块树中引入一个新的、具名的模块。

模块可以任意嵌套。

一个模块示例:

#![allow(unused)]
fn main() {
mod math {
    type Complex = (f64, f64);
    fn sin(f: f64) -> f64 {
        /* ... */
      unimplemented!();
    }
    fn cos(f: f64) -> f64 {
        /* ... */
      unimplemented!();
    }
    fn tan(f: f64) -> f64 {
        /* ... */
      unimplemented!();
    }
}
}

模块定义在其所在的模块或块的 类型命名空间 中。

在模块内的同一命名空间中定义多个同名 项 是错误的。有关限制和遮蔽行为的更多细节,请参阅 作用域章节

unsafe 关键字在语法上允许出现在 mod 关键字之前,但在语义层面会被拒绝。这允许宏消费该语法并利用 unsafe 关键字,然后将其从 词法单元 流中移除。

外部源文件模块

不带主体的模块从外部文件加载。当模块没有 path 属性时,文件的路径反映了逻辑 模块路径

祖先模块路径组件是目录,而模块的内容位于一个文件名与模块名相同并带有 .rs 扩展名的文件中。例如,以下模块结构可以具有对应如下的文件系统结构:

模块路径文件系统路径文件内容
cratelib.rsmod util;
crate::utilutil.rsmod config;
crate::util::configutil/config.rs

模块文件名也可以是以模块命名的目录,其内容位于该目录内名为 mod.rs 的文件中。上述示例也可以交替表示为将 crate::util 的内容放在名为 util/mod.rs 的文件中。不允许同时存在 util.rsutil/mod.rs

注意

rustc 1.30 之前,使用 mod.rs 文件是加载带有嵌套子项模块的唯一方式。鼓励使用新的命名约定,因为它更一致,并能避免在项目中出现许多名为 mod.rs 的文件。

path属性

用于加载外部文件模块的目录和文件可以通过 path 属性来影响。

对于不在内联模块块内部的模块上的 path 属性,文件路径相对于源文件所在的目录。例如,以下代码片段将根据其所在位置使用显示的路径:

#[path = "foo.rs"]
mod c;
源文件c 的文件位置c 的模块路径
src/a/b.rssrc/a/foo.rscrate::a::b::c
src/a/mod.rssrc/a/foo.rscrate::a::c

对于内联模块块内部的 path 属性,文件路径的相对位置取决于 path 属性所在的源文件类型。“mod-rs” 源文件是根模块(如 lib.rsmain.rs)和文件名为 mod.rs 的模块。“non-mod-rs” 源文件是所有其它模块文件。位于 mod-rs 文件中内联模块块内部的 path 属性的路径相对于 mod-rs 文件所在的目录,其中包含作为目录的内联模块组件。对于 non-mod-rs 文件,情况相同,不同之处在于路径起始于一个以 non-mod-rs 模块命名的目录。例如,以下代码片段将根据其所在位置使用显示的路径:

mod inline {
    #[path = "other.rs"]
    mod inner;
}
源文件inner 的文件位置inner 的模块路径
src/a/b.rssrc/a/b/inline/other.rscrate::a::b::inline::inner
src/a/mod.rssrc/a/inline/other.rscrate::a::inline::inner

结合上述关于内联模块及其内部嵌套模块的 path 属性规则的示例(适用于 mod-rs 和 non-mod-rs 文件):

#[path = "thread_files"]
mod thread {
    // 相对于此源文件的目录,从 `thread_files/tls.rs` 加载 `local_data` 模块。
    #[path = "tls.rs"]
    mod local_data;
}

模块上的属性

模块与所有 项 一样,接受外部属性。它们也接受内部属性:对于带有主体的模块是在 { 之后,或者在源文件的开头,在可选的 BOM 和 shebang 之后。

对模块有意义的内置属性有 cfgdeprecateddoclint 检查属性path 以及 no_implicit_prelude。模块也接受宏属性。

extern crate 声明

Syntax
ExternCrateextern crate CrateRef AsClause? ;

CrateRefIDENTIFIER | self

AsClauseas ( IDENTIFIER | _ )

一个 extern crate 声明 指定了对外部 crate 的依赖。

外部 crate 随后在 类型命名空间 中作为给定的 标识符 绑定到声明作用域中。

此外,如果 extern crate 出现在 crate 根中,则该 crate 名称也会被添加到 外部预导入 中,使其在所有模块的作用域内自动可用。

as 子句可用于将导入的 crate 绑定到不同的名称。

外部 crate 在编译时解析为特定的 soname,并且对该 soname 的运行时链接要求会被传递给链接器,以便在运行时加载。soname 在编译时通过扫描编译器的库路径,并将提供的可选 crate_name 与外部 crate 编译时声明的 crate_name 属性 进行匹配来解析。如果没有提供 crate_name,则假定使用默认的 name 属性,该属性等于 extern crate 声明中给出的 标识符

可以导入 self crate,这会创建到当前 crate 的绑定。在这种情况下,必须使用 as 子句来指定要绑定到的名称。

三个 extern crate 声明的示例:

extern crate pcre;

extern crate std; // 等同于:extern crate std as std;

extern crate std as ruststd; // 以另一个名称链接到 'std'

在命名 Rust crate 时,禁止使用连字符。然而,Cargo 包可以使用它们。在这种情况下,当 Cargo.toml 没有指定 crate 名称时,Cargo 会透明地将 - 替换为 _(详见 RFC 940)。

示例:

// 导入 Cargo 包 hello-world
extern crate hello_world; // 连字符被替换为下划线

下划线导入

外部 crate 依赖可以通过在 extern crate foo as _ 形式中使用下划线来声明,而不将其名称绑定到作用域内。这对于只需要链接但从不引用的 crate 可能很有用,并且可以避免被报告为未使用。

[macro_use 属性] 照常工作,并将宏名称导入到 [macro_use 预导入] 中。

no_link属性

no_link 属性 可以应用于 extern crate 项以防止链接该 crate。

注意

这很有帮助,例如,当只需要 crate 的宏时。

例子

#[no_link]
extern crate other_crate;

other_crate::some_macro!();

no_link 属性使用 MetaWord 语法。

no_link 属性只能应用于 extern crate 声明。

注意

rustc 会忽略在其他位置的使用,但会针对其发出 lint 警告。这在将来可能会变成一个错误。

extern crate 声明上只有第一次使用 no_link 有效。

注意

rustc 会针对第一次之后的任何使用发出 lint 警告。这在将来可能会变成一个错误。

use声明

Syntax
UseDeclarationuse UseTree ;

UseTree
      ( SimplePath? :: )? *
    | ( SimplePath? :: )? { ( UseTree ( , UseTree )* ,? )? }
    | SimplePath ( as ( IDENTIFIER | _ ) )?

一个 use 声明 创建一个或多个局部名称绑定,作为某个其他 路径 的同义词。通常, use 声明用于缩短引用模块 项 所需的路径。这些声明可以出现在 模块 中,通常在顶部。 一个 use 声明有时也称为 导入 (import) ,如果它是公共的,则称为 重新导出 (re-export)

use 声明支持许多方便的快捷方式:

  • 使用大括号语法 use a::b::{c, d, e::f, g::h::i}; 同时绑定具有共同前缀的路径列表。
  • 使用 self 关键字同时绑定具有共同前缀的路径列表及其共同的父模块,例如 use a::b::{self, c, d::e};
  • 使用 use p::q::r as x; 语法将目标名称重新绑定为新的局部名称。这也可以与最后两个特性一起使用: use a::b::{self as ab, c as abc}
  • 使用星号通配符语法 use a::b::*; 绑定所有匹配给定前缀的路径。
  • 多次嵌套前述特性的组,例如 use a::b::{self as ab, c, d::{*, e::f}};

use 声明的示例:

use std::collections::hash_map::{self, HashMap};

fn foo<T>(_: T){}
fn bar(map1: HashMap<String, usize>, map2: hash_map::HashMap<String, usize>){}

fn main() {
    // use 声明也可以存在于函数内部
    use std::option::Option::{Some, None};

    // 等价于 'foo(vec![std::option::Option::Some(1.0f64),
    // std::option::Option::None]);'
    foo(vec![Some(1.0f64), None]);

    // `hash_map` 和 `HashMap` 都在作用域内。
    let map1 = HashMap::new();
    let map2 = hash_map::HashMap::new();
    bar(map1, map2);
}

use可见性

默认情况下,与 项 一样, use 声明对其包含的模块是私有的。与 项 一样,如果用 pub 关键字限定, use 声明也可以是公共的。这样的 use 声明用于 重新导出 (re-export) 一个名称。因此,公共 use 声明可以 重定向 (redirect) 某个公共名称到不同的目标定义:即使是位于不同模块内具有私有规范路径的定义。

如果一系列此类重定向形成循环或无法明确解析,则它们会引发编译时错误。

重新导出的示例:

mod quux {
    pub use self::foo::{bar, baz};
    pub mod foo {
        pub fn bar() {}
        pub fn baz() {}
    }
}

fn main() {
    quux::bar();
    quux::baz();
}

在此示例中,模块 quux 重新导出了在 foo 中定义的两个公共名称。

use路径

use 项中允许的 路径 遵循 简单路径 语法格式,并且与表达式中可用的路径相似。 它们可以为以下内容创建绑定:

它们不能导入 关联项泛型参数局部变量、包含 Self 的路径或 工具属性。更多限制如下所述。

use 将为所有导入实体的 命名空间 创建绑定,但 self 导入仅从类型命名空间导入(如下所述)。 例如,以下示例说明了在两个命名空间中为相同名称创建绑定:

#![allow(unused)]
fn main() {
mod stuff {
    pub struct Foo(pub i32);
}

// 同时导入 `Foo` 类型和 `Foo` 构造器。
use stuff::Foo;

fn example() {
    let ctor = Foo; // 使用值命名空间中的 `Foo`。
    let x: Foo = ctor(123); // 使用类型命名空间中的 `Foo`。
}
}

2018 版次差异

在 2015 版次中, use 路径相对于 crate 根。例如:

mod foo {
    pub mod example { pub mod iter {} }
    pub mod baz { pub fn foobaz() {} }
}
mod bar {
    // 从 crate 根解析 `foo`。
    use foo::example::iter;
    // `::` 前缀显式地从 crate 根解析 `foo`。
    use ::foo::baz::foobaz;
}

fn main() {}

2015 版次不允许 use 声明引用 外部预导入。 因此,在 2015 版次中仍然需要 extern crate 声明才能在 use 声明中引用外部 crate。 从 2018 版次开始, use 声明可以像 extern crate 一样指定外部 crate 依赖项。

as重命名

as 关键字可用于更改导入实体的名称。 例如:

#![allow(unused)]
fn main() {
// 为函数 `foo` 创建一个非公共别名 `bar`。
use inner::foo as bar;

mod inner {
    pub fn foo() {}
}
}

大括号语法

大括号可用于路径的最后一个段中,以从前一个段导入多个实体,或者,如果前一个段不存在,则从当前作用域导入。 大括号可以嵌套,创建路径树,其中每个段组都与其父级逻辑组合以创建完整路径。

#![allow(unused)]
fn main() {
// 创建以下绑定:
// - `std::collections::BTreeSet`
// - `std::collections::hash_map`
// - `std::collections::hash_map::HashMap`
use std::collections::{BTreeSet, hash_map::{self, HashMap}};
}

一个空大括号不导入任何内容,尽管会验证其前导路径是否可访问。

2018 版次差异

在 2015 版次中,路径是相对于 crate 根的,因此像 use {foo, bar}; 这样的导入会从 crate 根导入名称 foobar,而从 2018 版次开始,这些名称是相对于当前作用域的。

self导入

关键字 self 可在 大括号语法 中使用,以其自身名称创建父实体的绑定。

mod stuff {
    pub fn foo() {}
    pub fn bar() {}
}
mod example {
    // 为 `stuff` 和 `foo` 创建绑定。
    use crate::stuff::{self, foo};
    pub fn baz() {
        foo();
        stuff::bar();
    }
}
fn main() {}

self 只从父实体的 类型命名空间 创建绑定。 例如,在以下代码中,只导入了 foo 模块:

mod bar {
    pub mod foo {}
    pub fn foo() {}
}

// 这仅导入模块 `foo`。函数 `foo` 存在于
// 值命名空间中,且未被导入。
use bar::foo::{self};

fn main() {
    foo(); //~ 错误 `foo` 是一个模块
}

注意

self 也可以用作路径的第一个段。 self 作为第一个段的用法与在 use 大括号内的用法在逻辑上是相同的;它表示父段的当前模块,或者如果没有父段,则表示当前模块。有关前导 self 的含义的更多信息,请参见路径章节中的 self

通配符导入

* 字符可用作 use 路径的最后一个段,以从前一个段的实体中导入所有可导入的实体。 例如:

#![allow(unused)]
fn main() {
// 为 `bar` 创建一个非公共别名。
use foo::*;

mod foo {
    fn i_am_private() {}
    enum Example {
        V1,
        V2,
    }
    pub fn bar() {
        // 为 `Example` 枚举的 `V1` 和 `V2` 创建局部别名。
        use Example::*;
        let x = V1;
    }
}
}

允许 项 和命名导入遮蔽来自同一 命名空间 中通配符导入的名称。 也就是说,如果同一命名空间中已由另一个 项 定义了某个名称,则通配符导入将被遮蔽。 例如:

#![allow(unused)]
fn main() {
// 这为 `clashing::Foo` 元组结构体构造器创建了一个绑定,
// 但没有导入其类型,因为那会与此处定义的 `Foo` 结构体冲突。
//
// 注意,此处的定义顺序并不重要。
use clashing::*;
struct Foo {
    field: f32,
}

fn do_stuff() {
    // 使用 `clashing::Foo` 的构造器。
    let f1 = Foo(123);
    // 结构体表达式使用上面定义的 `Foo` 结构体的类型。
    let f2 = Foo { field: 1.0 };
    // 由于通配符导入, `Bar` 也在作用域内。
    let z = Bar {};
}

mod clashing {
    pub struct Foo(pub i32);
    pub struct Bar {}
}
}

注意

对于不允许遮蔽的区域,请参见 名称解析歧义

* 不能用作第一个或中间段。

* 不能用于将模块的内容导入自身(例如 use self::*; )。

2018 版次差异

在 2015 版次中,路径是相对于 crate 根的,因此像 use *; 这样的导入是有效的,它意味着从 crate 根导入所有内容。这不能在 crate 根本身中使用。

下划线导入

可以通过使用下划线形式 use path as _ 导入 项 而不绑定到名称。这对于导入一个 特型 特别有用,以便可以使用其方法而无需导入该 特型 的符号,例如,如果该 特型 的符号可能与其他符号冲突。另一个例子是链接外部 crate 而不导入其名称。

星号通配符导入将以不可命名的形式导入带有 _ 的 项。

mod foo {
    pub trait Zoo {
        fn zoo(&self) {}
    }

    impl<T> Zoo for T {}
}

use self::foo::Zoo as _;
struct Zoo;  // 下划线导入避免与此项发生命名冲突。

fn main() {
    let z = Zoo;
    z.zoo();
}

独特的、不可命名的符号在宏展开后创建,这样宏可以安全地发出对 _ 导入的多个引用。例如,以下代码不应产生错误:

#![allow(unused)]
fn main() {
macro_rules! m {
    ($item: item) => { $item $item }
}

m!(use std as _;);
// 这将展开为:
// use std as _;
// use std as _;
}

限制

以下是有效 use 声明的限制:

  • use crate; 必须使用 as 来定义要绑定 crate 根的名称。
  • use {self}; 是一个错误;使用 self 时必须有一个前导段。
  • 与任何 项 定义一样, use 导入不能在模块或块中的同一命名空间内创建相同名称的重复绑定。
  • 包含 $crateuse 路径不允许在 macro_rules 展开中使用。
  • use 路径不能通过 类型别名 引用枚举变体。例如:
    #![allow(unused)]
    fn main() {
    enum MyEnum {
        MyVariant
    }
    type TypeAlias = MyEnum;
    
    use MyEnum::MyVariant; //~ 正常
    use TypeAlias::MyVariant; //~ 错误
    }

函数

Syntax
Function
    FunctionQualifiers fn IDENTIFIER GenericParams?
        ( FunctionParameters? )
        FunctionReturnType? WhereClause?
        ( BlockExpression | ; )

FunctionQualifiersconst? async?1 ItemSafety?2 ( extern Abi? )?

ItemSafetysafe3 | unsafe

AbiSTRING_LITERAL | RAW_STRING_LITERAL

FunctionParameters
      SelfParam ,?
    | ( SelfParam , )? FunctionParam ( , FunctionParam )* ,?

SelfParamOuterAttribute* ( ShorthandSelf | TypedSelf )

ShorthandSelf → ( & | & Lifetime )? mut? self

TypedSelfmut? self : Type

FunctionParamOuterAttribute* ( FunctionParamPattern | ... | Type4 )

FunctionParamPatternPatternNoTopAlt : ( Type | ... )

FunctionReturnType-> Type

一个 函数 (function) 由一个 (即函数的 主体 (body) )、一个名称、一组参数以及一个输出类型组成。除了名称以外,这些都是可选的。

函数使用关键字 fn 声明,它在所在的模块或块的 值命名空间 中定义给定的名称。

函数可以通过参数声明一组 输入 变量,调用者通过这些参数将实参传递给函数;函数还可以声明在完成后返回给调用者的值的 输出 类型

如果未显式说明输出类型,则默认为 单元类型

当被引用时,一个 函数 会产生一个对应的零大小的 函数项类型 的一等 ,当它被调用时,会执行对该函数的直接调用。

例如,这是一个简单的函数:

#![allow(unused)]
fn main() {
fn answer_to_life_the_universe_and_everything() -> i32 {
    return 42;
}
}

safe 函数在语义上仅允许在 extern 中使用。

函数参数

函数参数是不可反驳的 模式,因此任何在没有 else 的 let 绑定中有效的模式作为参数也同样有效:

#![allow(unused)]
fn main() {
fn first((value, _): (i32, i32)) -> i32 { value }
}

如果第一个参数是一个 SelfParam,这表明该函数是一个 方法

带有 self 参数的函数只能作为 特型实现 中的 关联函数 出现。

带有 ... 词法单元 的参数表示 变参函数,且只能用作 外部块 函数的最后一个参数。变参参数可以有一个可选的标识符,例如 args: ...

函数主体

函数的主体块在概念上被包装在另一个块中,该块首先绑定参数模式,然后 return 函数主体的值。这意味着如果块的尾随表达式被求值,它最终会返回给调用者。像往常一样,函数主体内显式的返回表达式如果被执行,将短路该隐式返回。

例如,上述函数的行为就像它被写成:

// argument_0 是从调用者传递的实际第一个参数
let (value, _) = argument_0;
return {
    value
};

没有主体块的函数以分号结尾。这种形式只能出现在 特型外部块 中。

泛型函数

一个 泛型函数 允许在其签名中出现一个或多个 参数化类型 。每个类型参数必须在函数名之后、由尖括号包围且以逗号分隔的列表中显式声明。

#![allow(unused)]
fn main() {
// foo 对 A 和 B 是泛型的

fn foo<A, B>(x: A, y: B) {
}
}

在函数签名和主体内部,类型参数的名称可以用作类型名称。

可以为类型参数指定 特型 界限,以允许在该类型的值上调用具有该 特型 的方法。这是使用 where 语法指定的:

#![allow(unused)]
fn main() {
use std::fmt::Debug;
fn foo<T>(x: T) where T: Debug {
}
}

当引用泛型函数时,其类型会根据引用的上下文进行实例化。例如,在这里调用 foo 函数:

#![allow(unused)]
fn main() {
use std::fmt::Debug;

fn foo<T>(x: &[T]) where T: Debug {
    // 细节已省略
}

foo(&[1, 2]);
}

将使用 i32 实例化类型参数 T

类型参数也可以在函数名之后的尾随 路径 组件中显式提供。如果没有足够的上下文来确定类型参数,这可能是必要的。例如,mem::size_of::<u32>() == 4

Extern函数限定符

extern 函数限定符允许提供可以使用特定 ABI 调用的函数 定义

extern "ABI" fn foo() { /* ... */ }

这些通常与 外部块 项结合使用,后者提供函数 声明 ,可用于调用函数而无需提供其 定义

unsafe extern "ABI" {
  unsafe fn foo(); /* 没有主体 */
  safe fn bar(); /* 没有主体 */
}
unsafe { foo() };
bar();

当函数 项 的 FunctionQualifiers 中省略了 "extern" Abi?* 时,将分配 ABI "Rust"。例如:

#![allow(unused)]
fn main() {
fn foo() {}
}

等同于:

#![allow(unused)]
fn main() {
extern "Rust" fn foo() {}
}

函数可以被外部代码调用,并且使用与 Rust 不同的 ABI 允许提供可以从其他编程语言(如 C)调用的函数:

#![allow(unused)]
fn main() {
// 声明一个具有 "C" ABI 的函数
extern "C" fn new_i32() -> i32 { 0 }

// 声明一个具有 "stdcall" ABI 的函数
#[cfg(any(windows, target_arch = "x86"))]
extern "stdcall" fn new_i32_stdcall() -> i32 { 0 }
}

就像 外部块 一样,当使用 extern 关键字并省略 "ABI" 时,使用的 ABI 默认为 "C"。也就是说,这段代码:

#![allow(unused)]
fn main() {
extern fn new_i32() -> i32 { 0 }
let fptr: extern fn() -> i32 = new_i32;
}

等同于:

#![allow(unused)]
fn main() {
extern "C" fn new_i32() -> i32 { 0 }
let fptr: extern "C" fn() -> i32 = new_i32;
}

展开

大多数 ABI 字符串有两种变体,一种带有 -unwind 后缀,另一种没有。Rust ABI 总是允许展开,因此没有 Rust-unwind ABI。ABI 的选择与运行时的 恐慌处理器 一起决定了从函数中展开时的行为。

下表说明了展开操作到达每种类型的 ABI 边界(使用相应 ABI 字符串的函数声明或定义)时的行为。请注意,Rust 运行时不受完全发生在另一种语言运行时内部的任何展开的影响,也无法对其产生影响,即在未到达 Rust ABI 边界的情况下抛出和捕获的展开。

panic-unwind 列是指通过 panic! 宏和类似的标准库机制进行的 恐慌,以及任何其他导致恐慌的 Rust 操作,例如数组索引越界或整数溢出。

“展开” ABI 类别是指 "Rust"(未标记为 extern 的 Rust 函数的隐式 ABI)、"C-unwind" 以及任何名称中带有 -unwind 的 ABI。“非展开” ABI 类别是指所有其他 ABI 字符串,包括 "C""stdcall"

原生展开是针对每个目标定义的。在支持抛出和捕获 C++ 异常的目标上,它指的是用于实现此功能的机制。某些平台实现了一种被称为 “强制展开” 的展开形式;Windows 上的 longjmpglibc 中的 pthread_exit 就是这样实现的。强制展开被明确排除在表中的“原生展开”列之外。

恐慌运行时ABIpanic-unwind原生展开 (非强制)
panic=unwind展开型展开展开
panic=unwind非展开型中止 (见下文注释)未定义行为
panic=abort展开型panic 在不展开的情况下中止中止
panic=abort非展开型panic 在不展开的情况下中止未定义行为

panic=unwind 的情况下,当 恐慌 被非展开 ABI 边界转变为中止时,要么不运行任何析构函数 (Drop 调用),要么运行直到 ABI 边界为止的所有析构函数。具体发生这两种行为中的哪一种是未指定的。

有关跨 FFI 边界展开的其他注意事项和限制,请参阅 恐慌文档中的相关部分

Const函数

有关 const 函数的定义,请参阅 const 函数

Async函数

函数可以被限定为 async,这也可以与 unsafe 限定符结合使用:

#![allow(unused)]
fn main() {
async fn regular_example() { }
async unsafe fn unsafe_example() { }
}

Async 函数在调用时不执行任何操作:相反,它们将参数捕获到一个 future 中。当被轮询时,该 future 将执行函数的主体。

一个 async 函数大致相当于一个返回 impl Future 并且以 async move 作为其主体的函数:

#![allow(unused)]
fn main() {
// 源码
async fn example(x: &str) -> usize {
    x.len()
}
}

大致相当于:

#![allow(unused)]
fn main() {
use std::future::Future;
// 脱糖后
fn example<'a>(x: &'a str) -> impl Future<Output = usize> + 'a {
    async move { x.len() }
}
}

实际的脱糖过程更为复杂:

  • 脱糖中的返回类型被假定为捕获了 async fn 声明中的所有生命周期参数。这可以在上面的脱糖示例中看到,它显式地超过了 'a 的生命周期,因此捕获了 'a
  • 主体中的 async move 捕获了所有函数参数,包括那些未使用或绑定到 _ 模式的参数。这确保了函数参数的销毁顺序与函数非 async 时相同,不同之处在于销毁发生在返回的 future 被完全 await 之后。

2018 版次差异

Async 函数仅从 Rust 2018 开始可用。

结合asyncunsafe

声明一个既是 async 又是 unsafe 的函数是合法的。生成的函数调用时是不安全的,并且(像任何 async 函数一样)返回一个 future。这个 future 只是一个普通的 future,因此 “await” 它不需要 unsafe 上下文:

#![allow(unused)]
fn main() {
// 返回一个在 await 时解引用 `x` 的 future。
//
// 健全性条件:在生成的 future 完成之前,`x` 必须可以安全地解引用。
async unsafe fn unsafe_example(x: *const i32) -> i32 {
  *x
}

async fn safe_example() {
    // 最初调用该函数需要 `unsafe` 块:
    let p = 22;
    let future = unsafe { unsafe_example(&p) };

    // 但这里不需要 `unsafe` 块。这将
    // 读取 `p` 的值:
    let q = future.await;
}
}

请注意,这种行为是脱糖为返回 impl Future 的函数的结果 —— 在这种情况下,我们脱糖到的函数是一个 unsafe 函数,但返回值保持不变。

Unsafe 在 async 函数上的使用方式与在其他函数上的使用方式完全相同:它表明该函数对调用者施加了一些额外的义务以确保健全性。与任何其他 unsafe 函数一样,这些条件可能会延伸到初始调用本身之外 —— 例如,在上面的代码片段中,unsafe_example 函数接受一个指针 x 作为参数,然后 (在被 await 时) 解引用该指针。这意味着 x 必须在 future 完成执行之前保持有效,而确保这一点是调用者的责任。

函数上的属性

函数允许使用 外部属性内部属性 允许直接在函数主体 内部的 { 之后使用。

这个例子展示了函数上的内部属性。该函数仅使用单词 “Example” 进行文档说明。

#![allow(unused)]
fn main() {
fn documented() {
    #![doc = "Example"]
}
}

注意

除了 lint 之外,在函数 项 上仅使用外部属性是符合习惯的。

对函数有意义的属性有:

函数参数上的属性

函数参数允许使用 外部属性,允许的 内置属性 限于 cfgcfg_attrallowwarndenyforbid

#![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) {
}

  1. async 限定符在 2015 版次中是不允许的。

  2. 与 Rust 2024 之前的版次相关 :在 extern 块内部,仅当 extern 被限定为 unsafe 时,才允许使用 safeunsafe 函数限定符。

  3. safe 函数限定符在语义上仅允许在 extern 块内部。

  4. 在 2015 版次中,仅包含类型的函数参数仅允许出现在 特型项 的关联函数中。

类型别名

Syntax
TypeAlias
    type IDENTIFIER GenericParams? ( : TypeParamBounds )?
        WhereClause?
        ( = Type WhereClause? )? ;

类型别名 为其所在的模块或代码块的 类型命名空间 中的现有 类型 定义一个新名称。 类型别名使用关键字 type 声明。 每个值都有一个单一、具体的类型,但可能实现几个不同的 特型,并且可能与几种不同的类型约束兼容。

例如,下面将类型 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); // 正常
let _ = TypeAlias(5); // 无法工作
}

当类型别名不作为 关联类型 使用时,必须包含一个 类型 且不得包含 TypeParamBounds

当类型别名在 特型 中作为 关联类型 使用时,不得包含 类型 规范,但可以包含 TypeParamBounds

当类型别名在 特型实现 中作为 关联类型 使用时,必须包含一个 类型 规范,且不得包含 TypeParamBounds

特型实现 的类型别名中,等号之前的 Where 子句(如 type TypeAlias<T> where T: Foo = Bar<T>)已被弃用。等号之后的 Where 子句(如 type TypeAlias<T> = Bar<T> where T: Foo)是首选。

结构体

结构体 是一个名义 结构体类型 ,使用关键字 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 )是对名义 枚举类型 以及一组 构造函数 的同步定义,这些构造函数可用于创建或模式匹配相应枚举类型的值。

枚举使用关键字 enum 声明。

enum 声明在其所在的模块或代码块的 类型命名空间 中定义枚举类型。

一个 enum 项及其用法的示例:

#![allow(unused)]
fn main() {
enum Animal {
    Dog,
    Cat,
}

let mut a: Animal = Animal::Dog;
a = Animal::Cat;
}

枚举构造函数可以具有命名或未命名字段:

#![allow(unused)]
fn main() {
enum Animal {
    Dog(String, f64),
    Cat { name: String, weight: f64 },
}

let mut a: Animal = Animal::Dog("Cocoa".to_string(), 37.2);
a = Animal::Cat { name: "Spotty".to_string(), weight: 2.7 };
}

在此示例中,Cat类结构体枚举变体 ,而 Dog 仅被称为枚举变体。

不包含字段的构造函数的枚举称为 无字段枚举 。例如,这是一个无字段枚举:

#![allow(unused)]
fn main() {
enum Fieldless {
    Tuple(),
    Struct{},
    Unit,
}
}

如果无字段枚举仅包含单元变体,则该枚举称为 仅单元枚举 。例如:

#![allow(unused)]
fn main() {
enum Enum {
    Foo = 3,
    Bar = 2,
    Baz = 1,
}
}

变体构造函数类似于 结构体 定义,并且可以通过枚举名称的路径进行引用,包括在 use 声明 中。

每个变体都在 类型命名空间 中定义其类型,尽管该类型不能用作类型说明符。 类元组和类单元变体还在 值命名空间 中定义了一个构造函数。

类结构体变体可以使用 结构体表达式 实例化。

类元组变体可以使用 调用表达式结构体表达式 实例化。

类单元变体可以使用 路径表达式结构体表达式 实例化。 例如:

#![allow(unused)]
fn main() {
enum Examples {
    UnitLike,
    TupleLike(i32),
    StructLike { value: i32 },
}

use Examples::*; // 为所有变体创建别名。
let x = UnitLike; // 常量项的路径表达式。
let x = UnitLike {}; // 结构体表达式。
let y = TupleLike(123); // 调用表达式。
let y = TupleLike { 0: 123 }; // 使用整数字段名称的结构体表达式。
let z = StructLike { value: 123 }; // 结构体表达式。
}

判别值

每个枚举实例都有一个 判别值 :逻辑上与之关联的整数,用于确定它持有哪个变体。

Rust 表示 下,判别值被解释为一个 isize 值。但是,编译器允许在其实际内存布局中使用更小的类型(或其他区分变体的方法)。

分配判别值

显式判别值

在两种情况下,变体的判别值可以通过在变体名称后跟 = 和一个 常量表达式 来显式设置:

  1. 如果该枚举是 “仅单元” 的。
  1. 如果使用了 原生表示 。例如:

    #![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
    Baz = 123,      // 123
    Quux,           // 124
}

let baz_discriminant = Foo::Baz as u32;
assert_eq!(baz_discriminant, 123);
}

限制

当两个变体共享同一个判别值时,这是一个错误。

#![allow(unused)]
fn main() {
enum SharedDiscriminantError {
    SharedA = 1,
    SharedB = 1
}

enum SharedDiscriminantError2 {
    Zero,       // 0
    One,        // 1
    OneToo = 1  // 1 (与前一个冲突!)
}
}

如果前一个判别值是判别值大小的最大值,而在其后紧跟一个未指定的判别值,这也是一个错误。

#![allow(unused)]
fn main() {
#[repr(u8)]
enum OverflowingDiscriminantError {
    Max = 255,
    MaxPlusOne // 本该是 256,但那会导致枚举溢出。
}

#[repr(u8)]
enum OverflowingDiscriminantError2 {
    MaxMinusOne = 254, // 254
    Max,               // 255
    MaxPlusOne         // 本该是 256,但那会导致枚举溢出。
}
}

访问判别值

通过 mem::discriminant

std::mem::discriminant 返回对枚举值判别值的不透明引用,该引用可以进行比较。这不能用于获取判别值的值。

转换

如果枚举是 仅单元 的(没有元组和结构体变体),则可以使用 数值转换 直接访问其判别值;例如:

#![allow(unused)]
fn main() {
enum Enum {
    Foo,
    Bar,
    Baz,
}

assert_eq!(0, Enum::Foo as isize);
assert_eq!(1, Enum::Bar as isize);
assert_eq!(2, Enum::Baz as isize);
}

无字段枚举 在没有显式判别值,或者只有单元变体是显式的情况下,可以进行转换。

#![allow(unused)]
fn main() {
enum Fieldless {
    Tuple(),
    Struct{},
    Unit,
}

assert_eq!(0, Fieldless::Tuple() as isize);
assert_eq!(1, Fieldless::Struct{} as isize);
assert_eq!(2, Fieldless::Unit as isize);

#[repr(u8)]
enum FieldlessWithDiscriminants {
    First = 10,
    Tuple(),
    Second = 20,
    Struct{},
    Unit,
}

assert_eq!(10, FieldlessWithDiscriminants::First as u8);
assert_eq!(11, FieldlessWithDiscriminants::Tuple() as u8);
assert_eq!(20, FieldlessWithDiscriminants::Second as u8);
assert_eq!(21, FieldlessWithDiscriminants::Struct{} as u8);
assert_eq!(22, FieldlessWithDiscriminants::Unit as u8);
}

指针转换

如果枚举指定了 原生表示 ,则可以通过不安全的指针转换可靠地访问判别值:

#![allow(unused)]
fn main() {
#[repr(u8)]
enum Enum {
    Unit,
    Tuple(bool),
    Struct{a: bool},
}

impl Enum {
    fn discriminant(&self) -> u8 {
        unsafe { *(self as *const Self as *const u8) }
    }
}

let unit_like = Enum::Unit;
let tuple_like = Enum::Tuple(true);
let struct_like = Enum::Struct{a: false};

assert_eq!(0, unit_like.discriminant());
assert_eq!(1, tuple_like.discriminant());
assert_eq!(2, struct_like.discriminant());
}

零变体枚举

没有变体的枚举被称为 零变体枚举 。因为它们没有有效值,所以无法被实例化。

#![allow(unused)]
fn main() {
enum ZeroVariants {}
}

零变体枚举等价于 never 类型 ,但它们不能被强制转换为其他类型。

#![allow(unused)]
fn main() {
enum ZeroVariants {}
let x: ZeroVariants = panic!();
let y: u32 = x; // 类型不匹配错误
}

变体可见性

枚举变体在语法上允许使用 可见性 注解,但在验证枚举时会被拒绝。这允许项在使用的不同上下文中以统一的语法进行解析。

#![allow(unused)]
fn main() {
macro_rules! mac_variant {
    ($vis:vis $name:ident) => {
        enum $name {
            $vis Unit,

            $vis Tuple(u8, u16),

            $vis Struct { f: u8 },
        }
    }
}

// 允许空的 vis。
mac_variant! { E }

// 这是允许的,因为它在通过验证之前已被删除。
#[cfg(false)]
enum E {
    pub U,
    pub(crate) T(u8),
    pub(super) T { f: String }
}
}

联合体

Syntax
Union
    union IDENTIFIER GenericParams? WhereClause? { StructFields? }

联合体 声明使用与 结构体 声明相同的 语法格式,除了用 union 代替 struct

联合体 声明在其所在的模块或代码块的 类型命名空间 中定义给定的名称。

#![allow(unused)]
fn main() {
#[repr(C)]
union MyUnion {
    f1: u32,
    f2: f32,
}
}

联合体 的关键属性是 联合体 的所有字段共享公共存储。因此,对 联合体 的一个字段进行写入可能会覆盖其其他字段,并且 联合体 的大小由其最大字段的大小决定。

联合体 字段类型仅限于以下类型子集:

  • Copy 类型
  • 引用(针对任意 T&T&mut T
  • ManuallyDrop<T>(针对任意 T
  • 仅包含允许的 联合体 字段类型的元组和数组

这种限制特别确保了 联合体 字段永远不需要被 drop。与 结构体 和枚举一样,可以为 联合体 impl Drop 以手动定义其在被 drop 时发生的情况。

编译器不接受没有任何字段的 联合体,但宏可以接受。

初始化联合体

联合体 类型的值可以使用与 结构体 类型相同的 语法格式 创建,不同之处在于它必须恰好指定一个字段:

#![allow(unused)]
fn main() {
union MyUnion { f1: u32, f2: f32 }

let u = MyUnion { f1: 1 };
}

上面的表达式创建了一个 MyUnion 类型的值,并使用字段 f1 初始化存储。可以使用与 结构体 字段相同的 语法格式 访问 联合体:

#![allow(unused)]
fn main() {
union MyUnion { f1: u32, f2: f32 }

let u = MyUnion { f1: 1 };
let f = unsafe { u.f1 };
}

读写联合体字段

联合体 没有“活动字段”的概念。相反,每次对 联合体 的访问只是将存储解释为用于访问的字段的类型。

读取 联合体 字段会按该字段的类型读取 联合体 的位。

字段可能具有非零偏移量(除非使用了 C 表示 );在这种情况下,将读取从字段偏移量开始的位。

程序员有责任确保数据在该字段的类型下是有效的。否则会导致 未定义行为 。例如,从 布尔类型 字段中读取值 3 是 未定义行为。实际上,在使用 C 表示 的 联合体 中,先写入然后再读取,类似于从用于写入的类型到用于读取的类型的 transmute

因此,所有对 联合体 字段的读取都必须放在 unsafe 块中:

#![allow(unused)]
fn main() {
union MyUnion { f1: u32, f2: f32 }
let u = MyUnion { f1: 1 };

unsafe {
    let f = u.f1;
}
}

通常,使用 联合体 的代码会为不安全的 联合体 字段访问提供安全包装。

相比之下,对 联合体 字段的写入是安全的,因为它们只是覆盖任意数据,不会导致 未定义行为。(注意,联合体 字段类型永远不会有 drop glue,因此 联合体 字段写入永远不会隐式地 drop 任何内容。)

在联合体上进行模式匹配

另一种访问 联合体 字段的方法是使用模式匹配。

在 联合体 字段上进行模式匹配使用与 结构体 模式相同的 语法格式,但模式必须恰好指定一个字段。

由于模式匹配就像使用特定字段读取 联合体,因此它也必须放在 unsafe 块中。

#![allow(unused)]
fn main() {
union MyUnion { f1: u32, f2: f32 }

fn f(u: MyUnion) {
    unsafe {
        match u {
            MyUnion { f1: 10 } => { println!("ten"); }
            MyUnion { f2 } => { println!("{}", f2); }
        }
    }
}
}

模式匹配可以将 联合体 作为更大结构的一个字段进行匹配。特别是,当使用 Rust 联合体 通过 FFI 实现 C 标签联合体时,这允许同时对标签和相应的字段进行匹配:

#![allow(unused)]
fn main() {
#[repr(u32)]
enum Tag { I, F }

#[repr(C)]
union U {
    i: i32,
    f: f32,
}

#[repr(C)]
struct Value {
    tag: Tag,
    u: U,
}

fn is_zero(v: Value) -> bool {
    unsafe {
        match v {
            Value { tag: Tag::I, u: U { i: 0 } } => true,
            Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true,
            _ => false,
        }
    }
}
}

指向联合体字段的引用

由于 联合体 字段共享公共存储,获得对 联合体 一个字段的写访问权可能会获得对其所有其余字段的写访问权。

借用检查规则必须进行调整以考虑到这一事实。因此,如果 联合体 的一个字段被借用,其所有其余字段也会在相同的生命周期内被借用。

#![allow(unused)]
fn main() {
union MyUnion { f1: u32, f2: f32 }
// 错误:一次不能多次将 `u`(通过 `u.f2`)借用为可变
fn test() {
    let mut u = MyUnion { f1: 1 };
    unsafe {
        let b1 = &mut u.f1;
//                    ---- 第一次可变借用发生在这里(通过 `u.f1`)
        let b2 = &mut u.f2;
//                    ^^^^ 第二次可变借用发生在这里(通过 `u.f2`)
        *b1 = 5;
    }
//  - 第一次借用在这里结束
    assert_eq!(unsafe { u.f1 }, 5);
}
}

如你所见,在许多方面(除了布局、安全性和所有权),联合体 的行为与 结构体 完全相同,这在很大程度上是由于继承了 结构体 的 语法格式 形状。对于 Rust 语言中许多未提及的方面也是如此(如私有性、名称解析、类型推导、 泛型 、 特型 实现、固有实现、一致性、模式检查等等)。

常量项

Syntax
ConstantItem
    const ( IDENTIFIER | _ ) : Type ( = Expression )? ;

常量项 是一个可选命名的 常量值 ,它不与程序中特定的内存位置相关联。

常量在本质上是在其被使用的地方内联的,这意味着在使用时它们被直接复制到相关的上下文中。这包括使用来自外部 crate 的常量,以及非 Copy 类型。指向同一常量的引用不一定保证指向相同的内存地址。

常量声明在其所在的模块或代码块的 值命名空间 中定义常量值。

常量必须显式指定类型。该类型必须具有 'static 生命周期:初始化程序中的任何引用都必须具有 'static 生命周期。常量类型中的引用默认为 'static 生命周期;请参见 静态生命周期省略

如果常量值符合 提升 条件,则指向该常量的引用将具有 'static 生命周期;否则,将创建一个临时变量。

#![allow(unused)]
fn main() {
const BIT1: u32 = 1 << 0;
const BIT2: u32 = 1 << 1;

const BITS: [u32; 2] = [BIT1, BIT2];
const STRING: &'static str = "bitstring";

struct BitsNStrings<'a> {
    mybits: [u32; 2],
    mystring: &'a str,
}

const BITS_N_STRINGS: BitsNStrings<'static> = BitsNStrings {
    mybits: BITS,
    mystring: STRING,
};
}

常量表达式仅在 特型定义 中可以省略。

带有析构函数的常量

常量可以包含析构函数。当值超出作用域时,析构函数将运行。

#![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,调用 drop。
    // 打印 "Dropped. Held 0."。
}
}

匿名常量

关联常量 不同, 自由 常量可以通过使用下划线代替名称来保持匿名。例如:

#![allow(unused)]
fn main() {
const _: () =  { struct _SameNameTwice; };

// 尽管名称与上面相同,但是 OK 的:
const _: () =  { struct _SameNameTwice; };
}

下划线导入 一样,宏可以在同一作用域内安全地多次生成同一个匿名常量。例如,以下代码不应产生错误:

#![allow(unused)]
fn main() {
macro_rules! m {
    ($item: item) => { $item $item }
}

m!(const _: () = (););
// 这将展开为:
// const _: () = ();
// const _: () = ();
}

求值

自由 常量始终在编译时 求值 以暴露 恐慌。即使在未使用的函数中也会发生这种情况:

#![allow(unused)]
fn main() {
// 编译时恐慌
const PANIC: () = std::unimplemented!();

fn unused_generic_function<T>() {
    // 失败的编译时断言
    const _: () = assert!(usize::BITS == 0);
}
}

静态项

Syntax
StaticItem
    ItemSafety?1 static mut? IDENTIFIER : Type ( = Expression )? ;

一个 静态项常量项 类似,不同之处在于它表示程序中由初始化表达式初始化的分配。指向该 静态项 的所有引用和裸指针都指向同一个分配。

静态项 具有 static 生命周期,它比 Rust 程序中的所有其他生命周期都要长。 静态项 在程序结束时不会调用 drop

如果 static 的大小至少为 1 字节,则此分配与所有其他此类 static 分配以及堆分配和栈分配变量是不相交的。然而,不可变 静态项 的存储可以与自身没有唯一地址的分配重叠,例如 提升项const

静态声明在它所在的模块或块的 值命名空间 中定义了一个静态值。

静态初始化器是一个在编译时求值的 常量表达式。静态初始化器可以引用并读取其他 静态项。从可变 静态项 读取时,它们读取该 静态项 的初始值。

包含非 内部可变 类型的非 mut 静态项 可能会被放置在只读内存中。

对 静态项 的所有访问都是安全的,但对 静态项 有一些限制:

  • 类型必须具有 Sync 特型 界限以允许线程安全访问。

初始化表达式在 外部块 中必须省略,而在自由 静态项 中必须提供。

从语义上讲,safeunsafe 限定符仅在 外部块 中使用时才被允许。

静态项与泛型

在 泛型 作用域(例如在全覆盖实现或默认实现)中定义的 静态项 将导致仅定义一个 静态项,就好像静态定义从当前作用域提取到了模块中一样。每一单态化实例将 有一个 项。

这段代码:

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_unsafe() -> u32 {
    unsafe {
        let ret = LEVELS;
        LEVELS += 1;
        return ret;
    }
}

// 作为 `bump_levels_unsafe` 的替代方案,假设我们有一个返回旧值的 atomic_add 函数,那么这个函数就是安全的。
// 只有在没有其他代码以非原子方式访问该静态项的情况下,此函数才是安全的。
// 如果此类访问是可能的(例如在 `bump_levels_unsafe` 中),那么这将需要是 `unsafe` 的,
// 以向调用者指示他们仍必须防范并发访问。
fn bump_levels_safe() -> u32 {
    unsafe {
        return atomic_add(&raw mut LEVELS, 1);
    }
}
}

可变静态项具有与普通 静态项 相同的限制,但类型不必实现 Sync 特型。

使用静态项或常量项

使用 常量项 还是 静态项 可能会令人困惑。通常情况下,应优先使用 常量项 而非 静态项,除非满足以下条件之一:

  • 正在存储大量数据。
  • 需要 静态项 的单地址属性。
  • 需要内部可变性。

  1. safeunsafe 函数限定符在语义上仅允许在 extern 块中使用。

特型

Syntax
Trait
    unsafe? trait IDENTIFIER GenericParams? ( : TypeParamBounds? )? WhereClause?
    {
        InnerAttribute*
        AssociatedItem*
    }

一个 特型 描述了一个类型可以实现的抽象接口。此接口由 关联项 组成,共有三种:

特型 声明在它所在的模块或块的 类型命名空间 中定义了一个 特型。

关联项 在其各自的命名空间中被定义为 特型 的成员。关联类型 定义在 类型命名空间 中。关联常量 和 关联函数 定义在 值命名空间 中。

所有 特型 都定义了一个隐式的 类型参数 Self,它指向 “实现此接口的类型” 。 特型 还可以包含额外的 类型参数。这些 类型参数(包括 Self)可以像 往常一样 受到其他 特型 等的约束。

特型 通过单独的 实现 为特定类型实现。

特型 函数可以通过用分号替换函数体来省略它。这表示 实现 必须定义该函数。如果 特型 函数定义了函数体,则该定义对于任何未覆盖它的 实现 都充当默认值。类似地,关联常量 可以省略等号和表达式,以表示 实现 必须定义常量值。 关联类型 绝不能定义类型,类型只能在 实现 中指定。

#![allow(unused)]
fn main() {
// 具有定义和不具有定义的特型关联项示例。
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) {}
}
}

特型 函数不允许是 const

特型界限

泛型 项 可以使用特型作为其类型参数的 界限

泛型特型

可以为 特型 指定 类型参数 以使其成为 泛型。这些出现在 特型 名称之后,使用与 泛型函数 相同的语法。

#![allow(unused)]
fn main() {
trait Seq<T> {
    fn len(&self) -> u32;
    fn elt_at(&self, n: u32) -> T;
    fn iter<F>(&self, f: F) where F: Fn(T);
}
}

Dyn兼容性

一个 dyn 兼容的 特型 可以作为 特型对象 的基础 特型。如果一个 特型 具备以下特质,则它是 dyn 兼容的 (dyn compatible)

  • 所有 父特型 也必须是 dyn 兼容的。
  • Sized 不能是一个 父特型。换句话说,它不能要求 Self: Sized
  • 它不能有任何 关联常量。
  • 它不能有任何带有 泛型 的 关联类型。
  • 所有 关联函数 必须要么可以从 特型对象 分派,要么是明确的不可分派:
    • 可分派函数必须:
      • 不具有任何 类型参数(尽管允许生命周期参数)。
      • 是一个除了在 接收者 类型中之外不使用 Self方法
      • 拥有以下类型之一的 接收者:
      • 不具有 不透明返回类型;即,
        • 不是 async fn(它具有隐藏的 Future 类型)。
        • 不具有 返回位置 impl Trait 类型 (fn example(&self) -> impl Trait)。
      • 不具有 where Self: Sized 界限(Self 的 接收者 类型 (即 self) 隐含了这一点)。
    • 明确的不可分派函数要求:
      • 具有 where Self: Sized 界限(Self 的 接收者 类型 (即 self) 隐含了这一点)。

注意

这个概念以前被称为 对象安全 (object safety)

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Arc;
use std::pin::Pin;
// dyn 兼容方法的示例。
trait TraitMethods {
    fn by_ref(self: &Self) {}
    fn by_ref_mut(self: &mut Self) {}
    fn by_box(self: Box<Self>) {}
    fn by_rc(self: Rc<Self>) {}
    fn by_arc(self: Arc<Self>) {}
    fn by_pin(self: Pin<&Self>) {}
    fn with_lifetime<'a>(self: &'a Self) {}
    fn nested_pin(self: Pin<Arc<Self>>) {}
}
struct S;
impl TraitMethods for S {}
let t: Box<dyn TraitMethods> = Box::new(S);
}
#![allow(unused)]
fn main() {
// 此特型是 dyn 兼容的,但这些方法不能在特型对象上分派。
trait NonDispatchable {
    // 非方法不能被分派。
    fn foo() where Self: Sized {}
    // Self 类型在运行时才已知。
    fn returns(&self) -> Self where Self: Sized;
    // `other` 可能是接收者的不同具体类型。
    fn param(&self, other: Self) where Self: Sized {}
    // 泛型与虚表 (vtable) 不兼容。
    fn typed<T>(&self, x: T) where Self: Sized {}
}

struct S;
impl NonDispatchable for S {
    fn returns(&self) -> Self where Self: Sized { S }
}
let obj: Box<dyn NonDispatchable> = Box::new(S);
obj.returns(); // 错误:不能通过 Self 返回值调用
obj.param(S);  // 错误:不能通过 Self 参数调用
obj.typed(1);  // 错误:不能通过泛型类型调用
}
#![allow(unused)]
fn main() {
use std::rc::Rc;
// dyn 不兼容特型的示例。
trait DynIncompatible {
    const CONST: i32 = 1;  // 错误:不能拥有关联常量

    fn foo() {}  // 错误:没有 Sized 的关联函数
    fn returns(&self) -> Self; // 错误:返回类型中存在 Self
    fn typed<T>(&self, x: T) {} // 错误:具有泛型类型参数
    fn nested(self: Rc<Box<Self>>) {} // 错误:嵌套接收者不能被分派
}

struct S;
impl DynIncompatible for S {
    fn returns(&self) -> Self { S }
}
let obj: Box<dyn DynIncompatible> = Box::new(S); // 错误
}
#![allow(unused)]
fn main() {
// `Self: Sized` 特型是 dyn 不兼容的。
trait TraitWithSize where Self: Sized {}

struct S;
impl TraitWithSize for S {}
let obj: Box<dyn TraitWithSize> = Box::new(S); // 错误
}
#![allow(unused)]
fn main() {
// 如果 `Self` 是一个类型参数,则 dyn 不兼容。
trait Super<A> {}
trait WithSelf: Super<Self> where Self: Sized {}

struct S;
impl<A> Super<A> for S {}
impl WithSelf for S {}
let obj: Box<dyn WithSelf> = Box::new(S); // 错误:不能使用 `Self` 类型参数
}

父特型

父特型 (Supertraits) 是指为了让某个类型实现特定 特型 而必须先为该类型实现的 特型。此外,任何被 特型 限界的 泛型特型对象 都可以访问其 父特型 的 关联项。

父特型 通过 特型 的 Self 类型上的 特型界限 来声明,并以此类推,包括那些 特型界限 中声明的 特型 的 父特型。 特型 成为其自身的 父特型 是一个错误。

拥有 父特型 的 特型 被称为其 父特型 的 子特型 (subtrait)

以下是声明 ShapeCircle 的 父特型 的示例。

#![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; }
}

下一个示例使用来自 Shapearea 函数为 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()
    }
}
}

下一个示例在 泛型 参数上调用 父特型 方法。

#![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` 的父特型 `Shape` 的 area 方法。
    println!("Area: {}", c.area());
    println!("Radius: {}", c.radius());
}
}

类似地,这里是一个在 特型对象 上调用 父特型 方法的示例。

#![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 关键字开头的 特型 项 表示 实现 该 特型 可能是 不安全 的。使用正确实现的 不安全特型 是安全的。 特型实现 也必须以 unsafe 关键字开头。

SyncSend 是 不安全特型 的示例。

参数模式

在没有函数体的 关联函数 中,参数仅允许 标识符_ 通配符模式,以及 Self 参数 允许的形式。 mut 标识符 目前是允许的,但它已被弃用,将来会变成一个硬错误。

#![allow(unused)]
fn main() {
trait T {
    fn f1(&self);
    fn f2(x: Self, _: i32);
}
}
#![allow(unused)]
fn main() {
trait T {
    fn f2(&x: &i32); // 错误:在没有函数体的函数中不允许使用模式
}
}

带有函数体的 关联函数 中的参数仅允许 不可驳模式。

#![allow(unused)]
fn main() {
trait T {
    fn f1((a, b): (i32, i32)) {} // OK:是不可驳模式
}
}
#![allow(unused)]
fn main() {
trait T {
    fn f1(123: i32) {} // 错误:模式是可驳的
    fn f2(Some(x): Option<i32>) {} // 错误:模式是可驳的
}
}

2018 版次差异

在 2018 版次 之前,关联函数参数的模式是可选的:

#![allow(unused)]
fn main() {
// 2015 版次
trait T {
    fn f(i32); // OK:参数标识符不是必需的
}
}

从 2018 版次 开始,模式不再是可选的。

2018 版次差异

在 2018 版次 之前,带有函数体的 关联函数 中的参数仅限于以下几种 模式:

#![allow(unused)]
fn main() {
// 2015 版次
trait T {
    fn f1((a, b): (i32, i32)) {} // 错误:不允许使用该模式
}
}

从 2018 开始,所有不可驳模式都是允许的,如 items.traits.params.patterns-with-body 中所述。

项可见性

特型项 在语法格式上允许 可见性 注解,但在验证 特型 时会被拒绝。这允许 项 在使用它们的不同上下文中以统一的语法格式进行解析。例如,空的 vis 宏片段说明符可以用于 特型项,其中该宏规则可以用于允许可见性的其他情况。

macro_rules! create_method {
    ($vis:vis $name:ident) => {
        $vis fn $name(&self) {}
    };
}

trait T1 {
    // 允许空的 `vis`。
    create_method! { method_of_t1 }
}

struct S;

impl S {
    // 这里允许可见性。
    create_method! { pub method_of_s }
}

impl T1 for S {}

fn main() {
    let s = S;
    s.method_of_t1();
    s.method_of_s();
}

实现

Syntax
ImplementationInherentImpl | TraitImpl

InherentImpl
    impl GenericParams? Type WhereClause? {
        InnerAttribute*
        AssociatedItem*
    }

TraitImpl
    unsafe? impl GenericParams? !? TypePath for Type
    WhereClause?
    {
        InnerAttribute*
        AssociatedItem*
    }

一个 实现 (implementation) 是一个将 项 与一个 实现类型 (implementing type) 相关联的 项。实现 使用关键字 impl 定义,并包含属于正在实现的类型的实例或静态属于该类型的函数。

实现 有两种类型:

  • 固有实现 (inherent implementations)
  • 特型 实现

固有实现

固有实现被定义为 impl 关键字、 泛型 类型声明、指向标称类型的路径、where 子句以及一组用大括号括起来的可关联 项 的序列。

该标称类型被称为 实现类型 (implementing type) ,而可关联 项 是该实现类型的 关联项 (associated items)

固有实现将包含的 项 与实现类型相关联。

固有实现可以包含 关联函数(包括 方法)和 关联常量

它们不能包含关联类型别名。

指向关联项的 路径 是指向实现类型的任何路径,后跟关联项的标识符作为路径的最后一个组件。

一个类型也可以有多个固有实现。实现类型必须定义在与原始类型定义相同的 crate 中。

pub mod color {
    pub struct Color(pub u8, pub u8, pub u8);

    impl Color {
        pub const WHITE: Color = Color(255, 255, 255);
    }
}

mod values {
    use super::color::Color;
    impl Color {
        pub fn red() -> Color {
            Color(255, 0, 0)
        }
    }
}

pub use self::color::Color;
fn main() {
    // 实现类型和 impl 在同一个模块中的实际路径。
    color::Color::WHITE;

    // 不同模块中的 impl 块仍然通过指向该类型的路径进行访问。
    color::Color::red();

    // 实现类型的重导出路径也有效。
    Color::red();

    // 无效,因为 values 中的 use 不是 pub。
    // values::Color::red();
}

特型实现

特型实现 的定义类似于固有实现,不同之处在于可选的 泛型 类型声明后面跟着一个 特型,然后是关键字 for,最后是到一个标称类型的路径。

该 特型 被称为 实现的特型 (implemented trait) 。实现类型实现该 实现的特型。

特型实现必须定义由 实现的特型 声明的所有非默认 关联项,可以重新定义由 实现的特型 定义的默认 关联项,并且不能定义任何其他 项。

指向关联项的路径是 < 后跟指向实现类型的路径,后跟 as,后跟指向 特型 的路径,后跟 > 作为路径组件,再后跟关联项的路径组件。

不安全特型 要求特型实现以 unsafe 关键字开头。

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Point {x: f64, y: f64};
type Surface = i32;
struct BoundingBox {x: f64, y: f64, width: f64, height: f64};
trait Shape { fn draw(&self, s: Surface); fn bounding_box(&self) -> BoundingBox; }
fn do_draw_circle(s: Surface, c: Circle) { }
struct Circle {
    radius: f64,
    center: Point,
}

impl Copy for Circle {}

impl Clone for Circle {
    fn clone(&self) -> Circle { *self }
}

impl Shape for Circle {
    fn draw(&self, s: Surface) { do_draw_circle(s, *self); }
    fn bounding_box(&self) -> BoundingBox {
        let r = self.radius;
        BoundingBox {
            x: self.center.x - r,
            y: self.center.y - r,
            width: 2.0 * r,
            height: 2.0 * r,
        }
    }
}
}

特型实现一致性

如果孤儿规则检查失败或存在重叠的实现实例,则 特型实现 被认为是不一致的。

当两个 特型实现 所针对的 特型 存在非空交集时,即这两个实现可以用相同的类型实例化,则它们发生重叠。

孤儿规则

孤儿规则 (orphan rule) 规定,只有当 特型 或实现中的至少一个类型定义在当前 crate 中时,才允许进行 特型实现。它防止了不同 crate 之间冲突的 特型实现,并且是确保一致性的关键。

一个孤儿实现是指为外部类型实现外部 特型。如果允许自由定义这些实现,则两个 crate 可能会以不兼容的方式为相同的类型实现相同的 特型,从而导致添加或更新依赖项可能会因实现冲突而破坏编译。

孤儿规则 允许库作者为其 特型 添加新的实现,而无需担心会破坏下游代码。如果没有这些限制,库将无法添加像 impl<T: Display> MyTrait for T 这样的实现,因为这可能会与下游实现产生冲突。

对于 impl<P1..=Pn> Trait<T1..=Tn> for T0,只有当以下至少一项为真时,impl 才是有效的:

  • Trait 是一个 本地特型
  • 以下全部成立:
    • 类型 T0..=Tn 中必须至少有一个是 本地类型。设 Ti 为第一个此类类型。
    • T0..Ti(不包括 Ti)中不得出现 未覆盖的类型 参数 P1..=Pn

仅对 未覆盖的 (uncovered) 类型参数的出现进行限制。

请注意,为了保持一致性,基础类型 是特殊的。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 {
    /* 将整数视为位序列 */
}
}

如果泛型参数在以下位置至少出现一次,则该参数会 约束 (constrain) 实现:

  • 实现的 特型(如果存在)
  • 实现类型
  • 作为包含另一个约束实现的参数的类型的 界限 中的 关联类型

类型参数和常量参数必须始终约束实现。如果生命周期在关联类型中使用,则生命周期必须约束实现。

产生约束的情况示例:

#![allow(unused)]
fn main() {
trait Trait{}
trait GenericTrait<T> {}
trait HasAssocType { type Ty; }
struct Struct;
struct GenericStruct<T>(T);
struct ConstGenericStruct<const N: usize>([(); N]);
// T 通过作为 GenericTrait 的参数来产生约束。
impl<T> GenericTrait<T> for i32 { /* ... */ }

// T 通过作为 GenericStruct 的参数来产生约束
impl<T> Trait for GenericStruct<T> { /* ... */ }

// 同样地,N 通过作为 ConstGenericStruct 的参数来产生约束
impl<const N: usize> Trait for ConstGenericStruct<N> { /* ... */ }

// T 通过作为类型 U 的界限中的关联类型来产生约束,而 U 本身是约束该特型的泛型参数。
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) { /* ... */ }
}

// T 在 U 的界限中被用作关联类型,但 U 并不产生约束。
impl<T, U> Struct where U: HasAssocType<Ty = T> { /* ... */ }

// T 在界限中使用,但不是作为关联类型使用,因此它不产生约束。
impl<T, U> GenericTrait<U> for u32 where U: GenericTrait<T> {}
}

允许的不产生约束的生命周期参数示例:

#![allow(unused)]
fn main() {
struct Struct;
impl<'a> Struct {}
}

不允许的不产生约束的生命周期参数示例:

#![allow(unused)]
fn main() {
struct Struct;
trait HasAssocType { type Ty; }
impl<'a> HasAssocType for Struct {
    type Ty = &'a Struct;
}
}

实现上的属性

实现可以在 impl 关键字之前包含外部 属性,并在包含关联项的大括号内包含内部 属性。内部属性必须位于任何关联项之前。此处有意义的属性是 cfgdeprecateddoclint 检查属性

外部块

Syntax
ExternBlock
    unsafe?1 extern Abi? {
        InnerAttribute*
        ExternalItem*
    }

ExternalItem
    OuterAttribute* (
        MacroInvocationSemi
      | Visibility? StaticItem
      | Visibility? Function
    )

外部块提供在当前 crate 中未 定义 而是 声明 的 项 的声明,是 Rust 外部函数接口 (FFI) 的基础。这些类似于未经检查的导入。

外部块中允许两种 项 声明函数静态项

仅允许在 unsafe 上下文 中调用在外部块中声明的 不安全 函数或访问 不安全 静态项。

外部块在其所在的模块或块的 值命名空间 中定义其函数和静态项。

从语义上讲,unsafe 关键字必须出现在外部块的 extern 关键字之前。

2024 版次差异

在 2024 版次 之前,unsafe 关键字是可选的。只有当外部块本身被标记为 unsafe 时,才允许使用 safeunsafe 项限定符。

函数

外部块中的函数声明方式与其它 Rust 函数相同,不同之处在于它们不能有函数体,而是以分号结尾。

参数中不允许使用模式,只能使用 IDENTIFIER_

允许使用 safeunsafe 函数限定符,但不允许使用其它函数限定符(例如 constasyncextern)。

外部块中的函数可以被 Rust 代码调用,就像在 Rust 中定义的函数一样。Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行转换。

在 extern 块中声明的函数隐式地是 unsafe 的,除非存在 safe 函数限定符。

当强制转换为函数指针时,在 extern 块中声明的函数具有类型 extern "abi" for<'l1, ..., 'lm> fn(A1, ..., An) -> R,其中 'l1、… 'lm 是其生命周期参数,A1、…、An 是其参数的声明类型,而 R 是声明的返回类型。

静态项

外部块中的 静态项 的声明方式与外部块之外的 静态项 相同,不同之处在于它们没有初始化其值的表达式。

除非在 extern 块中声明的 静态项 被限定为 safe,否则访问该 项 是 unsafe 的,无论它是否可变,因为由于某些任意(例如 C)代码负责初始化该 静态项,因此没有任何东西能保证该 静态项 内存处的位模式对于声明它的类型是有效的。

外部 静态项 既可以是不可变的,也可以是可变的,就像外部块之外的 静态项 一样。

在执行任何 Rust 代码之前, 必须 初始化不可变 静态项。仅在 Rust 代码从中读取之前初始化 静态项 是不够的。一旦 Rust 代码运行,修改不可变 静态项(从 Rust 内部或外部)就是 未定义行为 (UB),除非修改发生在 UnsafeCell 内部的字节上。

ABI

extern 关键字后面可以跟一个可选的 ABI 字符串。ABI 指定了块中函数的调用约定。调用约定定义了函数的低级接口,例如参数如何放置在寄存器或栈中,返回值如何传递,以及谁负责清理栈。

例子

#![allow(unused)]
fn main() {
// Windows API 接口。
unsafe extern "system" { /* ... */ }
}

如果未指定 ABI 字符串,则默认为 "C"

注意

不带显式 ABI 的 extern 语法正在逐步淘汰,因此最好始终显式写入 ABI。

有关更多详细信息,请参阅 Rust 问题 #134986

所有平台都支持以下 ABI 字符串:

  • unsafe extern "Rust" — Rust 函数和闭包的原生调用约定。当声明函数而不使用 extern fn 时,这是默认值。Rust ABI 不提供稳定性保证。
  • unsafe extern "C" — “C” ABI 与目标平台的主流 C 编译器选择的默认 ABI 匹配。
  • unsafe extern "system" — 这等同于 extern "C",但在 Windows x86_32 上除外,对于非变长参数函数,它等同于 "stdcall",而对于变长参数函数,它等同于 "C"

    注意

    由于 Windows 上正确的底层 ABI 与目标平台相关,因此在尝试链接不使用显式定义 ABI 的 Windows API 函数时,最好使用 extern "system"

  • extern "C-unwind"extern "system-unwind" — 分别与 "C""system" 相同,但在被调用者展开(通过 恐慌 或抛出 C++ 风格异常)时具有 不同的行为

还有一些平台特定的 ABI 字符串:

  • unsafe extern "aapcs" — ARM 的软浮点 ABI。

    • 仅适用于 ARM32 目标。
    • “aapcs” 与软浮点 ARM32 上的 “C” ABI 相同。
    • 对应于 clang 的 __attribute__((pcs("aapcs")))

    注意

    有关详细信息,请参阅:

  • unsafe extern "efiapi" — 用于 UEFI 函数的 ABI。
    • 仅适用于 x86 和 ARM 目标(32 位和 64 位)。

"C""system" 类似,大多数平台特定的 ABI 字符串也具有 对应的 -unwind 变体;具体而言,它们是:

  • "aapcs-unwind"
  • "cdecl-unwind"
  • "fastcall-unwind"
  • "stdcall-unwind"
  • "sysv64-unwind"
  • "thiscall-unwind"
  • "win64-unwind"

变长参数函数

外部块中的函数可以通过指定 ... 作为最后一个参数来成为变长的。变长参数可以可选地指定一个标识符。

#![allow(unused)]
fn main() {
unsafe extern "C" {
    unsafe fn foo(...);
    unsafe fn bar(x: i32, ...);
    unsafe fn with_name(format: *const u8, args: ...);
    // 安全性:此函数保证它根本不会访问变长参数。
    safe fn ignores_variadic_arguments(x: i32, ...);
}
}

警告

不应在 extern 块中的函数上使用 safe 限定符,除非该函数保证它根本不会访问变长参数。向变长参数函数传递非预期数量的参数或非预期类型的参数可能会导致 未定义行为

变长参数只能在具有以下 ABI 字符串或其对应 -unwind 变体extern 块中指定:

  • "aapcs"
  • "C"
  • "cdecl"
  • "efiapi"
  • "system"
  • "sysv64"
  • "win64"

外部块上的属性

以下 属性 控制外部块的行为。

link属性

link 属性 指定了编译器应针对 extern 块内的 项 进行链接的原生库的名称。

它使用 MetaListNameValueStr 语法来指定其输入。name 键是要链接的原生库的名称。kind 键是一个可选值,它指定了库的类型,具有以下可能的值:

  • dylib — 表示动态库。如果未指定 kind,则这是默认值。
  • static — 表示静态库。
  • framework — 表示 macOS 框架。这仅对 macOS 目标有效。
  • raw-dylib — 表示一个动态库,编译器将生成一个导入库来与其链接(详见下文的 dylibraw-dylib 比较)。这仅对 Windows 目标有效。

如果指定了 kind,则必须包含 name 键。

可选的 modifiers 参数是一种为要链接的库指定链接修饰符的方法。

修饰符指定为以逗号分隔的字符串,每个修饰符前缀为 +-,分别表示启用或禁用该修饰符。

目前不支持在单个 link 属性中指定多个 modifiers 参数,或者在同一个 modifiers 参数中指定多个相同的修饰符。
示例:#[link(name = "mylib", kind = "static", modifiers = "+whole-archive")]

当从宿主环境导入符号时,wasm_import_module 键可用于为 extern 块中的 项 指定 WebAssembly 模块 名称。如果未指定 wasm_import_module,则默认模块名称为 env

#[link(name = "crypto")]
unsafe extern {
    // …
}

#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern {
    // …
}

#[link(wasm_import_module = "foo")]
unsafe extern {
    // …
}

在空的外部块上添加 link 属性是有效的。您可以使用它来满足代码中其他地方(包括上游 crate)外部块的链接要求,而不是在每个外部块中都添加该属性。

链接修饰符:bundle

此修饰符仅与 static 链接类型兼容。使用任何其他类型都将导致编译器错误。

在构建 rlib 或 staticlib 时,+bundle 意味着原生静态库将被打包到 rlib 或 staticlib 归档中,然后在链接最终二进制文件期间从中检索。

在构建 rlib 时,-bundle 意味着原生静态库被 “按名称” 注册为该 rlib 的依赖项,并且仅在链接最终二进制文件期间才包含其中的目标文件,在最终链接期间也会执行按该名称的文件搜索。
在构建 staticlib 时,-bundle 意味着原生静态库根本不包含在归档中,某些更高级别的构建系统将需要在稍后链接最终二进制文件期间添加它。

在构建可执行文件或动态库等其他目标时,此修饰符没有效果。

此修饰符的默认值为 +bundle

有关此修饰符的更多实现细节,可以在 rustc 的 bundle 文档 中找到。

链接修饰符:whole-archive

此修饰符仅与 static 链接类型兼容。使用任何其他类型都将导致编译器错误。

+whole-archive 意味着静态库作为整个归档进行链接,而不丢弃任何目标文件。

此修饰符的默认值为 -whole-archive

有关此修饰符的更多实现细节,可以在 rustc 的 whole-archive 文档 中找到。

链接修饰符:verbatim

此修饰符与所有链接类型兼容。

+verbatim 意味着 rustc 本身不会向库名称添加任何目标平台指定的库前缀或后缀(如 lib.a),并将尽力向链接器请求同样的内容。

-verbatim 意味着 rustc 在将库名称传递给链接器之前,会向其添加目标平台特定的前缀和后缀,或者不会阻止链接器隐式添加它。

此修饰符的默认值为 -verbatim

有关此修饰符的更多实现细节,可以在 rustc 的 verbatim 文档 中找到。

dylibraw-dylib比较

在 Windows 上,与动态库链接要求向链接器提供一个导入库:这是一个特殊的静态库,它声明了动态库导出的所有符号,其方式使链接器知道它们必须在运行时动态加载。

指定 kind = "dylib" 会指示 Rust 编译器链接一个基于 name 键的导入库。链接器随后将使用其正常的库解析逻辑来查找该导入库。或者,指定 kind = "raw-dylib" 会指示编译器在编译期间生成一个导入库,并将该导入库提供给链接器。

raw-dylib 仅在 Windows 上受支持。针对其他平台使用它将导致编译器错误。

import_name_type

在 x86 Windows 上,函数名称会被 “修饰” (decorated)(即添加特定的前缀和/或后缀)以指示其调用约定。例如,一个名为 fn1 且没有参数的 stdcall 调用约定函数将被修饰为 _fn1@0。然而,PE 格式 也确实允许名称没有前缀或不被修饰。此外,MSVC 和 GNU 工具链对相同的调用约定使用不同的修饰,这意味着默认情况下,某些 Win32 函数无法通过 GNU 工具链使用 raw-dylib 链接类型进行调用。

为了允许这些差异,在使用 raw-dylib 链接类型时,您还可以使用以下值之一指定 import_name_type 键,以更改生成的导入库中函数的命名方式:

  • decorated:函数名称将使用 MSVC 工具链格式进行完整修饰。
  • noprefix:函数名称将使用 MSVC 工具链格式进行修饰,但跳过前导的 ?@ 或可选的 _
  • undecorated:函数名称将不被修饰。

如果未指定 import_name_type 键,则函数名称将使用目标工具链的格式进行完整修饰。

变量永远不会被修饰,因此 import_name_type 键对它们在生成的导入库中的命名方式没有影响。

import_name_type 键仅在 x86 Windows 上受支持。针对其他平台使用它将导致编译器错误。

link_name 属性 可以应用于外部块内的声明,以指定要为给定函数或静态项导入的符号。

例子

#![allow(unused)]
fn main() {
unsafe extern "C" {
    #[link_name = "actual_symbol_name"]
    safe fn name_in_rust();
}
}

link_name 属性使用 MetaNameValueStr 语法。

link_name 属性只能应用于外部块中的函数或 静态项。

注意

rustc 忽略在其他位置的使用,但会针对其发出 lint。这在将来可能会变成错误。

只有 项 上最后一次使用的 link_name 才会生效。

注意

rustc 会针对最后一次使用之前的任何使用发出 lint。这在将来可能会变成错误。

link_name 属性不得与 link_ordinal 属性一起使用。

link_ordinal 属性 可以应用于外部块内的声明,以指示生成要链接的导入库时要使用的数字序数。序数是 Windows 上动态库导出的每个符号的唯一编号,在加载库以查找该符号时可以使用序数,而不必按名称查找。

警告

link_ordinal 仅应在已知符号序数稳定的情况下使用:如果符号的序数在构建其包含的二进制文件时未显式设置,则系统会自动为其分配一个序数,并且分配的序数可能会在二进制文件的不同构建之间发生变化。

#![allow(unused)]
fn main() {
#[cfg(all(windows, target_arch = "x86"))]
#[link(name = "exporter", kind = "raw-dylib")]
unsafe extern "stdcall" {
    #[link_ordinal(15)]
    safe fn imported_function_stdcall(i: i32);
}
}

此属性仅与 raw-dylib 链接类型一起使用。使用任何其他类型都将导致编译器错误。

将此属性与 link_name 属性一起使用将导致编译器错误。

函数参数上的属性

外部函数参数上的属性遵循与 常规函数参数 相同的规则和限制。


  1. 从 2024 版次 开始,unsafe 关键字在语义上是必需的。

泛型参数

函数类型别名结构体枚举联合体特型实现 可以通过类型、常量和生命周期进行 参数化 (parameterized) 。这些参数列在尖括号 <...>中,通常紧跟在 项 的名称之后、其定义之前。对于没有名称的 实现,它们直接跟在 impl 后面。

泛型参数 的顺序受限,必须先是 生命周期参数,然后是混合在一起的 类型参数 和 常量参数。

GenericParams 列表中,同一个参数名不得声明多次。

带类型、常量和生命周期参数的 项 示例:

#![allow(unused)]
fn main() {
fn foo<'a, T>() {}
trait A<U> {}
struct Ref<'a, T> where T: 'a { r: &'a T }
struct InnerArray<T, const N: usize>([T; N]);
struct EitherOrderWorks<const N: bool, U>(U);
}

泛型参数 在其声明所在的 项 定义的作用域内。它们不在函数体内部声明的 项 的作用域内,如 项声明 中所述。有关更多详细信息,请参阅 泛型参数作用域

引用原始指针数组切片元组函数指针 也有生命周期或类型参数,但不是通过路径语法引用的。

'_'static 不是有效的 生命周期参数 名称。

常量泛型

常量泛型参数 (Const generic parameters) 允许 项 对常量值进行泛型化。

常量标识符在 值命名空间 中为 常量参数 引入一个名称,并且该 项 的所有实例必须使用给定类型的值进行实例化。

常量参数 唯一允许的类型是 u8u16u32u64u128usizei8i16i32i64i128isizecharbool

常量参数 可以用在任何可以使用 常量项 的地方,但在 类型数组重复表达式 中使用时,它必须是 独立的 (standalone) (如下所述)。也就是说,它们允许出现在以下位置:

  1. 作为应用于构成相关 项 签名一部分的任何类型的常量。
  2. 作为用于定义 关联常量 的常量表达式的一部分,或作为 关联类型 的参数。
  3. 作为该 项 中任何函数体内的任何运行时表达式的值。
  4. 作为该 项 中任何函数体内使用的任何类型的参数。
  5. 作为该 项 中任何字段类型的一部分。
#![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}]
}
}

路径 中的常量参数指定了用于该 项 的常量值。

该参数必须是 推断常量 或者是归属于 常量参数 类型的 常量表达式。常量表达式必须是 块表达式(用大括号包围),除非它是单个路径段(一个 IDENTIFIER)或 字面量(可能带有前导的 - 词法单元)。

注意

这种语法限制是必要的,以避免在解析类型内部的表达式时需要无限前瞻。

#![allow(unused)]
fn main() {
struct S<const N: i64>;
const C: i64 = 1;
fn f<const N: i64>() -> S<N> { S }

let _ = f::<1>(); // 字面量。
let _ = f::<-1>(); // 负数字面量。
let _ = f::<{ 1 + 2 }>(); // 常量表达式。
let _ = f::<C>(); // 单段路径。
let _ = f::<{ C + 1 }>(); // 常量表达式。
let _: S<1> = f::<_>(); // 推断常量。
let _: S<1> = f::<(((_)))>(); // 推断常量。
}

注意

在泛型参数列表中,推断常量 被解析为 推断类型,但在语义上被视为一种单独的 常量泛型参数

在需要常量参数的地方,可以使用 _(可选地由任意数量的匹配括号包围),称为 推断常量 (inferred const)路径规则数组表达式规则)。这要求编译器在可能的情况下根据周围信息推断常量参数。

#![allow(unused)]
fn main() {
fn make_buf<const N: usize>() -> [u8; N] {
    [0; _]
    //  ^ 推断出 `N`。
}
let _: [u8; 1024] = make_buf::<_>();
//                             ^ 推断出 `1024`。
}

注意

推断常量 在语义上不是一个 表达式,因此不被大括号接受。

#![allow(unused)]
fn main() {
fn f<const N: usize>() -> [u8; N] { [0; _] }
let _: [_; 1] = f::<{ _ }>();
//                    ^ 错误:这里不允许使用 `_`
}

推断常量 不能在 项 签名中使用。

#![allow(unused)]
fn main() {
fn f<const N: usize>(x: [u8; N]) -> [u8; _] { x }
//                                       ^ 错误:不允许
}

当泛型参数是否可以被解析为类型参数或常量参数存在歧义时,它总是被解析为类型。将参数放置在块表达式中可以强制将其解释为常量参数。

#![allow(unused)]
fn main() {
type N = u32;
struct Foo<const N: usize>;
// 以下是一个错误,因为 `N` 被解释为类型别名 `N`。
fn foo<const N: usize>() -> Foo<N> { todo!() } // 错误
// 可以通过包裹在大括号中来修复,以强制将其解释为 `N` 
// 常量参数:
fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok
}

与类型和生命周期参数不同, 常量参数 可以在不在 参数化项 内部使用的情况下声明,但 泛型实现 中描述的 实现 除外:

#![allow(unused)]
fn main() {
// ok
struct Foo<const N: usize>;
enum Bar<const M: usize> { A, B }

// 错误:未使用的参数
struct Baz<T>;
struct Biz<'a>;
struct Unconstrained;
impl<const N: usize> Unconstrained {}
}

在解析 特型界限 义务时,确定界限是否满足时不考虑 常量参数 所有实现的 完备性 (exhaustiveness) 。例如,在下文中,即使实现了 bool 类型所有可能的常量值,但 特型界限 未满足仍然是一个错误:

#![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); // 错误:特型界限 `Foo<B>: Bar` 未满足
}
}

Where子句

Where 子句 提供了另一种指定类型和生命周期参数界限的方法,以及一种为非类型参数的类型指定界限的方法。

for 关键字可用于引入 高阶生命周期。它只允许 LifetimeParam 参数。

#![allow(unused)]
fn main() {
struct A<T>
where
    T: Iterator,            // 也可以使用 A<T: Iterator>
    T::Item: Copy,          // 关联类型上的界限
    String: PartialEq<T>,   // 在 `String` 上的界限,使用类型参数
    i32: Default,           // 允许,但没什么用
{
    f: T,
}
}

属性

泛型生命周期和类型参数允许在其上使用 属性。虽然自定义派生属性可能会赋予其含义,但目前没有内置属性在此位置执行任何操作。

此示例显示使用自定义派生属性来修改 泛型参数 的含义。

// 假设 MyFlexibleClone 的派生宏将 `my_flexible_clone` 声明为
// 其理解的属性。
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
    a: *const H
}

关联项

Syntax
AssociatedItem
    OuterAttribute* (
        MacroInvocationSemi
      | ( Visibility? ( TypeAlias | ConstantItem | Function ) )
    )

关联项 (Associated Items) 是在 特型 中声明或在 实现 中定义的 项。它们之所以被称为关联项,是因为它们定义在一个关联类型上 —— 即实现中的类型。

它们是可以在模块中声明的 项 的一个子集。具体来说,包括 关联函数(包括方法)、关联类型关联常量

当关联项在逻辑上与被关联的 项 相关时,使用关联项非常有用。例如,Option 上的 is_some 方法本质上与 Options 相关,因此应该被关联。

每种关联项都有两种形式:包含实际实现的定义,以及为定义声明签名的声明。

正是这些声明构成了 特型 的契约,以及 泛型 类型上可用的功能。

关联函数和方法

关联函数 (Associated functions) 是与类型关联的 函数

关联函数声明 为关联函数定义声明一个签名。它的写法与函数 项 类似,只是函数体被替换为 ;

标识符是函数的名称。

关联函数的 泛型、参数列表、返回类型和 where 子句必须与关联函数声明的相同。

关联函数定义 定义了一个与另一个类型关联的函数。它的写法与 函数项 相同。

注意

一个常见的例子是名为 new 的关联函数,它返回一个与之关联的类型的值。

struct Struct {
    field: i32
}

impl Struct {
    fn new() -> Struct {
        Struct {
            field: 0i32
        }
    }
}

fn main () {
    let _struct = Struct::new();
}

当关联函数在 特型 上声明时,也可以使用指向该 特型 并在其后追加 特型 名称的 路径 来调用该函数。当发生这种情况时,它会被替换为 <_ 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 的关联函数被称为 方法 (methods) ,可以使用 方法调用运算符 调用,例如 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) {}
}
}

可以使用简写语法而不指定类型,它们具有以下等价形式:

简写等价形式
selfself: Self
&'lifetime selfself: &'lifetime Self
&'lifetime mut selfself: &'lifetime mut Self

注意

生命周期可以,且通常在这种简写中被省略。

如果 self 参数带有 mut 前缀,它就会变成一个可变变量,类似于使用 mut 标识符模式 的常规参数。例如:

#![allow(unused)]
fn main() {
trait Changer: Sized {
    fn change(mut self) {}
    fn modify(mut self: Box<Self>) {}
}
}

作为 特型 上方法的示例,请考虑以下内容:

#![allow(unused)]
fn main() {
type Surface = i32;
type BoundingBox = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn bounding_box(&self) -> BoundingBox;
}
}

这定义了一个具有两个方法的 特型。在 特型 处于作用域内时,所有具有该 特型 实现 的值都可以调用其 drawbounding_box 方法。

#![allow(unused)]
fn main() {
type Surface = i32;
type BoundingBox = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn bounding_box(&self) -> BoundingBox;
}

struct Circle {
    // ...
}

impl Shape for Circle {
    // ...
  fn draw(&self, _: Surface) {}
  fn bounding_box(&self) -> BoundingBox { 0i32 }
}

impl Circle {
    fn new() -> Circle { Circle{} }
}

let circle_shape = Circle::new();
let bounding_box = circle_shape.bounding_box();
}

2018 版次差异

在 2015 版次中,可以使用匿名参数声明 特型 方法(例如 fn foo(u8))。从 2018 版次开始,这种做法已被弃用并被视为错误。所有参数都必须有一个参数名称。

方法参数上的属性

方法参数上的属性遵循与 常规函数参数 相同的规则和限制。

关联类型

关联类型 (Associated types) 是与另一个类型关联的 类型别名

关联类型不能在 固有实现 中定义,也不能在 特型 中给出默认实现。

关联类型声明 为关联类型定义声明一个签名。它的写法有以下形式之一,其中 Assoc 是关联类型的名称,Params 是以逗号分隔的类型、生命周期或常量参数列表,Bounds 是以加号分隔的关联类型必须满足的特型界限列表,而 WhereBounds 是以逗号分隔的参数必须满足的界限列表:

type Assoc;
type Assoc: Bounds;
type Assoc<Params>;
type Assoc<Params>: Bounds;
type Assoc<Params> where WhereBounds;
type Assoc<Params>: Bounds where WhereBounds;

标识符是声明的类型别名的名称。

可选的特型界限必须由类型别名的实现满足。

关联类型上有一个隐式的 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 子句;这些通常被称为 泛型关联类型 (generic associated types)GATs 。如果类型 Thing 具有来自 特型 Trait 且带有泛型 <'a> 的关联类型 Item,则该类型可以被命名为 <Thing as Trait>::Item<'x>,其中 'x 是作用域内的某个生命周期。在这种情况下,在 impl 上的关联类型定义中,凡是出现 'a 的地方都会使用 'x

trait AssociatedType {
    // 关联类型声明
    type Assoc;
}

struct Struct;

struct OtherStruct;

impl AssociatedType for Struct {
    // 关联类型定义
    type Assoc = OtherStruct;
}

impl OtherStruct {
    fn new() -> OtherStruct {
        OtherStruct
    }
}

fn main() {
    // 使用关联类型以 <Struct as AssociatedType>::Assoc 形式引用 OtherStruct
    let _other_struct: OtherStruct = <Struct as AssociatedType>::Assoc::new();
}

带有泛型和 where 子句的关联类型示例:

struct ArrayLender<'a, T>(&'a mut [T; 16]);

trait Lend {
    // 泛型关联类型声明
    type Lender<'a> where Self: 'a;
    fn lend<'a>(&'a mut self) -> Self::Lender<'a>;
}

impl<T> Lend for [T; 16] {
    // 泛型关联类型定义
    type Lender<'a> = ArrayLender<'a, T> where Self: 'a;

    fn lend<'a>(&'a mut self) -> Self::Lender<'a> {
        ArrayLender(self)
    }
}

fn borrow<'a, T: Lend>(array: &'a mut T) -> <T as Lend>::Lender<'a> {
    array.lend()
}

fn main() {
    let mut array = [0usize; 16];
    let lender = borrow(&mut array);
}

关联类型容器示例

考虑以下 Container 特型 的示例。请注意,该类型可在方法签名中使用:

#![allow(unused)]
fn main() {
trait Container {
    type E;
    fn empty() -> Self;
    fn insert(&mut self, elem: Self::E);
}
}

为了让一个类型实现这个 特型,它不仅必须为每个方法提供实现,还必须指定类型 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); }
}
}

BoundsWhereBounds之间的关系

在这个例子中:

#![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子句

特型 上的泛型关联类型声明目前可能需要一系列 where 子句,这取决于 特型 中的函数以及 GAT 的使用方式。这些规则将来可能会放宽;更新可以在 泛型关联类型计划仓库 (generic associated types initiative repository) 中找到。

简而言之,为了最大限度地允许 impl 中关联类型的定义,这些 where 子句是必需的。为此,对于 GAT 作为输入或输出出现的任何函数,凡是 可以被证明在函数上成立 的子句(使用函数或 特型 的参数),也必须写在 GAT 自身之上。

#![allow(unused)]
fn main() {
trait LendingIterator {
    type Item<'x> where Self: 'x;
    fn next<'a>(&'a mut self) -> Self::Item<'a>;
}
}

在上面 next 函数中,由于 &'a mut self 的隐含界限,我们可以证明 Self: 'a;因此,我们必须在 GAT 自身上写下等效的界限:where Self: 'x

当 特型 中有多个函数使用该 GAT 时,将使用来自不同函数的界限的 交集 (intersection) ,而不是并集。

#![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_checkerT: '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。然而,由于 Item 被用于 Iterator 的界限中,where Self: 'a 子句在那里也是必需的。

最后,在 特型 中的 GAT 上显式使用的任何 'static 都不计入必需界限。

#![allow(unused)]
fn main() {
trait StaticReturn {
    type Y<'a>;
    fn foo(&self) -> Self::Y<'static>;
}
}

关联常量

关联常量 (Associated constants) 是与类型关联的 常量

关联常量声明 为关联常量定义声明一个签名。它的写法是 const,然后是一个标识符,然后是 :,然后是一个类型,最后以 ; 结束。

标识符是在路径中使用的常量的名称。类型是定义必须实现的类型。

关联常量定义 定义了一个与类型关联的常量。它的写法与 常量项 相同。

关联常量定义仅在被引用时才进行 常量求值。此外,包含 泛型参数 的定义会在单态化后进行求值。

struct Struct;
struct GenericStruct<const ID: i32>;

impl Struct {
    // 定义不会立即求值
    const PANIC: () = panic!("compile-time panic");
}

impl<const ID: i32> GenericStruct<ID> {
    // 定义不会立即求值
    const NON_ZERO: () = if ID == 0 {
        panic!("contradiction")
    };
}

fn main() {
    // 引用 Struct::PANIC 会导致编译错误
    let _ = Struct::PANIC;

    // 正常,ID 不是 0
    let _ = GenericStruct::<1>::NON_ZERO;

    // 求值 ID=0 的 NON_ZERO 时产生编译错误
    let _ = GenericStruct::<0>::NON_ZERO;
}

关联常量示例

一个基本示例:

trait ConstantId {
    const ID: i32;
}

struct Struct;

impl ConstantId for Struct {
    const ID: i32 = 1;
}

fn main() {
    assert_eq!(1, Struct::ID);
}

使用默认值:

trait ConstantIdDefault {
    const ID: i32 = 1;
}

struct Struct;
struct OtherStruct;

impl ConstantIdDefault for Struct {}

impl ConstantIdDefault for OtherStruct {
    const ID: i32 = 5;
}

fn main() {
    assert_eq!(1, Struct::ID);
    assert_eq!(5, OtherStruct::ID);
}

属性

Syntax
InnerAttribute# ! [ Attr ]

OuterAttribute# [ Attr ]

Attr
      SimplePath AttrInput?
    | unsafe ( SimplePath AttrInput? )

AttrInput
      DelimTokenTree
    | = Expression

一个 属性 是一个通用的、自由形式的元数据,根据名称、约定、语言和编译器版本进行解释。属性的建模基于 ECMA-335 中的属性,其 语法格式 源自 ECMA-334 (C#)。

内部属性,写在哈希符号 (#) 后带一个感叹号 (!),适用于声明该属性的所在形式。

例子

#![allow(unused)]
fn main() {
// 适用于 enclosing 模块或 crate 的通用元数据。
#![crate_type = "lib"]

// 内部属性适用于整个函数。
fn some_unused_variables() {
  #![allow(unused_variables)]

  let x = ();
  let y = ();
  let z = ();
}
}

外部属性,写在哈希符号后不带感叹号,适用于属性后面的形式。

例子

#![allow(unused)]
fn main() {
// 标记为单元测试的函数
#[test]
fn test_foo() {
    /* ... */
}

// 有条件编译的模块
#[cfg(target_os = "linux")]
mod bar {
    /* ... */
}

// 用于抑制警告/错误的 lint 属性
#[allow(non_camel_case_types)]
type int8_t = i8;
}

属性由属性的路径组成,后跟一个可选的带分隔符的词法单元树,其解释由属性定义。除了宏属性之外,属性还允许输入是一个等号 (=) 后跟一个表达式。有关更多详细信息,请参阅下面的 元项属性语法

属性的应用可能不安全。为避免在使用这些属性时出现未定义行为,必须满足某些编译器无法检查的义务。为了声明这些义务已满足,属性被 unsafe(..) 包裹,例如 #[unsafe(no_mangle)]

以下属性是不安全的:

属性可以分为以下几种:

属性可以应用于语言中的多种形式:

元项属性语法

“元项”是大多数 内置属性 用于 Attr 规则的 语法格式。它具有以下语法:

Syntax
MetaItem
      SimplePath
    | SimplePath = Expression
    | SimplePath ( MetaSeq? )

MetaSeq
    MetaItemInner ( , MetaItemInner )* ,?

MetaItemInner
      MetaItem
    | Expression

元项中的表达式必须宏展开为字面量表达式,其中不得包含整数或浮点类型后缀。非字面量表达式的表达式在语法上会被接受(并可传递给过程宏),但会在解析后被拒绝。

请注意,如果属性出现在另一个宏中,它将在该外部宏之后展开。例如,以下代码将首先展开 Serialize 过程宏,该宏必须保留 include_str! 调用才能被展开:

#[derive(Serialize)]
struct Foo {
    #[doc = include_str!("x.md")]
    x: u32
}

此外,属性中的宏只会在应用于该 项 的所有其他属性之后展开:

#[macro_attr1] // 首先展开
#[doc = mac!()] // `mac!` 第四次展开。
#[macro_attr2] // 第二次展开
#[derive(MacroDerive1, MacroDerive2)] // 第三次展开
fn foo() {}

各种内置属性使用元项语法的不同子集来指定它们的输入。以下语法规则展示了一些常用形式:

Syntax
MetaWord
    IDENTIFIER

MetaNameValueStr
    IDENTIFIER = ( STRING_LITERAL | RAW_STRING_LITERAL )

MetaListPaths
    IDENTIFIER ( ( SimplePath ( , SimplePath )* ,? )? )

MetaListIdents
    IDENTIFIER ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )

MetaListNameValueStr
    IDENTIFIER ( ( MetaNameValueStr ( , MetaNameValueStr )* ,? )? )

一些元项的示例如下:

样式示例
元字no_std
元名称值字符串doc = "example"
元列表路径allow(unused, clippy::inline_always)
元列表标识符macro_use(foo, bar)
元列表名称值字符串link(name = "CoreFoundation", kind = "framework")

活跃属性和惰性属性

属性要么是活跃的,要么是惰性的。在属性处理期间,活跃属性 会从它们所在的表单中移除自己,而 惰性属性 则保留在表单上。

cfgcfg_attr 属性是活跃的。属性宏 是活跃的。所有其他属性都是惰性的。

工具属性

编译器可能允许外部工具的属性,其中每个工具都位于 工具预导入 中的自己的模块中。属性路径的第一个段是工具的名称,带有一个或多个额外段,其解释由工具决定。

当工具未使用时,工具的属性会被接受,而不会发出警告。当工具在使用时,工具负责处理和解释其属性。

如果使用了 no_implicit_prelude 属性,则工具属性不可用。

#![allow(unused)]
fn main() {
// 告诉 rustfmt 工具不要格式化以下元素。
#[rustfmt::skip]
struct S {
}

// 控制 clippy 工具的“圈复杂度”阈值。
#[clippy::cyclomatic_complexity = "100"]
pub fn f() {}
}

注意

rustc 目前识别的工具包括 “clippy”, “rustfmt”, “diagnostic”, “miri” 和 “rust_analyzer”。

内置属性索引

以下是所有内置属性的索引。

  • 条件编译

    • cfg — 控制条件编译。
    • cfg_attr — 有条件地包含属性。
  • 测试

    • test — 将函数标记为测试。
    • ignore — 禁用测试函数。
    • should_panic — 表示测试应生成一个 恐慌。
  • 派生

  • 诊断

  • ABI、链接、符号和 FFI

    • link — 指定要与 extern 块链接的本地库。
    • link_name — 指定 extern 块中函数或静态变量的符号名称。
    • link_ordinal — 指定 extern 块中函数或静态变量的符号序号。
    • no_link — 阻止链接外部 crate。
    • repr — 控制类型布局。
    • crate_type — 指定 crate 的类型(库、可执行文件等)。
    • no_main — 禁用发出 main 符号。
    • export_name — 指定函数或静态变量的导出符号名称。
    • link_section — 指定函数或静态变量要使用的对象文件节。
    • no_mangle — 禁用符号名称编码。
    • used — 强制编译器将静态 项 保留在输出对象文件中。
    • crate_name — 指定 crate 名称。
  • 代码生成

    • inline — 提示内联代码。
    • cold — 提示函数不太可能被调用。
    • naked — 阻止编译器发出函数序言和尾声。
    • no_builtins — 禁用使用某些内置函数。
    • target_feature — 配置特定于平台的代码生成。
    • track_caller — 将父调用位置传递给 std::panic::Location::caller()
    • instruction_set — 指定用于生成函数代码的指令集。
  • 文档

  • 预导入

  • 模块

    • path — 指定模块的文件名。
  • 限制

  • 运行时

  • 特性

    • feature — 用于启用不稳定或实验性编译器 特性。有关 rustc 中实现的 特性,请参阅 The Unstable Book
  • 类型系统

    • non_exhaustive — 指示将来会为类型添加更多字段/变体。
  • 调试器

测试属性

以下 属性 用于指定执行测试的函数。在“测试”模式下编译 crate 会启用测试函数的构建以及执行测试的测试工具。启用测试模式还会启用 test 条件编译选项

test属性

test 属性 将一个函数标记为要作为测试执行。

例子

#![allow(unused)]
fn main() {
pub fn add(left: u64, right: u64) -> u64 { left + right }
#[test]
fn it_works() {
    let result = add(2, 2);
    assert_eq!(result, 4);
}
}

test 属性使用 元字 语法格式。

test 属性只能应用于单态的、不带参数且返回类型实现 Termination 特型 的 自由函数

注意

实现了 Termination 特型 的一些类型包括:

  • ()
  • Result<T, E> where T: Termination, E: Debug

test 在函数上的第一次使用才有效。

注意

rustc 对第一次使用之后的任何使用都会发出 lint 警告。这在将来可能会变为错误。

test 属性从标准库 预导入 std::prelude::v1::test 中导出。

这些函数只在测试模式下编译。

注意

测试模式通过向 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()?; // 预期成功
    do_the_thing(&state)?;          // 预期成功
    Ok(())
}
}

ignore属性

ignore 属性 可以与 test 属性 一起使用,以告知测试工具不要将该函数作为测试执行。

例子

#![allow(unused)]
fn main() {
#[test]
#[ignore]
fn check_thing() {
    // ...
}
}

注意

rustc 测试工具支持 --include-ignored 标志来强制运行被忽略的测试。

ignore 属性使用 元字元名称值字符串 语法格式。

ignore 属性的 元名称值字符串 形式提供了一种方法来指定忽略测试的原因。

例子

#![allow(unused)]
fn main() {
#[test]
#[ignore = "not yet implemented"] // 尚未实现
fn mytest() {
    // ...
}
}

ignore 属性只能应用于带有 test 属性注解的函数。

注意

rustc 在其他位置的使用会被忽略,但会发出 lint 警告。这在将来可能会变为错误。

ignore 在函数上的第一次使用才有效。

注意

rustc 对第一次使用之后的任何使用都会发出 lint 警告。这在将来可能会变为错误。

被忽略的测试在测试模式下仍然会被编译,但不会执行。

should_panic属性

should_panic 属性 会使测试仅在应用该属性的 测试函数 发生 恐慌 时才通过。

例子

#![allow(unused)]
fn main() {
#[test]
#[should_panic(expected = "values don't match")] // 预期为“值不匹配”
fn mytest() {
    assert_eq!(1, 2, "values don't match"); // 值不匹配
}
}

should_panic 属性有以下形式:

  • 元字

    例子

    #![allow(unused)]
    fn main() {
    #[test]
    #[should_panic]
    fn mytest() { panic!("error: some message, and more"); } // 错误:一些消息,还有更多
    }
  • 元名称值字符串 — 给定字符串必须出现在 恐慌 消息中,测试才能通过。

    例子

    #![allow(unused)]
    fn main() {
    #[test]
    #[should_panic = "some message"] // 一些消息
    fn mytest() { panic!("error: some message, and more"); } // 错误:一些消息,还有更多
    }
  • 元列表名称值字符串 — 与 元名称值字符串 语法格式 一样,给定字符串必须出现在 恐慌 消息中。

    例子

    #![allow(unused)]
    fn main() {
    #[test]
    #[should_panic(expected = "some message")] // 预期为“一些消息”
    fn mytest() { panic!("error: some message, and more"); } // 错误:一些消息,还有更多
    }

should_panic 属性只能应用于带有 test 属性注解的函数。

注意

rustc 在其他位置的使用会被忽略,但会发出 lint 警告。这在将来可能会变为错误。

should_panic 在函数上的第一次使用才有效。

注意

rustc 对第一次使用之后的任何使用都会发出未来兼容性警告。这在将来可能会变为错误。

当使用 元名称值字符串 形式或带有 expected 键的 元列表名称值字符串 形式时,给定字符串必须出现在 恐慌 消息中的某个位置,测试才能通过。

测试函数的返回类型必须是 ()

派生

derive 属性 调用一个或多个 派生宏,允许为数据结构自动生成新的 。你可以使用 过程宏 创建 derive 宏。

例子

PartialEq 派生宏 为 Foo<T> where T: PartialEq 发出一个 PartialEq实现Clone 派生宏 为 Clone 也做同样的事情。

#![allow(unused)]
fn main() {
#[derive(PartialEq, Clone)]
struct Foo<T> {
    a: i32,
    b: T,
}
}

生成的 impl 项 等同于:

#![allow(unused)]
fn main() {
struct Foo<T> { a: i32, b: T }
impl<T: PartialEq> PartialEq for Foo<T> {
    fn eq(&self, other: &Foo<T>) -> bool {
        self.a == other.a && self.b == other.b
    }
}

impl<T: Clone> Clone for Foo<T> {
    fn clone(&self) -> Self {
        Foo { a: self.a.clone(), b: self.b.clone() }
    }
}
}

derive 属性使用 MetaListPaths 语法格式 来指定要调用的 派生宏 的路径列表。

derive 属性只能应用于 结构体枚举联合体

derive 属性可以在一个 项 上使用任意次数。所有属性中列出的所有 派生宏 都会被调用。

derive 属性在标准库 预导入 中作为 core::prelude::v1::derive 导出。

内置派生在 语言预导入 中定义。内置派生的列表是:

内置派生在它们生成的 实现 上包含 automatically_derived 属性

在宏扩展期间,对于派生列表中的每个元素,相应的 派生宏 扩展为零个或多个

automatically_derived 属性

automatically_derived 属性 用于注解一个 实现,以表明它是由 派生宏 自动创建的。它没有直接作用,但工具和诊断 lint 可能会用它来检测这些自动生成的 实现。

例子

给定 struct Example 上的 #[derive(Clone)]派生宏 可能会生成:

#![allow(unused)]
fn main() {
struct Example;
#[automatically_derived]
impl ::core::clone::Clone for Example {
    #[inline]
    fn clone(&self) -> Self {
        Example
    }
}
}

automatically_derived 属性使用 MetaWord 语法格式。

automatically_derived 属性只能应用于 实现

注意

rustc 在其他位置的使用会被忽略,但会发出 lint 警告。这在将来可能会变为错误。

在一个 实现 上多次使用 automatically_derived 与使用一次效果相同。

注意

rustc 对第一次使用之后的任何使用都会发出 lint 警告。

automatically_derived 属性没有行为。

诊断属性

以下 属性 用于在编译期间控制或生成诊断消息。

Lint检查属性

Lint 检查命名了一种可能不理想的编码模式,例如不可达代码或遗漏的文档。

lint 属性 allowexpectwarndenyforbid 使用 MetaListPaths 语法格式 来指定 lint 名称列表,以更改属性所应用实体的 lint 级别。

对于任何 lint 检查 C

  • #[allow(C)] 覆盖对 C 的检查,以便违规行为将不被报告。
  • #[expect(C)] 表示预期会发出 lint C。如果预期未实现,则该属性将抑制 C 的发出或发出警告。
  • #[warn(C)] 警告 C 的违规行为,但继续编译。
  • #[deny(C)] 在遇到 C 的违规行为后发出错误信号,
  • #[forbid(C)]deny(C) 相同,但事后也禁止更改 lint 级别,

注意

通过 rustc -W help 可以找到 rustc 支持的 lint 检查,以及它们的默认设置,并记录在 rustc book 中。

#![allow(unused)]
fn main() {
pub mod m1 {
    // 此处忽略缺少文档
    #[allow(missing_docs)]
    pub fn undocumented_one() -> i32 { 1 }

    // 此处缺少文档会发出警告
    #[warn(missing_docs)]
    pub fn undocumented_too() -> i32 { 2 }

    // 此处缺少文档会发出错误
    #[deny(missing_docs)]
    pub fn undocumented_end() -> i32 { 3 }
}
}

lint 属性可以覆盖先前属性指定的级别,只要该级别不尝试更改被禁止的 lint (除了 deny,它在 forbid 上下文中是允许的,但会被忽略)。 先前的属性是指在语法树中更高级别的属性,或在同一实体上按从左到右源代码顺序列出的先前属性。

此示例展示了如何使用 allowwarn 来开启和关闭特定检查:

#![allow(unused)]
fn main() {
#[warn(missing_docs)]
pub mod m2 {
    #[allow(missing_docs)]
    pub mod nested {
        // 此处忽略缺少文档
        pub fn undocumented_one() -> i32 { 1 }

        // 此处缺少文档会发出警告,
        // 尽管上面有 allow。
        #[warn(missing_docs)]
        pub fn undocumented_two() -> i32 { 2 }
    }

    // 此处缺少文档会发出警告
    pub fn undocumented_too() -> i32 { 3 }
}
}

此示例展示了如何使用 forbid 来禁止对该 lint 检查使用 allowexpect

#![allow(unused)]
fn main() {
#[forbid(missing_docs)]
pub mod m3 {
    // 尝试切换警告在此处发出错误
    #[allow(missing_docs)]
    /// 返回 2。
    pub fn undocumented_too() -> i32 { 2 }
}
}

注意

rustc 允许在 命令行 上设置 lint 级别,并且还支持对报告的 lint 设置上限

Lint原因

所有 lint 属性都支持一个额外的 reason 参数,以提供添加某个属性的上下文。如果 lint 在定义的级别发出,此原因将作为 lint 消息的一部分显示。

#![allow(unused)]
fn main() {
// `keyword_idents` 默认是允许的。此处我们禁止它以
// 避免在更新 版次 时标识符的迁移。
#![deny(
    keyword_idents,
    reason = "we want to avoid these idents to be future compatible"
)]

// 此名称在 Rust 的 2015 版次 中是允许的。我们仍然旨在避免
// 这以便未来兼容并且不混淆最终用户。
fn dyn() {}
}

这是另一个示例,其中 lint 被允许并带有原因:

#![allow(unused)]
fn main() {
use std::path::PathBuf;

pub fn get_path() -> PathBuf {
    // `allow` 属性上的 `reason` 参数作为读者的文档。
    #[allow(unused_mut, reason = "this is only modified on some platforms")]
    let mut file_name = PathBuf::from("git");

    #[cfg(target_os = "windows")]
    file_name.set_extension("exe");

    file_name
}
}

#[expect]属性

#[expect(C)] 属性为 lint C 创建一个 lint 预期。如果同一位置的 #[warn(C)] 属性会导致 lint 发出,则该预期将被实现。如果预期未实现,因为 lint C 不会发出,则将在该属性处发出 unfulfilled_lint_expectations lint。

fn main() {
    // 这个 `#[expect]` 属性创建了一个 lint 预期,即 `unused_variables`
    // lint 将由以下语句发出。由于 `question` 变量被 `println!` 宏使用,
    // 因此此预期未实现。
    // 因此,`unfulfilled_lint_expectations` lint 将在此属性处发出。
    #[expect(unused_variables)]
    let question = "who lives in a pineapple under the sea?";
    println!("{question}");

    // 这个 `#[expect]` 属性创建了一个 lint 预期,它将被实现,因为
    // `answer` 变量从未被使用。通常会发出的 `unused_variables` lint 被抑制。
    // 不会为语句或属性发出警告。
    #[expect(unused_variables)]
    let answer = "SpongeBob SquarePants!";
}

lint 预期仅由被 expect 属性抑制的 lint 发出所实现。如果在作用域中使用 allowwarn 等其他级别属性修改了 lint 级别,则 lint 发出将相应处理,并且预期将保持未实现状态。

#![allow(unused)]
fn main() {
#[expect(unused_variables)]
fn select_song() {
    // 这将按 `warn` 属性定义发出 `unused_variables` lint 的警告级别。
    // 这不会实现函数上方的预期。
    #[warn(unused_variables)]
    let song_name = "Crab Rave";

    // `allow` 属性抑制了 lint 发出。这不会实现预期,
    // 因为它已被 `allow` 属性抑制,而不是函数上方的 `expect` 属性。
    #[allow(unused_variables)]
    let song_creator = "Noisestorm";

    // 这个 `expect` 属性将抑制变量处的 `unused_variables` lint 发出。
    // 函数上方的 `expect` 属性仍然不会被实现,因为此 lint 发出已被本地
    // expect 属性抑制。
    #[expect(unused_variables)]
    let song_version = "Monstercat Release";
}
}

如果 expect 属性包含多个 lint,则每个 lint 都单独预期。对于 lint 组,只要组内有一个 lint 被发出就足够了:

#![allow(unused)]
fn main() {
// 这个预期将被函数内未使用的值实现,
// 因为发出的 `unused_variables` lint 在 `unused` lint 组内。
#[expect(unused)]
pub fn thoughts() {
    let unused = "I'm running out of examples";
}

pub fn another_example() {
    // 此属性创建两个 lint 预期。`unused_mut` lint 将被抑制,
    // 从而实现第一个预期。`unused_variables` 不会发出,因为变量已被使用。
    // 因此,该预期将未实现,并会发出警告。
    #[expect(unused_mut, unused_variables)]
    let mut link = "https://www.rust-lang.org/";

    println!("Welcome to our community: {link}");
}
}

注意

#[expect(unfulfilled_lint_expectations)] 的行为目前定义为始终生成 unfulfilled_lint_expectations lint。

Lint组

Lint 可以组织成命名组,以便可以一起调整相关 lint 的级别。使用命名组等同于列出该组内的 lint。

#![allow(unused)]
fn main() {
// 这允许“unused”组中的所有 lint。
#[allow(unused)]
// 这将“unused”组中的“unused_must_use”lint 覆盖为 deny。
#[deny(unused_must_use)]
fn example() {
    // 这不会生成警告,因为“unused_variables”
    // lint 在“unused”组中。
    let x = 1;
    // 这会生成错误,因为结果未使用,并且
    // “unused_must_use”被标记为“deny”。
    std::fs::remove_file("some_file"); // 错误:必须使用的未使用的 `Result`
}
}

有一个名为“warnings”的特殊组,它包含所有警告级别的 lint。 “warnings”组忽略属性顺序,并应用于实体内所有本应发出警告的 lint。

#![allow(unused)]
fn main() {
unsafe fn an_unsafe_fn() {}
// 这两个属性的顺序无关紧要。
#[deny(warnings)]
// `unsafe_code` lint  normally "allow" by default.
#[warn(unsafe_code)]
fn example_err() {
    // 这是一个错误,因为 `unsafe_code` 警告已
    // 提升为“deny”。
    unsafe { an_unsafe_fn() } // 错误:使用了 `unsafe` 块
}
}

工具lint属性

工具 lint 允许使用作用域 lint,以 allowwarndenyforbid 某些工具的 lint。

工具 lint 仅在相关工具处于活动状态时才会被检查。如果像 allow 这样的 lint 属性引用了不存在的工具 lint,编译器在您使用该工具之前不会警告不存在的 lint。

否则,它们的工作方式与常规 lint 属性相同:

// set the entire `pedantic` clippy lint group to warn
#![warn(clippy::pedantic)]
// silence warnings from the `filter_map` clippy lint
#![allow(clippy::filter_map)]

fn main() {
    // ...
}

// silence the `cmp_nan` clippy lint just for this function
#[allow(clippy::cmp_nan)]
fn foo() {
    // ...
}

注意

rustc 目前识别 “clippy” 和 “rustdoc” 的工具 lint。

deprecated属性

deprecated 属性 将一个 项 标记为已弃用。rustc 将在使用 #[deprecated] 项 时发出警告。rustdoc 将显示 项 弃用信息,包括 since 版本和 note(如果可用)。

deprecated 属性有几种形式:

  • deprecated — 发出通用消息。
  • deprecated = "message" — 在弃用消息中包含给定的字符串。
  • MetaListNameValueStr 语法格式,带有两个可选字段:
    • since — 指定 项 被弃用时的版本号。rustc 目前不解释该字符串,但 Clippy 等外部工具可能会检查该值的有效性。
    • note — 指定应包含在弃用消息中的字符串。这通常用于提供有关弃用的解释和首选替代方案。

deprecated 属性可以应用于任何 特型项枚举变体结构体字段外部块项宏定义。它不能应用于 特型实现项。当应用于包含其他 项 的 项 (例如 模块实现)时,所有子 项 都继承弃用属性。

这里有一个例子:

#![allow(unused)]
fn main() {
#[deprecated(since = "5.2.0", note = "foo was rarely used. Users should instead use bar")]
pub fn foo() {}

pub fn bar() {}
}

RFC 包含了动机和更多细节。

must_use属性

must_use 属性 用于在值未“使用”时发出诊断警告。

must_use 属性可以应用于用户定义的复合类型(结构体枚举联合体)、函数特型

must_use 属性可以使用 MetaNameValueStr 语法格式 包含一条消息,例如 #[must_use = "example message"]。该消息将与警告一起给出。

当用于用户定义的复合类型时,如果 表达式语句表达式 具有该类型,则 unused_must_use lint 被违反。

#![allow(unused)]
fn main() {
#[must_use]
struct MustUse {
    // 一些字段
}

impl MustUse {
  fn new() -> MustUse { MustUse {} }
}

// 违反 `unused_must_use` lint。
MustUse::new();
}

当用于函数时,如果 表达式语句表达式 是对该函数的 调用表达式,则 unused_must_use lint 被违反。

#![allow(unused)]
fn main() {
#[must_use]
fn five() -> i32 { 5i32 }

// 违反 `unused_must_use` lint。
five();
}

当用于 特型声明 时,如果 表达式语句 对一个返回该 特型 的 impl traitdyn trait 的函数的 调用表达式 被未使用,则违反 unused_must_use lint。

#![allow(unused)]
fn main() {
#[must_use]
trait Critical {}
impl Critical for i32 {}

fn get_critical() -> impl Critical {
    4i32
}

// 违反 `unused_must_use` lint。
get_critical();
}

当用于 特型 声明中的函数时,当调用表达式是 特型 实现中的函数时,该行为也适用。

#![allow(unused)]
fn main() {
trait Trait {
    #[must_use]
    fn use_me(&self) -> i32;
}

impl Trait for i32 {
    fn use_me(&self) -> i32 { 0i32 }
}

// 违反 `unused_must_use` lint。
5i32.use_me();
}

当用于 特型 实现中的函数时,该属性不执行任何操作。

注意

包含该值的普通无操作表达式不会违反 lint。示例包括将值包装在一个不实现 Drop 的类型中然后不使用该类型,以及作为未使用的 块表达式 的最终表达式。

#![allow(unused)]
fn main() {
#[must_use]
fn five() -> i32 { 5i32 }

// 这些都不会违反 `unused_must_use` lint。
(five(),);
Some(five());
{ five() };
if true { five() } else { 0i32 };
match true {
    _ => five()
};
}

注意

当有意丢弃一个必须使用的值时,习惯上使用带有 _ 模式的 let 语句

#![allow(unused)]
fn main() {
#[must_use]
fn five() -> i32 { 5i32 }

// 不违反 `unused_must_use` lint。
let _ = five();
}

diagnostic工具属性命名空间

#[diagnostic] 属性命名空间是用于影响编译时错误消息的属性的归属。这些属性提供的提示不保证会被使用。

此命名空间中未知的属性会被接受,尽管它们可能会为未使用的属性发出警告。此外,已知属性的无效输入通常会是警告(详见属性定义)。这旨在允许将来添加或丢弃属性和更改输入,从而允许更改而无需保持无意义的属性或选项继续工作。

diagnostic::on_unimplemented属性

#[diagnostic::on_unimplemented] 属性是给编译器的一个提示,用于补充在需要 特型 但类型上未实现 特型 的情况下通常会生成的错误消息。

该属性应放置在 特型声明 上,尽管放置在其他位置不是错误。

该属性使用 MetaListNameValueStr 语法格式 来指定其输入,尽管任何格式错误的属性输入都不被视为错误,以提供向前和向后兼容性。

以下键具有给定含义:

  • message — 顶级错误消息的文本。
  • label — 错误消息中损坏代码内联显示的标签文本。
  • note — 提供额外备注。

note 选项可以出现多次,这将导致发出多条备注消息。

如果其他选项中的任何一个出现多次,则相关选项的第一次出现指定实际使用的值。随后的出现会生成警告。

任何未知键都会生成警告。

所有三个选项都接受一个字符串作为参数,使用与 std::fmt 字符串相同的格式进行解释。

带有给定命名参数的格式参数将被以下文本替换:

  • {Self} — 实现 特型 的类型名称。
  • { GenericParameterName } — 给定 泛型 参数的 泛型 参数的类型名称。

任何其他格式参数都会生成警告,但除此之外,将按原样包含在字符串中。

无效格式字符串可能会生成警告,但除此之外是允许的,但可能不会按预期显示。格式说明符可能会生成警告,但除此之外会被忽略。

在此示例中:

#[diagnostic::on_unimplemented(
    message = "My Message for `ImportantTrait<{A}>` implemented for `{Self}`",
    label = "My Label",
    note = "Note 1",
    note = "Note 2"
)]
trait ImportantTrait<A> {}

fn use_my_trait(_: impl ImportantTrait<i32>) {}

fn main() {
    use_my_trait(String::new());
}

编译器可能会生成一个看起来像这样的错误消息:

error[E0277]: My Message for `ImportantTrait<i32>` implemented for `String`
  --> src/main.rs:14:18
   |
14 |     use_my_trait(String::new());
   |     ------------ ^^^^^^^^^^^^^ My Label
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `ImportantTrait<i32>` is not implemented for `String`
   = note: Note 1
   = note: Note 2

diagnostic::do_not_recommend属性

#[diagnostic::do_not_recommend] 属性是给编译器的一个提示,指示它不要在诊断消息中显示带注解的 特型 实现。

注意

如果你知道该推荐通常对程序员没有用处,那么抑制该推荐可能会很有用。这种情况经常发生在广泛的、包罗万象的 impl 上。该推荐可能会将程序员引入歧途,或者 特型 实现可能是你不希望公开的内部细节,或者程序员可能无法满足其边界。

例如,在关于某个类型未实现所需 特型 的错误消息中,编译器可能会找到一个 特型 实现,如果不是因为 特型 实现中的特定边界,该实现将满足要求。编译器可能会告诉用户存在一个 impl,但问题在于 特型 实现中的边界。#[diagnostic::do_not_recommend] 属性可以用来告诉编译器 不要 告诉用户该 特型 实现,而是简单地告诉用户该类型未实现所需的 特型。

该属性应放置在 特型实现项 上,尽管放置在其他位置不是错误。

该属性不接受任何参数,尽管意外参数不被视为错误。

在以下示例中,有一个名为 AsExpression 的 特型,它用于将任意类型转换为 SQL 库中使用的 Expression 类型。有一个名为 check 的方法,它接受一个 AsExpression

pub trait Expression {
    type SqlType;
}

pub trait AsExpression<ST> {
    type Expression: Expression<SqlType = ST>;
}

pub struct Text;
pub struct Integer;

pub struct Bound<T>(T);
pub struct SelectInt;

impl Expression for SelectInt {
    type SqlType = Integer;
}

impl<T> Expression for Bound<T> {
    type SqlType = T;
}

impl AsExpression<Integer> for i32 {
    type Expression = Bound<Integer>;
}

impl AsExpression<Text> for &'_ str {
    type Expression = Bound<Text>;
}

impl<T> Foo for T where T: Expression {}

// 取消注释此行以更改推荐。
// #[diagnostic::do_not_recommend]
impl<T, ST> AsExpression<ST> for T
where
    T: Expression<SqlType = ST>,
{
    type Expression = T;
}

trait Foo: Expression + Sized {
    fn check<T>(&self, _: T) -> <T as AsExpression<<Self as Expression>::SqlType>>::Expression
    where
        T: AsExpression<Self::SqlType>,
    {
        todo!()
    }
}

fn main() {
    SelectInt.check("bar");
}

SelectInt 类型的 check 方法期望 Integer 类型。使用 i32 类型调用它会成功,因为它通过 AsExpression 特型 转换为 Integer。但是,使用字符串调用它则不会成功,并会生成一个可能看起来像这样的错误:

error[E0277]: 特型 边界 `&str: Expression` 未满足
  --> src/main.rs:53:15
   |
53 |     SelectInt.check("bar");
   |               ^^^^^ 特型 `Expression` 未为 `&str` 实现
   |
   = help: 以下其他类型实现了 特型 `Expression`:
             Bound<T>
             SelectInt
note: `&str` 实现 `AsExpression<Integer>` 所需
  --> src/main.rs:45:13
   |
45 | impl<T, ST> AsExpression<ST> for T
   |             ^^^^^^^^^^^^^^^^     ^
46 | where
47 |     T: Expression<SqlType = ST>,
   |        ------------------------ 此处引入的未满足的 特型 边界

通过将 #[diagnostic::do_not_recommend] 属性添加到 AsExpression 的通用 impl 中,消息更改为:

error[E0277]: 特型 边界 `&str: AsExpression<Integer>` 未满足
  --> src/main.rs:53:15
   |
53 |     SelectInt.check("bar");
   |               ^^^^^ 特型 `AsExpression<Integer>` 未为 `&str` 实现
   |
   = help: 特型 `AsExpression<Integer>` 未为 `&str` 实现
           但 特型 `AsExpression<Text>` 已为其实现
   = help: 对于该 特型 实现,预期为 `Text`,但找到 `Integer`

第一个错误消息包含了一个关于 &strExpression 关系的有些令人困惑的错误消息,以及通用 impl 中未满足的 特型 边界。添加 #[diagnostic::do_not_recommend] 后,它不再考虑通用 impl 作为推荐。消息应该更清晰一些,表明字符串无法转换为 Integer

代码生成属性

以下 属性 用于控制代码生成。

inline属性

inline 属性 建议是将所修饰函数代码的副本放置在调用者中,而不是生成对函数的调用。

例子

#![allow(unused)]
fn main() {
#[inline]
pub fn example1() {}

#[inline(always)]
pub fn example2() {}

#[inline(never)]
pub fn example3() {}
}

注意

rustc 在看起来值得时会自动内联函数。请谨慎使用此属性,因为内联决策不当可能会降低程序速度。

inline 属性的 语法格式 如下:

Syntax
InlineAttribute
      inline ( always )
    | inline ( never )
    | inline

InlineAttribute inline ( always ) inline ( never ) inline

inline 属性只能应用于带有 函数体 的函数——闭包异步块自由函数固有实现特型实现 中的 关联函数,以及 特型定义 中带有 默认定义 的关联函数。

注意

rustc 忽略在其他位置的使用,但会对其进行 lint 检查。这在将来可能会成为错误。

注意

尽管该属性可以应用于 闭包异步块,但其用处有限,因为我们尚不支持表达式上的属性。

#![allow(unused)]
fn main() {
// 我们允许在语句上使用属性。
#[inline] || (); // OK
#[inline] async {}; // OK
}
#![allow(unused)]
fn main() {
// 我们尚不支持在表达式上使用属性。
let f = #[inline] || (); // ERROR
}

inline 在函数上的首次使用才生效。

注意

rustc 会对首次使用之后的任何使用进行 lint 检查。这在将来可能会成为错误。

inline 属性支持以下模式:

  • #[inline] 建议 执行内联展开。
  • #[inline(always)] 建议 始终执行内联展开。
  • #[inline(never)] 建议 永不执行内联展开。

注意

无论何种形式,该属性都只是一个提示。编译器可能会忽略它。

inline 应用于 特型 中的函数时,它仅应用于 默认定义 的代码。

inline 应用于 异步函数异步闭包 时,它仅应用于生成的 poll 函数的代码。

注意

欲了解更多详情,请参阅 Rust issue #129347

如果函数通过 no_mangleexport_name 外部导出,则 inline 属性会被忽略。

cold属性

cold 属性 建议所修饰的函数不太可能被调用,这可能有助于编译器生成更好的代码。

例子

#![allow(unused)]
fn main() {
#[cold]
pub fn example() {}
}

cold 属性使用 MetaWord 语法格式。

cold 属性只能应用于带有 函数体 的函数——闭包异步块自由函数固有实现特型实现 中的 关联函数,以及 特型定义 中带有 默认定义 的关联函数。

注意

rustc 忽略在其他位置的使用,但会对其进行 lint 检查。这在将来可能会成为错误。

注意

尽管该属性可以应用于 闭包异步块,但其用处有限,因为我们尚不支持表达式上的属性。

cold 在函数上的首次使用才生效。

注意

rustc 会对首次使用之后的任何使用进行 lint 检查。这在将来可能会成为错误。

cold 应用于 特型 中的函数时,它仅应用于 默认定义 的代码。

naked属性

naked 属性 阻止编译器为所修饰的函数发出函数序言和函数尾声。

函数体 必须由且仅由一个 naked_asm! 宏调用组成。

不会为所修饰的函数生成函数序言或函数尾声。naked_asm! 块中的汇编代码构成了裸函数的完整函数体。

naked 属性是一个 不安全 属性。使用 #[unsafe(naked)] 注解函数附带的安全义务是,函数体必须遵守函数的调用约定,维护其签名,并且要么返回,要么发散(即,不能在汇编代码末尾之后继续执行)。

汇编代码可以假定调用栈和寄存器状态在入口处是有效的,符合函数的签名和调用约定。

除了多态函数单态化时,编译器不得复制汇编代码。

注意

保证汇编代码何时可以或不可以被复制对于定义符号的裸函数很重要。

unused_variables lint 在裸函数内部被抑制。

inline 属性不能应用于裸函数。

track_caller 属性不能应用于裸函数。

测试属性 不能应用于裸函数。

no_builtins属性

no_builtins 属性 禁用与对假定存在的库函数调用相关的某些代码模式的优化。

例子

#![allow(unused)]
#![no_builtins]
fn main() {
}

no_builtins 属性使用 MetaWord 语法格式。

no_builtins 属性只能应用于 crate 根。

no_builtins 属性的首次使用才生效。

注意

rustc 会对首次使用之后的任何使用进行 lint 检查。

target_feature属性

target_feature 属性 可以应用于函数,以启用该函数针对特定平台架构 特性 的代码生成。它使用 MetaListNameValueStr 语法格式,其中包含一个 enable 的键,其值是逗号分隔的 特性 名称字符串,以启用这些 特性。

#![allow(unused)]
fn main() {
#[cfg(target_feature = "avx2")]
#[target_feature(enable = "avx2")]
fn foo_avx2() {}
}

每个 目标架构 都有一组可以启用的 特性。为 crate 未编译的目标架构指定 特性 是一个错误。

在带有 target_feature 注解的函数中定义的闭包会从封闭函数继承该属性。

调用使用当前代码运行平台不支持的 特性 编译的函数是 未定义行为除非 平台明确文档说明这是安全的。

除非以下平台规则另有规定,否则适用以下限制:

  • 安全的 #[target_feature] 函数(以及继承该属性的闭包)只能在启用了被调用者启用所有 target_feature 的调用者中安全地调用。此限制不适用于 unsafe 上下文。
  • 安全的 #[target_feature] 函数(以及继承该属性的闭包)只能在启用了被强制转换者启用所有 target_feature 的上下文中被强制转换为 安全 的函数指针。此限制不适用于 unsafe 函数指针。

隐式启用的 特性 也包含在此规则中。例如,一个 sse2 函数可以调用标记有 sse 的函数。

#![allow(unused)]
fn main() {
#[cfg(target_feature = "sse2")] {
#[target_feature(enable = "sse")]
fn foo_sse() {}

fn bar() {
    // 在这里调用 `foo_sse` 是不安全的,因为我们必须首先确保 SSE 可用,
    // 即使 `sse` 在目标平台上默认启用或通过编译器标志手动启用。
    unsafe {
        foo_sse();
    }
}

#[target_feature(enable = "sse")]
fn bar_sse() {
    // 在这里调用 `foo_sse` 是安全的。
    foo_sse();
    || foo_sse();
}

#[target_feature(enable = "sse2")]
fn bar_sse2() {
    // 在这里调用 `foo_sse` 是安全的,因为 `sse2` 意味着 `sse`。
    foo_sse();
}
}
}

带有 #[target_feature] 属性 的函数 从不 实现 Fn 特型 家族,尽管从封闭函数继承 特性 的闭包会实现。

#[target_feature] 属性不允许在以下位置使用:

标记有 target_feature 的函数不会内联到不支持给定 特性 的上下文中。#[inline(always)] 属性不能与 target_feature 属性一起使用。

可用的特性

以下是可用 特性 名称的列表。

x86x86_64

在此平台上执行带有不支持 特性 的代码是未定义行为。因此,在此平台上使用 #[target_feature] 函数遵循 上述限制

特性隐式启用描述
adxADX — 多精度加进位指令扩展
aessse2AES — 高级加密标准
avxsse4.2AVX — 高级向量扩展
avx2avxAVX2 — 高级向量扩展 2
avx512bf16avx512bwAVX512-BF16 — 高级向量扩展 512 位 - Bfloat16 扩展
avx512bitalgavx512bwAVX512-BITALG — 高级向量扩展 512 位 - 位算法
avx512bwavx512fAVX512-BW — 高级向量扩展 512 位 - 字节和字指令
avx512cdavx512fAVX512-CD — 高级向量扩展 512 位 - 冲突检测指令
avx512dqavx512fAVX512-DQ — 高级向量扩展 512 位 - 双字和四字指令
avx512favx2, fma, f16cAVX512-F — 高级向量扩展 512 位 - 基础
avx512fp16avx512bwAVX512-FP16 — 高级向量扩展 512 位 - 浮点16 扩展
avx512ifmaavx512fAVX512-IFMA — 高级向量扩展 512 位 - 整数融合乘加
avx512vbmiavx512bwAVX512-VBMI — 高级向量扩展 512 位 - 向量字节操作指令
avx512vbmi2avx512bwAVX512-VBMI2 — 高级向量扩展 512 位 - 向量字节操作指令 2
avx512vlavx512fAVX512-VL — 高级向量扩展 512 位 - 向量长度扩展
avx512vnniavx512fAVX512-VNNI — 高级向量扩展 512 位 - 向量神经网络指令
avx512vp2intersectavx512fAVX512-VP2INTERSECT — 高级向量扩展 512 位 - 向量对交集到一对掩码寄存器
avx512vpopcntdqavx512fAVX512-VPOPCNTDQ — 高级向量扩展 512 位 - 向量人口计数指令
avxifmaavx2AVX-IFMA — 高级向量扩展 - 整数融合乘加
avxneconvertavx2AVX-NE-CONVERT — 高级向量扩展 - 无异常浮点转换指令
avxvnniavx2AVX-VNNI — 高级向量扩展 - 向量神经网络指令
avxvnniint16avx2AVX-VNNI-INT16 — 高级向量扩展 - 带有 16 位整数的向量神经网络指令
avxvnniint8avx2AVX-VNNI-INT8 — 高级向量扩展 - 带有 8 位整数的向量神经网络指令
bmi1BMI1 — 位操作指令集
bmi2BMI2 — 位操作指令集 2
cmpxchg16bcmpxchg16b — 原子地比较和交换 16 字节(128 位)数据
f16cavxF16C — 16 位浮点转换指令
fmaavxFMA3 — 三操作数融合乘加
fxsrfxsavefxrstor — 保存和恢复 x87 FPU、MMX 技术和 SSE 状态
gfnisse2GFNI — 伽罗瓦域新指令
klsse2KEYLOCKER — 英特尔密钥锁定指令
lzcntlzcnt — 前导零计数
movbemovbe — 字节交换后移动数据
pclmulqdqsse2pclmulqdq — 打包无进位乘法四字
popcntpopcnt — 设置为 1 的位数计数
rdrandrdrand — 读取随机数
rdseedrdseed — 读取随机种子
shasse2SHA — 安全散列算法
sha512avx2SHA512 — 安全散列算法与 512 位摘要
sm3avxSM3 — 商密 3 散列算法
sm4avx2SM4 — 商密 4 密码算法
sseSSE — 流式 SIMD 扩展
sse2sseSSE2 — 流式 SIMD 扩展 2
sse3sse2SSE3 — 流式 SIMD 扩展 3
sse4.1ssse3SSE4.1 — 流式 SIMD 扩展 4.1
sse4.2sse4.1SSE4.2 — 流式 SIMD 扩展 4.2
sse4asse3SSE4a — 流式 SIMD 扩展 4a
ssse3sse3SSSE3 — 补充流式 SIMD 扩展 3
tbmTBM — 尾随位操作
vaesavx2, aesVAES — 向量 AES 指令
vpclmulqdqavx, pclmulqdqVPCLMULQDQ — 四字向量无进位乘法
wideklklKEYLOCKER_WIDE — 英特尔宽密钥锁定指令
xsavexsave — 保存处理器扩展状态
xsavecxsavec — 保存处理器带压缩的扩展状态
xsaveoptxsaveopt — 保存处理器优化扩展状态
xsavesxsaves — 保存处理器主管扩展状态

aarch64

在此平台上,#[target_feature] 函数的使用遵循 上述限制

有关这些 特性 的更多文档可以在 ARM Architecture Reference Manualdeveloper.arm.com 上的其他位置找到。

注意

如果使用以下 特性 对,应同时将其标记为启用或禁用:

  • pacapacg,LLVM 目前将其实现为一个 特性。
特性隐式启用特性 名称
aesneonFEAT_AES & FEAT_PMULL — 高级 SIMD AES 和 PMULL 指令
bf16FEAT_BF16 — BFloat16 指令
btiFEAT_BTI — 分支目标识别
crcFEAT_CRC — CRC32 校验和指令
ditFEAT_DIT — 数据无关时序指令
dotprodneonFEAT_DotProd — 高级 SIMD Int8 点积指令
dpbFEAT_DPB — 数据缓存清理到持久点
dpb2dpbFEAT_DPB2 — 数据缓存清理到深度持久点
f32mmsveFEAT_F32MM — SVE 单精度浮点矩阵乘法指令
f64mmsveFEAT_F64MM — SVE 双精度浮点矩阵乘法指令
fcmaneonFEAT_FCMA — 浮点复数支持
fhmfp16FEAT_FHM — 半精度浮点 FMLAL 指令
flagmFEAT_FLAGM — 条件标志操作
fp16neonFEAT_FP16 — 半精度浮点数据处理
frinttsFEAT_FRINTTS — 浮点到整数辅助指令
i8mmFEAT_I8MM — Int8 矩阵乘法
jsconvneonFEAT_JSCVT — JavaScript 转换指令
lorFEAT_LOR — 有限排序区域扩展
lseFEAT_LSE — 大型系统扩展
mteFEAT_MTE & FEAT_MTE2 — 内存标记扩展
neonFEAT_AdvSimd & FEAT_FP — 浮点和高级 SIMD 扩展
pacaFEAT_PAUTH — 指针认证(地址认证)
pacgFEAT_PAUTH — 指针认证(通用认证)
panFEAT_PAN — 特权访问永不扩展
pmuv3FEAT_PMUv3 — 性能监视器扩展 (v3)
randFEAT_RNG — 随机数生成器
rasFEAT_RAS & FEAT_RASv1p1 — 可靠性、可用性和可服务性扩展
rcpcFEAT_LRCPC — 释放一致处理器一致
rcpc2rcpcFEAT_LRCPC2 — 带立即偏移的 RcPc
rdmneonFEAT_RDM — 舍入双精度乘累加
sbFEAT_SB — 推测屏障
sha2neonFEAT_SHA1 & FEAT_SHA256 — 高级 SIMD SHA 指令
sha3sha2FEAT_SHA512 & FEAT_SHA3 — 高级 SIMD SHA 指令
sm4neonFEAT_SM3 & FEAT_SM4 — 高级 SIMD SM3/4 指令
speFEAT_SPE — 统计分析扩展
ssbsFEAT_SSBS & FEAT_SSBS2 — 推测存储旁路安全
sveneonFEAT_SVE — 可伸缩向量扩展
sve2sveFEAT_SVE2 — 可伸缩向量扩展 2
sve2-aessve2, aesFEAT_SVE_AES & FEAT_SVE_PMULL128 — SVE AES 指令
sve2-bitpermsve2FEAT_SVE2_BitPerm — SVE 位置换
sve2-sha3sve2, sha3FEAT_SVE2_SHA3 — SVE SHA3 指令
sve2-sm4sve2, sm4FEAT_SVE2_SM4 — SVE SM4 指令
tmeFEAT_TME — 事务性内存扩展
vhFEAT_VHE — 虚拟化主机扩展

loongarch

在此平台上,#[target_feature] 函数的使用遵循 上述限制

特性隐式启用描述
fF — 单精度浮点指令
dfD — 双精度浮点指令
frecipeFRECIPE — 倒数近似指令
lasxlsxLASX — 256 位向量指令
lbtLBT — 二进制翻译指令
lsxdLSX — 128 位向量指令
lvzLVZ — 虚拟化指令

riscv32riscv64

在此平台上,#[target_feature] 函数的使用遵循 上述限制

有关这些 特性 的更多文档可以在它们各自的规范中找到。许多规范在 RISC-V ISA Manualversion 20250508,或 RISC-V GitHub Account 上的另一本手册中描述。

特性隐式启用描述
aA — 原子指令
cC — 压缩指令
mM — 整数乘法和除法指令
zbaZba — 地址生成指令
zbbZbb — 基本位操作
zbczbkcZbc — 无进位乘法
zbkbZbkb — 密码学位操作指令
zbkcZbkc — 密码学无进位乘法
zbkxZbkx — 交叉置换
zbsZbs — 单比特指令
zkzkn, zkr, zks, zkt, zbkb, zbkc, zkbxZk — 标量密码学
zknzknd, zkne, zknh, zbkb, zbkc, zkbxZkn — NIST 算法套件扩展
zkndZknd — NIST 套件: AES 解密
zkneZkne — NIST 套件: AES 加密
zknhZknh — NIST 套件: 哈希函数指令
zkrZkr — 熵源扩展
zkszksed, zksh, zbkb, zbkc, zkbxZks — 商密算法套件
zksedZksed — 商密套件: SM4 分组密码指令
zkshZksh — 商密套件: SM3 哈希函数指令
zktZkt — 数据无关执行延迟子集

wasm32wasm64

安全的 #[target_feature] 函数始终可以在 Wasm 平台上在安全上下文中使用。通过 #[target_feature] 属性 造成未定义行为是不可能的,因为尝试使用 Wasm 引擎不支持的指令会在加载时失败,而不会有被以编译器预期之外的方式解释的风险。

s390x

s390x 目标上,使用带有 #[target_feature] 属性 的函数遵循 上述限制

有关这些 特性 的更多文档可以在 z/Architecture Principles of Operation 的第 1 章“对 z/Architecture 的补充”部分中找到。

特性隐式启用描述
vector128 位向量指令
vector-enhancements-1vector向量增强 1
vector-enhancements-2vector-enhancements-1向量增强 2
vector-enhancements-3vector-enhancements-2向量增强 3
vector-packed-decimalvector向量压缩十进制
vector-packed-decimal-enhancementvector-packed-decimal向量压缩十进制增强
vector-packed-decimal-enhancement-2vector-packed-decimal-enhancement-2向量压缩十进制增强 2
vector-packed-decimal-enhancement-3vector-packed-decimal-enhancement-3向量压缩十进制增强 3
nnp-assistvectorNNP 辅助
miscellaneous-extensions-2杂项扩展 2
miscellaneous-extensions-3杂项扩展 3
miscellaneous-extensions-4杂项扩展 4

附加信息

有关根据编译时设置有选择地启用或禁用代码编译,请参阅 target_feature conditional compilation option。请注意,此选项不受 target_feature 属性 的影响,仅由整个 crate 启用的 特性 驱动。

可以在运行时使用标准库中平台特定的宏检查 特性 是否启用,例如 is_x86_feature_detectedis_aarch64_feature_detected

注意

rustc 为每个目标和 CPU 启用了一组默认 特性。可以使用 -C target-cpu 标志选择 CPU。可以使用 -C target-feature 标志为整个 crate 启用或禁用单个 特性。

track_caller属性

track_caller 属性可以应用于任何具有 "Rust" ABI 的函数,但入口点 fn main 除外。

当应用于 特型 声明中的函数和方法时,该属性适用于所有实现。如果 特型 提供带有该属性 的默认实现,则该属性也适用于覆盖实现。

当应用于 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 是一个提示,实现可能会提前停止其栈向上查找。有关重要的注意事项,请参阅 限制

示例

fcalls_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 被另一个带有属性 的函数 g 调用,而 g 又被 calls_g 调用时,fg 中的代码都观察到 gcalls_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 被另一个带有属性 的函数 h 调用,而 h 又被 calls_h 调用时,fgh 中的所有代码都观察到 hcalls_h 中的调用点:

#![allow(unused)]
fn main() {
#[track_caller]
fn f() {
    println!("{}", std::panic::Location::caller());
}
#[track_caller]
fn g() {
    println!("{}", std::panic::Location::caller());
    f();
}
#[track_caller]
fn h() {
    println!("{}", std::panic::Location::caller());
    g();
}

fn calls_h() {
    h(); // <-- prints this location three times, once itself, once from g(), once from f()
}
}

依此类推。

限制

此信息是一个提示,不要求实现保留它。

特别是,将带有 #[track_caller] 的函数强制转换为函数指针会创建一个 shim,该 shim 在观察者看来是在所修饰函数的定义点被调用,从而在虚拟调用中丢失实际的调用者信息。这种强制转换的一个常见示例是创建其方法带有属性 的 特型 对象。

注意

上述函数指针的 shim 是必要的,因为 rustc 在代码生成上下文中通过向函数 ABI 附加一个隐式参数来实现 track_caller,但对于间接调用来说,这会是不健全的,因为该参数不是函数类型的一部分,并且给定的函数指针类型可能引用也可能不引用带有该属性 的函数。创建 shim 隐藏了函数指针调用者中的隐式参数,从而保持了健全性。

instruction_set属性

instruction_set 属性 指定函数在代码生成过程中将使用的指令集。这允许在单个程序中混合使用多个指令集。

例子

#[instruction_set(arm::a32)]
fn arm_code() {}

#[instruction_set(arm::t32)]
fn thumb_code() {}

instruction_set 属性使用 MetaListPaths 语法格式来指定一个由架构家族名称和指令集名称组成的单一路径。

instruction_set 属性只能应用于带有 函数体 的函数——闭包异步块自由函数固有实现特型实现 中的 关联函数,以及 特型定义 中带有 默认定义 的关联函数。

注意

rustc 忽略在其他位置的使用,但会对其进行 lint 检查。这在将来可能会成为错误。

注意

尽管该属性可以应用于 闭包异步块,但其用处有限,因为我们尚不支持表达式上的属性。

instruction_set 属性在函数上只能使用一次。

instruction_set 属性只能与支持给定值的目标一起使用。

当使用 instruction_set 属性时,函数中的任何内联汇编都必须使用指定的指令集而不是目标默认指令集。

ARM上的instruction_set

当目标为 ARMv4TARMv5te 架构时,instruction_set 支持的值有:

  • arm::a32 — 将函数生成为 A32 “ARM” 代码。
  • arm::t32 — 将函数生成为 T32 “Thumb” 代码。

如果将函数的地址作为函数指针,则地址的低位将取决于所选的指令集:

  • 对于 arm::a32 (“ARM”),它将为 0。
  • 对于 arm::t32 (“Thumb”),它将为 1。

限制

以下 属性 影响编译时限制。

recursion_limit属性

recursion_limit 属性 可以应用于 crate 级别,以设置可能无限递归的编译时操作的最大深度,例如宏展开或自动解引用。

它使用 MetaNameValueStr 语法格式 来指定递归深度。

注意

rustc 中的默认值为 128。

#![allow(unused)]
#![recursion_limit = "4"]

fn main() {
macro_rules! a {
    () => { a!(1); };
    (1) => { a!(2); };
    (2) => { a!(3); };
    (3) => { a!(4); };
    (4) => { };
}

// 此展开失败,因为它需要的递归深度大于 4。
a!{}
}
#![allow(unused)]
#![recursion_limit = "1"]

fn main() {
// 此失败,因为它需要两个递归步骤才能自动解引用。
(|_: &u8| {})(&&&1);
}

type_length_limit属性

type_length_limit 属性 设置在单态化期间构造具体类型时允许的最大类型替换次数。

注意

rustc 仅在 nightly -Zenforce-type-length-limit 标志处于活动状态时强制执行此限制。

欲了解更多信息,请参阅 Rust PR #127670

例子

#![type_length_limit = "4"]

fn f<T>(x: T) {}

// 此编译失败,因为单态化为
// `f::<((((i32,), i32), i32), i32)>` 需要多于
// 4 个类型元素。
f(((((1,), 2), 3), 4));

注意

rustc 中的默认值为 1048576

type_length_limit 属性使用 MetaNameValueStr 语法格式。字符串中的值必须是非负数。

type_length_limit 属性只能应用于 crate 根。

注意

rustc 忽略在其他位置的使用,但会对其进行 lint 检查。这在将来可能会成为错误。

type_length_limit 上的首次使用才生效。

注意

rustc 会对首次使用之后的任何使用进行 lint 检查。这在将来可能会成为错误。

类型系统属性

以下 属性 用于更改类型的使用方式。

non_exhaustive属性

non_exhaustive 属性 表示以后可能会向类型或变体中添加更多字段或变体。

它可以应用于 结构体枚举 和 enum 变体。

non_exhaustive 属性使用 MetaWord 语法格式,因此不接受任何输入。

在定义它的 crate 内部, non_exhaustive 没有效果。

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub struct Config {
    pub window_width: u16,
    pub window_height: u16,
}

#[non_exhaustive]
pub struct Token;

#[non_exhaustive]
pub struct Id(pub u64);

#[non_exhaustive]
pub enum Error {
    Message(String),
    Other,
}

pub enum Message {
    #[non_exhaustive] Send { from: u32, to: u32, contents: String },
    #[non_exhaustive] Reaction(u32),
    #[non_exhaustive] Quit,
}

// 在定义它的 crate 内部,可以像往常一样构造非详尽结构体。
let config = Config { window_width: 640, window_height: 480 };
let token = Token;
let id = Id(4);

// 在定义它的 crate 内部,可以对非详尽结构体进行详尽匹配。
let Config { window_width, window_height } = config;
let Token = token;
let Id(id_number) = id;

let error = Error::Other;
let message = Message::Reaction(3);

// 在定义它的 crate 内部,可以对非详尽枚举进行详尽匹配。
match error {
    Error::Message(ref s) => { },
    Error::Other => { },
}

match message {
    // 在定义它的 crate 内部,可以对非详尽变体进行详尽匹配。
    Message::Send { from, to, contents } => { },
    Message::Reaction(id) => { },
    Message::Quit => { },
}
}

在定义它的 crate 之外,带有 non_exhaustive 标注的类型具有一些限制,以便在添加新字段或变体时保持向后兼容性。

在定义它的 crate 之外,无法构造非详尽类型:

以下构造示例在定义它的 crate 之外时无法编译:

// 这些是在上游 crate 中定义并标注为 `#[non_exhaustive]` 的类型。
use upstream::{Config, Token, Id, Error, Message};

// 无法构造 Config 的实例;如果在 upstream 的新版本中添加了新字段,
// 这将导致编译失败,因此是不允许的。
let config = Config { window_width: 640, window_height: 480 };

// 无法构造 Token 的实例;如果添加了新字段,那么
// 它将不再是一个单元结构体,因此由其作为单元结构体创建的
// 同名常量在 crate 外部是不公开的;这段代码无法编译。
let token = Token;

// 无法构造 Id 的实例;如果添加了新字段,那么
// 其构造函数签名将会改变,因此其构造函数
// 在 crate 外部是不公开的;这段代码无法编译。
let id = Id(5);

// 可以构造 Error 的实例;引入新变体
// 不会导致编译失败。
let error = Error::Message("foo".to_string());

// 无法构造 Message::Send 或 Message::Reaction 的实例;
// 如果在 upstream 的新版本中添加了新字段,那么这将会
// 导致编译失败,因此是不允许的。
let message = Message::Send { from: 0, to: 1, contents: "foo".to_string(), };
let message = Message::Reaction(0);

// 无法构造 Message::Quit 的实例;如果这在 upstream 中被转换为
// 元组枚举变体,这将导致编译失败。
let message = Message::Quit;

在定义它的 crate 外部匹配非详尽类型时存在一些限制:

  • 在对非详尽变体( 结构体枚举变体 )进行模式匹配时,必须使用包含 ..结构体模式 。元组枚举变体的构造函数的 可见性 被降低到不大于 pub(crate)
  • 在对非详尽 枚举 进行模式匹配时,匹配某个变体不会对匹配臂的详尽性做出贡献。当在定义它的 crate 之外时,以下匹配示例无法编译:
// 这些是在上游 crate 中定义并标注为 `#[non_exhaustive]` 的类型。
use upstream::{Config, Token, Id, Error, Message};

// 在不包含通配符臂的情况下,无法匹配非详尽枚举。
match error {
  Error::Message(ref s) => { },
  Error::Other => { },
  // 使用 `_ => {},` 可以编译
}

// 在没有通配符的情况下,无法匹配非详尽结构体。
if let Ok(Config { window_width, window_height }) = config {
    // 使用 `..` 可以编译
}

// 除非使用带有通配符的大括号结构体语法,否则
// 无法匹配非详尽单元结构体或元组结构体。
// 这将编译为 `let Token { .. } = token;`
let Token = token;
// 这将编译为 `let Id { 0: id_number, .. } = id;`
let Id(id_number) = id;

match message {
  // 在不包含通配符的情况下,无法匹配非详尽结构体枚举变体。
  Message::Send { from, to, contents } => { },
  // 无法匹配非详尽元组或单元枚举变体。
  Message::Reaction(type) => { },
  Message::Quit => { },
}

也不允许在包含任何非详尽变体的枚举上使用数值转换( as )。

例如,可以转换以下枚举,因为它不包含任何非详尽变体:

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum Example {
    First,
    Second
}
}

但是,如果枚举包含哪怕一个非详尽变体,转换也会导致错误。考虑同一枚举的这个修改版本:

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum EnumWithNonExhaustiveVariants {
    First,
    #[non_exhaustive]
    Second
}
}
use othercrate::EnumWithNonExhaustiveVariants;

// 错误:无法转换在另一个 crate 中定义且带有非详尽变体的枚举
let _ = EnumWithNonExhaustiveVariants::First as u8;

在下游 crate 中,非详尽类型始终被视为 inhabited 。

调试器属性

以下 属性 用于在使用 GDB 或 WinDbg 等第三方调试器时增强调试体验。

debugger_visualizer属性

debugger_visualizer 属性 可用于将调试器可视化工具文件嵌入到调试信息中。这改进了显示值时的调试器体验。

例子

#![debugger_visualizer(natvis_file = "Example.natvis")]
#![debugger_visualizer(gdb_script_file = "example.py")]

debugger_visualizer 属性使用 MetaListNameValueStr 语法来指定其输入。必须指定以下键之一:

debugger_visualizer 属性只能应用于 模块 或 crate 根。

debugger_visualizer 属性可以在一个形式上使用任意多次。所有指定的可视化工具文件都将被加载。

debugger_visualizer与Natvis结合使用

Natvis 是一个基于 XML 的框架,适用于 Microsoft 调试器(如 Visual Studio 和 WinDbg),它使用声明性规则来自定义类型的显示。有关 Natvis 格式的详细信息,请参阅 Microsoft 的 Natvis 文档

此属性仅支持在 -windows-msvc 目标上嵌入 Natvis 文件。

Natvis 文件的路径由 natvis_file 键指定,该路径是相对于源文件的路径。

例子

#![debugger_visualizer(natvis_file = "Rectangle.natvis")]

struct FancyRect {
    x: f32,
    y: f32,
    dx: f32,
    dy: f32,
}

fn main() {
    let fancy_rect = FancyRect { x: 10.0, y: 10.0, dx: 5.0, dy: 5.0 };
    println!("在此设置断点");
}

Rectangle.natvis 包含:

<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
    <Type Name="foo::FancyRect">
      <DisplayString>({x},{y}) + ({dx}, {dy})</DisplayString>
      <Expand>
        <Synthetic Name="LowerLeft">
          <DisplayString>({x}, {y})</DisplayString>
        </Synthetic>
        <Synthetic Name="UpperLeft">
          <DisplayString>({x}, {y + dy})</DisplayString>
        </Synthetic>
        <Synthetic Name="UpperRight">
          <DisplayString>({x + dx}, {y + dy})</DisplayString>
        </Synthetic>
        <Synthetic Name="LowerRight">
          <DisplayString>({x + dx}, {y})</DisplayString>
        </Synthetic>
      </Expand>
    </Type>
</AutoVisualizer>

在 WinDbg 下查看时,fancy_rect 变量将显示如下:

> Variables:
  > fancy_rect: (10.0, 10.0) + (5.0, 5.0)
    > LowerLeft: (10.0, 10.0)
    > UpperLeft: (10.0, 15.0)
    > UpperRight: (15.0, 15.0)
    > LowerRight: (15.0, 10.0)

debugger_visualizer与GDB结合使用

GDB 支持使用结构化的 Python 脚本,称为 pretty printer,它描述了类型应如何在调试器视图中可视化。有关 pretty printer 的详细信息,请参阅 GDB 的 pretty printing 文档

注意

在 GDB 下调试二进制文件时,嵌入式 pretty printer 不会自动加载。

有两种方法可以启用自动加载嵌入式 pretty printer:

  1. 启动 GDB 时带上额外参数,以显式添加目录或二进制文件到自动加载安全路径:gdb -iex "add-auto-load-safe-path safe-path path/to/binary" path/to/binary。有关更多信息,请参阅 GDB 的 auto-loading 文档
  2. $HOME/.config/gdb 下创建一个名为 gdbinit 的文件(如果目录不存在,您可能需要创建它)。在该文件中添加以下行:add-auto-load-safe-path path/to/binary

这些脚本使用 gdb_script_file 键嵌入,该键是相对于源文件的路径。

例子

#![debugger_visualizer(gdb_script_file = "printer.py")]

struct Person {
    name: String,
    age: i32,
}

fn main() {
    let bob = Person { name: String::from("Bob"), age: 10 };
    println!("在此设置断点");
}

printer.py 包含:

import gdb

class PersonPrinter:
    "打印一个 Person"

    def __init__(self, val):
        self.val = val
        self.name = val["name"]
        self.age = int(val["age"])

    def to_string(self):
        return "{} is {} years old.".format(self.name, self.age)

def lookup(val):
    lookup_tag = val.type.tag
    if lookup_tag is None:
        return None
    if "foo::Person" == lookup_tag:
        return PersonPrinter(val)

    return None

gdb.current_objfile().pretty_printers.append(lookup)

当 crate 的调试可执行文件传递给 GDB1 时,print bob 将显示:

"Bob" is 10 years old.

collapse_debuginfo属性

collapse_debuginfo 属性 控制在为调用此宏的代码生成调试信息时,是否将来自宏定义的代码位置折叠到与宏的调用点关联的单个位置。

例子

#![allow(unused)]
fn main() {
#[collapse_debuginfo(yes)]
macro_rules! example {
    () => {
        println!("你好!");
    };
}
}

当使用调试器时,调用 example 宏可能看起来像是调用一个函数。也就是说,当您单步执行到调用点时,它可能会显示宏调用而不是展开的代码。

collapse_debuginfo 属性的 语法格式 是:

Syntax
CollapseDebuginfoAttributecollapse_debuginfo ( CollapseDebuginfoOption )

CollapseDebuginfoOption
      yes
    | no
    | external

collapse_debuginfo 属性只能应用于 macro_rules 定义

collapse_debuginfo 属性在一个宏上只能使用一次。

collapse_debuginfo 属性接受以下选项:

  • #[collapse_debuginfo(yes)] — 调试信息中的代码位置被折叠。
  • #[collapse_debuginfo(no)] — 调试信息中的代码位置不被折叠。
  • #[collapse_debuginfo(external)] — 仅当宏来自不同的 crate 时,调试信息中的代码位置才被折叠。

对于没有此属性的宏,external 行为是默认值,除非它们是内置宏。对于内置宏,默认值是 yes

注意

rustc 有一个 -C collapse-macro-debuginfo CLI 选项,可以覆盖默认行为以及任何 #[collapse_debuginfo] 属性的值。


  1. 注意:这假设您正在使用 rust-gdb 脚本,该脚本为 String 等标准库类型配置了 pretty printer。

语句与表达式

Rust 主要 是一种表达式语言。这意味着大多数产生值或产生副作用的求值形式都由 表达式 这一统一的 语法格式 类别引导。每种表达式通常都可以 嵌套 在另一种表达式中,表达式的求值规则包括指定表达式产生的值以及其子表达式本身的求值顺序。

相比之下,语句的用途 主要 是包含表达式求值并明确其顺序。

语句

Syntax
Statement
      ;
    | Item
    | LetStatement
    | ExpressionStatement
    | OuterAttribute* MacroInvocationSemi

语句 (statement) 的组成部分,而块又是外部 表达式函数 的组成部分。

Rust 有两种语句: 声明语句表达式语句

声明语句

声明语句 是指在封闭的语句块中引入一个或多个 名称 的语句。声明的名称可能表示新变量或新

两种声明语句分别是项声明和 let 语句。

项声明

项声明语句 的 语法格式 与 模块 中的 项声明 相同。

在语句块中声明项会将其 作用域 限制在包含该语句的块中。该项及其可能声明的任何子项都不会被赋予 规范路径

此规则的例外是,只要项以及(如果适用) 特型 是可访问的,由 实现 定义的关联项在外部作用域中仍然是可访问的。在其他方面的含义与在模块内部声明该项相同。

不会隐式捕获包含函数的 泛型 参数、参数和局部变量。例如, inner 无法访问 outer_var

#![allow(unused)]
fn main() {
fn outer() {
  let outer_var = true;

  fn inner() { /* outer_var 在此处不在作用域内 */ }

  inner();
}
}

let语句

Syntax
LetStatement
    OuterAttribute* let PatternNoTopAlt ( : Type )?
    (
          = Expression
        | = Expressionexcept LazyBooleanExpression or end with a } else BlockExpression
    )? ;

  • let 语句* 通过 模式 引入一组新 变量。模式后面可以跟一个可选的类型标注,然后要么结束,要么后面跟一个初始化表达式以及一个可选的 else 块。

如果未给出类型标注,编译器将推断类型;如果没有足够的类型信息进行明确推断,则报错。

由变量声明引入的任何变量从声明点到封闭块作用域结束都是可见的,除非它们被另一个变量声明遮蔽。

如果没有 else 块,模式必须是不可驳回的。如果有 else 块,模式可以是可驳回的。

如果模式不匹配(这要求它是可驳回的),则执行 else 块。 else 块必须始终发散(求值为 never 类型)。

#![allow(unused)]
fn main() {
let (mut v, w) = (vec![1, 2, 3], 42); // 绑定可以是 mut 或 const
let Some(t) = v.pop() else { // 可驳回模式需要 else 块
    panic!(); // else 块必须发散
};
let [u, v] = [v[0], v[1]] else { // 此模式是不可驳回的,因此编译器
                                 // 会发出 lint,因为 else 块是多余的。
    panic!();
};
}

表达式语句

Syntax
ExpressionStatement
      ExpressionWithoutBlock ;
    | ExpressionWithBlock ;?

表达式语句 是指对 表达式 求值并忽略其结果的语句。通常,表达式语句的目的是触发其表达式求值的副作用。

在允许使用语句的上下文中,仅由 块表达式 或控制流表达式组成的表达式可以省略末尾的分号。这可能会导致将其解析为独立语句还是解析为另一个表达式的一部分之间产生歧义;在这种情况下,它被解析为语句。

ExpressionWithBlock 表达式用作语句时,其类型必须是单元类型。

#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];
v.pop();          // 忽略 pop 返回的元素
if v.is_empty() {
    v.push(5);
} else {
    v.remove(0);
}                 // 分号可以省略。
[1];              // 独立的表达式语句,不是索引表达式。
}

当省略末尾分号时,结果必须是类型 ()

#![allow(unused)]
fn main() {
// 错误:块的类型是 i32,而不是 ()
// Error: expected `()` because of default return type
// if true {
//   1
// }

// 正确:块的类型是 i32
if true {
  1
} else {
  2
};
}

语句上的属性

语句接受 外部属性。在语句上有意义的属性是 cfglint 检查属性

表达式

Syntax
Expression
      ExpressionWithoutBlock
    | ExpressionWithBlock

ExpressionWithoutBlock
    OuterAttribute*
    (
        LiteralExpression
      | PathExpression
      | OperatorExpression
      | GroupedExpression
      | ArrayExpression
      | AwaitExpression
      | IndexExpression
      | TupleExpression
      | TupleIndexingExpression
      | StructExpression
      | CallExpression
      | MethodCallExpression
      | FieldExpression
      | ClosureExpression
      | AsyncBlockExpression
      | ContinueExpression
      | BreakExpression
      | RangeExpression
      | ReturnExpression
      | UnderscoreExpression
      | MacroInvocation
    )

ExpressionWithBlock
    OuterAttribute*
    (
        BlockExpression
      | ConstBlockExpression
      | UnsafeBlockExpression
      | LoopExpression
      | IfExpression
      | MatchExpression
    )

一个表达式可能有两种角色:它总是产生一个 ,并且可能产生 副作用

表达式 求值为 一个值,并在 求值 过程中产生副作用。

许多表达式包含子表达式,称为该表达式的 操作数

每种表达式的含义决定了几件事:

  • 在对表达式求值时是否对操作数求值
  • 对操作数求值的顺序
  • 如何组合操作数的值以获得表达式的值

通过这种方式,表达式的结构决定了执行的结构。块只是另一种表达式,因此块、语句、表达式以及再次嵌套的块可以递归地相互嵌套到任意深度。

注意

我们给表达式的操作数命名以便于讨论,但这些名称并不稳定,可能会被更改。

表达式优先级

Rust 运算符和表达式的优先级按以下顺序排列,从强到弱。同一优先级水平的二元运算符按其结合性给出的顺序进行分组。

运算符/表达式结合性
路径
方法调用
字段表达式从左到右
函数调用, 数组索引
?
一元 - ! * 借用
as从左到右
* / %从左到右
+ -从左到右
<< >>从左到右
&从左到右
^从左到右
|从左到右
== != < > <= >=需要括号
&&从左到右
||从左到右
.. ..=需要括号
= += -= *= /= %=
&= |= ^= <<= >>=
从右到左
return break 闭包

操作数的求值顺序

以下表达式列表都以相同的方式对其操作数求值,如列表后所述。其他表达式要么不接受操作数,要么根据其各自页面的描述有条件地对其求值。

  • 解引用表达式
  • 错误传播表达式
  • 取反表达式
  • 算术和逻辑二元运算符
  • 比较运算符
  • 类型转换表达式
  • 分组表达式
  • 数组表达式
  • Await 表达式
  • 索引表达式
  • 元组表达式
  • 元组索引表达式
  • 结构体表达式
  • 调用表达式
  • 方法调用表达式
  • 字段表达式
  • Break 表达式
  • 范围表达式
  • 返回表达式

这些表达式的操作数在应用表达式的效果之前进行求值。接受多个操作数的表达式按照源代码中编写的顺序从左到右进行求值。

注意

哪些子表达式是表达式的操作数,由前一节所述的表达式优先级决定。

例如,两个 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])、字段 引用 (expr.f) 以及括号括起来的位置表达式。

所有其他表达式都是值表达式。

值表达式 是代表实际值的表达式。

以下环境是 位置表达式 环境:

注意

在历史上,位置表达式被称为 左值 (lvalues),而值表达式被称为 右值 (rvalues)

被赋值者表达式 是出现在 赋值 表达式左操作数中的表达式。显式地,被赋值者表达式有:

在被赋值者表达式内部允许使用任意括号。

移动和复制类型

当位置表达式在值表达式环境中求值,或在模式中通过值绑定时,它表示该内存位置中 持有 的值。

如果该值的类型实现了 Copy,那么该值将被复制。

在其余情况下,如果该类型是 Sized,则可能会移动该值。

只有以下位置表达式可以被移出:

在移出一个求值为局部变量的位置表达式后,该位置将被去初始化,并且在重新初始化之前不能再次从中读取。

在所有其他情况下,尝试在值表达式环境中使用位置表达式都会报错。

可变性

为了使位置表达式能被 赋值、可变 借用隐式可变借用 或绑定到包含 ref mut 的模式,它必须是 可变的。我们称这些为 可变位置表达式。相比之下,其他位置表达式称为 不可变位置表达式

以下表达式可以是可变位置表达式环境:

  • 当前未被借用的可变 变量
  • 可变 static
  • 临时值
  • 字段:这在可变位置表达式环境中对子表达式求值。
  • *mut T 指针的 解引用
  • 类型为 &mut T 的变量或变量字段的解引用。 注意:这是下一条规则要求的例外。
  • 实现了 DerefMut 的类型的解引用: 这随后要求被解引用的值在可变位置表达式环境中求值。
  • 实现了 IndexMut 的类型的 数组索引: 这随后在可变位置表达式环境中对被索引的值(而非索引本身)求值。

临时变量

在大多数位置表达式环境中使用值表达式时,会创建一个临时的未命名内存位置并将其初始化为该值。该表达式求值为该位置,除非被 提升static。临时变量的 销毁范围 通常是封闭语句的末尾。

超级宏

某些内置宏可能会创建 临时变量,其 作用域 可能会被 延长。这些临时变量是 超级临时变量,而这些宏是 超级宏。这些宏的 调用超级宏调用表达式。这些宏的参数可能是 超级操作数

注意

当超级宏调用表达式是一个 延长表达式 时,它的超级操作数也是 延长表达式,并且超级临时变量的 作用域 会被 延长。参见 destructors.scope.lifetime-extension.exprs

format_args!

除了格式化字符串参数外,传递给 format_args! 的所有参数都是 超级操作数

#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
// 由于调用是一个延长表达式且参数是一个超级操作数,
// 内部块是一个延长表达式,
// 因此在其尾部表达式中创建的临时变量的作用域被延长了。
let _ = format_args!("{}", { &temp() }); // OK
}

format_args! 的超级操作数是 隐式借用 的,因此是 位置表达式环境。当 值表达式 被作为参数传递时,它会创建一个 超级临时变量

#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
let x = format_args!("{}", temp());
x; // <-- 临时变量被延长,允许在此处使用。
}

format_args! 调用的展开有时会创建其他内部 超级临时变量

#![allow(unused)]
fn main() {
let x = {
    // 此调用创建一个内部临时变量。
    let x = format_args!("{:?}", 0);
    x // <-- 临时变量被延长,允许在此处使用。
}; // <-- 临时变量在此处被销毁。
x; // 错误
}
#![allow(unused)]
fn main() {
// 此调用不创建内部临时变量。
let x = { let x = format_args!("{}", 0); x };
x; // OK
}

注意

关于 format_args! 何时会或不会创建内部临时变量的细节目前尚未指定。

pin!

pin! 的参数是 超级操作数

#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 同上 `format_args!`。
let _ = pin!({ &temp() }); // OK
}

pin! 的参数是 值表达式环境,并创建一个 超级临时变量

#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 参数被求值为超级临时变量。
let x = pin!(temp());
// 临时变量被延长,允许在此处使用。
x; // OK
}

隐式借用

某些表达式会通过隐式借用将一个表达式视为位置表达式。例如,可以直接比较两个无固定大小的 切片 是否相等,因为 == 运算符会隐式借用其操作数:

#![allow(unused)]
fn main() {
let c = [1, 2, 3];
let d = vec![1, 2, 3];
let a: &[i32];
let b: &[i32];
a = &c;
b = &d;
// ...
*a == *b;
// 等效形式:
::std::cmp::PartialEq::eq(&*a, &*b);
}

隐式借用可能会在以下表达式中发生:

重载特型

许多以下运算符和表达式也可以使用 std::opsstd::cmp 中的 特型 对其他类型进行重载。这些 特型 在 core::opscore::cmp 中也以相同的名称存在。

表达式属性

表达式前的 外部属性 仅在少数特定情况下允许:

在以下情况前绝不允许:

字面量表达式

Syntax
LiteralExpression
      CHAR_LITERAL
    | STRING_LITERAL
    | RAW_STRING_LITERAL
    | BYTE_LITERAL
    | BYTE_STRING_LITERAL
    | RAW_BYTE_STRING_LITERAL
    | C_STRING_LITERAL
    | RAW_C_STRING_LITERAL
    | INTEGER_LITERAL
    | FLOAT_LITERAL
    | true
    | false

字面量表达式 是由单个 词法单元 组成的表达式,而不是一系列 词法单元 ,它立即并直接地表示它所求得的值,而不是通过名称或其他求值规则来引用它。

字面量是 常量表达式 的一种形式,因此(主要)在编译时求值。

前面描述的每种词法 字面量 形式都可以构成字面量表达式,关键字 truefalse 也是如此。

#![allow(unused)]
fn main() {
"hello";   // 字符串类型
'5';       // 字符类型
5;         // 整数类型
}

在下面的描述中, 词法单元 的 字符串表示 是指输入中与 词法分析器 语法片段中的 词法单元 生成式匹配的字符序列。

注意

此字符串表示永远不会包含紧跟在 U+000D (CR) 之后的字符 U+000A (LF): 这对字符之前会被转换为单个 U+000A (LF)。

转义

下面对文本字面量表达式的描述使用了几种形式的 转义

每种形式的转义都具有以下特征:

  • 转义序列 : 一个字符序列,总是以 U+005C (\) 开头
  • 转义值 : 单个字符或空字符序列

在下面的转义定义中:

  • 八进制数字 是范围 [0-7] 中的任何字符。
  • 十六进制数字 是范围 [0-9], [a-f], 或 [A-F] 中的任何字符。

简单转义

下表第一列中出现的每个字符序列都是一个转义序列。

在每种情况下,转义值都是第二列相应条目中给出的字符。

转义序列转义值
\0U+0000 (NUL)
\tU+0009 (HT)
\nU+000A (LF)
\rU+000D (CR)
\"U+0022 (QUOTATION MARK)
\'U+0027 (APOSTROPHE)
\\U+005C (REVERSE SOLIDUS)

8位转义

转义序列由 \x 后跟两个十六进制数字组成。

转义值是其 Unicode 标量值 为将转义序列中的最后两个字符解释为十六进制整数的结果的字符,就像通过基数为 16 的 u8::from_str_radix 处理一样。

注意

因此,转义值具有 u8 范围内的 Unicode 标量值

7位转义

转义序列由 \x 后跟一个八进制数字,然后是一个十六进制数字组成。

转义值是其 Unicode 标量值 为将转义序列中的最后两个字符解释为十六进制整数的结果的字符,就像通过基数为 16 的 u8::from_str_radix 处理一样。

Unicode 转义

转义序列由 \u{, 后跟一系列字符(每个字符都是十六进制数字或 _ ), 最后跟 } 组成。

转义值是其 Unicode 标量值 为将转义序列中包含的十六进制数字解释为十六进制整数的结果的字符,就像通过基数为 16 的 u32::from_str_radix 处理一样。

注意

CHAR_LITERALSTRING_LITERAL 词法单元 的允许形式确保存在这样的字符。

字符串续行转义

转义序列由 \ 紧跟 U+000A (LF) 组成,以及在下一个非空白字符之前的所有后续空白字符。 为此,空白字符为 U+0009 (HT), U+000A (LF), U+000D (CR), 和 U+0020 (SPACE)。

转义值是一个空字符序列。

注意

这种形式的转义效果是字符串续行会跳过后续的空白字符,包括额外的换行符。因此 a, bc 是相等的:

#![allow(unused)]
fn main() {
let a = "foobar";
let b = "foo\
         bar";
let c = "foo\

     bar";

assert_eq!(a, b);
assert_eq!(b, c);
}

跳过额外的换行符(如示例 c 所示)可能会令人困惑且出乎意料。此行为将来可能会进行调整。在做出决定之前,建议避免依赖于通过行续行跳过多个换行符。有关更多信息,请参阅 此问题

字符字面量表达式

字符字面量表达式由单个 CHAR_LITERAL 词法单元 组成。

该表达式的类型是原始 char 类型。

词法单元 必须没有后缀。

词法单元 的 字面量内容 是该 词法单元 的字符串表示中第一个 U+0027 (') 之后且最后一个 U+0027 (') 之前的字符序列。

字面量表达式的 表示字符 按如下方式从字面量内容导出:

  • 否则,表示字符是构成字面量内容的单个字符。

该表达式的值是与表示字符的 Unicode 标量值 对应的 char

注意

CHAR_LITERAL 词法单元 的允许形式确保这些规则总是产生单个字符。

字符字面量表达式的示例:

#![allow(unused)]
fn main() {
'R';                               // R
'\'';                              // '
'\x52';                            // R
'\u{00E6}';                        // 拉丁文小写字母 AE (U+00E6)
}

字符串字面量表达式

字符串字面量表达式由单个 STRING_LITERALRAW_STRING_LITERAL 词法单元 组成。

该表达式的类型是向原始 str 类型的共享引用(具有 static 生命周期)。 也就是说,类型是 &'static str

词法单元 必须没有后缀。

词法单元 的 字面量内容 是该 词法单元 的字符串表示中第一个 U+0022 (") 之后且最后一个 U+0022 (") 之前的字符序列。

字面量表达式的 表示字符串 是按如下方式从字面量内容导出的字符序列:

  • 如果 词法单元 是 RAW_STRING_LITERAL ,则表示字符串与字面量内容完全相同。

该表达式的值是一个指向静态分配的 str 的引用,该字符串包含表示字符串的 UTF-8 编码。

字符串字面量表达式的示例:

#![allow(unused)]
fn main() {
"foo"; r"foo";                     // foo
"\"foo\""; r#""foo""#;             // "foo"

"foo #\"# bar";
r##"foo #"# bar"##;                // foo #"# bar

"\x52"; "R"; r"R";                 // R
"\\x52"; r"\x52";                  // \x52
}

字节字面量表达式

字节字面量表达式由单个 BYTE_LITERAL 词法单元 组成。

该表达式的类型是原始 u8 类型。

词法单元 必须没有后缀。

词法单元 的 字面量内容 是该 词法单元 的字符串表示中第一个 U+0027 (') 之后且最后一个 U+0027 (') 之前的字符序列。

字面量表达式的 表示字符 按如下方式从字面量内容导出:

  • 如果字面量内容是以下形式之一的转义序列,则表示字符是该转义序列的转义值:
  • 否则,表示字符是构成字面量内容的单个字符。

该表达式的值是表示字符的 Unicode 标量值

注意

BYTE_LITERAL 词法单元 的允许形式确保这些规则总是产生单个字符,其 Unicode 标量值在 u8 范围内。

字节字面量表达式的示例:

#![allow(unused)]
fn main() {
b'R';                              // 82
b'\'';                             // 39
b'\x52';                           // 82
b'\xA0';                           // 160
}

字节串字面量表达式

字节串字面量表达式由单个 BYTE_STRING_LITERALRAW_BYTE_STRING_LITERAL 词法单元 组成。

该表达式的类型是对一个元素类型为 u8 的数组的共享引用(具有 static 生命周期)。 也就是说,类型是 &'static [u8; N] ,其中 N 是下面描述的表示字符串中的字节数。

词法单元 必须没有后缀。

词法单元 的 字面量内容 是该 词法单元 的字符串表示中第一个 U+0022 (") 之后且最后一个 U+0022 (") 之前的字符序列。

字面量表达式的 表示字符串 是按如下方式从字面量内容导出的字符序列:

  • 如果 词法单元 是 BYTE_STRING_LITERAL ,则字面量内容中出现的以下任何形式的每个转义序列都将被替换为该转义序列的转义值。

    这些替换按从左到右的顺序进行。 例如, 词法单元 b"\\x41" 被转换为字符 \ x 4 1

该表达式的值是一个指向静态分配数组的引用,该数组按相同顺序包含表示字符串中字符的 Unicode 标量值

注意

BYTE_STRING_LITERALRAW_BYTE_STRING_LITERAL 词法单元 的允许形式确保这些规则总是产生 u8 范围内的数组元素值。

字节串字面量表达式的示例:

#![allow(unused)]
fn main() {
b"foo"; br"foo";                     // foo
b"\"foo\""; br#""foo""#;             // "foo"

b"foo #\"# bar";
br##"foo #"# bar"##;                 // foo #"# bar

b"\x52"; b"R"; br"R";                // R
b"\\x52"; br"\x52";                  // \x52
}

C字符串字面量表达式

C 字符串字面量表达式由单个 C_STRING_LITERALRAW_C_STRING_LITERAL 词法单元 组成。

该表达式的类型是对标准库 CStr 类型的共享引用(具有 static 生命周期)。 也就是说,类型是 &'static core::ffi::CStr

词法单元 必须没有后缀。

词法单元 的 字面量内容 是该 词法单元 的字符串表示中第一个 " 之后且最后一个 " 之前的字符序列。

字面量表达式的 表示字节 是按如下方式从字面量内容导出的字节序列:

  • 如果 词法单元 是 RAW_C_STRING_LITERAL ,则表示字节是字面量内容的 UTF-8 编码。

注意

C_STRING_LITERALRAW_C_STRING_LITERAL 词法单元 的允许形式确保表示字节永远不会包含空字节。

该表达式的值是一个指向静态分配的 CStr 的引用,其字节数组包含表示字节,后跟一个空字节。

C 字符串字面量表达式的示例:

#![allow(unused)]
fn main() {
c"foo"; cr"foo";                     // foo
c"\"foo\""; cr#""foo""#;             // "foo"

c"foo #\"# bar";
cr##"foo #"# bar"##;                 // foo #"# bar

c"\x52"; c"R"; cr"R";                // R
c"\\x52"; cr"\x52";                  // \x52

c"æ";                                // 拉丁文小写字母 AE (U+00E6)
c"\u{00E6}";                         // 拉丁文小写字母 AE (U+00E6)
c"\xC3\xA6";                         // 拉丁文小写字母 AE (U+00E6)

c"\xE6".to_bytes();                  // [230]
c"\u{00E6}".to_bytes();              // [195, 166]
}

整数字面量表达式

整数字面量表达式由单个 INTEGER_LITERAL 词法单元 组成。

如果 词法单元 具有 后缀 ,则后缀必须是 原始整数类型 之一的名称: u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, 或 isize ,并且表达式具有该类型。

如果 词法单元 没有后缀,则表达式的类型通过类型推导确定:

  • 如果可以从周围的程序上下文中 唯一 确定整数类型,则该表达式具有该类型。
  • 如果程序上下文对类型的约束不足,则默认为有符号 32 位整数 i32
  • 如果程序上下文对类型的约束过度,则被视为静态类型错误。

整数字面量表达式的示例:

#![allow(unused)]
fn main() {
123;                               // 类型 i32
123i32;                            // 类型 i32
123u32;                            // 类型 u32
123_u32;                           // 类型 u32
let a: u64 = 123;                  // 类型 u64

0xff;                              // 类型 i32
0xff_u8;                           // 类型 u8

0o70;                              // 类型 i32
0o70_i16;                          // 类型 i16

0b1111_1111_1001_0000;             // 类型 i32
0b1111_1111_1001_0000i64;          // 类型 i64

0usize;                            // 类型 usize
}

该表达式的值从 词法单元 的字符串表示中按如下方式确定:

  • 通过检查字符串的前两个字符来选择整数基数,如下所示:

    • 0b 表示基数 2
    • 0o 表示基数 8
    • 0x 表示基数 16
    • 否则基数为 10。
  • 如果基数不是 10,则从字符串中移除前两个字符。
  • 从字符串中移除任何后缀。
  • 从字符串中移除任何下划线。
  • 字符串被转换为 u128 值,就像通过具有所选基数的 u128::from_str_radix 处理一样。 如果值不适合 u128 ,则是编译器错误。
  • u128 值通过 数值强转 转换为表达式的类型。

注意

如果字面量的值不适合表达式的类型,最终的强转将截断该值。 rustc 包含一个名为 overflowing_literalslint 检查 ,默认为 deny ,它会拒绝发生这种情况的表达式。

注意

例如, -1i8求负运算符 对字面量表达式 1i8 的应用,而不是单个整数字面量表达式。有关表示有符号类型的最小负值(绝对值最大)的注意事项,请参阅 溢出

浮点数字面量表达式

浮点数字面量表达式具有以下两种形式之一:

如果 词法单元 具有 后缀 ,则后缀必须是 原始浮点类型 之一的名称: f32f64 ,并且表达式具有该类型。

如果 词法单元 没有后缀,则表达式的类型通过类型推导确定:

  • 如果可以从周围的程序上下文中 唯一 确定浮点类型,则该表达式具有该类型。
  • 如果程序上下文对类型的约束不足,则默认为 f64
  • 如果程序上下文对类型的约束过度,则被视为静态类型错误。

浮点数字面量表达式的示例:

#![allow(unused)]
fn main() {
123.0f64;        // 类型 f64
0.1f64;          // 类型 f64
0.1f32;          // 类型 f32
12E+99_f64;      // 类型 f64
5f32;            // 类型 f32
let x: f64 = 2.; // 类型 f64
}

该表达式的值从 词法单元 的字符串表示中按如下方式确定:

  • 从字符串中移除任何后缀。
  • 从字符串中移除任何下划线。

注意

例如, -1.0求负运算符 对字面量表达式 1.0 的应用,而不是单个浮点数字面量表达式。

注意

infNaN 不是字面量 词法单元 。可以使用 f32::INFINITY, f64::INFINITY, f32::NAN, 和 f64::NAN 常量来代替字面量表达式。在 rustc 中,大到足以被求值为无穷大的字面量将触发 overflowing_literals lint 检查。

布尔字面量表达式

布尔字面量表达式由关键字 truefalse 之一组成。

该表达式的类型是原始 布尔类型 ,其值为:

  • 如果关键字是 true ,则为 true
  • 如果关键字是 false ,则为 false

路径表达式

Syntax
PathExpression
      PathInExpression
    | QualifiedPathInExpression

一个在表达式语境中使用的 路径 表示一个局部变量或一个 项 。

解析为局部变量或静态变量的路径表达式是 位置表达式 ;其他路径是 值表达式

使用 static mut 变量需要一个 unsafe

#![allow(unused)]
fn main() {
mod globals {
    pub static STATIC_VAR: i32 = 5;
    pub static mut STATIC_MUT_VAR: i32 = 7;
}
let local_var = 3;
local_var;
globals::STATIC_VAR;
unsafe { globals::STATIC_MUT_VAR };
let some_constructor = Some::<i32>;
let push_integer = Vec::<i32>::push;
let slice_reverse = <[i32]>::reverse;
}

关联常量的求值方式与 const 相同。

块表达式

Syntax
BlockExpression
    {
        InnerAttribute*
        Statements?
    }

Statements
      Statement+
    | Statement+ ExpressionWithoutBlock
    | ExpressionWithoutBlock

一个 块表达式 ,或称 ,是一个控制流表达式,也是 项 和变量声明的匿名命名空间作用域。

作为控制流表达式,块会顺序执行其组成的非 项 声明语句,然后执行其最后的可选表达式。

作为匿名命名空间作用域, 项 声明仅在块内部有效,而由 let 语句声明的变量从下一条语句开始到块结束为止在作用域内有效。 有关更多详细信息,请参阅 作用域 章节。

块的 语法格式 为 { ,然后是任何 内部属性 ,接着是任意数量的 语句 ,然后是一个可选表达式(称为最终操作数),最后是 }

通常要求语句后跟分号,但有两个例外:

  1. 项 声明语句不需要后跟分号。
  2. 表达式语句通常需要后跟分号,除非其外部表达式是流控制表达式。

此外,允许语句之间存在额外的分号,但这些分号不影响语义。

在对块表达式求值时,除 项 声明语句外的每个语句都会按顺序执行。

然后执行最终操作数(如果有)。

块的类型是最终操作数的类型,如果省略最终操作数,则为 ()

#![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

Syntax
AsyncBlockExpressionasync move? BlockExpression

一个 async 块 是块表达式的一种变体,其求值结果为一个 future。

块的最终表达式(如果存在)决定了 future 的结果值。

执行 async 块类似于执行闭包表达式: 它的直接效果是产生并返回一个匿名类型。

然而,闭包返回实现了一个或多个 std::ops::Fn 特型 的类型,而 async 块返回的类型实现了 std::future::Future 特型 。

该类型的实际数据格式未指定。

注意

rustc 生成的 future 类型大致相当于一个枚举,每个 await 点对应一个变体,每个变体存储从其对应点恢复所需的数据。

2018 版次差异

async 块仅从 Rust 2018 开始可用。

捕获模式

async 块使用与闭包相同的 捕获模式 从其环境中捕获变量。 与闭包类似,当写成 async { .. } 时,每个变量的捕获模式将从块的内容中推导出来。 然而, async move { .. } 块会将所有引用的变量移动到生成的 future 中。

Async语境

由于 async 块构造一个 future,它们定义了一个 async 语境 ,而该语境又可以包含 await 表达式 。 Async 语境由 async 块以及 async 函数体建立,后者的语义是根据 async 块定义的。

控制流运算符

async 块就像函数边界一样,非常类似于闭包。

因此, ? 运算符和 return 表达式都影响 future 的输出,而不是外层函数或其他语境。 也就是说,在 async 块内 return <expr> 将返回 <expr> 的结果作为 future 的输出。 同样地,如果 <expr>? 传播一个错误,该错误将作为 future 的结果传播。

最后, breakcontinue 关键字不能用于从 async 块中跳出。 因此,以下代码是非法的:

#![allow(unused)]
fn main() {
loop {
    async move {
        break; // error[E0267]: 在 `async` 块内部使用 `break`
    }
}
}

const

Syntax
ConstBlockExpressionconst BlockExpression

一个 const 块 是块表达式的一种变体,其主体在编译时而非运行时求值。

const 块允许你定义一个常量值,而无需定义新的 常量项 ,因此它们有时也被称为 内联 const 。 它还支持类型推导,因此不需要指定类型,这与 常量项 不同。

const 块能够引用作用域内的泛型参数,这与 自由 常量项不同。 它们被脱糖为在作用域内带有泛型参数的常量项(类似于关联常量,但没有它们与之关联的 特型 或类型)。 例如,这段代码:

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    const { std::mem::size_of::<T>() + 1 }
}
}

等价于:

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    {
        struct Const<T>(T);
        impl<T> Const<T> {
            const CONST: usize = std::mem::size_of::<T>() + 1;
        }
        Const::<T>::CONST
    }
}
}

如果 const 块表达式在运行时执行,则保证会对该常量求值,即使其返回值被忽略:

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    // 如果这段代码被执行,那么断言肯定已经在编译时求值了。
    const { assert!(std::mem::size_of::<T>() > 0); }
    // 在这里,我们可以编写依赖于类型为非零大小的 unsafe 代码。
    /* ... */
    42
}
}

如果 const 块表达式不在运行时执行,它可能会也可能不会被求值:

#![allow(unused)]
fn main() {
if false {
    // 程序构建时可能会也可能不会发生 恐慌 。
    const { panic!(); }
}
}

unsafe

Syntax
UnsafeBlockExpressionunsafe BlockExpression

_ 有关何时使用 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() };
}

标签块表达式

标签块表达式在 循环和其他可中断表达式 章节中有文档说明。

块表达式上的属性

在以下情况下,允许在块表达式的左大括号后直接使用 内部属性

在块表达式上有意义的属性是 cfglint 检查属性

例如,此函数在 unix 平台上返回 true ,在其他平台上返回 false

#![allow(unused)]
fn main() {
fn is_unix_platform() -> bool {
    #[cfg(unix)] { true }
    #[cfg(not(unix))] { false }
}
}

运算符表达式

Rust 语言为内置类型定义了运算符。

下面许多运算符也可以使用 std::opsstd::cmp 中的 特型 来重载。

溢出

在调试模式下编译时,整数运算符在溢出时会产生 恐慌 。 可以使用 -C debug-assertions-C overflow-checks 编译器标志来更直接地控制此行为。 以下情况被视为溢出:

  • +* 或二元 - 产生的值大于最大值,或小于可存储的最小值时。
  • 对任何有符号整数类型的最小负值应用一元 - ,除非操作数是 字面量表达式 (或单独存在于一个或多个 分组表达式 中的字面量表达式)。
  • 使用 /% ,其中左侧参数是有符号整数类型的最小整数,而右侧参数是 -1 。 由于历史原因,即使禁用了 -C overflow-checks ,也会进行这些检查。
  • 使用 <<>> ,其中右侧参数大于或等于左侧参数类型中的位数,或者是负数。

注意

一元 - 后的字面量表达式的例外情况意味着诸如 -128_i8let j: i8 = -(128) 之类的形式永远不会导致 恐慌 ,并且具有预期的值 -128。

在这些情况下,字面量表达式已经具有其类型的最小负值(例如, 128_i8 的值为 -128),因为整数字面量会根据 整数字面量表达式 中的描述被截断为其类型。

由于补码溢出约定,这些最小负值的取负操作保持原值不变。

rustc 中,这些最小负数表达式也会被 overflowing_literals lint 检查忽略。

借用运算符

Syntax
BorrowExpression
      ( & | && ) Expression
    | ( & | && ) mut Expression
    | ( & | && ) raw const Expression
    | ( & | && ) raw mut Expression

& (共享借用)和 &mut (可变借用)运算符是一元前缀运算符。

当应用于 位置表达式 时,此表达式会产生一个指向该值所引用的位置的引用(指针)。

在引用的持续时间内,内存位置也处于借用状态。 对于共享借用( & ),这意味着该位置不可被修改,但可以被读取或再次共享。 对于可变借用( &mut ),在借用过期之前,不能以任何方式访问该位置。

&mut 在可变位置表达式语境中对其操作数求值。

如果 &&mut 运算符应用于 值表达式 ,则会创建一个 临时值

这些运算符不能被重载。

#![allow(unused)]
fn main() {
{
    // 创建一个值为 7 的临时变量,持续作用域为当前块。
    let shared_reference = &7;
}
let mut array = [-2, 3, 9];
{
    // 在此作用域内可变地借用 `array`。
    // `array` 只能通过 `mutable_reference` 使用。
    let mutable_reference = &mut array;
}
}

尽管 && 是单个 词法单元 ( 惰性 ‘与’ 运算符 ),但在借用表达式的语境中使用时,它的作用相当于两次借用:

#![allow(unused)]
fn main() {
// 含义相同:
let a = &&  10;
let a = & & 10;

// 含义相同:
let a = &&&&  mut 10;
let a = && && mut 10;
let a = & & & & mut 10;
}

原始借用运算符

&raw const&raw mut原始借用运算符

这些运算符的操作数表达式在位置表达式语境中求值。

&raw const expr 随后会创建一个指向给定位置的 *const T 类型的常量原始指针,而 &raw mut expr 会创建一个 *mut T 类型的可变原始指针。

每当位置表达式可能求得的位置未正确对齐,或者根据其类型未存储有效值,或者创建引用会引入不正确的别名假设时,必须使用原始借用运算符而不是借用运算符。 在这些情况下,使用借用运算符会因创建无效引用而导致 未定义行为 ,但仍可以构造原始指针。

以下是通过 packed 结构体 创建指向未对齐位置的原始指针的示例:

#![allow(unused)]
fn main() {
#[repr(packed)]
struct Packed {
    f1: u8,
    f2: u16,
}

let packed = Packed { f1: 1, f2: 2 };
// `&packed.f2` 会创建一个未对齐的引用,因此会导致未定义行为!
let raw_f2 = &raw const packed.f2;
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);
}

以下是创建指向不包含有效值的位置的原始指针的示例:

#![allow(unused)]
fn main() {
use std::mem::MaybeUninit;

struct Demo {
    field: bool,
}

let mut uninit = MaybeUninit::<Demo>::uninit();
// `&uninit.as_mut().field` 会创建一个对未初始化 `bool` 的引用,
// 因此会导致未定义行为!
let f1_ptr = unsafe { &raw mut (*uninit.as_mut_ptr()).field };
unsafe { f1_ptr.write(true); }
let init = unsafe { uninit.assume_init() };
}

解引用运算符

Syntax
DereferenceExpression* Expression

* (解引用)运算符也是一元前缀运算符。

当应用于 指针 时,它表示指向的位置。

如果表达式的类型是 &mut T*mut T ,并且是局部变量、局部变量的(嵌套)字段或可变 位置表达式 ,则可以对产生的内存位置进行赋值。

对原始指针进行解引用需要 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);
}

try传播表达式

Syntax
TryPropagationExpressionExpression ?

try 传播表达式使用内部表达式的值和 Try 特型 来决定是产生一个值(如果产生,产生什么值),还是向调用者返回一个值(如果返回,返回什么值)。

例子

#![allow(unused)]
fn main() {
use std::num::ParseIntError;
fn try_to_parse() -> Result<i32, ParseIntError> {
    let x: i32 = "123".parse()?; // `x` 是 `123`。
    let y: i32 = "24a".parse()?; // 立即返回 `Err()`。
    Ok(x + y)                    // 不运行。
}

let res = try_to_parse();
println!("{res:?}");
assert!(res.is_err())
}
#![allow(unused)]
fn main() {
fn try_option_some() -> Option<u8> {
    let val = Some(1)?;
    Some(val)
}
assert_eq!(try_option_some(), Some(1));

fn try_option_none() -> Option<u8> {
    let val = None?;
    Some(val)
}
assert_eq!(try_option_none(), None);
}
use std::ops::ControlFlow;

pub struct TreeNode<T> {
    value: T,
    left: Option<Box<TreeNode<T>>>,
    right: Option<Box<TreeNode<T>>>,
}

impl<T> TreeNode<T> {
    pub fn traverse_inorder<B>(&self, f: &mut impl FnMut(&T) -> ControlFlow<B>) -> ControlFlow<B> {
        if let Some(left) = &self.left {
            left.traverse_inorder(f)?;
        }
        f(&self.value)?;
        if let Some(right) = &self.right {
            right.traverse_inorder(f)?;
        }
        ControlFlow::Continue(())
    }
}

fn main() {
    let n = TreeNode {
        value: 1,
        left: Some(Box::new(TreeNode{value: 2, left: None, right: None})),
        right: None,
    };
    let v = n.traverse_inorder(&mut |t| {
        if *t == 2 {
            ControlFlow::Break("found")
        } else {
            ControlFlow::Continue(())
        }
    });
    assert_eq!(v, ControlFlow::Break("found"));
}

注意

Try 特型 目前不稳定,因此不能为用户类型实现。

try 传播表达式目前大致等价于:

#![allow(unused)]
fn main() {
#![ feature(try_trait_v2) ]
fn example() -> Result<(), ()> {
let expr = Ok(());
match core::ops::Try::branch(expr) {
    core::ops::ControlFlow::Continue(val) => val,
    core::ops::ControlFlow::Break(residual) =>
        return core::ops::FromResidual::from_residual(residual),
}
Ok(())
}
}

注意

try 传播运算符有时被称为 问号运算符? 运算符try 运算符

try 传播运算符可以应用于具有以下类型的表达式:

  • Result<T, E>
    • Result::Ok(val) 求值为 val
    • Result::Err(e) 返回 Result::Err(From::from(e))
  • Option<T>
    • Option::Some(val) 求值为 val
    • Option::None 返回 Option::None
  • ControlFlow<B, C>
    • ControlFlow::Continue(c) 求值为 c
    • ControlFlow::Break(b) 返回 ControlFlow::Break(b)
  • Poll<Result<T, E>>
    • Poll::Ready(Ok(val)) 求值为 Poll::Ready(val)
    • Poll::Ready(Err(e)) 返回 Poll::Ready(Err(From::from(e)))
    • Poll::Pending 求值为 Poll::Pending
  • Poll<Option<Result<T, E>>>
    • Poll::Ready(Some(Ok(val))) 求值为 Poll::Ready(Some(val))
    • Poll::Ready(Some(Err(e))) 返回 Poll::Ready(Some(Err(From::from(e))))
    • Poll::Ready(None) 求值为 Poll::Ready(None)
    • Poll::Pending 求值为 Poll::Pending

求负运算符

Syntax
NegationExpression
      - Expression
    | ! Expression

这是最后两个一元运算符。

下表总结了它们在原始类型上的行为,以及用于为其他类型重载这些运算符的 特型 。 请记住,有符号整数始终使用补码表示。 所有这些运算符的操作数都在 值表达式语境 中求值,因此会被移动或复制。

符号整数bool浮点数重载特型
-取负*取负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);
}

算术和逻辑二元运算符

Syntax
ArithmeticOrLogicalExpression
      Expression + Expression
    | Expression - Expression
    | Expression * Expression
    | Expression / Expression
    | Expression % Expression
    | Expression & Expression
    | Expression | Expression
    | Expression ^ Expression
    | Expression << Expression
    | Expression >> Expression

二元运算符表达式都使用中缀记法编写。

下表总结了算术和逻辑二元运算符在原始类型上的行为,以及用于为其他类型重载这些运算符的 特型 。 请记住,有符号整数始终使用补码表示。 所有这些运算符的操作数都在 值表达式语境 中求值,因此会被移动或复制。

符号整数bool浮点数重载特型重载复合赋值特型
+加法加法std::ops::Addstd::ops::AddAssign
-减法减法std::ops::Substd::ops::SubAssign
*乘法乘法std::ops::Mulstd::ops::MulAssign
/除法*†除法std::ops::Divstd::ops::DivAssign
%取余**†取余std::ops::Remstd::ops::RemAssign
&按位与逻辑与std::ops::BitAndstd::ops::BitAndAssign
``按位或逻辑或std::ops::BitOr
^按位异或逻辑异或std::ops::BitXorstd::ops::BitXorAssign
<<左移std::ops::Shlstd::ops::ShlAssign
>>右移***std::ops::Shrstd::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);
}

比较运算符

Syntax
ComparisonExpression
      Expression == Expression
    | Expression != Expression
    | Expression > Expression
    | Expression < Expression
    | Expression >= Expression
    | Expression <= Expression

比较运算符也为原始类型和标准库中的许多类型定义。

在链式调用比较运算符时需要括号。例如,表达式 a == b == c 是无效的,可以写成 (a == b) == c

与算术和逻辑运算符不同,重载这些运算符的 特型 被更广泛地用于展示一个类型如何被比较,并且很可能被使用这些 特型 作为界限的函数假设为定义了实际的比较。 标准库中的许多函数和 声明宏 随后可以使用该假设(尽管不能以此来确保安全性)。

与上面的算术和逻辑运算符不同,这些运算符隐式地对其操作数进行共享借用,并在 位置表达式语境 中对它们求值:

#![allow(unused)]
fn main() {
let a = 1;
let b = 1;
a == b;
// 等价于
::std::cmp::PartialEq::eq(&a, &b);
}

这意味着操作数不需要被移出。

符号含义重载方法
==等于std::cmp::PartialEq::eq
!=不等于std::cmp::PartialEq::ne
>大于std::cmp::PartialOrd::gt
<小于std::cmp::PartialOrd::lt
>=大于等于std::cmp::PartialOrd::ge
<=小于等于std::cmp::PartialOrd::le

以下是正在使用的比较运算符示例。

#![allow(unused)]
fn main() {
assert!(123 == 123);
assert!(23 != -12);
assert!(12.5 > 12.2);
assert!([1, 2, 3] < [1, 3, 4]);
assert!('A' <= 'B');
assert!("World" >= "Hello");
}

惰性布尔运算符

Syntax
LazyBooleanExpression
      Expression || Expression
    | Expression && Expression

运算符 ||&& 可以应用于布尔类型的操作数。 || 运算符表示逻辑‘或’, && 运算符表示逻辑‘与’。

它们与 |& 的不同之处在于,只有当左侧操作数尚未确定表达式结果时,才会对右侧操作数求值。 也就是说,只有当左侧操作数求值为 false 时, || 才会对其右侧操作数求值;而只有当左侧操作数求值为 true 时, && 才会对其右侧操作数求值。

#![allow(unused)]
fn main() {
let x = false || true; // true
let y = false && panic!(); // false, 不会对 `panic!()` 求值
}

类型转换表达式

Syntax
TypeCastExpressionExpression as TypeNoBounds

类型转换表达式由二元运算符 as 表示。

执行 as 表达式会将左侧的值转换为右侧的类型。

as 表达式的一个示例:

#![allow(unused)]
fn main() {
fn sum(values: &[f64]) -> f64 { 0.0 }
fn len(values: &[f64]) -> i32 { 0 }
fn average(values: &[f64]) -> f64 {
    let sum: f64 = sum(values);
    let size: f64 = len(values) as f64;
    sum / size
}
}

as 可用于显式执行 类型强制转换 ,以及以下额外的转换。 任何既不符合强制转换规则也不符合表中条目的转换都是编译器错误。 这里 *T 表示 *const T*mut Tm 代表引用类型中可选的 mut ,以及指针类型中的 mutconst

e 的类型Ue as U 执行的转换
整数或浮点数类型整数或浮点数类型数值转换
枚举整数类型枚举转换
boolchar整数类型原始类型到整数转换
u8charu8char 转换
*T*V 1指针到指针转换
*TT: Sized整数类型指针到地址转换
整数类型*VV: Sized地址到指针转换
&m₁ [T; n]*m₂ T 2数组到指针转换
*m₁ [T; n]*m₂ T 2数组到指针转换
函数项函数指针函数项到函数指针转换
函数项*VV: Sized函数项到指针转换
函数项整数函数项到地址转换
函数指针*VV: Sized函数指针到指针转换
函数指针整数函数指针到地址转换
闭包 3函数指针闭包到函数指针转换

语义

数值转换

  • 在两个大小相同的整数之间转换(例如 i32 -> u32)是无操作的(Rust 对固定整数的负值使用补码)

    #![allow(unused)]
    fn main() {
    assert_eq!(42i8 as u8, 42u8);
    assert_eq!(-1i8 as u8, 255u8);
    assert_eq!(255u8 as i8, -1i8);
    assert_eq!(-1i16 as u16, 65535u16);
    }
  • 从较大的整数转换为较小的整数(例如 u32 -> u8)将发生截断

    #![allow(unused)]
    fn main() {
    assert_eq!(42u16 as u8, 42u8);
    assert_eq!(1234u16 as u8, 210u8);
    assert_eq!(0xabcdu16 as u8, 0xcdu8);
    
    assert_eq!(-42i16 as i8, -42i8);
    assert_eq!(1234u16 as i8, -46i8);
    assert_eq!(0xabcdi32 as i8, -51i8);
    }
  • 从较小的整数转换为较大的整数(例如 u8 -> u32)将

    • 如果源是无符号的,则进行零扩展
    • 如果源是有符号的,则进行符号扩展
    #![allow(unused)]
    fn main() {
    assert_eq!(42i8 as i16, 42i16);
    assert_eq!(-17i8 as i16, -17i16);
    assert_eq!(0b1000_1010u8 as u16, 0b0000_0000_1000_1010u16, "零扩展");
    assert_eq!(0b0000_1010i8 as i16, 0b0000_0000_0000_1010i16, "符号扩展 0");
    assert_eq!(0b1000_1010u8 as i8 as i16, 0b1111_1111_1000_1010u16 as i16, "符号扩展 1");
    }
  • 从浮点数转换为整数将向零舍入

    • NaN 将返回 0
    • 大于最大整数值的值(包括 INFINITY ),将饱和为整数类型的最大值。
    • 小于最小整数值的值(包括 NEG_INFINITY ),将饱和为整数类型的最小值。
    #![allow(unused)]
    fn main() {
    assert_eq!(42.9f32 as i32, 42);
    assert_eq!(-42.9f32 as i32, -42);
    assert_eq!(42_000_000f32 as i32, 42_000_000);
    assert_eq!(std::f32::NAN as i32, 0);
    assert_eq!(1_000_000_000_000_000f32 as i32, 0x7fffffffi32);
    assert_eq!(std::f32::NEG_INFINITY as i32, -0x80000000i32);
    }
  • 从整数转换为浮点数将产生最接近的浮点数 *

    • 如有必要,根据 roundTiesToEven 模式进行舍入 ***
    • 溢出时,产生(与输入符号相同的)无穷大
    • 注意:在当前的数值类型集中,溢出只能发生在 u128 as f32 且值大于或等于 f32::MAX + (0.5 ULP)
    #![allow(unused)]
    fn main() {
    assert_eq!(1337i32 as f32, 1337f32);
    assert_eq!(123_456_789i32 as f32, 123_456_790f32, "已舍入");
    assert_eq!(0xffffffff_ffffffff_ffffffff_ffffffff_u128 as f32, std::f32::INFINITY);
    }
  • 从 f32 转换为 f64 是完美的且无损的

    #![allow(unused)]
    fn main() {
    assert_eq!(1_234.5f32 as f64, 1_234.5f64);
    assert_eq!(std::f32::INFINITY as f64, std::f64::INFINITY);
    assert!((std::f32::NAN as f64).is_nan());
    }
  • 从 f64 转换为 f32 将产生最接近的 f32 **

    • 如有必要,根据 roundTiesToEven 模式进行舍入 ***
    • 溢出时,产生(与输入符号相同的)无穷大
    #![allow(unused)]
    fn main() {
    assert_eq!(1_234.5f64 as f32, 1_234.5f32);
    assert_eq!(1_234_567_891.123f64 as f32, 1_234_567_890f32, "已舍入");
    assert_eq!(std::f64::INFINITY as f32, std::f32::INFINITY);
    assert!((std::f64::NAN as f32).is_nan());
    }

* 如果硬件不原生支持具有这种舍入模式和溢出行为的整数到浮点数转换,则这些转换可能会比预期的慢。

** 如果硬件不原生支持具有这种舍入模式和溢出行为的 f64 到 f32 转换,则这些转换可能会比预期的慢。

*** 按照 IEEE 754-2008 §4.3.1 的定义:选择最接近的浮点数,如果在两个浮点数中间,则优先选择最低有效位为偶数的那个。

枚举转换

将枚举转换为其判别值,然后根据需要使用数值转换。 转换仅限于以下几种枚举:

#![allow(unused)]
fn main() {
enum Enum { A, B, C }
assert_eq!(Enum::A as i32, 0);
assert_eq!(Enum::B as i32, 1);
assert_eq!(Enum::C as i32, 2);
}

如果枚举实现了 Drop ,则不允许进行转换。

原始类型到整数转换

  • false 转换为 0true 转换为 1
  • char 转换为码位的值,然后根据需要使用数值转换。
#![allow(unused)]
fn main() {
assert_eq!(false as i32, 0);
assert_eq!(true as i32, 1);
assert_eq!('A' as i32, 65);
assert_eq!('Ö' as i32, 214);
}

u8char 转换

转换为具有相应码位的 char

#![allow(unused)]
fn main() {
assert_eq!(65u8 as char, 'A');
assert_eq!(214u8 as 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);
}

指针到指针转换

*const T / *mut T 可以转换为 *const U / *mut U ,具有以下行为:

  • 如果 TU 都是有大小的,则指针保持不变并返回。
  • 如果 TU 都是无大小的,则指针也保持不变并返回。 特别地,元数据会被精确保留。

    例如,从 *const [T]*const [U] 的转换会保留元素的数量。 请注意,因此此类转换不一定保留指针引用对象的大小 (例如,将 *const [u16] 转换为 *const [u8] 将产生一个原始指针,它指向的对象大小是原来的一半)。 对于 str 和任何其无大小尾部是切片类型的复合类型(如 struct Foo(i32, [u8])(u64, Foo) ),情况也是如此。

  • 如果 T 是无大小的且 U 是有大小的,则转换会丢弃完成宽指针 T 的所有元数据,并产生一个由无大小指针的数据部分组成的细指针 U

赋值表达式

Syntax
AssignmentExpressionExpression = Expression

一个 赋值表达式 将一个值移动到一个指定的位置。

赋值表达式由一个 可变 赋值目标表达式 (即 赋值目标操作数 ),后跟一个等号( = )和一个 值表达式 (即 被赋值操作数 )组成。

在其最基本的形式中,赋值目标表达式是一个 位置表达式 ,我们首先讨论这种情况。

解构赋值的更一般情况在下面讨论,但这种情况总是分解为对位置表达式的顺序赋值,这可以被视为更基本的情况。

基本赋值

求值赋值表达式始于对其操作数求值。 首先对被赋值操作数求值,然后对赋值目标表达式求值。

对于解构赋值,赋值目标表达式的子表达式按从左到右的顺序求值。

注意

这与其他表达式不同,因为右侧操作数在左侧操作数之前求值。

然后它的效果是首先 释放 赋值位置的值,除非该位置是未初始化的局部变量或局部变量的未初始化字段。

接着,它要么将分配的值 复制或移动 到赋值位置。

赋值表达式总是产生 单元值

示例:

#![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};

// 脱糖为:

{
    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() {
fn temp() {}
fn f<T>(x: T) -> T { x }
let x;
(x = f(&temp()), x); // OK
}

相反,在解构赋值中,临时变量在脱糖后的 let 语句结束时被释放。由于这发生在尝试给 x 赋值之前,如下所示,它将失败。

#![allow(unused)]
fn main() {
fn temp() {}
fn f<T>(x: T) -> T { x }
let x;
[x] = [f(&temp())]; // 错误
}

这被脱糖为:

#![allow(unused)]
fn main() {
fn temp() {}
fn f<T>(x: T) -> T { x }
let x;
{
    let [_x] = [f(&temp())];
    //                     ^
    //      临时变量在这里被释放。
    x = _x; // 错误
}
}

注意

由于脱糖,解构赋值的被赋值操作数(右侧)是新引入块内的 扩展表达式

在下面,因为 临时作用域 被扩展到了此引入块的末尾,所以赋值是被允许的。

#![allow(unused)]
fn main() {
fn temp() {}
let x;
[x] = [&temp()]; // OK
}

这被脱糖为:

#![allow(unused)]
fn main() {
fn temp() {}
let x;
{ let [_x] = [&temp()]; x = _x; } // OK
}

然而,如果我们尝试使用 x ,即使在同一语句中,也会得到一个错误,因为 临时变量 在此引入块的末尾被释放。

#![allow(unused)]
fn main() {
fn temp() {}
let x;
([x] = [&temp()], x); // 错误
}

这被脱糖为:

#![allow(unused)]
fn main() {
fn temp() {}
let x;
(
    {
        let [_x] = [&temp()];
        x = _x;
    }, // <-- 临时变量在这里被释放。
    x, // 错误
);
}

复合赋值表达式

Syntax
CompoundAssignmentExpression
      Expression += Expression
    | Expression -= Expression
    | Expression *= Expression
    | Expression /= Expression
    | Expression %= Expression
    | Expression &= Expression
    | Expression |= Expression
    | Expression ^= Expression
    | Expression <<= Expression
    | Expression >>= Expression

复合赋值表达式 将算术和逻辑二元运算符与赋值表达式结合在一起。

例如:

#![allow(unused)]
fn main() {
let mut x = 5;
x += 1;
assert!(x == 6);
}

复合赋值的语法是一个 可变 位置表达式 (即 被赋值操作数 ),然后是一个由运算符后跟 = 组成的单个 词法单元 (无空格),最后是一个 值表达式 (即 修改操作数 )。

与其他位置操作数不同,被赋值的操作数必须是一个位置表达式。

尝试使用值表达式是编译器错误,而不是将其提升为临时变量。

复合赋值表达式的求值得取决于操作数的类型。

如果在单态化之前已知两个操作数的类型都是原始类型,则首先对右侧求值,接着对左侧求值,然后通过将运算符应用于两侧的值来修改由左侧求值得出的位置。

use core::{num::Wrapping, ops::AddAssign};

trait Equate {}
impl<T> Equate for (T, T) {}

fn f1(x: (u8,)) {
    let mut order = vec![];
    // 首先对右侧求值,因为两个操作数都是原始类型。
    { order.push(2); x }.0 += { order.push(1); x }.0;
    assert!(order.is_sorted());
}

fn f2(x: (Wrapping<u8>,)) {
    let mut order = vec![];
    // 首先对左侧求值,因为 `Wrapping<_>` 不是原始类型。
    { order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
    assert!(order.is_sorted());
}

fn f3<T: AddAssign<u8> + Copy>(x: (T,)) where (T, u8): Equate {
    let mut order = vec![];
    // 首先对左侧求值,因为其中一个操作数是泛型参数,
    // 即使该泛型参数由于 where 子句界限可以与原始类型统一。
    { order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
    assert!(order.is_sorted());
}

fn main() {
    f1((0u8,));
    f2((Wrapping(0u8),));
    // 我们提供一个原始类型作为泛型参数,但这不会影响单态化后 `f3` 中的求值顺序。
    f3::<u8>((0u8,));
}

注意

这是不寻常的。在其他地方,从左到右求值是常规。

更多示例请参阅 求值顺序测试

否则,此表达式是使用该运算符对应的 特型 (见 expr.arith-logic.behavior )并以左侧作为 接收者 、右侧作为下一个参数调用其方法的语法糖。

例如,以下两个语句是等价的:

#![allow(unused)]
fn main() {
use std::ops::AddAssign;
fn f<T: AddAssign + Copy>(mut x: T, y: T) {
    x += y; // 语句 1。
    x.add_assign(y); // 语句 2。
}
}

注意

令人惊讶的是,将其进一步脱糖为完全限定的方法调用并不等价,因为当通过 自动引用 获取指向第一个操作数的可变引用时,存在特殊的借用检查器行为。

#![allow(unused)]
fn main() {
use std::ops::AddAssign;
fn f<T: AddAssign + Copy>(mut x: T) {
    // 这里我们将 `x` 同时用作左侧和右侧。因为调用特型方法所需的
    // 左侧可变借用是通过自动引用隐式获取的,所以这是 OK 的。
    x += x; //~ OK
    x.add_assign(x); //~ OK
}
}
#![allow(unused)]
fn main() {
use std::ops::AddAssign;
fn f<T: AddAssign + Copy>(mut x: T) {
    // 我们不能将上面的代码脱糖为下面的形式,因为一旦我们为了传递第一个参数
    // 而获取了 `x` 的可变借用,我们就不能在第二个参数中按值传递 `x` ,
    // 因为可变引用仍然有效。
    <T as AddAssign>::add_assign(&mut x, x);
    //~^ 错误:不能使用 `x` 因为它已被可变借用
}
}
#![allow(unused)]
fn main() {
use std::ops::AddAssign;
fn f<T: AddAssign + Copy>(mut x: T) {
    // 同上。
    (&mut x).add_assign(x);
    //~^ 错误:不能使用 `x` 因为它已被可变借用
}
}

与普通赋值表达式一样,复合赋值表达式总是产生 单元值

警告

避免编写依赖于复合赋值中操作数求值顺序的代码,因为它可能是不寻常且令人惊讶的。


  1. 其中 TV 具有兼容的元数据:

    • V: Sized ,或
    • 都是切片元数据( *[u16] -> *[u8]*str -> *(u8, [u32]) ),或
    • 都是相同的特型对象元数据,除了删除自动 特型 ( *dyn Debug -> *(u16, dyn Debug)*dyn Debug + Send -> *dyn Debug
      • 注意:只有当主 特型 将自动 特型 作为超级 特型 时,才允许 增加 自动 特型 (给定 trait T: Send {}*dyn T -> *dyn T + Send 是有效的,但 *dyn Debug -> *dyn Debug + Send 则无效)
      • 注意:泛型(包括生命周期)必须匹配( *dyn T<'a, A> -> *dyn T<'b, B> 要求 'a = 'bA = B
  2. 仅当 m₁mutm₂const 时。允许将 mut 引用/指针转换为 const 指针。 ↩2

  3. 只有不捕获(封闭)任何局部变量的闭包才能转换为函数指针。

分组表达式

Syntax
GroupedExpression( Expression )

一个 括号表达式 包裹单个表达式,求值为该表达式。 括号表达式的 语法格式 是一个 (,然后是一个表达式,称为 被包裹的操作数,接着是一个 )

括号表达式求值为被包裹的操作数的值。

与其他表达式不同,括号表达式既是 位置表达式和值表达式。 当被包裹的操作数是一个位置表达式时,它就是一个位置表达式;当被包裹的操作数是一个值表达式时,它就是一个值表达式。

括号可用于显式修改表达式中子表达式的优先级顺序。

一个括号表达式的例子:

#![allow(unused)]
fn main() {
let x: i32 = 2 + 3 * 4; // 未加括号
let y: i32 = (2 + 3) * 4; // 已加括号
assert_eq!(x, 14);
assert_eq!(y, 20);
}

一个必须使用括号的例子是调用 结构体 成员中的函数指针:

#![allow(unused)]
fn main() {
struct A {
   f: fn() -> &'static str
}
impl A {
   fn f(&self) -> &'static str {
       "The method f"
   }
}
let a = A{f: || "The field f"};

assert_eq!( a.f (), "The method f");
assert_eq!((a.f)(), "The field f");
}

数组和数组索引表达式

数组表达式

Syntax
ArrayExpression[ ArrayElements? ]

ArrayElements
      Expression ( , Expression )* ,?
    | Expression ; Expression

数组表达式 构造 数组。 数组表达式有两种形式。

第一种形式列出数组中的每一个值。

这种形式的 语法格式 是在方括号中包含一个由逗号分隔的具有统一类型的表达式列表。

这会产生一个数组,按编写顺序包含这些值。

第二种形式的 语法格式 是在方括号中包含两个由分号 (;) 分隔的表达式。

; 之前的表达式称为 重复操作数

; 之后的表达式称为 长度操作数

长度操作数必须是一个 推断常量 或者是一个 usize 类型的 常量表达式(例如 字面量常量 项)。

#![allow(unused)]
fn main() {
const C: usize = 1;
let _: [u8; C] = [0; 1]; // 字面量。
let _: [u8; C] = [0; C]; // 常量 项。
let _: [u8; C] = [0; _]; // 推断常量。
let _: [u8; C] = [0; (((_)))]; // 推断常量。
}

注意

在数组表达式中,一个 推断常量 被解析为一个 表达式,但在语义上被视为一种单独的 常量 泛型 参数

这种形式的数组表达式创建一个长度为长度操作数值的数组,其中每个元素都是重复操作数的副本。 也就是说,[a; b] 创建一个包含 ba 的值的副本的数组。

如果长度操作数的值大于 1,则要求重复操作数的类型实现 Copy、或者是一个 常量块表达式、或者是一个指向常量 项 的 路径

当重复操作数是一个常量块或指向常量 项 的路径时,它将被评估长度操作数指定的次数。

如果该值为 0,则常量块或常量 项 根本不会被评估。

对于既不是常量块也不是指向常量 项 的路径的表达式,它仅被评估一次,然后结果被复制长度操作数的值次。

#![allow(unused)]
fn main() {
[1, 2, 3, 4];
["a", "b", "c", "d"];
[0; 128];              // 包含 128 个零的数组
[0u8, 0u8, 0u8, 0u8,];
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 二维数组
const EMPTY: Vec<i32> = Vec::new();
[EMPTY; 2];
}

数组和切片索引表达式

Syntax
IndexExpressionExpression [ Expression ]

数组切片 类型的值可以通过在它们后面编写一个由方括号括起来的 usize 类型表达式(索引)来进行索引。 当数组是可变的时,生成的 内存位置 可以被赋值。

对于其他类型,索引表达式 a[b] 等同于 *std::ops::Index::index(&a, b),或者在可变位置表达式上下文中等同于 *std::ops::IndexMut::index_mut(&mut a, b)。 与方法一样,Rust 也会在 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];  // 恐慌

let arr = ["a", "b"];
arr[10];                  // 警告:索引越界
}

数组索引表达式可以通过实现 IndexIndexMut 特型 来为数组和切片以外的类型实现。

元组和元组索引表达式

元组表达式

Syntax
TupleExpression( TupleElements? )

TupleElements → ( Expression , )+ Expression?

一个 元组表达式 构造 元组值

元组表达式 的 语法格式 是括号括起来的、逗号分隔的表达式列表,称为 元组初始化操作数

一元 元组表达式 要求在其 元组初始化操作数 之后加一个逗号,以便与 括号表达式 区分开。

元组表达式 是 值表达式,它求值为新构造的 元组类型 的值。

元组初始化操作数 的数量是构造出的元组的元数。

没有任何 元组初始化操作数 的 元组表达式 会产生单元元组。

对于其他 元组表达式,第一个写入的 元组初始化操作数 初始化字段 0,随后的操作数初始化下一个更高的字段。 例如,在 元组表达式 ('a', 'b', 'c') 中,'a' 初始化字段 0 的值,'b' 字段 1,而 'c' 字段 2

元组表达式 及其类型的示例:

表达式类型
()() (单元)
(0.0, 4.5)(f64, f64)
("x".to_string(), )(String, )
("a", 4usize, true)(&'static str, usize, bool)

元组索引表达式

Syntax
TupleIndexingExpressionExpression . TUPLE_INDEX

一个 元组索引表达式 访问 元组元组结构体 的字段。

元组索引表达式 的 语法格式 是一个表达式(称为 元组操作数 ),然后是一个 .,最后是一个元组索引。

元组索引 的 语法格式 是一个没有前导零、下划线或后缀的 十进制字面量。 例如 02 是有效的元组索引,但 010_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);
}

注意

字段访问表达式 不同,元组索引表达式 可以是 调用表达式 的函数操作数,因为它不会与方法调用混淆,因为方法名不能是数字。

注意

尽管数组和切片也有元素,你必须使用 数组或切片索引表达式切片模式 来访问它们的元素。

结构体表达式

Syntax
StructExpression
    PathInExpression { ( StructExprFields | StructBase )? }

StructExprFields
    StructExprField ( , StructExprField )* ( , StructBase | ,? )

StructExprField
    OuterAttribute*
    (
        IDENTIFIER
      | ( IDENTIFIER | TUPLE_INDEX ) : Expression
    )

StructBase.. Expression

一个 结构体表达式 创建一个结构体、枚举或联合体值。 它由指向 结构体枚举变体联合体 项 的路径,以及随后该 项 的字段值组成。

以下是结构体表达式的示例:

#![allow(unused)]
fn main() {
struct Point { x: f64, y: f64 }
struct NothingInMe { }
mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } }
enum Enum { Variant {} }
Point {x: 10.0, y: 20.0};
NothingInMe {};
let u = game::User {name: "Joe", age: 35, score: 100_000};
Enum::Variant {};
}

注意

元组结构体和元组枚举变体通常使用引用 值命名空间中的构造函数调用表达式 来实例化。这些不同于使用花括号引用类型命名空间中构造函数的结构体表达式。

#![allow(unused)]
fn main() {
struct Position(i32, i32, i32);
Position(0, 0, 0);  // 创建元组结构体的典型方式。
let c = Position;  // `c` 是一个接受 3 个参数的函数。
let pos = c(8, 6, 7);  // 创建一个 `Position` 值。

enum Version { Triple(i32, i32, i32) };
Version::Triple(0, 0, 0);
let f = Version::Triple;
let ver = f(8, 6, 7);
}

调用路径的最后一段不能引用类型别名:

#![allow(unused)]
fn main() {
trait Tr { type T; }
impl<T> Tr for T { type T = T; }

struct Tuple();
enum Enum { Tuple() }

// <Unit as Tr>::T(); // 导致错误 —— `::T` 是一个类型,而不是一个值
<Enum as Tr>::T::Tuple(); // 确定
}

单元结构体和单元枚举变体通常使用引用 值命名空间中的常量路径表达式 来实例化。

#![allow(unused)]
fn main() {
struct Gamma;
// Gamma 单元值,引用值命名空间中的常量。
let a = Gamma;
// 与 `a` 完全相同的值,但是使用引用类型命名空间的结构体表达式构造的。
let b = Gamma {};

enum ColorSpace { Oklch }
let c = ColorSpace::Oklch;
let d = ColorSpace::Oklch {};
}

带字段的结构体表达式

带有花括号包围字段的结构体表达式允许你以任意顺序指定每个单独字段的值。 字段名与其值之间用冒号分隔。

联合体 类型的值只能使用此语法创建,并且必须指定恰好一个字段。

函数式更新语法

构造结构体类型值的结构体表达式可以以语法 .. 后跟一个表达式结束,以表示函数式更新。

.. 之后的表达式(基准)必须具有与正在形成的新结构体类型相同的结构体类型。

整个表达式使用指定的字段的给定值,并从基准表达式中移动 (move) 或复制 (copy) 剩余的字段。

与所有结构体表达式一样,结构体的所有字段必须是 可见 的,即使是那些没有明确命名的字段。

#![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);
}

结构体表达式不能直接用于 loopif 表达式的头部,也不能用于 if letmatch 表达式的 审查对象。 然而,如果结构体表达式位于另一个表达式之内(例如在 括号 内),则可以用于这些情况。

字段名可以是十进制整数值,用于指定构造元组结构体的索引。 这可以与基准结构体一起使用,以填充未指定的其余索引:

#![allow(unused)]
fn main() {
struct Color(u8, u8, u8);
let c1 = Color(0, 0, 0);  // 创建元组结构体的典型方式。
let c2 = Color{0: 255, 1: 127, 2: 0};  // 按索引指定字段。
let c3 = Color{1: 0, ..c2};  // 使用基准结构体填充所有其他字段。
}

结构体字段初始化简写

当初始化具有命名(但不是编号)字段的数据结构(结构体、枚举、联合体)时,允许将 fieldname 写为 fieldname: fieldname 的简写。 这允许一种更紧凑、重复更少的语法。 例如:

#![allow(unused)]
fn main() {
struct Point3d { x: i32, y: i32, z: i32 }
let x = 0;
let y_value = 0;
let z = 0;
Point3d { x: x, y: y_value, z: z };
Point3d { x, y: y_value, z };
}

调用表达式

Syntax
CallExpressionExpression ( CallParams? )

CallParamsExpression ( , Expression )* ,?

一个 调用表达式 调用一个函数。 调用表达式 的 语法格式 是一个表达式(称为 函数操作数 ),后跟一个括号括起来的、逗号分隔的表达式列表(称为 参数操作数 )。

如果函数最终返回,则该表达式完成。

对于 非函数类型,表达式 f(...) 会根据 函数操作数 使用以下 特型 之一的方法:

如果需要,将进行自动借用。 函数操作数 也会根据需要进行 自动解引用

一些 调用表达式 的示例:

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 { 0 }
let three: i32 = add(1i32, 2i32);
let name: &'static str = (|| "Rust")();
}

消除函数调用歧义

所有函数调用都是更显式的 完全限定语法 的语法糖。

函数调用可能需要完全限定,具体取决于考虑到作用域内 项 时调用的歧义性。

注意

在过去,术语 “Unambiguous Function Call Syntax”、“Universal Function Call Syntax” 或 “UFCS” 已被用于文档、issue、RFC 和其他社区著作中。然而,这些术语缺乏描述力,并且可能会使手头的问题更加混淆。为了便于搜索,我们在这里提及它们。

经常会出现几种导致方法或关联函数调用的接收者或指代对象产生歧义的情况。 这些情况可能包括:

  • 多个作用域内的 特型 为相同类型定义了同名方法
  • 自动解引用 (Auto-deref) 是不希望的;例如,区分智能指针本身的方法和指针指代对象的方法
  • 不带参数的方法(如 default())并返回类型的属性(如 size_of()

为了解决歧义,程序员可以使用更具体的路径、类型或 特型 来引用其所需的方法或函数。

例如,

trait Pretty {
    fn print(&self);
}

trait Ugly {
    fn print(&self);
}

struct Foo;
impl Pretty for Foo {
    fn print(&self) {}
}

struct Bar;
impl Pretty for Bar {
    fn print(&self) {}
}
impl Ugly for Bar {
    fn print(&self) {}
}

fn main() {
    let f = Foo;
    let b = Bar;

    // 我们可以这样做,因为对于 Foo 类型,我们只有一个名为 print 的项
    f.print();
    // 更显式,而且对于 Foo 来说不是必需的
    Foo::print(&f);
    // 如果你不喜欢这种简洁方式的话
    <Foo as Pretty>::print(&f);

    // b.print(); // 错误:发现多个 'print'
    // Bar::print(&b); // 仍然是错误:发现多个 print

    // 因为作用域内的项定义了 print,所以这是必需的
    <Bar as Pretty>::print(&b);
}

有关更多细节和动机,请参阅 RFC 132

方法调用表达式

Syntax
MethodCallExpressionExpression . PathExprSegment ( CallParams? )

一个 方法调用 由一个表达式( 接收者 )、一个点、一个表达式路径段和一个括号括起来的表达式列表组成。

方法调用被解析为特定 特型 上的关联 方法,如果左侧的准确 self 类型已知,则静态分派到方法;如果左侧表达式是一个间接的 特型对象

#![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 之后将 &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,在以下位置搜索具有该类型接收者的 可见 方法:

  1. T 的固有方法(直接在 T 上实现的方法)。
  2. T 实现的任何 可见 特型 提供的方法。 如果 T 是类型参数,则首先查找由 T 上的 特型 约束提供的方法。 然后查找作用域内所有剩余的方法。

注意

查找按顺序对每个类型进行,这偶尔会导致令人惊讶的结果。下面的代码将打印 “In trait impl!”,因为首先查找 &self 方法,在找到结构体的 &mut self 方法之前就找到了 特型 方法。

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。 一旦查找到方法,如果由于这些原因之一(或多个)而无法调用,结果将是编译器错误。

如果到达某一步存在多个可能的方法,例如泛型方法或 特型 被视为相同,则这是一个编译器错误。 这些情况需要使用 消除歧义的函数调用语法 进行方法和函数调用。

2021 版次差异

在 2021 版次之前,在搜索可见方法期间,如果候选接收者类型是 数组类型,则会忽略标准库 IntoIterator 特型 提供的方法。

为此目的使用的 版次 由代表方法名的 词法单元 决定。

此特殊情况将来可能会被删除。

警告

对于 特型对象,如果存在与 特型 方法同名的固有方法,则在尝试在方法调用表达式中调用该方法时会给出编译器错误。 相反,你可以使用 消除歧义的函数调用语法 来调用该方法,在这种情况下,它调用的是 特型 方法,而不是固有方法。 没有办法调用固有方法。 只要不在 特型对象 上定义与 特型 方法同名的固有方法就没事。

字段访问表达式

Syntax
FieldExpressionExpression . IDENTIFIER

一个 字段表达式 是一个 位置表达式,它求值为 结构体联合体 的字段位置。

当操作数是 可变的 时,字段表达式也是可变的。

字段表达式的 语法格式 是一个表达式,称为 容器操作数,然后是一个 .,最后是一个 标识符

字段表达式不能后面跟着圆括号括起来的逗号分隔的表达式列表,因为那会被解析为 方法调用表达式。 也就是说,它们不能作为 调用表达式 的函数操作数。

注意

将字段表达式包装在 括号表达式 中以便在调用表达式中使用。

#![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)() // 包含字段表达式的调用表达式

自动解引用

如果容器操作数的类型实现了 DerefDerefMut(取决于操作数是否 可变的),它会被 自动解引用 尽可能多次,以使字段访问成为可能。 这个过程简称为 autoderef

借用

结构体的字段或对结构体的引用在借用时被视为独立的实体。 如果结构体没有实现 Drop 且存储在局部变量中,这也适用于将其各个字段移出。 如果自动解引用是通过除 Box 以外的用户定义类型进行的,则此规则也不适用。

#![allow(unused)]
fn main() {
struct A { f1: String, f2: String, f3: String }
let mut x: A;
x = A {
    f1: "f1".to_string(),
    f2: "f2".to_string(),
    f3: "f3".to_string()
};
let a: &mut String = &mut x.f1; // x.f1 被可变借用
let b: &String = &x.f2;         // x.f2 被不可变借用
let c: &String = &x.f2;         // 可以再次借用
let d: String = x.f3;           // 移出 x.f3
}

闭包表达式

Syntax
ClosureExpression
    async?1
    move?
    ( || | | ClosureParameters? | )
    ( Expression | -> TypeNoBounds BlockExpression )

ClosureParametersClosureParam ( , ClosureParam )* ,?

ClosureParamOuterAttribute* PatternNoTopAlt ( : Type )?

一个 闭包表达式 ,也称为 lambda 表达式或 lambda,定义了一个 闭包类型 并求值为该类型的一个值。 闭包表达式的 语法格式 是一个可选的 async 关键字,一个可选的 move 关键字,然后是由管道符号(|)分隔的逗号分隔的 模式 列表(称为 闭包参数 ),每个参数后面可以可选地跟着 : 和一个类型,然后是一个可选的 -> 和一个类型(称为 返回类型 ),最后是一个表达式(称为 闭包体操作数 )。

每个模式后的可选类型是该模式的类型标注。

如果有返回类型,闭包体必须是一个

闭包表达式表示一个函数,它将参数列表映射到参数之后的表达式上。 就像 let 绑定 一样,闭包参数是不可反驳的 模式,其类型标注是可选的,如果未给出,将从上下文中推导。

每个闭包表达式都有一个唯一的、匿名的类型。

重要的是,闭包表达式会 捕获其环境 ,而普通的 函数定义 则不会。

如果没有 move 关键字,闭包表达式会 推导它如何从其环境中捕获每个变量,优先通过共享引用进行捕获,从而实际上借用了闭包体内提到的所有外部变量。

如果需要,编译器将推导应该采用可变引用,或者应该从环境中移动或复制值(取决于它们的类型)。

通过在闭包前加上 move 关键字,可以强制闭包通过复制或移动值来捕获其环境。 这通常用于确保闭包的生命周期是 'static

闭包特型实现

闭包类型实现哪些特型取决于变量如何被捕获、被捕获变量的类型以及 async 的存在。 有关闭包如何以及何时实现 FnFnMutFnOnce,请参见 调用特型与强转 章节。 如果每个被捕获变量的类型也都实现了该特型,则闭包类型就会实现 SendSync

Async闭包

带有 async 关键字标记的闭包表示它们是异步的,其方式类似于 异步函数

调用 Async 闭包不会执行任何工作,而是求值为一个实现了 Future 的值,该值对应于闭包体的计算。

#![allow(unused)]
fn main() {
async fn takes_async_callback(f: impl AsyncFn(u64)) {
    f(0).await;
    f(1).await;
}

async fn example() {
    takes_async_callback(async |i| {
        core::future::ready(i).await;
        println!("done with {i}.");
    }).await;
}
}

2018 版次差异

Async 闭包仅从 Rust 2018 版次开始可用。

示例

在此示例中,我们定义了一个函数 ten_times,它接受一个高阶函数参数,然后我们使用闭包表达式作为参数来调用它,随后是一个从其环境移动值的闭包表达式。

#![allow(unused)]
fn main() {
fn ten_times<F>(f: F) where F: Fn(i32) {
    for index in 0..10 {
        f(index);
    }
}

ten_times(|j| println!("hello, {}", j));
// 带有类型标注
ten_times(|j: i32| -> () { println!("hello, {}", j) });

let word = "konnichiwa".to_owned();
ten_times(move |j| println!("{}, {}", word, j));
}

闭包参数上的属性

闭包参数上的属性遵循与 普通函数参数 相同的规则和限制。


  1. async 限定符在 2015 版次中不允许使用。

循环及其他可中断表达式

Syntax
LoopExpression
    LoopLabel? (
        InfiniteLoopExpression
      | PredicateLoopExpression
      | IteratorLoopExpression
      | LabelBlockExpression
    )

Rust 支持四种循环表达式:

所有四种类型的循环都支持 break 表达式标签

除标签块表达式外,所有表达式都支持 continue 表达式

只有 loop 和标签块表达式支持 求值为非平凡值

无限循环

Syntax
InfiniteLoopExpressionloop BlockExpression

loop 表达式连续重复执行其主体: loop { println!("I live."); }

没有关联 break 表达式的 loop 表达式是发散的,其类型为 !

包含关联 break 表达式loop 表达式可能会终止,并且其类型必须与 break 表达式的值兼容。

谓词循环

Syntax
PredicateLoopExpressionwhile Conditions BlockExpression

while 循环表达式允许在条件集保持为真的情况下重复求值一个块。

while 表达式的 语法格式 是一个或多个由 && 分隔的条件操作数序列,后跟一个 块表达式

条件操作数必须是具有 布尔类型表达式 或条件 let 匹配。 如果所有条件操作数求值结果均为 true,且所有 let 模式都成功匹配其 待匹配值,则执行循环体块。

在循环体成功执行后,会重新求值条件操作数,以确定是否应再次执行主体。

如果任何条件操作数求值为 false 或任何 let 模式未匹配其待匹配值,则不执行主体,并在 while 表达式之后继续执行。

一个 while 表达式求值为 ()

示例:

#![allow(unused)]
fn main() {
let mut i = 0;

while i < 10 {
    println!("hello");
    i = i + 1;
}
}

while let模式

while 条件中的 let 模式允许在模式匹配成功时将新变量绑定到作用域中。 以下示例演示了使用 let 模式进行的绑定:

#![allow(unused)]
fn main() {
let mut x = vec![1, 2, 3];

while let Some(y) = x.pop() {
    println!("y = {}", y);
}

while let _ = 5 {
    // 不可反驳模式总是为真
    println!("Irrefutable patterns are always true");
    break;
}
}

while let 循环等效于包含 match 表达式loop 表达式,如下所示。

'label: while let PATS = EXPR {
    /* 循环体 */
}

等效于

'label: loop {
    match EXPR {
        PATS => { /* 循环体 */ },
        _ => break,
    }
}

可以使用 | 运算符指定多个模式。这与 match 表达式中的 | 具有相同的语义:

#![allow(unused)]
fn main() {
let mut vals = vec![2, 3, 1, 2, 2];
while let Some(v @ 1) | Some(v @ 2) = vals.pop() {
    // 打印 2, 2, 然后是 1
    println!("{}", v);
}
}

while条件链

多个条件操作数可以用 && 分隔。这些与 if 条件链 具有相同的语义和限制。

以下是一个链接多个表达式的示例,混合了 let 绑定和布尔表达式,并且表达式能够引用之前表达式中的模式绑定:

fn main() {
    let outer_opt = Some(Some(1i32));

    while let Some(inner_opt) = outer_opt
        && let Some(number) = inner_opt
        && number == 1
    {
        println!("Peek a boo");
        break;
    }
}

迭代器循环

Syntax
IteratorLoopExpression
    for Pattern in Expressionexcept StructExpression BlockExpression

一个 for 表达式是一个用于循环遍历由 std::iter::IntoIterator 实现提供的元素的语法结构。

如果迭代器产生一个值,该值将与不可反驳模式匹配,执行循环体,然后控制流返回到 for 循环头部。 如果迭代器为空,则 for 表达式完成。

数组内容的 for 循环示例:

#![allow(unused)]
fn main() {
let v = &["apples", "cake", "coffee"];

for text in v {
    println!("I like {}.", text);
}
}

一系列整数上的 for 循环示例:

#![allow(unused)]
fn main() {
let mut sum = 0;
for n in 1..11 {
    sum += n;
}
assert_eq!(sum, 55);
}

for 循环等效于包含 match 表达式loop 表达式,如下所示:

'label: for PATTERN in iter_expr {
    /* 循环体 */
}

等效于

{
    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 () = { /* 循环体 */ };
        },
    };
    result
}

这里的 IntoIteratorIteratorOption 始终是标准库 项 ,而不是当前作用域中解析为这些名称的任何内容。

变量名 nextiterval 仅用于说明,它们实际上没有用户可以键入的名称。

注意

外部 match 用于确保 iter_expr 中的任何 临时值 在循环结束前不会被丢弃。next 在被赋值前声明,是因为这通常能让类型推导更准确。

循环标签

Syntax
LoopLabelLIFETIME_OR_LABEL :

循环表达式可以可选地带有一个 标签。标签写在循环表达式之前的生命周期,例如 'foo: loop { break 'foo; }'bar: while false {}'humbug: for _ in 0..0 {}

如果存在标签,则嵌套在该循环内的带标签 breakcontinue 表达式可以退出该循环或返回到其头部。 参见 break 表达式continue 表达式

标签遵循局部变量的 卫生性 和 遮蔽 规则。例如,这段代码将打印 “outer loop”:

#![allow(unused)]
fn main() {
'a: loop {
    'a: loop {
        break 'a;
    }
    print!("outer loop");
    break 'a;
}
}

'_ 不是有效的循环标签。

break表达式

Syntax
BreakExpressionbreak LIFETIME_OR_LABEL? Expression?

当遇到 break 时,关联的循环体执行将立即终止,例如:

#![allow(unused)]
fn main() {
let mut last = 0;
for x in 1..100 {
    if x > 12 {
        break;
    }
    last = x;
}
assert_eq!(last, 12);
}

break 表达式通常与包围该 break 表达式的最内层 loopforwhile 循环相关联, 但可以使用 标签 来指定受影响的是哪个外层循环。 示例:

#![allow(unused)]
fn main() {
'outer: loop {
    while true {
        break 'outer;
    }
}
}

break 表达式仅允许在循环体中使用,并具有 breakbreak 'label 或(见下文break EXPRbreak 'label EXPR 形式之一。

标签块表达式

Syntax
LabelBlockExpressionBlockExpression

标签块表达式与块表达式完全一样,除了它们允许在块内使用 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表达式

Syntax
ContinueExpressioncontinue LIFETIME_OR_LABEL?

当遇到 continue 时,关联循环体的当前迭代将立即终止,将控制权返回给循环 头部

while 循环的情况下,头部是控制循环的条件操作数。

for 循环的情况下,头部是控制循环的调用表达式。

break 一样,continue 通常与最内层包围的循环相关联,但 continue 'label 可用于指定受影响的循环。

continue 表达式仅允许在循环体中使用。

break和循环值

当与 loop 关联时,break 表达式可用于通过 break EXPRbreak 'label EXPR 形式从该循环返回一个值,其中 EXPR 是一个结果将从 loop 返回的表达式。 例如:

#![allow(unused)]
fn main() {
let (mut a, mut b) = (1, 1);
let result = loop {
    if b > 10 {
        break b;
    }
    let c = a + b;
    a = b;
    b = c;
};
// 斐波那契数列中第一个大于 10 的数字:
assert_eq!(result, 13);
}

loop 有关联 break 的情况下,它不被认为是发散的,并且 loop 必须具有与每个 break 表达式兼容的类型。 没有表达式的 break 被认为等同于带有表达式 ()break

范围表达式

....= 运算符将根据下表构造 std::ops::Range (或 core::ops::Range) 变体之一的对象:

示例:

#![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 表达式的 语法格式 是一个或多个由 && 分隔的条件操作数序列,后跟一个结果块、任意数量的 else if 条件和块,以及一个可选的尾随 else 块。

条件操作数必须是具有 布尔类型表达式 或条件 let 匹配。

如果所有条件操作数求值结果均为 true,且所有 let 模式都成功匹配其 待匹配值,则执行结果块,并跳过任何后续的 else ifelse 块。

如果任何条件操作数求值为 false 或任何 let 模式未匹配其待匹配值,则跳过结果块,并对任何后续的 else if 条件求值。

如果所有 ifelse if 条件求值结果均为 false ,则执行任何 else 块。

一个 if 表达式求值为所执行块的相同值,如果没有块被执行,则求值为 ()

在一个 if 表达式中,所有情况下的类型必须相同。

#![allow(unused)]
fn main() {
let x = 3;
if x == 4 {
    println!("x is four");
} else if x == 3 {
    println!("x is three");
} else {
    println!("x is something else");
}

// `if` 可以用作表达式。
let y = if 12 * 15 > 150 {
    "Bigger"
} else {
    "Smaller"
};
assert_eq!(y, "Bigger");
}

if let模式

if 条件中的 let 模式允许在模式匹配成功时将新变量绑定到作用域中。

以下示例演示了使用 let 模式进行的绑定:

#![allow(unused)]
fn main() {
let dish = ("Ham", "Eggs");

// 此主体将被跳过,因为模式被反驳了。
if let ("Bacon", b) = dish {
    println!("Bacon is served with {}", b);
} else {
    // 改为对该块求值。
    println!("No bacon will be served");
}

// 此主体将执行。
if let ("Ham", b) = dish {
    println!("Ham is served with {}", b);
}

if let _ = 5 {
    println!("Irrefutable patterns are always true");
}
}

可以使用 | 运算符指定多个模式。这与 match 表达式 中的 | 具有相同的语义:

#![allow(unused)]
fn main() {
enum E {
    X(u8),
    Y(u8),
    Z(u8),
}
let v = E::Y(12);
if let E::X(n) | E::Y(n) = v {
    assert_eq!(n, 12);
}
}

条件链

多个条件操作数可以用 && 分隔。

类似于 && 惰性布尔表达式,每个操作数从左到右求值,直到某个操作数求值为 falselet 匹配失败,此时后续操作数将不再求值。

每个模式的绑定都会被放入作用域中,以便后续的条件操作数和结果块使用。

以下是一个链接多个表达式的示例,混合了 let 绑定和布尔表达式,并且表达式能够引用之前表达式中的模式绑定:

#![allow(unused)]
fn main() {
fn single() {
    let outer_opt = Some(Some(1i32));

    if let Some(inner_opt) = outer_opt
        && let Some(number) = inner_opt
        && number == 1
    {
        println!("Peek a boo");
    }
}
}

在不使用条件链的情况下,上述代码等效于:

#![allow(unused)]
fn main() {
fn nested() {
    let outer_opt = Some(Some(1i32));

    if let Some(inner_opt) = outer_opt {
        if let Some(number) = inner_opt {
            if number == 1 {
                println!("Peek a boo");
            }
        }
    }
}
}

如果任何条件操作数是 let 模式,由于与 let 待匹配值存在歧义和优先级关系,任何条件操作数都不能是 || 惰性布尔运算符表达式。如果需要 || 表达式,可以使用括号。例如:

#![allow(unused)]
fn main() {
let foo = Some(123);
let condition1 = true;
let condition2 = false;
// 此处需要括号。
if let Some(x) = foo && (condition1 || condition2) { /*...*/ }
}

2024 版次差异

在 2024 版次之前,不支持 let 链。也就是说,if 表达式中不允许使用 LetChain 语法。

match表达式

Syntax
MatchExpression
    match Scrutinee {
        InnerAttribute*
        MatchArms?
    }

ScrutineeExpressionexcept StructExpression

MatchArms
    ( MatchArm => ( ExpressionWithoutBlock , | ExpressionWithBlock ,? ) )*
    MatchArm => Expression ,?

MatchArmOuterAttribute* Pattern MatchArmGuard?

MatchArmGuardif Expression

一个 match 表达式 根据模式进行分支。 发生的精确匹配形式取决于 模式

一个 match 表达式有一个 待匹配值 表达式 ,它是要与模式进行比较的值。

待匹配值表达式和模式必须具有相同的类型。

match 的行为根据待匹配值表达式是 位置表达式或值表达式 而有所不同。

如果待匹配值表达式是一个 值表达式,它首先被求值为一个临时位置,并将结果值按顺序与各个臂中的模式进行比较,直到找到匹配项。 第一个具有匹配模式的臂被选为 match 的分支目标,由该模式绑定的任何变量都会赋值给该臂所在块中的局部变量,然后控制流进入该块。

当待匹配值表达式是一个 位置表达式 时,匹配不会分配临时位置; 然而,按值绑定可能会从内存位置进行复制或移动。 在可能的情况下,首选对位置表达式进行匹配,因为这些匹配的生命周期继承自位置表达式的生命周期,而不是被限制在匹配内部。

match 表达式的一个示例:

#![allow(unused)]
fn main() {
let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
    _ => println!("something else"),
}
}

在模式内绑定的变量的作用域仅限于匹配守卫和该臂的表达式。

绑定模式 (移动、复制或引用)取决于模式。

多个匹配模式可以用 | 运算符连接。 每个模式将按从左到右的顺序进行测试,直到找到成功的匹配。

#![allow(unused)]
fn main() {
let x = 9;
let message = match x {
    0 | 1  => "not many",
    2 ..= 9 => "a few",
    _      => "lots"
};

assert_eq!(message, "a few");

// 演示模式匹配顺序。
struct S(i32, i32);

match S(1, 2) {
    S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1),
    _ => panic!(),
}
}

注意

2..=9 是一个 范围模式 ,而不是一个 范围表达式。因此,只有范围模式支持的那些类型的范围才能在匹配臂中使用。

在每个由 | 分隔的模式中的每个绑定必须出现在该臂的所有模式中。

具有相同名称的每个绑定必须具有相同的类型,并具有相同的绑定模式。

匹配臂守卫

匹配臂可以接受 匹配臂守卫 以进一步细化匹配情况的准则。

模式守卫出现在模式之后,由 if 关键字后跟一个 bool 类型的表达式组成。

当模式匹配成功时,将执行模式守卫表达式。 如果表达式求值为真,则模式匹配成功。

否则,将测试下一个模式,包括同一臂中由 | 运算符连接的其他匹配。

#![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);
}

模式守卫可以引用它们所跟随的模式中绑定的变量。

在求值守卫之前,会对变量匹配的待匹配值部分进行共享引用。 在求值守卫期间,访问该变量时将使用此共享引用。

只有当守卫求值为真时,值才会从待匹配值移动或复制到变量中。 这允许在守卫内部使用共享借用,而不会在守卫匹配失败的情况下从待匹配值中移出。

此外,通过在求值守卫时持有共享引用,还可以防止在守卫内部进行修改。

匹配臂上的属性

匹配臂上允许使用外部属性。 在匹配臂上有意义的属性只有 cfglint 检查属性

内部属性 被允许直接放在匹配表达式的左大括号之后,其所在的表达式上下文与 块表达式上的属性 相同。

return表达式

Syntax
ReturnExpressionreturn Expression?

return 表达式由关键字 return 表示。

求值 return 表达式会将其参数移动到当前函数调用的指定输出位置,销毁当前函数的活动帧,并将控制权转移给调用者帧。

一个 return 表达式的示例:

#![allow(unused)]
fn main() {
fn max(a: i32, b: i32) -> i32 {
    if a > b {
        return a;
    }
    return b;
}
}

await表达式

Syntax
AwaitExpressionExpression . await

await 表达式是一种语法结构,用于挂起由 std::future::IntoFuture 的实现提供的计算,直到给定的 future 准备好产生一个值。

await 表达式的 语法格式 是一个具有实现了 IntoFuture 特型的类型的表达式(称为 future 操作数),后跟 词法单元 .,然后是 await 关键字。

await 表达式仅在 异步上下文(如 async fnasync 闭包async)中合法。

更具体地说,一个 await 表达式具有以下效果。

  1. 通过在 future 操作数上调用 IntoFuture::into_future 来创建一个 future。
  2. 将该 future 求值为一个 future tmp
  3. 使用 Pin::new_unchecked 固定 tmp
  4. 然后通过调用 Future::poll 方法并向其传递当前 任务上下文 来轮询此固定的 future;
  5. 如果对 poll 的调用返回 Poll::Pending,则 future 返回 Poll::Pending,并挂起其状态,以便当周围的异步上下文被重新轮询时,执行返回到步骤 3;
  6. 否则对 poll 的调用必须返回 Poll::Ready,在这种情况下,Poll::Ready 变体中包含的值将用作 await 表达式本身的结果。

2018 版次差异

await 表达式仅从 Rust 2018 版次开始提供。

任务上下文

任务上下文是指当异步上下文本身被轮询时提供给当前 异步上下文Context。由于 await 表达式仅在异步上下文中合法,因此必须存在某种可用的任务上下文。

近似脱糖

实际上,await 表达式大致等效于以下非规范性的脱糖:

match operand.into_future() {
    mut pinned => loop {
        let mut pin = unsafe { Pin::new_unchecked(&mut pinned) };
        match Pin::future::poll(Pin::borrow(&mut pin), &mut current_context) {
            Poll::Ready(r) => break r,
            Poll::Pending => yield Poll::Pending,
        }
    }
}

其中 yield 伪代码返回 Poll::Pending,并且在重新调用时从该点恢复执行。变量 current_context 是指从异步环境中获取的上下文。

_表达式

Syntax
UnderscoreExpression_

下划线表达式由符号 _ 表示,用于在解构赋值中表示占位符。

它们只能出现在赋值语句的左侧。

请注意,这与 通配符模式 不同。

_ 表达式的示例:

#![allow(unused)]
fn main() {
let p = (1, 2);
let mut a = 0;
(_, a) = p;

struct Position {
    x: u32,
    y: u32,
}

Position { x: a, y: _ } = Position{ x: 2, y: 3 };

// 未使用的结果,赋值给 `_` 用于声明意图并消除警告
_ = 2 + 2;
// 触发 unused_must_use 警告
// 2 + 2;

// 在 let 绑定中使用通配符模式的等效技术
let _ = 2 + 2;
}

模式

Syntax
Pattern|? PatternNoTopAlt ( | PatternNoTopAlt )*

PatternNoTopAlt
      PatternWithoutRange
    | RangePattern

PatternWithoutRange
      LiteralPattern
    | IdentifierPattern
    | WildcardPattern
    | RestPattern
    | ReferencePattern
    | StructPattern
    | TupleStructPattern
    | TuplePattern
    | GroupedPattern
    | SlicePattern
    | PathPattern
    | MacroInvocation

模式用于根据结构匹配值,并可选地将变量绑定到这些结构内部的值。 它们也用于变量声明以及函数和闭包的参数。

下面示例中的模式做了四件事:

  • 测试 person 是否有名为 car 的字段且填充了内容。
  • 测试该人的 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);
}
}

模式用于:

解构

模式可以用来 解构 结构体枚举元组 。 解构将一个值分解为其组成部分。 使用的 语法格式 与创建此类值时几乎相同。

被匹配 表达式具有 结构体 、 枚举 或 元组 类型的模式中, 通配符模式 (_) 代表 单个 数据字段,而 等等剩余模式 (..) 代表特定变体的 所有 剩余字段。

当解构具有命名(但未编号)字段的数据结构时,允许将 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!("Shouldn't reach here");
} else if let (a, 4) = (3, 4) {    // "(a, 4)" 是可反驳的,且会匹配
    println!("Matched ({}, 4)", a);
}
}

字面量模式

Syntax
LiteralPattern-? LiteralExpression

字面量模式 匹配的值与字面量创建的值完全相同。由于负数不是 字面量 ,模式中的字面量可以前缀一个可选的减号,其作用类似于取反运算符。

警告

C string and raw C string literals are accepted in literal patterns, but &CStr doesn’t implement structural equality (#[derive(Eq, PartialEq)]) and therefore any such match on a &CStr will be rejected with a type error.

字面量模式始终是可反驳的。

示例:

#![allow(unused)]
fn main() {
for i in -2..5 {
    match i {
        -1 => println!("It's minus one"),
        1 => println!("It's a one"),
        2|4 => println!("It's either a two or a four"),
        _ => println!("Matched none of the arms"),
    }
}
}

标识符模式

Syntax
IdentifierPatternref? mut? IDENTIFIER ( @ PatternNoTopAlt )?

标识符模式将它们匹配的值绑定到 值命名空间 中的变量。

标识符在模式中必须是唯一的。

该变量将遮蔽作用域中同名的任何变量。 新绑定的 作用域 取决于使用模式的上下文(例如 let 绑定或 match 臂)。

仅由标识符(可能带有 mut )组成的模式匹配任何值并将其绑定到该标识符。 这是变量声明以及函数和闭包参数中最常用的模式。

#![allow(unused)]
fn main() {
let mut variable = 10;
fn sum(x: i32, y: i32) -> i32 {
   x + y
}
}

要将模式匹配的值绑定到变量,请使用 variable @ subpattern 语法格式。 例如,下面将值 2 绑定到 e (不是整个范围:这里的范围是一个范围子模式)。

#![allow(unused)]
fn main() {
let x = 2;

match x {
    e @ 1 ..= 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}
}

默认情况下,标识符模式根据被匹配值是否实现了 Copy ,将变量绑定到被匹配值的副本或从中移动。

这可以通过使用 ref 关键字更改为绑定到引用,或者使用 ref mut 更改为可变引用。例如:

#![allow(unused)]
fn main() {
let a = Some(10);
match a {
    None => (),
    Some(value) => (),
}

match a {
    None => (),
    Some(ref value) => (),
}
}

在第一个 match 表达式中,值被复制(或移动)。 在第二个 match 中,指向相同内存位置的引用被绑定到变量值。 需要此语法格式是因为在解构子模式中, & 运算符不能应用于值的字段。 例如,以下内容无效:

#![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 不是被匹配的对象。 它的目的完全是为了使匹配的绑定成为引用,而不是潜在地复制或移动匹配的内容。

路径模式 优先于标识符模式。

注意

When a pattern is a single-segment identifier, the grammar is ambiguous whether it means an IdentifierPattern or a PathPattern. This ambiguity can only be resolved after name resolution.

#![allow(unused)]
fn main() {
const EXPECTED_VALUE: u8 = 42;
//    ^^^^^^^^^^^^^^ 此常量在作用域内会影响下面模式的处理方式。
//                   patterns below are treated.

fn check_value(x: u8) -> Result<u8, u8> {
    match x {
        EXPECTED_VALUE => Ok(x),
    //  ^^^^^^^^^^^^^^ 被解析为解析为常量 `42` 的 `PathPattern` 。
    //                 the constant `42`.
        other_value => Err(x),
    //  ^^^^^^^^^^^ 被解析为 `IdentifierPattern` 。
    }
}

// If `EXPECTED_VALUE` were treated as an `IdentifierPattern` above,
// that pattern would always match, making the function always return
// `Ok(_) regardless of the input.
assert_eq!(check_value(42), Ok(42));
assert_eq!(check_value(43), Err(43));
}

如果指定了 refref mut 且标识符遮蔽了常量,则这是一个错误。

如果 @ 子模式是不可反驳的或者没有指定子模式,则标识符模式是不可反驳的。

绑定模式

为了提供更好的易用性,模式以不同的 绑定模式 运行,以便更轻松地将引用绑定到值。 当非引用模式匹配引用值时,它将自动被视为 refref mut 绑定。 示例:

#![allow(unused)]
fn main() {
let x: &Option<i32> = &Some(3);
if let Some(y) = x {
    // y 被转换为 `ref y` 且其类型为 &i32
}
}

非引用模式 包括除绑定、 通配符模式 (_) 、引用类型的 const 模式 以及 引用模式 之外的所有模式。

如果绑定模式没有显式具有 refref mutmut ,那么它使用 默认绑定模式 来确定变量如何绑定。

默认绑定模式以使用移动语义的 “move” 模式开始。

匹配模式时,编译器从模式外部开始并向内工作。

每当使用非引用模式匹配引用时,它会自动解引用该值并更新默认绑定模式。

引用将默认绑定模式设置为 ref

可变引用将模式设置为 ref mut ,除非模式已经是 ref ,在这种情况下它保持为 ref

如果自动解引用的值仍然是引用,则对其进行解引用并重复此过程。

只有当默认绑定模式为 “move” 时,绑定模式才可以显式指定 refref mut 绑定模式,或者使用 mut 指定可变性。例如,这些是不被接受的:

#![allow(unused)]
fn main() {
let [mut x] = &[()]; //~ ERROR
let [ref x] = &[()]; //~ ERROR
let [ref mut x] = &mut [()]; //~ ERROR
}

2024 版次差异

Before the 2024 edition, bindings could explicitly specify a ref or ref mut binding mode even when the default binding mode was not “move”, and they could specify mutability on such bindings with mut. In these editions, specifying mut on a binding set the binding mode to “move” regardless of the current default binding mode.

类似地,引用模式仅在默认绑定模式为 “move” 时出现。例如,这是不被接受的:

#![allow(unused)]
fn main() {
let [&x] = &[&()]; //~ ERROR
}

2024 版次差异

Before the 2024 edition, reference patterns could appear even when the default binding mode was not “move”, and had both the effect of matching against the scrutinee and of causing the default binding mode to be reset to “move”.

移动绑定和引用绑定可以混合在同一个模式中。 这样做会导致绑定到的对象发生部分移动,之后该对象将无法使用。 这仅在类型不可复制时适用。

在下面的示例中, nameperson 中移出。 尝试将 person 作为一个整体使用或使用 person.name 会因为 部分移动 而导致错误。

示例:

#![allow(unused)]
fn main() {
struct Person {
   name: String,
   age: u8,
}
let person = Person{ name: String::from("John"), age: 23 };
// `name` 从 person 中移出且 `age` 被引用
let Person { name, ref age } = person;
}

通配符模式

Syntax
WildcardPattern_

通配符模式 (下划线符号)匹配任何值。 它用于在值无关紧要时忽略它们。

在其他模式内部,它匹配单个数据字段(与匹配剩余字段的 .. 相反)。

与标识符模式不同,它不会复制、移动或借用它匹配的值。

示例:

#![allow(unused)]
fn main() {
let x = 20;
let (a, _) = (10, x);   // x 始终被 _ 匹配
assert_eq!(a, 10);

// 忽略一个函数/闭包参数
let real_part = |a: f64, _: f64| { a };

// 忽略 结构体 中的一个字段
struct RGBA {
   r: f32,
   g: f32,
   b: f32,
   a: f32,
}
let color = RGBA{r: 0.4, g: 0.1, b: 0.9, a: 0.5};
let RGBA{r: red, g: green, b: blue, a: _} = color;
assert_eq!(color.r, red);
assert_eq!(color.g, green);
assert_eq!(color.b, blue);

// 接受任何 Some,无论其值为何
let x = Some(10);
if let Some(_) = x {}
}

通配符模式始终是不可反驳的。

剩余模式

Syntax
RestPattern..

剩余模式.. 词法单元)充当变长模式,匹配前后尚未匹配的零个或多个元素。

它只能用于 元组元组结构体切片 模式,并且只能作为这些模式中的元素之一出现一次。 仅对于 切片模式 ,它也允许在 标识符模式 中使用。

剩余模式始终是不可反驳的。

示例:

#![allow(unused)]
fn main() {
let words = vec!["a", "b", "c"];
let slice = &words[..];
match slice {
    [] => println!("slice is empty"),
    [one] => println!("single element {}", one),
    [head, tail @ ..] => println!("head={} tail={:?}", head, tail),
}

match slice {
    // 忽略除最后一个元素(必须是 "!")之外的所有内容。
    [.., "!"] => println!("!!!"),

    // `start` 是除最后一个元素(必须是 "z")之外的所有内容的切片。
    [start @ .., "z"] => println!("starts with: {:?}", start),

    // `end` 是除第一个元素(必须是 "a")之外的所有内容的切片。
    ["a", end @ ..] => println!("ends with: {:?}", end),

    // 'whole' 是整个切片且 `last` 是最后一个元素
    whole @ [.., last] => println!("the last element of {:?} is {}", whole, last),

    rest => println!("{:?}", rest),
}

if let [.., penultimate, _] = slice {
    println!("next to last is {}", penultimate);
}

let tuple = (1, 2, 3, 4, 5);
// 剩余模式也可用于元组和元组
// 结构体模式。
match tuple {
    (1, .., y, z) => println!("y={} z={}", y, z),
    (.., 5) => println!("tail must be 5"),
    (..) => println!("matches everything else"),
}
}

范围模式

范围模式 匹配由其边界定义的范围内的标量值。 它们包含一个 符号....= )和一侧或两侧的边界。

符号左侧的边界称为 下边界 。 右侧的边界称为 上边界

排他性范围模式 匹配从下边界到(但不包括)上边界的所有值。 它写为下边界,后跟 .. ,再后跟上边界。

例如,模式 'm'..'p' 将仅匹配 'm''n''o' ,特别 包括 'p'

包含性范围模式 匹配从下边界到并包括上边界的所有值。 它写为下边界,后跟 ..= ,再后跟上边界。

例如,模式 'm'..='p' 将仅匹配值 'm''n''o''p'

起点范围模式 匹配大于或等于下边界的所有值。 它写为下边界,后跟 ..

例如, 1.. 将匹配任何大于或等于 1 的整数,如 1、9 或 9001,或者 9007199254740991(如果它具有适当的大小),但不匹配 0,对于有符号整数也不匹配负数。

终点排他性范围模式 匹配所有小于上边界的值。 它写为 .. 后跟上边界。

例如, ..10 将匹配任何小于 10 的整数,如 9、1、0,对于有符号整数类型,还匹配所有负值。

终点包含性范围模式 匹配所有小于或等于上边界的值。 它写为 ..= 后跟上边界。

例如, ..=10 将匹配任何小于或等于 10 的整数,如 10、1、0,对于有符号整数类型,还匹配所有负值。

范围模式必须非空;它必须至少跨越其类型可能值集中的一个值。换句话说:

  • a..=b 中,必须满足 a ≤ b。例如,出现范围模式 10..=0 是一个错误,但允许 10..=10
  • a..b 中,必须满足 a < b。例如,出现范围模式 10..010..10 是一个错误。
  • ..b 中,b 必须不是其类型的最小值。例如,出现范围模式 ..-128i8..f64::NEG_INFINITY 是一个错误。

边界写为以下之一:

  • 字符、字节、整数或浮点数字面量。
  • 一个 - 后跟整数或浮点数字面量。
  • 一个 路径

注意

We syntactically accept more than this for a RangePatternBound. We later reject the other things semantically.

如果边界写为路径,则在宏解析后,该路径必须解析为 char 类型、整数类型或浮点类型的常量项。

范围模式匹配其上边界和下边界的类型,这两者必须是同一类型。

如果边界是一个 路径 ,则边界匹配该类型,并具有路径解析到的 常量 的值。

如果边界是一个字面量,则边界匹配该类型,并具有相应 字面量表达式 的值。

如果边界是一个前面带有 - 的字面量,则边界匹配与相应 字面量表达式 相同的类型,并具有对相应字面量表达式的值进行 取反 后的值。

对于浮点范围模式,常量不能是 NaN

示例:

#![allow(unused)]
fn main() {
let c = 'f';
let valid_variable = match c {
    'a'..='z' => true,
    'A'..='Z' => true,
    'α'..='ω' => true,
    _ => false,
};

let ph = 10;
println!("{}", match ph {
    0..7 => "acid",
    7 => "neutral",
    8..=14 => "base",
    _ => unreachable!(),
});

let uint: u32 = 5;
match uint {
    0 => "zero!",
    1.. => "positive number!",
};

// 使用指向常量的路径:
const TROPOSPHERE_MIN : u8 = 6;
const TROPOSPHERE_MAX : u8 = 20;

const STRATOSPHERE_MIN : u8 = TROPOSPHERE_MAX + 1;
const STRATOSPHERE_MAX : u8 = 50;

const MESOSPHERE_MIN : u8 = STRATOSPHERE_MAX + 1;
const MESOSPHERE_MAX : u8 = 85;

let altitude = 70;

println!("{}", match altitude {
    TROPOSPHERE_MIN..=TROPOSPHERE_MAX => "troposphere",
    STRATOSPHERE_MIN..=STRATOSPHERE_MAX => "stratosphere",
    MESOSPHERE_MIN..=MESOSPHERE_MAX => "mesosphere",
    _ => "outer space, maybe",
});

pub mod binary {
    pub const MEGA : u64 = 1024*1024;
    pub const GIGA : u64 = 1024*1024*1024;
}
let n_items = 20_832_425;
let bytes_per_item = 12;
if let size @ binary::MEGA..=binary::GIGA = n_items * bytes_per_item {
    println!("It fits and occupies {} bytes", size);
}

trait MaxValue {
    const MAX: u64;
}
impl MaxValue for u8 {
    const MAX: u64 = (1 << 8) - 1;
}
impl MaxValue for u16 {
    const MAX: u64 = (1 << 16) - 1;
}
impl MaxValue for u32 {
    const MAX: u64 = (1 << 32) - 1;
}
// 使用限定路径:
println!("{}", match 0xfacade {
    0 ..= <u8 as MaxValue>::MAX => "fits in a u8",
    0 ..= <u16 as MaxValue>::MAX => "fits in a u16",
    0 ..= <u32 as MaxValue>::MAX => "fits in a u32",
    _ => "too big",
});
}

当固定宽度整数和 char 类型的范围模式跨越一个类型的所有可能值集时,它们是不可反驳的。 例如, 0u8..=255u8 是不可反驳的。

整数类型的值范围是从其最小值到最大值的闭区间。

char 类型的值范围恰好是那些包含所有 Unicode 标量值的范围: '\u{0000}'..='\u{D7FF}''\u{E000}'..='\u{10FFFF}'

RangeFromPattern 不能用作 切片模式 中子模式的顶层模式。 例如,模式 [1.., _] 不是一个有效的模式。

2021 版次差异

Before the 2021 edition, range patterns with both a lower and upper bound may also be written using ... in place of ..=, with the same meaning.

引用模式

Syntax
ReferencePattern → ( & | && ) mut? PatternWithoutRange

引用模式对正在匹配的指针进行解引用,从而借用它们。

例如,在 x: &i32 上的这两个 match 是等价的:

#![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 关键字会解引用一个可变引用。可变性必须与引用的可变性匹配。

引用模式始终是不可反驳的。

结构体模式

Syntax
StructPattern
    PathInExpression {
        StructPatternElements?
    }

StructPatternElements
      StructPatternFields ( , | , StructPatternEtCetera )?
    | StructPatternEtCetera

StructPatternFields
    StructPatternField ( , StructPatternField )*

StructPatternField
    OuterAttribute*
    (
        TUPLE_INDEX : Pattern
      | IDENTIFIER : Pattern
      | ref? mut? IDENTIFIER
    )

StructPatternEtCetera..

结构体模式匹配 结构体 、 枚举 和 联合体 值,这些值需满足其子模式定义的所有准则。 它们也用于 解构 一个 结构体 、 枚举 或 联合体 值。

在 结构体 模式中,字段通过名称、索引(在元组结构体的情况下)引用,或者使用 .. 忽略:

#![allow(unused)]
fn main() {
struct Point {
    x: u32,
    y: u32,
}
let s = Point {x: 1, y: 1};

match s {
    Point {x: 10, y: 20} => (),
    Point {y: 10, x: 20} => (),    // 顺序无关紧要
    Point {x: 10, ..} => (),
    Point {..} => (),
}

struct PointTuple (
    u32,
    u32,
);
let t = PointTuple(1, 2);

match t {
    PointTuple {0: 10, 1: 20} => (),
    PointTuple {1: 10, 0: 20} => (),   // 顺序无关紧要
    PointTuple {0: 10, ..} => (),
    PointTuple {..} => (),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
}
let m = Message::Quit;

match m {
    Message::Quit => (),
    Message::Move {x: 10, y: 20} => (),
    Message::Move {..} => (),
}
}

如果不使用 .. ,则用于匹配 结构体 的 结构体 模式需要指定所有字段:

#![allow(unused)]
fn main() {
struct Struct {
   a: i32,
   b: char,
   c: bool,
}
let mut struct_value = Struct{a: 10, b: 'X', c: false};

match struct_value {
    Struct{a: 10, b: 'X', c: false} => (),
    Struct{a: 10, b: 'X', ref c} => (),
    Struct{a: 10, b: 'X', ref mut c} => (),
    Struct{a: 10, b: 'X', c: _} => (),
    Struct{a: _, b: _, c: _} => (),
}
}

用于匹配 联合体 的 结构体 模式必须恰好指定一个字段(参见 在联合体上进行模式匹配 )。

IDENTIFIER 语法格式匹配任何值,并将其绑定到与给定字段同名的变量。它是 fieldname: fieldname 的简写。 refmut 限定符可以包含在内,其行为如 patterns.ident.ref 中所述。

#![allow(unused)]
fn main() {
struct Struct {
   a: i32,
   b: char,
   c: bool,
}
let struct_value = Struct{a: 10, b: 'X', c: false};

let Struct { a, b, c } = struct_value;
}

如果 PathInExpression 解析为具有多个变体的枚举构造函数,或者其子模式之一是可反驳的,则 结构体 模式是可反驳的。

结构体 模式根据从 类型命名空间 中的 PathInExpression 解析出的 结构体 、 联合体 或枚举变体进行匹配。有关更多详细信息,请参见 patterns.tuple-struct.namespace

元组结构体模式

元组结构体模式匹配元组结构体和枚举值,这些值需满足其子模式定义的所有准则。 它们也用于 解构 元组结构体或枚举值。

如果 PathInExpression 解析为具有多个变体的枚举构造函数,或者其子模式之一是可反驳的,则元组结构体模式是可反驳的。

元组结构体模式根据从 值命名空间 中的 PathInExpression 解析出的元组结构体或 类元组枚举变体 进行匹配。

注意

Conversely, a struct pattern for a tuple struct or tuple-like enum variant, e.g. S { 0: _ }, matches against the tuple struct or variant whose constructor is resolved in the type namespace.

enum E1 { V(u16) }
enum E2 { V(u32) }

// 只从类型命名空间导入 `E1::V`。
mod _0 {
    const V: () = (); // 用于命名空间掩蔽。
    pub(super) use super::E1::*;
}
use _0::*;

// 只从值命名空间导入 `E2::V`。
mod _1 {
    struct V {} // 用于命名空间掩蔽。
    pub(super) use super::E2::*;
}
use _1::*;

fn f() {
    // 此结构体模式根据在类型命名空间中
    // 找到其构造函数的类元组枚举变体
    // 进行匹配。
    let V { 0: ..=u16::MAX } = (loop {}) else { loop {} };
    // 此元组结构体模式根据在值命名空间中
    // 找到其构造函数的类元组枚举变体
    // 进行匹配。
    let V(..=u32::MAX) = (loop {}) else { loop {} };
}
// 在函数中使用 super 的奇怪行为所必需的。
fn main() {}

The Lang team has made certain decisions, such as in PR #138458, that raise questions about the desirability of using the value namespace in this way for patterns, as described in PR #140593. It might be prudent to not intentionally rely on this nuance in your code.

元组模式

Syntax
TuplePattern( TuplePatternItems? )

TuplePatternItems
      Pattern ,
    | RestPattern
    | Pattern ( , Pattern )+ ,?

元组模式匹配满足其子模式定义的所有准则的元组值。 它们也用于 解构 元组。

带有单个 RestPattern 的形式 (..) 是一种特殊形式,不需要逗号,并且匹配任何大小的元组。

当元组模式的一个子模式是可反驳的时,该元组模式是可反驳的。

使用元组模式的示例:

#![allow(unused)]
fn main() {
let pair = (10, "ten");
let (a, b) = pair;

assert_eq!(a, 10);
assert_eq!(b, "ten");
}

分组模式

Syntax
GroupedPattern( Pattern )

将模式括在括号中可用于显式控制复合模式的优先级。 例如,紧邻范围模式的引用模式(如 &0..=5 )具有歧义且不被允许,但可以用括号表示。

#![allow(unused)]
fn main() {
let int_reference = &3;
match int_reference {
    &(0..=5) => (),
    _ => (),
}
}

切片模式

Syntax
SlicePattern[ SlicePatternItems? ]

SlicePatternItemsPattern ( , Pattern )* ,?

切片模式既可以匹配固定大小的数组,也可以匹配动态大小的切片。

#![allow(unused)]
fn main() {
// 固定大小
let arr = [1, 2, 3];
match arr {
    [1, _, _] => "starts with one",
    [a, b, c] => "starts with something else",
};
}
#![allow(unused)]
fn main() {
// 动态大小
let v = vec![1, 2, 3];
match v[..] {
    [a, b] => { /* 此臂不适用,因为长度不匹配 */ }
    [a, b, c] => { /* 此臂将适用 */ }
    _ => { /* 此通配符是必需的,因为长度在静态时是未知的 */ }
};
}

匹配数组时,只要每个元素都是不可反驳的,切片模式就是不可反驳的。

匹配切片时,只有在具有单个 .. 剩余模式 或以 .. 剩余模式作为子模式的 标识符模式 的形式下,它才是不可反驳的。

在切片内,没有下边界和上边界的范围模式必须括在括号中(如 (a..) ),以澄清其目的是匹配单个切片元素。 具有下边界和上边界的范围模式(如 a..=b )不需要括在括号中。

路径模式

Syntax
PathPatternPathExpression

路径模式 是指常量值、或者没有字段的 结构体 或枚举变体的模式。

未限定路径模式可以引用:

  • 枚举变体
  • 结构体
  • 常量
  • 关联常量

限定路径模式只能引用关联常量。

当路径模式引用 结构体 或只有一个变体的枚举变体,或者引用其类型不可反驳的常量时,它们是不可反驳的。 当它们引用可反驳的常量或具有多个变体的枚举变体时,它们是可反驳的。

常量模式

T 类型的常量 C 用作模式时,我们首先检查 T: PartialEq

此外,我们要求 C 的值 具有(递归)结构相等性 ,其递归定义如下:

  • 整数以及 strboolchar 值始终具有结构相等性。
  • 如果元组、数组和切片的所有字段/元素都具有结构相等性,则它们具有结构相等性。 (特别是, ()[] 始终具有结构相等性。)
  • 如果引用指向的值具有结构相等性,则该引用具有结构相等性。
  • 如果 结构体 或 枚举 类型的值其 PartialEq 实例是通过 #[derive(PartialEq)] 派生的,并且所有字段(对于枚举:活跃变体的字段)都具有结构相等性,则该值具有结构相等性。
  • 如果原始指针被定义为常量整数(然后进行转换/变形),则它具有结构相等性。
  • 如果浮点值不是 NaN ,则它具有结构相等性。
  • 其他任何东西都不具有结构相等性。

特别地, C 的值必须在模式构建时(即预单态化阶段)已知。 这意味着涉及 泛型 参数的关联常量不能用作模式。

C 的值不能包含对可变静态变量( static mut 项 或内部可变 static 项)或 extern 静态变量的任何引用。

在确保满足所有条件后,常量值将被转换为模式,其行为现在完全就像直接编写了该模式一样。 特别是,它完全参与穷尽性检查。 (对于原始指针,常量是编写此类模式的唯一方法。对于这些类型,只有 _ 被认为是穷尽的。)

或模式

或模式 是匹配两个或多个子模式之一的模式(例如 A | B | C )。 它们可以任意嵌套。 从 语法格式 上讲,或模式允许出现在允许其他模式出现的任何地方(由 Pattern 产生式表示),但 let 绑定以及函数和闭包参数(由 PatternNoTopAlt 产生式表示)除外。

静态语义

  1. 给定某种深度的模式 p | q (对于任意模式 pq ),如果满足以下条件,则认为该模式是格式不良的:

    • p 推断出的类型与为 q 推断出的类型不统一,或者
    • pq 中没有引入相同的绑定集,或者
    • pq 中具有相同名称的任何两个绑定的类型在类型或绑定模式方面不统一。

    在上述所有实例中,类型的统一都是精确的,且隐式 类型强制转换 不适用。

  1. 在对表达式 match e_s { a_1 => e_1, ... a_n => e_n } 进行类型检查时,对于包含 p_i | q_i 形式模式的每个 match 臂 a_i ,如果在其存在的深度 d 处, e_s 在深度 d 处的片段类型与 p_i | q_i 不统一,则模式 p_i | q_i 被认为是格式不良的。
  1. 关于穷尽性检查,模式 p | q 被视为同时覆盖了 pq 。 对于某些构造函数 c(x, ..) ,分配律适用,使得 c(p | q, ..rest) 覆盖的值集与 c(p, ..rest) | c(q, ..rest) 覆盖的值集相同。 这可以递归应用,直到除了存在于顶层的模式之外,不再有 p | q 形式的嵌套模式。

    请注意,这里的 “构造函数” 指的不是元组结构体模式,而是指任何乘积类型的模式。 这包括枚举变体、元组结构体、具有命名字段的 结构体 、数组、元组和切片。

动态语义

  1. 在深度 d 处,将 被匹配 表达式 e_s 与模式 c(p | q, ..rest) 进行模式匹配的动态语义(其中 c 是某种构造函数, pq 是任意模式, restc 中可选的任何剩余潜在因子)被定义为与 c(p, ..rest) | c(q, ..rest) 的动态语义相同。

与其他无定界模式的优先级

正如本章其他地方所示,有几种类型的模式在 语法格式 上是无定界的,包括标识符模式、引用模式和或模式。 或模式始终具有最低优先级。 这允许我们为将来可能出现的类型注记(type ascription)特性预留 语法格式 空间,并减少歧义。 例如, x @ A(..) | B(..) 将导致错误,即 x 未在所有模式中绑定。 &A(x) | B(x) 将导致不同子模式中 x 之间的类型不匹配。


  1. ObsoleteRangePattern 语法格式 在 2021 版次中已被移除。

类型系统

类型

Rust 程序中的每个变量、 项 和值都有一个类型。 类型 定义了对持有它的内存的解释以及可以对该值执行的操作。

内置类型紧密地集成到语言中,其方式非常复杂,无法在用户定义类型中模拟。

用户定义类型的能力有限。

类型列表如下:

类型表达式

Syntax
Type
      TypeNoBounds
    | ImplTraitType
    | TraitObjectType

TypeNoBounds
      ParenthesizedType
    | ImplTraitTypeOneBound
    | TraitObjectTypeOneBound
    | TypePath
    | TupleType
    | NeverType
    | RawPointerType
    | ReferenceType
    | ArrayType
    | SliceType
    | InferredType
    | QualifiedPathInType
    | BareFunctionType
    | MacroInvocation

上面 类型 语法格式规则中定义的 类型表达式 是引用类型的语法格式。它可以引用:

  • 用于消除歧义的 括号
  • 展开为类型表达式的

括号类型

Syntax
ParenthesizedType( Type )

在某些情况下,类型的组合可能会产生歧义。在类型周围使用括号以避免歧义。例如, 引用类型 中用于 特型边界+ 运算符不清楚边界应用于何处,因此需要使用括号。需要这种消除歧义的语法格式规则使用 TypeNoBounds 规则而不是 类型

#![allow(unused)]
fn main() {
use std::any::Any;
type T<'a> = &'a (dyn Any + Send);
}

递归类型

标称类型 —— 结构体枚举联合体 —— 可能是递归的。也就是说,每个 enum 变体或 structunion 字段可以直接或间接地引用包围它的 enumstruct 类型本身。

这种递归有限制:

  • 递归类型必须在递归中包含一个标称类型(不只是 类型别名 ,或者其他结构化类型,如 数组元组 )。因此 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 * 。

该类型的值可以使用 字面量表达式 创建,使用对应于同名值的关键字 truefalse

该类型是 语言预导入 的一部分, 名称bool

布尔类型的对象其 大小和对齐 均为 1 。

值 false 的位模式为 0x00 ,值 true 的位模式为 0x01 。布尔类型的对象具有任何其他位模式都是 未定义行为

布尔类型是各种 表达式 中许多操作数的类型:

注意

布尔类型的作用类似于但不是 枚举类型 。在实践中,这主要意味着构造函数不与类型相关联 (例如 bool::true) 。

与所有原始类型一样,布尔类型 实现特型 CloneCopySizedSendSync

注意

有关库操作,请参阅 标准库文档

布尔值的运算

当对布尔类型的操作数使用某些运算符表达式时,它们按照 布尔逻辑 的规则进行求值。

逻辑非

b!b
truefalse
falsetrue

逻辑或

aba | b
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

逻辑与

aba & b
truetruetrue
true`false“false
falsetruefalse
falsefalsefalse

逻辑异或

aba ^ b
truetruefalse
truefalsetrue
falsetruetrue
falsefalsefalse

比较

aba == b
truetruetrue
truefalsefalse
falsetruefalse
falsefalsetrue
aba > b
truetruefalse
truefalsetrue
falsetruefalse
falsefalsefalse
  • a != b!(a == b) 相同
  • a >= ba == b | a > b 相同
  • a < b!(a >= b) 相同
  • a <= ba == b | a < b 相同

位有效性

bool 的单个字节保证被初始化 (换句话说, transmute::<bool, u8>(...) 总是健全的 – 但由于某些位模式是无效的 bool ,其反向操作并不总是健全的) 。

数值类型

整数类型

无符号整数类型包括:

类型最小值最大值
u8028-1
u160216-1
u320232-1
u640264-1
u12802128-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” 浮点类型分别是 f32f64

平台相关整数类型

usize 类型是一个无符号整数类型,其位数与平台的指针类型相同。它可以表示进程中的每个内存地址。

注意

虽然 usize 可以表示每个 * 地址 * ,但将 * 指针 * 转换为 usize 并不一定是可逆的操作。 有关更多信息,请参阅 类型转换表达式std::ptr 以及特别是 来源 的文档。

isize 类型是一个有符号补码整数类型,其位数与平台的指针类型相同。对象和数组大小的理论上限是最大 isize 值。这确保了 isize 可用于计算对象或数组中指针之间的差值,并且可以寻址对象内的每个字节以及末尾之后的一个字节。

usizeisize 至少为 16 位宽。

注意

许多 Rust 代码可能会假设指针、 usizeisize 要么是 32 位要么是 64 位。因此,对 16 位指针的支持是有限的,并且可能需要库的显式关注和确认才能支持。

位有效性

对于每个数值类型 TT 的位有效性等同于 [u8; size_of::<T>()] 的位有效性。未初始化的字节不是有效的 u8

文本类型

charstr 类型保存文本数据。

char 类型的值是一个 Unicode 标量值 (即不是代理对(surrogate)的码位),表示为 0x0000 到 0xD7FF 或 0xE000 到 0x10FFFF 范围内的 32 位无符号字。

创建一个超出此范围的 char 会立即导致 未定义行为[char] 实际上是一个长度为 1 的 UCS-4 / UTF-32 字符串。

str 类型的值的表示方式与 [u8] 相同,即 8 位无符号字节的切片。然而,Rust 标准库对 str 做了额外的假设:在 str 上操作的方法假定并确保其中的数据是有效的 UTF-8。使用非 UTF-8 缓冲区调用 str 方法可能会在现在或将来导致 未定义行为

由于 str动态大小类型 ,它只能通过指针类型实例化,例如 &str&str 的布局与 &[u8] 的布局相同。

布局和位有效性

char 在所有平台上保证具有与 u32 相同的大小和对齐。

char 的每个字节都保证被初始化(换句话说, transmute::<char, [u8; size_of::<char>()]>(...) 总是健全的——但由于某些位模式是无效的 char ,反向操作并不总是健全的)。

Never类型

Syntax
NeverType!

never type ! 是一种没有值的类型,表示永远不会完成的计算结果。

! 类型的表达式可以 隐式类型转换 为任何其他类型。

! 类型目前 只能 出现在函数返回类型中,表示这是一个永远不会返回的发散函数。

#![allow(unused)]
fn main() {
fn foo() -> ! {
    panic!("This call never returns.");
}
}
#![allow(unused)]
fn main() {
unsafe extern "C" {
    pub safe fn no_return_extern_func() -> !;
}
}

元组类型

Syntax
TupleType
      ( )
    | ( ( Type , )+ Type? )

元组类型 是一系列用于存放其它类型的异构列表的 结构化类型 1

元组类型的语法是一个括号括起来的、逗号分隔的类型列表。

一元元组要求在其元素类型后加一个逗号,以便与 括号括起来的类型 区分开来。

元组类型的字段数量等于类型列表的长度。此字段数量决定了元组的 元数 。具有 n 个字段的元组被称为 n 元元组 。例如,具有 2 个字段的元组是 2 元元组。

元组的字段使用与其在类型列表中的位置匹配的递增数字名称命名。第一个字段是 0 。第二个字段是 1 。依此类推。每个字段的类型是元组类型列表中相同位置的类型。

由于便利和历史原因,没有字段的元组类型 ( () ) 通常被称为 unitunit 类型 。它的唯一值也被称为 unitunit 值

一些元组类型的示例:

  • () (unit)
  • (i32,) (一元元组)
  • (f64, f64)
  • (String, i32)
  • (i32, String) (与上一个示例不同的类型)
  • (i32, f64, Vec<String>, Option<bool>)

此类型的值是使用 元组表达式 构造的。此外,如果没有其他有意义的值可以求值,各种表达式都将产生 unit 值。

元组字段可以通过 元组索引表达式模式匹配 来访问。


  1. 如果 结构化类型 的内部类型等效,则它们始终等效。对于元组的标称版本,请参阅 元组结构体

数组类型

Syntax
ArrayType[ Type ; Expression ]

数组是 T 类型的 N 个元素的固定大小序列。数组类型写为 [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> 标准库类型提供了一种堆分配的可调大小数组类型。

切片类型

Syntax
SliceType[ Type ]

切片是一种 动态大小类型 ,表示一个 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[..];
}

切片的所有元素总是被初始化的,并且在安全方法和运算符中访问切片总是会进行边界检查。

结构体类型

一个 结构体 类型 是其他类型的异构乘积,被称为该类型的 字段1

结构体 的新实例可以通过 结构体表达式 构造。

默认情况下, 结构体 的内存布局是未定义的,以便允许编译器进行诸如字段重排之类的优化,但它可以通过 repr 属性 固定。在任何一种情况下,字段都可以在相应的 结构体 表达式 中以任何顺序给出;生成的 结构体 值将始终具有相同的内存布局。

结构体 的字段可以用 可见性修饰符 限定,以允许在模块外部访问 结构体 中的数据。

一个 元组结构体 类型就像 结构体 类型一样,除了字段是匿名的。

一个 类单元结构体 类型就像 结构体 类型一样,除了它没有字段。通过关联的 结构体表达式 构造的一个值是这种类型中唯一的值。


  1. 结构体 类型类似于 C 中的 结构体 类型、ML 系列的 记录 类型或 Lisp 系列的 结构体 类型。

枚举类型

一个 枚举类型 是一种标称的、异构的离散联合类型,由 enum 的名称表示。1

一个 enum 声明了该类型以及若干 变体 ,每个变体都有独立的名称,并且具有 结构体 、元组结构体或类单元结构体的 语法格式 。

enum 的新实例可以通过 结构体表达式 构造。

任何 enum 值消耗的内存与其对应的 enum 类型中最大的 变体 一样多,外加存储判别式所需的大小。

枚举类型不能作为类型在 结构上 被表示,而必须通过对 enum 的命名引用来表示。


  1. enum 类型类似于 Haskell 中的 data 构造函数声明,或 Limbo 中的 pick ADT

联合体类型

一个 联合体 类型 是一种标称的、异构的类 C 联合体,由 union 的名称表示。

联合体 没有 “active field” 的概念。相反,每次对 联合体 的访问都会将 联合体 内容的一部分转录为被访问字段的类型。

由于转录可能导致意外或未定义的行为,因此从 联合体 字段中读取数据需要 unsafe

联合体 字段类型也被限制为类型的一个子集,以确保它们永远不需要丢弃。有关更多详细信息,请参阅 文档。

默认情况下, union 的内存布局是未定义的(特别是,字段 一定要在偏移量 0 处),但 #[repr(...)] 属性可以用来固定布局。

函数项类型

当引用时,函数 项 ,或者类元组 结构体 或枚举 变体 的构造函数,会产生其 函数项类型 的一个零大小值。

该类型明确标识了函数 —— 它的名称、它的类型参数以及它的早绑定生命周期参数(但不包括它的晚绑定生命周期参数,后者仅在调用函数时分配) —— 因此该值不需要包含实际的函数指针,并且在调用函数时不需要间接寻址。

没有直接引用函数项类型的 语法格式 ,但编译器在错误消息中会将该类型显示为类似于 fn(u32) -> i32 {fn_name} 的内容。

由于函数项类型明确标识了函数,不同函数的 项 类型 —— 不同的 项 ,或具有不同 泛型 的相同 项 —— 是不同的,将它们混合会产生类型错误:

#![allow(unused)]
fn main() {
fn foo<T>() { }
let x = &mut foo::<i32>;
*x = foo::<u32>; //~ 错误:类型不匹配
}

然而,存在从函数项到具有相同签名的 函数指针隐式类型转换,这不仅在直接预期函数指针的情况下使用函数项时触发,而且在具有相同签名的不同函数项类型出现在同一个 ifmatch 的不同分支中时也会触发:

#![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>
};
}

所有函数项都实现 CopyCloneSendSync

FnFnMutFnOnce 都会被实现,除非函数具有以下任何一种情况:

闭包类型

闭包表达式 产生一个具有唯一的、匿名的且无法写出的类型的闭包值。 一个闭包类型大约等同于一个包含捕获值的结构体。 例如,以下闭包:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point { x: i32, y: i32 }
struct Rectangle { left_top: Point, right_bottom: Point }

fn f<F : FnOnce() -> String> (g: F) {
    println!("{}", g());
}

let mut rect = Rectangle {
    left_top: Point { x: 1, y: 1 },
    right_bottom: Point { x: 0, y: 0 }
};

let c = || {
    rect.left_top.x += 1;
    rect.right_bottom.x += 1;
    format!("{:?}", rect.left_top)
};
f(c); // 打印 "Point { x: 2, y: 1 }"。
}

生成一个大致如下的闭包类型:

// 注意:这并非准确的转换方式,仅用于说明。

struct Closure<'a> {
    left_top : &'a mut Point,
    right_bottom_x : &'a mut i32,
}

impl<'a> FnOnce<()> for Closure<'a> {
    type Output = String;
    extern "rust-call" fn call_once(self, args: ()) -> String {
        self.left_top.x += 1;
        *self.right_bottom_x += 1;
        format!("{:?}", self.left_top)
    }
}

使得对 f 的调用就像这样:

f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.right_bottom.x });

捕获模式

一个 捕获模式 决定了环境中的 位置表达式 是如何被借用或移动到闭包中的。 捕获模式有:

  1. 不可变借用 (ImmBorrow) — 位置表达式作为 共享引用 被捕获。
  2. 唯一不可变借用 (UniqueImmBorrow) — 类似于不可变借用,但必须是唯一的,如 下文 所述。
  3. 可变借用 (MutBorrow) — 位置表达式作为 可变引用 被捕获。
  4. 移动 (ByValue) — 位置表达式通过 移动值 到闭包中而被捕获。

来自环境的位置表达式按与捕获值在闭包体内的使用方式相兼容的第一种模式进行捕获。 该模式不受闭包周围代码的影响,例如相关变量或字段的生命周期,或者闭包本身的生命周期。

Copy

实现了 Copy 的值如果被移动到闭包中,将以 ImmBorrow 模式捕获。

#![allow(unused)]
fn main() {
let x = [0; 1024];
let c = || {
    let y = x; // x 通过 ImmBorrow 捕获
};
}

异步输入捕获

异步闭包总是捕获所有输入参数,无论它们是否在闭包体中使用。

捕获精度

一个 捕获路径 是一个序列,起始于环境中的变量,后跟该变量的零个或多个位置投影。

一个 位置投影 是对变量应用的 字段访问元组索引解引用(以及自动解引用)、数组或切片索引 表达式或 模式解构

注意

rustc 中,模式解构会被脱糖为一系列解引用以及字段或元素访问。

闭包借用或移动捕获路径,该路径可能会根据下述规则被截断。

例如:

#![allow(unused)]
fn main() {
struct SomeStruct {
    f1: (i32, i32),
}
let s = SomeStruct { f1: (1, 2) };

let c = || {
    let x = s.f1.1; // s.f1.1 通过 ImmBorrow 捕获
};
c();
}

这里的捕获路径是局部变量 s ,后跟一个字段访问 .f1 ,然后是一个元组索引 .1 。 这个闭包捕获了 s.f1.1 的不可变借用。

共享前缀

在捕获路径及其祖先路径都被闭包捕获的情况下,祖先路径以两者中最高的捕获模式捕获, CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode) ,使用严格弱序:

ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue

注意这可能需要递归应用。

#![allow(unused)]
fn main() {
// 在这个例子中,有三个具有共享祖先的不同捕获路径:
fn move_value<T>(_: T){}
let s = String::from("S");
let t = (s, String::from("T"));
let mut u = (t, String::from("U"));

let c = || {
    println!("{:?}", u); // u 通过 ImmBorrow 捕获
    u.1.truncate(0); // u.0 通过 MutBorrow 捕获
    move_value(u.0.0); // u.0.0 通过 ByValue 捕获
};
c();
}

总的来说,这个闭包将通过 ByValue 捕获 u

最右侧共享引用解引用截断

如果解引用应用于共享引用,捕获路径将在捕获路径中最右侧的解引用处截断。

允许这种截断是因为通过共享引用读取的字段将始终通过共享引用或副本读取。 当额外的精度在借用检查的角度下没有任何益处时,这有助于减小捕获的大小。

之所以是 最右侧 解引用,是为了帮助避免产生比必要生命周期更短的生命周期。 考虑以下示例:

#![allow(unused)]
fn main() {
struct Int(i32);
struct B<'a>(&'a i32);

struct MyStruct<'a> {
   a: &'static Int,
   b: B<'a>,
}

fn foo<'a, 'b>(m: &'a MyStruct<'b>) -> impl FnMut() + 'static {
    let c = || drop(&m.a.0);
    c
}
}

如果要捕获 m ,那么闭包将不再能比 'static 存活得更久,因为 m 被限制在 'a 。相反,它通过 ImmBorrow 捕获 (*(*m).a)

通配符模式绑定

闭包仅捕获需要读取的数据。使用 通配符模式 绑定一个值不会读取该值,因此该位置不会被捕获。

#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。
let x = S;
let c = || {
    let _ = x;  // 不捕获 `x`。
};
let c = || match x {
    _ => (), // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();
}

解构元组、结构体和单 变体 枚举本身不会导致读取或位置被捕获。

注意

来自其他 crate 的带有 #[non_exhaustive] 标记的枚举总是被视为具有多个 变体 。参见 type.closure.capture.precision.discriminants.non_exhaustive

#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。

// 解构元组不会导致读取或捕获。
let x = (S,);
let c = || {
    let (..) = x; // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();

// 解构类单元结构体不会导致读取或捕获。
let x = S;
let c = || {
    let S = x; // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();

// 解构结构体不会导致读取或捕获。
struct W<T>(T);
let x = W(S);
let c = || {
    let W(..) = x; // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();

// 解构单变体枚举不会导致读取或捕获。
enum E<T> { V(T) }
let x = E::V(S);
let c = || {
    let E::V(..) = x; // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();
}

RestPattern (..) 或 StructPatternEtCetera (也是 ..) 匹配的字段不会被读取,且这些字段不会被捕获。

#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。
let x = (S, S);
let c = || {
    let (x0, ..) = x;  // 通过 `ByValue` 捕获 `x.0`。
};
// 只有第一个元组字段被闭包捕获。
x.1; // OK: `x.1` 可以在这里被移动。
c();
}

不支持对数组和切片的部分捕获;即使使用通配符模式匹配、索引或子切片,整个切片或数组也总是会被捕获。

#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。
let mut x = [S, S];
let c = || {
    let [x0, _] = x; // 通过 `ByValue` 捕获全部 `x`。
};
let _ = &mut x[1]; // 错误:借用了已移动的值。
}

与通配符匹配的值仍必须已初始化。

#![allow(unused)]
fn main() {
let x: u8;
let c = || {
    let _ = x; // 错误:绑定 `x` 未初始化。
};
}

为读取判别式而捕获

如果模式匹配读取了判别式,则包含该判别式的位置将通过 ImmBorrow 被捕获。

与具有多于一个 变体 的枚举的 变体 进行匹配会读取判别式,从而通过 ImmBorrow 捕获该位置。

#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。
let mut x = (Some(S), S);
let c = || match x {
    (None, _) => (),
//   ^^^^
// 此模式需要读取判别式,这导致 `x.0` 被 `ImmBorrow` 捕获。
    _ => (),
};
let _ = &mut x.0; // 错误:不能将 `x.0` 借用为可变。
//           ^^^
// 闭包仍然存活,所以 `x.0` 在这里仍然被不可变借用。
c();
}
#![allow(unused)]
fn main() {
struct S; // 一个非 `Copy` 类型。
let x = (Some(S), S);
let c = || match x { // 通过 `ImmBorrow` 捕获 `x.0`。
    (None, _) => (),
    _ => (),
};
// 尽管 `x.0` 因判别式读取而被捕获,但 `x.1` 未被捕获。
x.1; // OK: `x.1` 可以在这里被移动。
c();
}

与单 变体 枚举的唯一 变体 进行匹配不会读取判别式,也不会捕获该位置。

#![allow(unused)]
fn main() {
enum E<T> { V(T) } // 一个单变体枚举。
let x = E::V(());
let c = || {
    let E::V(_) = x; // 不捕获 `x`。
};
x; // OK: `x` 可以在这里被移动。
c();
}

如果 #[non_exhaustive] 应用于在外部 crate 中定义的枚举,则出于决定是否发生读取的目的,该枚举被视为具有多个 变体 ,即使它实际上只有一个 变体 。

即使除被匹配的 变体 外的所有 变体 都是未入驻的,使得模式 不可驳回,如果本来会读取判别式,则判别式仍会被读取。

#![allow(unused)]
fn main() {
enum Empty {}
let mut x = Ok::<_, Empty>(42);
let c = || {
    let Ok(_) = x; // 通过 `ImmBorrow` 捕获 `x`。
};
let _ = &mut x; // 错误:不能将 `x` 借用为可变。
c();
}

捕获与范围模式

范围模式 进行匹配会读取被匹配的位置,即使范围包含了该类型的所有可能值,并会通过 ImmBorrow 捕获该位置。

#![allow(unused)]
fn main() {
let mut x = 0u8;
let c = || {
    let 0..=u8::MAX = x; // 通过 `ImmBorrow` 捕获 `x`。
};
let _ = &mut x; // 错误:不能将 `x` 借用为可变。
c();
}

捕获与切片模式

将切片与除仅包含单个 剩余模式(即 [..] )以外的 切片模式 进行匹配,将被视为对切片长度的读取,并通过 ImmBorrow 捕获切片。

#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // 通过 `ImmBorrow` 捕获 `*x`。
    &mut [] => (),
//       ^^
// 这匹配一个长度恰好为零的切片。为了知道被检查对象是否匹配,
// 必须读取长度,从而导致切片被捕获。
    _ => (),
};
let _ = &mut *x; // 错误:不能将 `*x` 借用为可变。
c();
}
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // 不捕获 `*x`。
    [..] => (),
//   ^^ 剩余模式。
};
let _ = &mut *x; // OK: `*x` 可以在这里被借用。
c();
}

注意

也许令人惊讶的是,尽管长度包含在切片的(宽) 指针 中,但被视为读取并被捕获的是 被指物 (切片)的位置。

#![allow(unused)]
fn main() {
fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l {
    // 闭包存活时间超过 `'l`,因为它捕获了 `**x`。如果
    // 它捕获的是 `*x`,它将存活得不够长,
    // 无法满足 `impl Fn() + 'l` 界限。
    || match *x { // 通过 `ImmBorrow` 捕获 `**x`。
        &[] => (),
        _ => (),
    }
}
}

这样,该行为与在被检查对象中解引用到切片是一致的。

#![allow(unused)]
fn main() {
fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l {
    || match **x { // 通过 `ImmBorrow` 捕获 `**x`。
        [] => (),
        _ => (),
    }
}
}

有关详细信息,请参见 Rust PR #138961

由于数组的长度由其类型固定,因此将数组与切片模式匹配本身不会捕获该位置。

#![allow(unused)]
fn main() {
let x: [u8; 1] = [0];
let c = || match x { // 不捕获 `x`。
    [_] => (), // 长度是固定的。
};
x; // OK: `x` 可以在这里被移动。
c();
}

在移动语境中捕获引用

由于不允许从引用中移出字段, move 闭包将仅捕获捕获路径的前缀,该前缀一直延伸到但不包括引用的第一次解引用。 引用本身将被移动到闭包中。

#![allow(unused)]
fn main() {
struct T(String, String);

let mut t = T(String::from("foo"), String::from("bar"));
let t_mut_ref = &mut t;
let mut c = move || {
    t_mut_ref.0.push_str("123"); // 通过 ByValue 捕获 `t_mut_ref`
};
c();
}

裸指针解引用

由于解引用裸指针是 unsafe 的,闭包将仅捕获捕获路径的前缀,该前缀一直延伸到但不包括裸指针的第一次解引用。

#![allow(unused)]
fn main() {
struct T(String, String);

let t = T(String::from("foo"), String::from("bar"));
let t_ptr = &t as *const T;

let c = || unsafe {
    println!("{}", (*t_ptr).0); // 通过 ImmBorrow 捕获 `t_ptr`
};
c();
}

联合体 字段

由于访问 联合体 字段是 unsafe 的,闭包将仅捕获捕获路径的前缀,该前缀一直延伸到 联合体 本身。

#![allow(unused)]
fn main() {
union U {
    a: (i32, i32),
    b: bool,
}
let u = U { a: (123, 456) };

let c = || {
    let x = unsafe { u.a.0 }; // 通过 ByValue 捕获 `u`
};
c();

// 这也包括写入字段。
let mut u = U { a: (123, 456) };

let mut c = || {
    u.b = true; // 通过 MutBorrow 捕获 `u`
};
c();
}

对未对齐struct的引用

由于创建对结构体中未对齐字段的引用是 未定义行为 , 闭包将仅捕获捕获路径的前缀,该前缀一直延伸到但不包括对使用 packed 表示 的结构体的第一次字段访问。 这包括所有字段,甚至是那些已对齐的字段,以防止将来结构体中的任何字段发生更改时产生兼容性问题。

#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(i32, i32);

let t = T(2, 5);
let c = || {
    let a = t.0; // 通过 ImmBorrow 捕获 `t`
};
// 从 `t` 中复制是可以的。
let (a, b) = (t.0, t.1);
c();
}

类似地,获取未对齐字段的地址也会捕获整个结构体:

#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(String, String);

let mut t = T(String::new(), String::new());
let c = || {
    let a = std::ptr::addr_of!(t.1); // 通过 ImmBorrow 捕获 `t`
};
let a = t.0; // 错误:无法移出 `t.0` ,因为它已被借用
c();
}

但如果它不是 packed 的,上述代码就可以工作,因为它能精确地捕获字段:

#![allow(unused)]
fn main() {
struct T(String, String);

let mut t = T(String::new(), String::new());
let c = || {
    let a = std::ptr::addr_of!(t.1); // 通过 ImmBorrow 捕获 `t.1`
};
// 这里的移动是允许的。
let a = t.0;
c();
}

Box与其他Deref实现

BoxDeref 特型 实现与其他 Deref 实现受到的待遇不同,因为它被视为一个特殊的实体。

例如,让我们看看涉及 RcBox 的例子。 *rc 被脱糖为对 Rc 上定义的特型方法 deref 的调用,但由于 *box 受到不同待遇,因此可以对 Box 的内容进行精确捕获。

具有非move闭包的Box

在非 move 闭包中,如果 Box 的内容没有被移动到闭包体中,则 Box 的内容会被精确捕获。

#![allow(unused)]
fn main() {
struct S(String);

let b = Box::new(S(String::new()));
let c_box = || {
    let x = &(*b).0; // 通过 ImmBorrow 捕获 `(*b).0`
};
c_box();

// 将 `Box` 与另一个实现了 Deref 的类型进行对比:
let r = std::rc::Rc::new(S(String::new()));
let c_rc = || {
    let x = &(*r).0; // 通过 ImmBorrow 捕获 `r`
};
c_rc();
}

然而,如果 Box 的内容被移动到闭包中,那么该 box 会被整体捕获。这样做是为了尽量减少需要移动到闭包中的数据量。

#![allow(unused)]
fn main() {
// 这与上面的例子相同,除了闭包
// 移动值而不是获取其引用。

struct S(String);

let b = Box::new(S(String::new()));
let c_box = || {
    let x = (*b).0; // 通过 ByValue 捕获 `b`
};
c_box();
}

具有move闭包的Box

类似于在非 move 闭包中移动 Box 的内容,在 move 闭包中读取 Box 的内容将整体捕获 Box

#![allow(unused)]
fn main() {
struct S(i32);

let b = Box::new(S(10));
let c_box = move || {
    let x = (*b).0; // 通过 ByValue 捕获 `b`
};
}

捕获中的唯一不可变借用

捕获可以通过一种称为 唯一不可变借用 的特殊借用发生,这种借用在语言的其他地方无法使用,也无法显式写出。它发生在修改可变引用的引用物时,如下例所示:

#![allow(unused)]
fn main() {
let mut b = false;
let x = &mut b;
let mut c = || {
    // x 的一个 ImmBorrow 和一个 MutBorrow。
    let a = &x;
    *x = true; // `x` 通过 UniqueImmBorrow 捕获
};
// 下面这行是一个错误:
// let y = &x;
c();
// 然而,下面这样是可以的。
let z = &x;
}

在这种情况下,由于 x 不是 mut ,所以无法以可变方式借用 x 。但与此同时,不可变地借用 x 会使赋值操作非法,因为 & &mut 引用可能不是唯一的,因此无法安全地用于修改值。所以使用了唯一不可变借用:它不可变地借用 x ,但像可变借用一样,它必须是唯一的。

在上面的示例中,取消对 y 的声明的注释将产生错误,因为它会违反闭包对 x 借用的唯一性; z 的声明是有效的,因为闭包的生命周期在块结束时已过期,释放了借用。

调用特型与隐式类型转换

闭包类型都实现了 FnOnce ,表示它们可以通过消耗闭包的所有权来调用一次。此外,一些闭包还实现了更具体的调用 特型 :

  • 不从任何捕获变量中移出的闭包实现了 FnMut ,表示它可以通过可变引用调用。
  • 不修改或从任何捕获变量中移出的闭包实现了 Fn ,表示它可以通过共享引用调用。

注意

move 闭包仍可能实现 FnFnMut ,即使它们通过移动捕获变量。这是因为由闭包类型实现的 特型 是由闭包对捕获值的操作决定的,而不是由它如何捕获它们决定的。

非捕获闭包 是不从其环境中捕获任何内容的闭包。非异步、非捕获的闭包可以被 隐式类型转换 为具有匹配签名的函数指针(例如, 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);
}

异步闭包特型

异步闭包在是否实现 FnMutFn 方面有进一步的限制。

异步闭包返回的 Future 具有与闭包类似的捕获特性。它根据位置表达式的使用方式从异步闭包中捕获它们。如果异步闭包具有以下任一属性,则称其向其 Future 借出 (lending)

  • Future 包含一个可变捕获。
  • 异步闭包通过值捕获,除非该值是通过解引用投影访问的。

如果异步闭包向其 Future 借出,则 实现 FnMutFnFnOnce 始终会被实现。

示例:可变捕获的第一种情况可以通过以下方式说明:

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {}

fn f() {
    let mut x = 1i32;
    let c = async || {
        x = 2;  // x 通过 MutBorrow 捕获
    };
    takes_callback(c);  // 错误:异步闭包未实现 `FnMut`
}
}

普通值捕获的第二种情况可以通过以下方式说明:

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}

fn f() {
    let x = &1i32;
    let c = async move || {
        let a = x + 2;  // x 通过 ByValue 捕获
    };
    takes_callback(c);  // 错误:异步闭包未实现 `Fn`
}
}

第二种情况的例外可以通过使用解引用来说明,这确实允许实现 FnFnMut

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}

fn f() {
    let x = &1i32;
    let c = async move || {
        let a = *x + 2;
    };
    takes_callback(c);  // OK:实现了 `Fn`
}
}

异步闭包实现 AsyncFnAsyncFnMutAsyncFnOnce 的方式,与普通闭包实现 FnFnMutFnOnce 的方式类似;也就是说,取决于其体中对捕获变量的使用。

其他特型

所有闭包类型都实现 Sized 。此外,如果闭包存储的捕获类型允许,闭包类型还会实现以下 特型 :

SendSync 的规则与普通结构体类型的规则一致,而 CloneCopy 的行为就像是 派生 的。对于 Clone ,捕获值的克隆顺序未指定。

由于捕获通常是通过引用进行的,因此会产生以下一般规则:

  • 如果所有捕获的值都是 Sync ,则闭包是 Sync
  • 如果所有通过非唯一不可变引用捕获的值都是 Sync ,且所有通过唯一不可变或可变引用、复制或移动捕获的值都是 Send ,则闭包是 Send
  • 如果闭包未通过唯一不可变或可变引用捕获任何值,且它通过复制或移动捕获的所有值分别实现了 CloneCopy ,则闭包是 CloneCopy

丢弃顺序

如果闭包通过值捕获复合类型(如结构体、元组和枚举)的一个字段,则该字段的生命周期现在将与闭包绑定。因此,复合类型的互不相交字段可能会在不同时间被丢弃。

#![allow(unused)]
fn main() {
{
    let tuple =
      (String::from("foo"), String::from("bar")); // --+
    { //                                               |
        let c = || { // ----------------------------+  |
            // tuple.0 被捕获到闭包中                |  |
            drop(tuple.0); //                       |  |
        }; //                                       |  |
    } // 'c' 和 'tuple.0' 在这里被丢弃 -------------+  |
} // tuple.1 在这里被丢弃 -----------------------------+
}

2018版次及更早版本

闭包类型差异

在2018版次及更早版本中,闭包总是整体捕获变量,而没有精确的捕获路径。这意味着对于 闭包类型 章节中使用的示例,生成的闭包类型将如下所示:

struct Closure<'a> {
    rect : &'a mut Rectangle,
}

impl<'a> FnOnce<()> for Closure<'a> {
    type Output = String;
    extern "rust-call" fn call_once(self, args: ()) -> String {
        self.rect.left_top.x += 1;
        self.rect.right_bottom.x += 1;
        format!("{:?}", self.rect.left_top)
    }
}

并且对 f 的调用将如下工作:

f(Closure { rect: rect });

捕获精度差异

复合类型(如结构体、元组和枚举)总是被整体捕获,而不是按单个字段捕获。因此,可能需要借用到局部变量中以便捕获单个字段:

#![allow(unused)]
fn main() {
use std::collections::HashSet;

struct SetVec {
    set: HashSet<u32>,
    vec: Vec<u32>
}

impl SetVec {
    fn populate(&mut self) {
        let vec = &mut self.vec;
        self.set.iter().for_each(|&n| {
            vec.push(n);
        })
    }
}
}

相反,如果闭包直接使用 self.vec ,那么它将尝试通过可变引用捕获 self 。但由于 self.set 已经被借用用于迭代,代码将无法编译。

如果使用了 move 关键字,那么所有的捕获都是通过移动进行的,或者对于 Copy 类型通过复制进行,无论借用是否可行。 move 关键字通常用于允许闭包存活时间超过捕获的值,例如闭包被返回或用于派生新线程的情况。

无论数据是否会被闭包读取(即在通配符模式的情况下),如果在闭包内提到了闭包外定义的变量,该变量都将被整体捕获。

丢弃顺序差异

由于复合类型是被整体捕获的,因此通过值捕获其中一种复合类型的闭包将在闭包被丢弃的同时丢弃整个捕获的变量。

#![allow(unused)]
fn main() {
{
    let tuple =
      (String::from("foo"), String::from("bar"));
    {
        let c = || { // --------------------------+
            // tuple 被捕获到闭包中                  |
            drop(tuple.0); //                     |
        }; //                                     |
    } // 'c' 和 'tuple' 在这里被丢弃 --------------+
}
}

指针类型

所有指针都是显式的一等值。 它们可以被移动或复制,存储进数据 结构体 中,并从函数中返回。

引用(&&mut)

Syntax
ReferenceType& Lifetime? mut? TypeNoBounds

共享引用(&)

共享引用指向由其他值拥有的内存。

当创建一个值的共享引用时,它会阻止该值的直接修改。 内部可变性 在某些情况下为此提供了例外。 顾名思义,一个值可以存在任意数量的共享引用。 共享引用类型写为 &type ,或者在需要指定显式生命周期时写为 &'a type

复制引用是一个 “浅层” 操作: 它只涉及复制指针本身,也就是说,指针是 Copy 的。 释放引用对它所指向的值没有影响,但引用一个 临时值 会在引用本身的作用域内使其保持存活。

可变引用(&mut)

可变引用指向由其他值拥有的内存。 可变引用类型写为 &mut type&'a mut type

可变引用(尚未被借用的)是访问其指向的值的唯一方式,因此它不是 Copy 的。

裸指针(*const*mut)

Syntax
RawPointerType* ( mut | const ) TypeNoBounds

裸指针是没有安全或存活性保证的指针。 裸指针写为 *const T*mut T 。 例如 *const i32 表示一个指向 32 位整数的裸指针。

复制或丢弃裸指针对任何其他值的生命周期都没有影响。

解引用裸指针是一个 unsafe 操作

这也可以通过重借用 (&*&mut * ) 来将裸指针转换为引用。 通常不鼓励使用裸指针; 它们的存在是为了支持与外部代码的互操作性,以及编写性能关键型或底层函数。

比较裸指针时,是按它们的地址进行比较,而不是按它们指向的内容进行比较。 在将裸指针与 动态大小类型 进行比较时,它们的附加数据也会被比较。

裸指针可以使用 &raw const 直接创建为 *const 指针,使用 &raw mut 直接创建为 *mut 指针。

智能指针

标准库包含除引用和裸指针之外的其他 “智能指针” 类型。

位有效性

尽管在大多数平台上生成的机器代码中,指针和引用与 usize 相似, 但目前尚未确定将引用或指针类型转换为非指针类型的语义。 因此,将指针或引用类型 P 转换为 [u8; size_of::<P>()] 可能是不合法的。

对于瘦裸指针(即针对 T: SizedP = *const TP = *mut T ), 反向(从整数或整数数组转换为 P )始终是有效的。 然而,通过这种转换产生的指针可能无法被解引用(即使 T 的大小为零也不行)。

函数指针类型

一个函数指针类型,使用 fn 关键字编写,指向一个在编译时身份不一定确定的函数。

一个将 Binop 定义为函数指针类型的示例:

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 {
    x + y
}

let mut x = add(5,7);

type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);
}

函数指针可以通过来自 函数项 以及非捕获、非异步的 闭包隐式类型转换 来创建。

unsafe 限定符表示该类型的值是一个 不安全函数 ,而 extern 限定符表示它是一个 外部函数

要使函数成为变长参数函数,其 extern ABI 必须是 items.extern.variadic.conventions 中列出的之一。

函数指针参数上的属性

函数指针参数上的属性遵循与 常规函数参数 相同的规则和限制。

特型对象

Syntax
TraitObjectTypedyn? TypeParamBounds

TraitObjectTypeOneBounddyn? TraitBound

一个 特型对象 是实现了的一组特型的另一种类型的不透明值。这组特型由一个 dyn 兼容基础特型 加上任意数量的 自动特型 组成。

特型对象实现了基础特型、其自动特型以及基础特型的任何 超特型

特型对象写作关键字 dyn 后跟一组特型界限,但对特型界限有以下限制。

非自动特型不得超过一个,生命周期不得超过一个,且不允许退出界限(例如 ?Sized )。此外,特型路径可以用括号括起来。

例如,给定一个特型 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 版次差异

在 2021 版次 之前, dyn 关键字可以省略。

2018 版次差异

在 2015 版次 中,如果特型对象的第一个界限是以 :: 开头的路径,那么 dyn 将被视为路径的一部分。可以将第一个路径放在括号中来解决这个问题。因此,如果你想要一个具有 ::your_module::Trait 特型的特型对象,你应该写成 dyn (::your_module::Trait)

从 2018 版次 开始, dyn 是一个真正的关键字,不允许出现在路径中,因此括号不是必需的。

如果基础特型互为别名,且自动特型集合相同,生命周期界限也相同,则两个特型对象类型互为别名。例如, dyn Trait + Send + UnwindSafedyn Trait + UnwindSafe + Send 相同。

由于值属于哪种具体类型是不透明的,特型对象是 动态大小类型 。像所有 DST 一样,特型对象在某种类型的指针后面使用;例如 &dyn SomeTraitBox<dyn SomeTrait> 。特型对象指针的每个实例包括:

  • 一个指向实现了 SomeTrait 的类型 T 实例的指针
  • 一个 虚方法表 ,通常简称为 vtable ,其中包含 T 针对 SomeTrait 及其 超特型 的每个方法的实现指针(即函数指针)。

特型对象的目的是允许方法的 “后期绑定” 。在特型对象上调用方法会导致运行时的虚拟分派:即从特型对象 vtable 中加载函数指针并间接调用。每个 vtable 条目的实际实现可以因对象而异。

一个特型对象的例子:

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>);
}

在这个例子中,特型 Printableprint 的类型签名和 main 中的类型转换表达式中都作为特型对象出现。

特型对象生命周期界限

由于特型对象可以包含引用,这些引用的生命周期需要作为特型对象的一部分来表达。该生命周期写为 Trait + 'a 。有一些 默认值 允许通常以合理的选择推断此生命周期。

Impl trait

Syntax
ImplTraitTypeimpl TypeParamBounds

ImplTraitTypeOneBoundimpl TraitBound

impl Trait 提供了指定实现了特定特型的未命名但具体类型的方法。 它可以出现在两类地方:参数位置(在这里它可以作为函数的匿名泛型参数)和返回位置(在这里它可以作为抽象返回类型)。

#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}

// 参数位置:匿名类型参数
fn foo(arg: impl Trait) {
}

// 返回位置:抽象返回类型
fn bar() -> impl Trait {
}
}

匿名类型参数

注意

这通常被称为 “参数位置的 impl Trait” 。(术语 “parameter” 在这里更准确,但 “参数位置的 impl Trait” 是该特性开发期间使用的措辞,并且在实现的某些部分中仍然保留。)

函数可以使用 impl 后跟一组特型界限来声明一个具有匿名类型的参数。 调用者必须提供一个满足匿名类型参数所声明的界限的类型,并且函数只能使用通过匿名类型参数的特型界限可用的方法。

例如,这两种形式几乎是等价的:

#![allow(unused)]
fn main() {
trait Trait {}

// 泛型类型参数
fn with_generic_type<T: Trait>(arg: T) {
}

// 参数位置的 impl Trait
fn with_impl_trait(arg: impl Trait) {
}
}

也就是说,参数位置的 impl Trait 是类似于 <T: Trait> 的泛型类型参数的语法糖,只是该类型是匿名的,并且不会出现在 GenericParams 列表中。

注意

对于函数参数,泛型类型参数和 impl Trait 并不完全等价。对于像 <T: Trait> 这样的泛型参数,调用者可以选择在调用处使用 GenericArgs 显式指定 T 的泛型参数,例如 foo::<usize>(1) 。将参数从其中一种形式更改为另一种形式可能会对函数的调用者构成破坏性变更,因为这改变了泛型参数的数量。

抽象返回类型

注意

这通常被称为 “返回位置的 impl Trait” 。

函数可以使用 impl Trait 来返回一个抽象返回类型。 这些类型代表另一个具体类型,调用者只能使用由指定的 Trait 声明的方法。

函数中每个可能的返回值必须解析为相同的具体类型。

返回位置的 impl Trait 允许函数返回一个非装箱(unboxed)的抽象类型。 这在 闭包 和迭代器中特别有用。 例如,闭包具有唯一的、不可写的类型。 以前,从函数返回闭包的唯一方法是使用 特型对象

#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
}

这可能会因堆分配和动态分派而产生性能损失。 当时无法完全指定闭包的类型,只能使用 Fn 特型。 这意味着特型对象是必需的。 然而,通过 impl Trait ,可以更简单地编写:

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}

这也避免了使用装箱特型对象的缺点。

类似地,迭代器的具体类型可能会变得非常复杂,包含链中所有先前迭代器的类型。 返回 impl Iterator 意味着函数只将其返回类型暴露为 Iterator 特型界限,而不是显式指定涉及的所有其他迭代器类型。

特型和特型实现中的返回位置 impl Trait

特型中的函数也可以使用 impl Trait 作为匿名关联类型的语法。

特型中关联函数的返回类型里的每个 impl Trait 都会被脱糖为一个匿名的关联类型。出现在实现函数的签名中的返回类型用于确定该关联类型的值。

捕获

每个返回位置的 impl Trait 抽象类型背后都有一些隐藏的具体类型。为了让这个具体类型使用泛型参数,该泛型参数必须被抽象类型 捕获

自动捕获

返回位置的 impl Trait 抽象类型会自动捕获所有作用域内的泛型参数,包括泛型类型、常量和生命周期参数(包括高阶参数)。

2024 版次差异

在 2024 版次 之前,在自由函数以及固有实现的关联函数和方法上,未出现在抽象返回类型界限中的泛型生命周期参数不会被自动捕获。

精确捕获

由返回位置 impl Trait 抽象类型捕获的泛型参数集合可以通过 use<..> 界限 进行显式控制。如果存在,则仅捕获 use<..> 界限中列出的泛型参数。例如:

#![allow(unused)]
fn main() {
fn capture<'a, 'b, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
  //                                      ~~~~~~~~~~~~~~~~~~~~~~~
  //                                     仅捕获 `'a` 和 `T` 。
  (x, y)
}
}

目前,界限列表中只能出现一个 use<..> 界限,必须包含所有作用域内的类型和常量泛型参数,并且必须包含出现在抽象类型其他界限中的所有生命周期参数。

use<..> 界限内,任何存在的生命周期参数必须出现在所有类型和常量泛型参数之前,并且如果省略生命周期 ('_) 被允许出现在 impl Trait 返回类型中,则它可以存在。

因为所有作用域内的类型参数都必须按名称包含,所以 use<..> 界限不能用于使用了参数位置 impl Trait 的项的签名中,因为这些项的作用域内有匿名类型参数。

特型定义中关联函数中存在的任何 use<..> 界限必须包含特型的所有泛型参数,包括特型隐式的 Self 泛型类型参数。

泛型和返回位置 impl Trait 之间的区别

在参数位置, impl Trait 在语义上与泛型类型参数非常相似。 然而,两者在返回位置有显著区别。 使用 impl Trait ,与泛型类型参数不同,函数选择返回类型,而调用者不能选择返回类型。

函数:

#![allow(unused)]
fn main() {
trait Trait {}
fn foo<T: Trait>() -> T {
    // ...
panic!()
}
}

允许调用者确定返回类型 T ,并且函数返回该类型。

函数:

#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}
fn foo() -> impl Trait {
    // ...
}
}

不允许调用者确定返回类型。 相反,函数选择返回类型,但只承诺它将实现 Trait

限制

impl Trait 只能作为非 extern 函数的参数或返回类型出现。 它不能作为 let 绑定的类型、字段类型,或出现在类型别名中。

类型参数

在具有类型参数声明的 项 的主体内,其类型参数的名称就是类型:

#![allow(unused)]
fn main() {
fn to_vec<A: Clone>(xs: &[A]) -> Vec<A> {
    if xs.is_empty() {
        return vec![];
    }
    let first: A = xs[0].clone();
    let mut rest: Vec<A> = to_vec(&xs[1..]);
    rest.insert(0, first);
    rest
}
}

这里, first 的类型为 A ,引用了 to_vecA 类型参数;而 rest 的类型为 Vec<A> ,这是一个元素类型为 A 的向量。

推断类型

Syntax
InferredType_

推断类型 要求 编译器 根据可用的周边信息尽可能地推断类型。

例子

推断类型 常用于 泛型 参数:

#![allow(unused)]
fn main() {
let x: Vec<_> = (0..10).collect();
}

推断类型 不能用于 项 签名。

动态大小类型

大多数类型在编译时具有固定的大小,并实现了 Sized 特型。大小仅在运行时已知的类型被称为 动态大小类型 (DST) ,或者非正式地称为不定长类型。 切片特型对象strDST 的示例。

这类类型只能在某些情况下使用:

  • 指向 DST指针类型 是定长的,但其大小是定长类型指针的两倍
    • 指向切片和 str 的指针还存储了元素的数量。
    • 指向特型对象的指针还存储了一个指向 vtable 的指针。
  • DST 可以作为具有特殊 ?Sized 边界的泛型类型参数的类型参数提供。当相应的关联类型声明具有 ?Sized 边界时,它们也可以用于关联类型定义。默认情况下,任何类型参数或关联类型都具有 Sized 边界,除非使用 ?Sized 将其放宽。
  • 可以为 DST 实现特型。与泛型类型参数不同,在特型定义中 Self: ?Sized 是默认的。
  • 结构体可以将 DST 作为其最后一个字段;这使得结构体本身也成为一个 DST

注意

变量、 函数参数、 常量 项 和 静态 项 必须是 Sized 的。

类型布局

类型的布局是其大小、对齐方式以及其字段的相对偏移量。对于枚举,判别式如何布局和解释也是类型布局的一部分。

类型布局可能会在每次编译时发生变化。我们不打算准确记录具体的操作,而只记录目前所保证的内容。

请注意,即使布局相同的类型在跨函数边界传递的方式上仍可能有所不同。关于类型的函数调用 ABI 兼容性,请参阅 此处

大小和对齐

所有值都有对齐方式和大小。

值的 对齐方式 指明了存储该值时哪些地址是有效的。对齐方式为 n 的值必须存储在 n 的倍数的地址上。例如,对齐方式为 2 的值必须存储在偶数地址上,而对齐方式为 1 的值可以存储在任何地址上。对齐方式以字节为单位,且必须至少为 1,并且始终是 2 的幂。值的对齐方式可以通过 align_of_val 函数检查。

值的 大小 是具有该 项 类型的数组中连续元素之间的字节偏移量,包括对齐填充。值的大小始终是其对齐方式的倍数。请注意,某些类型是零大小的;0 被认为是任何对齐方式的倍数(例如,在某些平台上,类型 [u16; 0] 的大小为 0,对齐方式为 2)。值的大小可以通过 size_of_val 函数检查。

如果某种类型的所有值都具有相同的大小和对齐方式,且两者在编译时都是已知的,则该类型实现了 Sized 特型,并且可以通过 size_ofalign_of 函数进行检查。不是 Sized 的类型被称为 动态大小类型 。由于 Sized 类型的所有值都共享相同的大小和对齐方式,因此我们分别将这些共享值称为类型的大小和类型的对齐方式。

原语数据布局

大多数原语的大小如下表所示。

类型size_of::<Type>()
bool1
u8 / i81
u16 / i162
u32 / i324
u64 / i648
u128 / i12816
usize / isize见下文
f324
f648
char4

usizeisize 的大小足以包含目标平台上的每个地址。例如,在 32 位目标上,这是 4 字节,而在 64 位目标上,这是 8 字节。

原语的对齐方式取决于平台。 在大多数情况下,它们的对齐方式等于它们的大小,但可能会更小。 特别地,i128u128 通常对齐到 4 或 8 字节,尽管它们的大小是 16;在许多 32 位平台上,i64u64f64 仅对齐到 4 字节,而不是 8 字节。

指针和引用布局

指针和引用具有相同的布局。指针或引用的可变性不会改变布局。

指向定长类型的指针具有与 usize 相同的大小和对齐方式。

指向不定长类型的指针是定长的。其大小和对齐方式保证至少等于指针的大小和对齐方式。

注意

尽管你不应该依赖这一点,但目前所有指向 DST 的指针的大小都是 usize 大小的两倍,并且具有相同的对齐方式。

数组布局

数组 [T; N] 的大小为 size_of::<T>() * N ,并且具有与 T 相同的对齐方式。数组的布局使得数组中从零开始的第 n 个元素相对于数组起始位置的偏移量为 n * size_of::<T>() 字节。

切片布局

切片与其切取的数组部分具有相同的布局。

注意

这是关于原始 [T] 类型的,而不是指向切片的指针( &[T]Box<[T]> 等)。

str布局

字符串切片是字符的 UTF-8 表示,其布局与 [u8] 类型的切片相同。引用 &str 与引用 &[u8] 具有相同的布局。

元组布局

元组根据 Rust 表示法 进行布局。

此处的例外是单元元组( () ),它作为零大小类型,保证大小为 0,对齐方式为 1。

特型对象布局

特型对象与该特型对象所属的值具有相同的布局。

注意

这是关于原始特型对象类型的,而不是指向特型对象的指针( &dyn TraitBox<dyn Trait> 等)。

闭包布局

闭包没有布局保证。

表示法

所有用户定义的复合类型( 结构体 、 枚举 和 联合体 )都有一个 表示法 ,用于指定该类型的布局。

类型可能的表示法有:

可以通过对其应用 repr 属性来更改类型的表示法。以下示例显示了一个具有 C 表示法的结构体。

#![allow(unused)]
fn main() {
#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}
}

可以使用 alignpacked 修饰符分别提高或降低对齐方式。它们会修改属性中指定的表示法。如果没有指定表示法,则修改默认表示法。

#![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 表示法且包含具有 Rust 表示法的结构体 Inner 的结构体,其内部 Inner 的布局不会改变。

Rust表示法

Rust 表示法是没有 repr 属性的标称类型的默认表示法。通过 repr 属性显式使用此表示法与完全省略该属性的效果保证相同。

此表示法做出的唯一数据布局保证是那些为了健全性所必需的。它们是:

  1. 字段正确对齐。
  2. 字段不重叠。
  3. 类型的对齐方式至少是其字段的最大对齐方式。

正式地说,第一个保证意味着任何字段的偏移量都可以被该字段的对齐方式整除。

第二个保证意味着字段可以按序排列,使得任何字段的偏移量加上大小小于或等于排序中下一个字段的偏移量。该顺序不必与类型声明中指定字段的顺序相同。

请注意,第二个保证并不意味着字段具有不同的地址:零大小类型可能与同一结构体中的其他字段具有相同的地址。

此表示法没有做出其他数据布局保证。

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;
}

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 语言中等效的 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);  // 来自 b 的大小为 6,
                                                      // 根据 a 的对齐方式
                                                      // 向上舍入为 8。
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() {
// 此 Enum 与 ... 具有相同的表示法
#[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;
}

原语表示法

原语表示法 是与原语整数类型同名的表示法。即: u8u16u32u64u128usizei8i16i32i64i128isize

原语表示法只能应用于枚举,并且根据枚举是否有字段而具有不同的行为。对于 零变体枚举 ,使用原语表示法是错误的。将两个原语表示法结合在一起是错误的。

无字段枚举的原语表示法

对于 无字段枚举 ,原语表示法将大小和对齐方式设置为与同名原语类型相同。例如,具有 u8 表示法的无字段枚举只能具有 0 到 255(含)之间的判别式。

带字段枚举的原语表示法

原语表示法枚举的表示法是每个带字段变体的 repr(C) 结构体的 repr(C) 联合体。联合体中每个结构体的第一个字段是删除所有字段的枚举的原语表示法版本(“标签”),其余字段是该变体的字段。

注意

如果标签在联合体中被赋予了自己的成员,这种表示法不会改变,只要这能让你的操作更清晰(尽管为了遵循 C++ 标准,标签成员应该包装在一个 struct 中)。

#![allow(unused)]
fn main() {
// 此 enum 与 ... 具有相同的表示法
#[repr(u8)]
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ... 此联合体。
#[repr(C)]
union MyEnumRepr {
    A: MyVariantA,
    B: MyVariantB,
    C: MyVariantC,
    D: MyVariantD,
}

// 这是判别式枚举。
#[repr(u8)]
#[derive(Copy, Clone)]
enum MyEnumDiscriminant { A, B, C, D }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantA(MyEnumDiscriminant, u32);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantB(MyEnumDiscriminant, f32, u64);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantC { tag: MyEnumDiscriminant, x: u32, y: u8 }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantD(MyEnumDiscriminant);
}

将带字段枚举的原语表示法与#[repr(C)]结合

对于带字段的枚举,也可以将 repr(C) 和原语表示法结合使用(例如 repr(C, u8) )。这会通过将判别式枚举的表示法更改为所选原语来修改 repr(C) 。因此,如果你选择了 u8 表示法,则判别式枚举的大小和对齐方式将为 1 字节。

前面 示例中的判别式枚举随后变为:

#![allow(unused)]
fn main() {
#[repr(C, u8)] // 添加了 `u8`
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ...

#[repr(u8)] // 所以这里使用 `u8` 代替 `C`
enum MyEnumDiscriminant { A, B, C, D }

// ...
}

例如,对于 repr(C, u8) 枚举,不可能有 257 个唯一的判别式(“标签”),而仅具有 repr(C) 属性的相同枚举编译时不会有任何问题。

除了 repr(C) 之外,使用原语表示法还可以改变枚举的大小(相对于 repr(C) 形式):

#![allow(unused)]
fn main() {
#[repr(C)]
enum EnumC {
    Variant0(u8),
    Variant1,
}

#[repr(C, u8)]
enum Enum8 {
    Variant0(u8),
    Variant1,
}

#[repr(C, u16)]
enum Enum16 {
    Variant0(u8),
    Variant1,
}

// C 表示法的大小取决于平台
assert_eq!(std::mem::size_of::<EnumC>(), 8);
// 在 Enum8::Variant0 中,判别式占一个字节,值占一个字节
assert_eq!(std::mem::size_of::<Enum8>(), 2);
// 在 Enum16::Variant0 中,判别式占两个字节,值占一个字节,
// 加上一个字节的填充。
assert_eq!(std::mem::size_of::<Enum16>(), 4);
}

对齐修饰符

alignpacked 修饰符可以分别用于提高或降低 结构体 和 联合体 的对齐方式。 packed 还可以改变字段之间的填充(尽管它不会改变任何字段内部的填充)。 alignpacked 本身不提供有关结构体布局或枚举变体布局中字段顺序的保证,尽管它们可以与确实提供此类保证的表示法(如 C )结合使用。

对齐方式被指定为一个整数参数,形式为 #[repr(align(x))]#[repr(packed(x))] 。对齐值必须是 1 到 229 之间的 2 的幂。对于 packed ,如果没有给出值,如 #[repr(packed)] ,则值为 1。

对于 align ,如果指定的对齐方式小于没有 align 修饰符的类型的对齐方式,则对齐方式不受影响。

对于 packed ,如果指定的对齐方式大于没有 packed 修饰符的类型的对齐方式,则对齐方式和布局不受影响。

为了定位字段,每个字段的对齐方式是指定对齐方式和该字段类型对齐方式中的较小者。

字段间填充保证是满足每个字段(可能已改变的)对齐方式所需的最小值(尽管请注意, packed 本身不提供有关字段排序的任何保证)。这些规则的一个重要结果是,具有 #[repr(packed(1))] (或 #[repr(packed)] )的类型将没有字段间填充。

alignpacked 修饰符不能应用于同一类型,并且 packed 类型不能传递性地包含另一个 align 过的类型。 alignpacked 只能应用于 RustC 表示法。

align 修饰符也可以应用于 enum 。 应用时,对 enum 对齐方式的影响与该 enum 被包装在具有相同 align 修饰符的新类型 struct 中相同。

注意

不允许对非对齐字段进行引用,因为这是 未定义行为 。当字段由于对齐修饰符而非对齐时,请考虑使用以下选项来进行引用和解引用:

#![allow(unused)]
fn main() {
#[repr(packed)]
struct Packed {
    f1: u8,
    f2: u16,
}
let mut e = Packed { f1: 1, f2: 2 };
// 不要创建对字段的引用,而是将值复制到局部变量中。
let x = e.f2;
// 或者在像 `println!` 这样创建引用的情况下,使用大括号将其更改为值的副本。
println!("{}", {e.f2});
// 或者如果你需要一个指针,使用用于读取和写入的非对齐方法,
// 而不是直接解构指针。
let ptr: *const u16 = &raw const e.f2;
let value = unsafe { ptr.read_unaligned() };
let mut_ptr: *mut u16 = &raw mut e.f2;
unsafe { mut_ptr.write_unaligned(3) }
}

transparent表示法

transparent 表示法只能用于 结构体 或具有单个变体的 枚举 ,该变体具有:

  • 任意数量的大小为 0 且对齐方式为 1 的字段(例如 PhantomData<T> ),以及
  • 至多一个其他字段。

具有此表示法的结构体和枚举具有与唯一的非大小 0 非对齐 1 字段(如果存在)相同的布局和 ABI,否则为单元布局。

这与 C 表示法不同,因为具有 C 表示法的结构体始终具有 C struct 的 ABI,而具有 transparent 表示法且具有原语字段的结构体将具有该原语字段的 ABI。

由于这种表示法将类型布局委托给另一种类型,因此它不能与任何其他表示法一起使用。

内部可变性

有时一个类型在拥有多个别名时需要被修改。在 Rust 中,这是通过一种被称为 内部可变性 的模式实现的。

如果一个类型的内部状态可以通过指向它的 共享引用 来改变,那么该类型就具有内部可变性。

这违背了通常的 要求 ,即共享引用指向的值不能被修改。

std::cell::UnsafeCell<T> 类型是唯一允许禁用此要求的方式。当 UnsafeCell<T> 被不可变地起别名时,修改它包含的 T ,或者获取它的一个可变引用,仍然是安全的。

与所有其他类型一样,拥有多个 &mut UnsafeCell<T> 别名是未定义行为。

其他具有内部可变性的类型可以通过使用 UnsafeCell<T> 作为字段来创建。标准库提供了多种提供安全内部可变性 API 的类型。

例如, std::cell::RefCell<T> 使用运行时借用检查来确保围绕多个引用的通常规则。

std::sync::atomic 模块包含了一些包装值的类型,这些值只能通过原子操作访问,从而允许该值在线程间共享和修改。

子类型和变型

子类型是隐式的,可以发生在类型检查或推断的任何阶段。

子类型仅限于两种情况:关于生命周期的变型,以及具有高阶生命周期的类型之间。如果我们从类型中擦除生命周期,那么唯一的子类型关系将仅源于类型相等。

考虑以下示例:字符串字面量始终具有 'static 生命周期。尽管如此,我们仍可以将 s 赋值给 t

#![allow(unused)]
fn main() {
fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}
}

由于 'static 的存活时间长于生命周期参数 'a,因此 &'static str&'a str 的子类型。

高阶 函数指针特型对象 具有另一种子类型关系。它们是那些通过替换高阶生命周期而得到的类型的子类型。一些示例:

#![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;

// 这对特型对象的作用类似
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;
}

变型

变型是泛型类型相对于其参数所具有的属性。泛型类型在参数上的 变型 是指参数的子类型关系如何影响类型的子类型关系。

  • 如果 TU 的子类型意味着 F<T>F<U> 的子类型,则 F<T>T 上是 协变 的(子类型关系“透传”)
  • 如果 TU 的子类型意味着 F<U>F<T> 的子类型,则 F<T>T 上是 逆变
  • 否则, F<T>T 上是 不变 的(无法推导出子类型关系)

类型的变型自动确定如下

类型'a 的变型T 的变型
&'a T协变协变
&'a mut T协变不变
*const T协变
*mut T不变
[T][T; n]协变
fn() -> T协变
fn(T) -> ()逆变
std::cell::UnsafeCell<T>不变
std::marker::PhantomData<T>协变
dyn 特型<T> + 'a协变不变

其他 structenumunion 类型的变型是通过查看其字段类型的变型来确定的。如果参数被用于具有不同变型的位置,那么该参数就是不变的。例如,以下结构体在 'aT 上是协变的,在 'b'cU 上是不变的。

#![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 在结构体中是不变的。
}
}

当在 structenumunion 之外使用时,参数的变型是在每个位置分别检查的。

#![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
    // 并扩大逆变位置上的 'middle。
    let _: fn(&'static ()) -> &'short () = f;
}
}

特型与生命周期界限

Syntax
TypeParamBoundsTypeParamBound ( + TypeParamBound )* +?

TypeParamBoundLifetime | TraitBound | UseBound

TraitBound
      ( ? | ForLifetimes )? TypePath
    | ( ( ? | ForLifetimes )? TypePath )

LifetimeBounds → ( Lifetime + )* Lifetime?

Lifetime
      LIFETIME_OR_LABEL
    | 'static
    | '_

UseBounduse UseBoundGenericArgs

UseBoundGenericArgs
      < >
    | < ( UseBoundGenericArg , )* UseBoundGenericArg ,? >

UseBoundGenericArg
      Lifetime
    | IDENTIFIER
    | Self

[特型][Trait] 和生命周期界限为 [泛型项][generic] 提供了一种限制哪些类型和生命周期可以用作其参数的方法。可以在 [where 子句][where clause] 中的任何类型上提供界限。对于某些常见情况,也有较短的形式:

  • 在声明 [泛型参数][generic] 后编写的界限:fn f<A: Copy>() {}fn f<A>() where A: Copy {} 相同。
  • 在特型声明中作为 [父特型][supertraits]trait Circle : Shape {} 等同于 trait Circle where Self : Shape {}
  • 在特型声明中作为 [关联类型][associated types] 上的界限:trait A { type B: Copy; } 等同于 trait A where Self::B: Copy { type B; }

使用 项 时必须满足其上的界限。在对泛型 项 进行类型检查和借用检查时,界限可用于确定某个类型是否实现了某个特型。例如,给定 Ty: Trait

  • 在泛型函数的函数体中,可以在 Ty 值上调用来自 Trait 的方法。同样,也可以使用 Trait 上的关联常量。
  • 可以使用来自 Trait 的关联类型。
  • 具有 T: Trait 界限的泛型函数和类型可以使用 Ty 作为 T
#![allow(unused)]
fn main() {
type Surface = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn name() -> &'static str;
}

fn draw_twice<T: Shape>(surface: Surface, sh: T) {
    sh.draw(surface);           // 可以调用方法,因为 T: Shape
    sh.draw(surface);
}

fn copy_and_draw_twice<T: Copy>(surface: Surface, sh: T) where T: Shape {
    let shape_copy = sh;        // 不会移动 sh,因为 T: Copy
    draw_twice(surface, sh);    // 可以使用泛型函数,因为 T: Shape
}

struct Figure<S: Shape>(S, S);

fn name_figure<U: Shape>(
    figure: Figure<U>,          // 类型 Figure<U> 是良构的,因为 U: Shape
) {
    println!(
        "Figure of two {}",
        U::name(),              // 可以使用关联函数
    );
}
}

不使用 项 的参数或 [高阶生命周期][higher-ranked lifetimes] 的界限会在定义 项 时进行检查。此类界限若为假则是错误的。

在使用 项 时,还会检查某些泛型类型的 [Copy][Clone][Sized] 界限,即使该使用并未提供具体类型。在可变引用、 [特型对象][trait object][切片][slice] 上将 CopyClone 作为界限是错误的。在特型对象或切片上将 Sized 作为界限是错误的。

#![allow(unused)]
fn main() {
struct A<'a, T>
where
    i32: Default,           // 允许,但没用
    i32: Iterator,          // 错误:`i32` 不是迭代器
    &'a mut T: Copy,        // (使用时)错误:特型界限未满足
    [T]: Sized,             // (使用时)错误:大小在编译时无法确定
{
    f: &'a T,
}
struct UsesA<'a, T>(A<'a, T>);
}

特型和生命周期界限也用于命名 [特型对象][trait objects]

?Sized

? 仅用于放宽针对 [类型参数][type parameters][关联类型][associated types] 的隐式 [Sized] 特型界限。 ?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 不满足。

高阶特型界限

Syntax
ForLifetimesfor GenericParams

特型界限可以是生命周期上的 高阶 界限。这些界限指定了 对于所有 生命周期都成立的界限。例如,像 for<'a> &'a T: PartialEq<i32> 这样的界限需要类似如下的实现

#![allow(unused)]
fn main() {
struct T;
impl<'a> PartialEq<i32> for &'a T {
    // ...
   fn eq(&self, other: &i32) -> bool {true}
}
}

并可用于将具有任何生命周期的 &'a Ti32 进行比较。

此处只能使用高阶界限,因为引用的生命周期比函数上任何可能的生命周期参数都要短:

#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}

高阶生命周期也可以紧接在特型之前指定:唯一的区别是生命周期参数的 作用域 ,它仅延伸到后续特型的末尾,而不是整个界限。此函数等同于上一个函数。

#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where F: for<'a> Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}

隐含界限

类型为良构所需的生命周期界限有时会被推断出来。

#![allow(unused)]
fn main() {
fn requires_t_outlives_a<'a, T>(x: &'a T) {}
}

要求类型参数 T 长于 'a ,以便 &'a T 类型是良构的。这是被推断出来的,因为函数签名包含类型 &'a T ,该类型仅在 T: 'a 成立时才有效。

隐含界限会被添加到函数的所有参数和输出中。在 requires_t_outlives_a 内部,即使没有显式指定,也可以假设 T: 'a 成立:

#![allow(unused)]
fn main() {
fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}

fn requires_t_outlives_a<'a, T>(x: &'a T) {
    // 这可以编译,因为 `T: 'a` 是由
    // 引用类型 `&'a T` 隐含的。
    requires_t_outlives_a_not_implied::<'a, T>();
}
}
#![allow(unused)]
fn main() {
fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}
fn not_implied<'a, T>() {
    // 这会报错,因为 `T: 'a` 不是由
    // 函数签名隐含的。
    requires_t_outlives_a_not_implied::<'a, T>();
}
}

只有生命周期界限是隐含的,特型界限仍必须显式添加。因此,以下示例会导致错误:

#![allow(unused)]
fn main() {
use std::fmt::Debug;
struct IsDebug<T: Debug>(T);
// 错误[E0277]:`T` 未实现 `Debug`
fn doesnt_specify_t_debug<T>(x: IsDebug<T>) {}
}

对于任何类型的类型定义和 impl 块,也会推断出生命周期界限:

#![allow(unused)]
fn main() {
struct Struct<'a, T> {
    // 这要求 `T: 'a` 是良构的,
    // 由编译器推断。
    field: &'a T,
}

enum Enum<'a, T> {
    // 这要求 `T: 'a` 是良构的,
    // 由编译器推断。
    //
    // 注意,即使仅使用
    // `Enum::OtherVariant`,也要求 `T: 'a`。
    SomeVariant(&'a T),
    OtherVariant,
}

trait Trait<'a, T: 'a> {}

// 这会报错,因为 `T: 'a` 未由 impl 标题中的任何类型隐含。
//     impl<'a, T> Trait<'a, T> for () {}

// 这可以编译,因为 `T: 'a` 是由 self 类型 `&'a T` 隐含的。
impl<'a, T> Trait<'a, T> for &'a T {}
}

use界限

某些界限列表可能包含 use<..> 界限,以控制哪些泛型参数会被 impl Trait 抽象返回类型 捕获。有关更多详细信息,请参阅 精确捕获

隐式类型转换

隐式类型转换 是改变值类型的隐式操作。它们在特定位置自动发生,并且对实际进行转换的类型有严格限制。

任何允许隐式转换的转换也可以通过 类型转换运算符 as 显式执行。

隐式转换最初在 RFC 401 中定义,并在 RFC 1558 中得到扩展。

转换点

隐式转换只能发生在程序中的某些转换点;这些位置通常是所需类型明确或可以通过从明确类型传播(无需类型推断)导出的地方。可能的转换点有:

  • 给出显式类型的 let 语句。

    例如,在以下代码中, &mut 42 被隐式转换为 &i8 类型:

    #![allow(unused)]
    fn main() {
    let _: &i8 = &mut 42;
    }
  • staticconst 项 声明(类似于 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) 的转换点。每个子表达式都是对应类型的转换点,例如,第 0 个子表达式是转换为 U_0 类型的转换点。
  • 括号子表达式( (e) ):如果表达式具有类型 U ,则子表达式是转换为 U 的转换点。
  • 代码块:如果代码块具有类型 U ,则代码块中的最后一个表达式(如果不是以分号结尾)是转换为 U 的转换点。这包括属于控制流语句(如 if / else )一部分的代码块,前提是该代码块具有已知类型。

转换类型

允许在以下类型之间进行隐式转换:

  • 如果 TU子类型 ,则 TU自反情况
  • T_1T_3 ,其中 T_1 可隐式转换为 T_2T_2 可隐式转换为 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>

    并且可以通过 非定长转换T 获得 U

  • 函数 项 类型到 fn 指针
  • 非捕获闭包到 fn 指针
  • ! 到任何 T

非定长转换

以下转换被称为 非定长转换 ,因为它们与将类型转换为非定长类型有关,并且在上述其他转换不允许的少数情况下是允许的。它们仍然可以发生在隐式转换可以发生的任何其他地方。

两个 特型 UnsizeCoerceUnsized 被用于协助此过程并将其公开给库使用。以下转换是内置的,如果 T 可以通过其中之一隐式转换为 U ,则将提供 TUnsize<U> 的实现:

  • [T; n][T]
  • Tdyn U ,当 T 实现 U + Sized ,且 Udyn 兼容 的。
  • dyn Tdyn U ,当 UT父特型 之一时。
    • 这允许丢弃自动特型,即允许 dyn T + Autodyn U
    • 如果主特型将自动特型作为父特型,则允许添加自动特型,即给定 trait T: U + Send {} ,允许 dyn Tdyn T + Send 或到 dyn U + Send 的隐式转换。
  • Foo<..., T, ...>Foo<..., U, ...> ,当:
    • Foo 是一个 结构体 。
    • T 实现了 Unsize<U>
    • Foo 的最后一个字段具有涉及 T 的类型。
    • 如果该字段的类型为 Bar<T> ,则 Bar<T> 实现了 Unsize<Bar<U>>
    • T 不是任何其他字段类型的一部分。

此外,当 T 实现了 Unsize<U>CoerceUnsized<Foo<U>> 时,类型 Foo<T> 可以实现 CoerceUnsized<Foo<U>> 。这允许它提供到 Foo<U> 的非定长转换。

注意

虽然非定长转换的定义及其实现已经稳定,但特型本身尚未稳定,因此不能在稳定版 Rust 中直接使用。

最小上界转换

在某些语境中,编译器必须将多个类型一起进行隐式转换,以尝试找到最通用的类型。这被称为 “最小上界” (LUB)转换。LUB 转换仅在以下情况下使用:

  • 寻找一系列 if 分支的共同类型。
  • 寻找一系列 match 臂的共同类型。
  • 寻找数组元素的共同类型。
  • 寻找具有多个返回语句的闭包的返回类型。
  • 检查具有多个返回语句的函数的返回类型。

在每种情况下,都有一组类型 T0..Tn 需要相互转换为某个目标类型 T_t ,该类型在开始时是未知的。

计算 LUB 转换是迭代进行的。目标类型 T_t 最初为类型 T0 。对于每个新类型 Ti ,我们考虑:

  • 如果 Ti 可以隐式转换为当前目标类型 T_t ,则不作更改。
  • 否则,检查 T_t 是否可以隐式转换为 Ti ;如果是,则将 T_t 更改为 Ti 。(此检查还取决于迄今为止考虑的所有源表达式是否都具有隐式转换。)
  • 如果都不是,尝试计算 T_tTi 的共同超类型,这将成为新的目标类型。

示例:

#![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 时会检查 abc 的 LUB 转换结果是否为 i32

警告

这种描述显然是非正式的。更精确的描述预计将作为更精确地规范 Rust 类型检查器总体工作的一部分来推进。

析构函数

当一个 已初始化 变量临时变量 离开 作用域 时,其 析构函数 会被运行,或者说它被 销毁(dropped)赋值 也会运行其左侧操作数(如果已初始化)的析构函数。如果一个变量已被部分初始化,则仅销毁其已初始化的字段。

类型 T 的析构函数由以下部分组成:

  1. 如果 T: Drop,则调用 <T as core::ops::Drop>::drop
  2. 递归运行其所有字段的析构函数。
    • 结构体 的字段按声明顺序销毁。
    • 活跃 枚举变体 的字段按声明顺序销毁。
    • 元组 的字段按顺序销毁。
    • 数组 或拥有所有权的 切片 的元素从第一个元素到最后一个元素依次销毁。
    • 闭包 通过移动(move)捕获的变量按未指定的顺序销毁。
    • 特型对象 运行底层类型的析构函数。
    • 其他类型不会导致进一步的销毁操作。

如果必须手动运行析构函数(例如在实现自己的智能指针时),可以使用 core::ptr::drop_in_place

一些示例:

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}

let mut overwritten = PrintOnDrop("drops when overwritten");
overwritten = PrintOnDrop("drops when scope ends"); // 覆盖时销毁

let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second"));

let moved;
// 赋值时不运行析构函数。
moved = PrintOnDrop("Drops when moved");
// 现在销毁,但之后变为未初始化状态。
moved;

// 未初始化不会销毁。
let uninitialized: PrintOnDrop;

// 部分移动后,仅销毁剩余字段。
let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten"));
// 执行部分移动,仅保留 `partial_move.0` 为初始化状态。
core::mem::forget(partial_move.1);
// 当 partial_move 的作用域结束时,仅销毁第一个字段。
}

销毁作用域

每个变量或临时变量都与一个 销毁作用域 相关联。当控制流离开销毁作用域时,与该作用域相关的所有变量都将按声明(对于变量)或创建(对于临时变量)的逆序进行销毁。

可以通过使用 matchloopbreakforifwhile 表达式替换为等效表达式来确定销毁作用域。

重载运算符与内置运算符不作区分,且不考虑 绑定模式

给定一个函数或闭包,存在以下销毁作用域:

  • 整个函数
  • 每个块,包括函数体
    • 块表达式 的情况下,块的作用域和表达式的作用域是同一个作用域。
  • match 表达式的每个分支(arm)

销毁作用域按如下方式相互嵌套。当同时离开多个作用域时(例如从函数返回时),变量按从内向外的顺序销毁。

  • 整个函数作用域是最外层作用域。
  • 函数体块包含在整个函数的作用域内。
  • 表达式语句中表达式的父作用域是该语句的作用域。
  • let 语句 的初始化器的父作用域是该 let 语句的作用域。
  • 语句作用域的父作用域是包含该语句的块的作用域。
  • match 守卫(guard)表达式的父作用域是该守卫所属分支的作用域。
  • 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");
}

模式中的变量按模式内声明顺序的逆序销毁。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let (declared_first, declared_last) = (
    PrintOnDrop("Dropped last"),
    PrintOnDrop("Dropped first"),
);
}

出于销毁顺序的目的, 或模式 按第一个子模式给出的顺序声明绑定。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
// 先销毁 `x` 后销毁 `y`。
fn or_pattern_drop_order<T>(
    (Ok([x, y]) | Err([y, x])): Result<[T; 2], [T; 2]>
//   ^^^^^^^^^^   ^^^^^^^^^^^ 这是第二个子模式。
//   |
//   这是第一个子模式。
//
//   在第一个子模式中,`x` 在 `y` 之前声明。由于它是
//   第一个子模式,即使匹配的是第二个子模式(其中绑定的声明顺序相反),
//   也会使用该顺序。
) {}

// 这里我们匹配第一个子模式,销毁按第一个子模式中的声明顺序发生。
or_pattern_drop_order(Ok([
    PrintOnDrop("Declared first, dropped last"),
    PrintOnDrop("Declared last, dropped first"),
]));

// 这里我们匹配第二个子模式,销毁仍按第一个子模式中的声明顺序发生。
or_pattern_drop_order(Err([
    PrintOnDrop("Declared last, dropped first"),
    PrintOnDrop("Declared first, dropped last"),
]));
}

临时作用域

表达式的 临时作用域 是指当表达式用于 位置上下文 时,用于持有该表达式结果的临时变量的作用域,除非该表达式被 提升

除生命周期延长外,表达式的临时作用域是包含该表达式的最小作用域,且为以下之一:

注意

match 表达式的 受查表达式 不是临时作用域,因此受查表达式中的临时变量可以在 match 表达式之后销毁。例如, match 1 { ref mut z => z };1 的临时变量存活到语句结束。

注意

解构赋值 的脱糖限制了其赋值操作数(右侧值)的临时作用域。有关详细信息,请参阅 expr.assign.destructure.tmp-scopes

2024 版次差异

2024 版次增加了两条新的临时作用域收窄规则: if let 临时变量在 else 块之前销毁,块的尾随表达式的临时变量在尾随表达式评估后立即销毁。

一些示例:

#![allow(unused)]
fn main() {
#![allow(irrefutable_let_patterns)]
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let local_var = PrintOnDrop("local var");

// 一旦条件评估完毕就会被销毁
if PrintOnDrop("If condition").0 == "If condition" {
    // 在块结束时销毁
    PrintOnDrop("If body").0
} else {
    unreachable!()
};

if let "if let scrutinee" = PrintOnDrop("if let scrutinee").0 {
    PrintOnDrop("if let consequent").0
    // `if let consequent` 在此处销毁
}
// `if let scrutinee` 在此处销毁
else {
    PrintOnDrop("if let else").0
    // `if let else` 在此处销毁
};

while let x = PrintOnDrop("while let scrutinee").0 {
    PrintOnDrop("while let loop body").0;
    break;
    // `while let loop body` 在此处销毁。
    // `while let scrutinee` 在此处销毁。
}

// 在第一个 || 之前销毁
(PrintOnDrop("first operand").0 == ""
// 在 ) 之前销毁
|| PrintOnDrop("second operand").0 == "")
// 在 ; 之前销毁
|| PrintOnDrop("third operand").0 == "";

// 受查表达式在函数结束时、局部变量之前销毁
// (因为这是函数体块的尾随表达式)。
match PrintOnDrop("Matched value in final expression") {
    // 一旦条件评估完毕就会被销毁
    _ if PrintOnDrop("guard condition").0 == "" => (),
    _ => (),
}
}

操作数

在评估表达式的其他操作数时,也会创建临时变量来持有该表达式的操作数结果。这些临时变量与具有该操作数的表达式的作用域相关联。由于一旦表达式评估完毕,临时变量就会被移走,因此销毁它们没有影响,除非表达式的一个操作数跳出了表达式、返回或发生了 恐慌

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
loop {
    // 元组表达式未完成评估,因此操作数按逆序销毁
    (
        PrintOnDrop("Outer tuple first"),
        PrintOnDrop("Outer tuple second"),
        (
            PrintOnDrop("Inner tuple first"),
            PrintOnDrop("Inner tuple second"),
            break,
        ),
        PrintOnDrop("Never created"),
    );
}
}

常量提升

当一个值表达式可以写在常量中并被借用,且该借用可以在表达式最初编写的地方被解引用而不会改变运行时行为时,就会发生将该表达式提升到 'static 槽位的操作。也就是说,被提升的表达式可以在编译时评估,且结果值不包含 内部可变性析构函数 (这些属性尽可能根据值来确定,例如 &None 始终具有类型 &'static Option<_> ,因为它不包含任何被禁止的内容)。

临时生命周期延长

注意

临时生命周期延长的具体规则可能会发生变化。此处仅描述当前行为。

let 语句中,表达式的临时作用域有时会 延长 到包含该 let 语句的块的作用域。根据某些语法规则,当通常的临时作用域太小时会执行此操作。例如:

#![allow(unused)]
fn main() {
let x = &mut 0;
// 通常临时变量现在已经被销毁了,但 `0` 的临时变量
// 存活到块结束。
println!("{}", x);
}

生命周期延长也适用于 staticconst 项 ,这使得临时变量存活到程序结束。例如:

#![allow(unused)]
fn main() {
const C: &Vec<i32> = &Vec::new();
// 通常这将是一个悬空引用,因为 `Vec` 仅存在于 `C` 的
// 初始化表达式内部,但这里的借用被延长了生命周期,
// 因此它实际上具有 `'static` 生命周期。
println!("{:?}", C);
}

如果 借用解引用表达式字段表达式元组索引表达式 具有延长的临时作用域,则其操作数也具有。如果 索引表达式 具有延长的临时作用域,则被索引的表达式也具有延长的临时作用域。

基于模式的延长

一个 延长模式 是以下之一:

  • 通过引用或可变引用绑定的 标识符模式

    #![allow(unused)]
    fn main() {
    fn temp() {}
    let ref x = temp(); // 通过引用绑定。
    x;
    let ref mut x = temp(); // 通过可变引用绑定。
    x;
    }
  • 结构体模式元组模式元组结构体模式切片模式或模式 ,其中至少一个直接子模式是延长模式。

    #![allow(unused)]
    fn main() {
    use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
    static X: AtomicU64 = AtomicU64::new(0);
    struct W<T>(T);
    impl<T> Drop for W<T> { fn drop(&mut self) { X.fetch_add(1, Relaxed); } }
    let W { 0: ref x } = W(()); // 结构体模式。
    x;
    let W(ref x) = W(()); // 元组结构体模式。
    x;
    let (W(ref x),) = (W(()),); // 元组模式。
    x;
    let [W(ref x), ..] = [W(())]; // 切片模式。
    x;
    let (Ok(W(ref x)) | Err(&ref x)) = Ok(W(())); // 或模式。
    x;
    //
    // 以上所有临时变量在这里仍然存活。
    assert_eq!(0, X.load(Relaxed));
    }

因此 ref xV(ref x)[ref x, y] 都是延长模式,但 x&ref x&(ref x,) 则不是。

如果 let 语句中的模式是延长模式,则初始化器表达式的临时作用域将被延长。

#![allow(unused)]
fn main() {
fn temp() {}
// 这是一个延长模式,因此临时作用域被延长。
let ref x = *&temp(); // 正常
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 这既不是延长模式也不是延长表达式,
// 因此临时变量在分号处销毁。
let &ref x = *&&temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 这不是延长模式,但它是一个延长表达式,
// 因此临时变量存活超过了 `let` 语句。
let &ref x = &*&temp(); // 正常
x;
}

基于表达式的延长

对于带有初始化器的 let 语句, 延长表达式 是以下表达式之一:

注意

解构赋值 的脱糖使其赋值操作数(右侧值)成为新引入块内的延长表达式。有关详细信息,请参阅 expr.assign.destructure.tmp-ext

因此 &mut 0(&1, &mut 2)Some(&mut 3) 中的借用表达式都是延长表达式。而 &0 + &1f(&mut 0) 中的借用则不是。

延长 借用 表达式的操作数其 临时作用域延长

延长 超级宏调用 表达式的 超级临时变量作用域延长

注意

rustc 不将延长 数组 表达式的 数组重复操作数 视为延长表达式。是否应该这样做是一个悬而未决的问题。

有关详细信息,请参阅 Rust issue #146092

示例

以下是表达式具有延长临时作用域的一些示例:

#![allow(unused)]
fn main() {
use core::pin::pin;
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
static X: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)] struct S;
impl Drop for S { fn drop(&mut self) { X.fetch_add(1, Relaxed); } }
const fn temp() -> S { S }
let x = &temp(); // 借用的操作数。
x;
let x = &raw const *&temp(); // 原生借用的操作数。
assert_eq!(X.load(Relaxed), 0);
let x = &temp() as &dyn Send; // 转换的操作数。
x;
let x = (&*&temp(),); // 元组构造器的操作数。
x;
struct W<T>(T);
let x = W(&temp()); // 元组结构体构造器的参数。
x;
let x = Some(&temp()); // 元组枚举变体构造器的参数。
x;
let x = { [Some(&temp())] }; // 块的最终表达式。
x;
let x = const { &temp() }; // `const` 块的最终表达式。
x;
let x = unsafe { &temp() }; // `unsafe` 块的最终表达式。
x;
let x = if true { &temp() } else { &temp() };
//              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//           `if`/`else` 块的最终表达式。
x;
let x = match () { _ => &temp() }; // `match` 分支表达式。
x;
let x = pin!(temp()); // 超级宏调用表达式的超级操作数。
x;
let x = pin!({ &mut temp() }); // 同上。
x;
let x = format_args!("{:?}", temp()); // 同上。
x;
//
// 以上所有临时变量在这里仍然存活。
assert_eq!(0, X.load(Relaxed));
}

以下是表达式不具有延长临时作用域的一些示例:

#![allow(unused)]
fn main() {
fn temp() {}
// 函数调用的参数不是延长表达式。临时变量在分号处销毁。
let x = core::convert::identity(&temp()); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// 方法调用的接收者不是延长表达式。
let x = (&temp()).use_temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// match 表达式的受查表达式不是延长表达式。
let x = match &temp() { x => x }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// `async` 块的最终表达式不是延长表达式。
let x = async { &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 闭包的最终表达式不是延长表达式。
let x = || &temp(); // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 循环 break 的操作数不是延长表达式。
let x = loop { break &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
fn temp() {}
// 带有标签的 break 的操作数不是延长表达式。
let x = 'a: { break 'a &temp() }; // 错误
x;
}
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 仅当调用是延长表达式时,`pin!` 的参数才是延长表达式。
// 由于它不是,内部块不是延长表达式,因此其尾随表达式中的
// 临时变量会立即销毁。
pin!({ &temp() }); // 错误
}
#![allow(unused)]
fn main() {
fn temp() {}
// 同上。
format_args!("{:?}", { &temp() }); // 错误
}

不运行析构函数

手动抑制析构函数

core::mem::forget 可用于防止运行变量的析构函数,而 core::mem::ManuallyDrop 提供了一个包装器来防止变量或字段被自动销毁。

注意

通过 core::mem::forget 或其他手段防止析构函数运行是安全的,即使它的类型不是 'static 。除了本文档定义的保证运行析构函数的地方外,为了健全性,类型 不可 安全地依赖于析构函数的运行。

无需展开的进程终止

有一些方法可以在不进行 展开 的情况下终止进程,在这种情况下将不运行析构函数。

标准库提供了 std::process::exitstd::process::abort 来显式执行此操作。此外,如果 恐慌处理器 设置为 abort ,发生恐慌时将始终终止进程而不运行析构函数。

还有一个需要注意的情况:当恐慌达到 非展开 ABI 边界 时,要么不运行任何析构函数,要么运行直到该 ABI 边界为止的所有析构函数。

生命周期省略

Rust 有一些规则允许在编译器可以推断出合理的默认选择的各种地方省略生命周期。

函数中的生命周期省略

为了使常用模式更符合人机工程学,生命周期参数可以在 函数项函数指针闭包特型 签名中被 省略 。以下规则用于为省略的生命周期推断生命周期参数。

省略无法推断的生命周期参数是错误的。

占位符生命周期 '_ 也可以用于以相同方式推断生命周期。对于路径中的生命周期,首选使用 '_

特型对象生命周期遵循不同的规则,将在 下文 讨论。

  • 参数中每个省略的生命周期都成为一个独特的生命周期参数。
  • 如果参数中恰好使用了一个生命周期(无论是否省略),则该生命周期将分配给 所有 省略的输出生命周期。

在方法签名中还有另一条规则

  • 如果接收者的类型是 &Self&mut Self ,那么指向 Self 的引用的生命周期将被分配给所有省略的输出生命周期参数。

例如:

#![allow(unused)]
fn main() {
trait T {}
trait ToCStr {}
struct Thing<'a> {f: &'a i32}
struct Command;

trait Example {
fn print1(s: &str);                                   // 省略
fn print2(s: &'_ str);                                // 同样省略
fn print3<'a>(s: &'a str);                            // 展开

fn debug1(lvl: usize, s: &str);                       // 省略
fn debug2<'a>(lvl: usize, s: &'a str);                // 展开

fn substr1(s: &str, until: usize) -> &str;            // 省略
fn substr2<'a>(s: &'a str, until: usize) -> &'a str;  // 展开

fn get_mut1(&mut self) -> &mut dyn T;                 // 省略
fn get_mut2<'a>(&'a mut self) -> &'a mut dyn T;       // 展开

fn args1<T: ToCStr>(&mut self, args: &[T]) -> &mut Command;                  // 省略
fn args2<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // 展开

fn other_args1<'a>(arg: &str) -> &'a str;             // 省略
fn other_args2<'a, 'b>(arg: &'b str) -> &'a str;      // 展开

fn new1(buf: &mut [u8]) -> Thing<'_>;                 // 省略 - 推荐做法
fn new2(buf: &mut [u8]) -> Thing;                     // 省略
fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>;          // 展开
}

type FunPtr1 = fn(&str) -> &str;                      // 省略
type FunPtr2 = for<'a> fn(&'a str) -> &'a str;        // 展开

type FunTrait1 = dyn Fn(&str) -> &str;                // 省略
type FunTrait2 = dyn for<'a> Fn(&'a str) -> &'a str;  // 展开
}
#![allow(unused)]
fn main() {
// 以下示例展示了不允许省略生命周期参数的情况。

trait Example {
// 无法推断,因为没有可供推断的参数。
fn get_str() -> &str;                                 // 非法

// 无法推断,无法确定是从第一个还是第二个参数借用的。
fn frob(s: &str, t: &str) -> &str;                    // 非法
}
}

默认特型对象生命周期

特型对象 所持有的引用的假设生命周期被称为其 默认对象生命周期界限 。这些在 RFC 599 中定义,并在 RFC 1156 中进行了修正。

当生命周期界限被完全省略时,将使用这些默认对象生命周期界限,而不是上面定义的生命周期参数省略规则。

如果使用 '_ 作为生命周期界限,那么该界限遵循通常的省略规则。

如果特型对象被用作泛型类型的类型参数,那么首先使用包含类型来尝试推断界限。

  • 如果来自包含类型的界限是唯一的,那么它就是默认值
  • 如果来自包含类型的界限不止一个,则必须指定显式界限

如果这两条规则都不适用,则使用特型上的界限:

  • 如果特型定义时带有一个单一的生命周期 界限 ,则使用该界限。
  • 如果任何生命周期界限使用了 'static ,则使用 'static
  • 如果特型没有生命周期界限,那么在表达式中生命周期会被推断,在表达式之外则是 'static
#![allow(unused)]
fn main() {
// 对于以下特型...
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 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 {}
}

conststatic省略

引用类型的 常量静态 声明除非指定了显式生命周期,否则都具有 隐式 '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,
};
}

请注意,如果 staticconst 项 包含函数或闭包引用,而这些引用本身又包含引用,编译器将首先尝试标准的省略规则。如果无法通过通常规则解析生命周期,则会报错。例如:

#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc(a: &Foo, b: &Bar, c: &Baz) -> usize {42}
// 解析为 `for<'a> fn(&'a str) -> &'a str`。
const RESOLVED_SINGLE: fn(&str) -> &str = |x| x;

// 解析为 `for<'a, 'b, 'c> Fn(&'a Foo, &'b Bar, &'c Baz) -> usize`。
const RESOLVED_MULTIPLE: &dyn Fn(&Foo, &Bar, &Baz) -> usize = &somefunc;
}
#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc<'a,'b>(a: &'a Foo, b: &'b Bar) -> &'a Baz {unimplemented!()}
// 没有足够的信息来确定返回引用的生命周期界限
// 与参数生命周期的关系,因此这是一个错误。
const RESOLVED_STATIC: &dyn Fn(&Foo, &Bar) -> &Baz = &somefunc;
//                                            ^
// 此函数的返回类型包含一个借用值,但签名
// 没有说明它是从参数 1 还是参数 2 借用的
}

特殊类型和特型

标准库 中存在的某些类型和 特型 是 Rust 编译器所熟知的。本章记录了这些类型和 特型 的特殊 特性 。

Box<T>

Box<T> 具有一些 Rust 目前不允许用户定义类型使用的特殊 特性 。

  • Box<T>解引用运算符 会产生一个可以被移出的位置。这意味着 * 运算符和 Box<T> 的析构函数是内置在语言中的。
  • 方法 可以接收 Box<Self> 作为接收者。
  • 在与 T 相同的 crate 中,可以为 Box<T> 实现某个 特型 ,而 孤儿规则 会阻止其他 泛型 类型这样做。

Rc<T>

方法 可以接收 Rc<Self> 作为接收者。

Arc<T>

方法 可以接收 Arc<Self> 作为接收者。

Pin<P>

方法 可以接收 Pin<P> 作为接收者。

UnsafeCell<T>

std::cell::UnsafeCell<T> 用于 内部可变性 。它确保编译器不会对这些类型执行错误的优化。

它还确保具有内部可变性类型的 static 不会被放置在标记为只读的内存中。

PhantomData<T>

std::marker::PhantomData<T> 是一种零大小、最小对齐的类型,为了 型变析构检查自动特型 的目的,它被视为拥有一个 T

运算符特型

std::opsstd::cmp 中的 特型 用于重载 运算符索引表达式调用表达式

DerefDerefMut

除了重载一元 * 运算符外, DerefDerefMut 还用于 方法解析解引用强转

Drop

Drop 特型 提供了一个 析构函数 ,每当该类型的值要被销毁时就会运行。

Copy

Copy 特型 会改变实现它的类型的语义。

类型实现 Copy 的值在赋值时会被复制而不是移动。

Copy 只能为未实现 Drop 且其字段均为 Copy 的类型实现。对于枚举,这意味着所有变体的所有字段都必须是 Copy 。对于 联合体 ,这意味着所有变体都必须是 Copy

Copy 由编译器为以下内容实现:

  • 未捕获任何值或仅捕获 Copy 类型值的 闭包

Clone

Clone 特型 是 Copy 的父特型,因此它也需要编译器生成的实现。

它由编译器为以下类型实现:

  • 具有内置 Copy 实现的类型(见上文)
  • 仅捕获 Clone 类型的值或不从环境中捕获值的 闭包

Send

Send 特型 表示该类型的值可以安全地从一个线程发送到另一个线程。

Sync

Sync 特型 表示该类型的值可以安全地在多个线程之间共享。

此 特型 必须为不可变 static 中使用的所有类型实现。

Termination

Termination 特型 表示 main 函数测试函数 可接受的返回类型。

自动特型

SendSyncUnpinUnwindSafeRefUnwindSafe 特型 是 自动特型 。自动特型具有特殊属性。

如果未针对给定类型的自动 特型 编写显式实现或否定实现,则编译器会根据以下规则自动实现它:

  • 如果 T 实现了该 特型 ,则 &T&mut T*const T*mut T[T; n][T] 也会实现。
  • 函数项类型和函数指针会自动实现该 特型 。
  • 结构体 、枚举、 联合体 和元组的所有字段都实现了该 特型 ,则它们也会实现。
  • 如果闭包所有捕获的类型都实现了该 特型 ,则该闭包也会实现。通过共享引用捕获 T 并通过值捕获 U 的闭包将实现 &TU 都实现的任何自动 特型 。

对于 泛型 类型(将上述内置类型视为关于 T 的 泛型 ),如果存在 泛型 实现,那么对于那些本可以使用该实现但因不满足必要 特型 约束而无法使用的类型,编译器不会自动实现它。例如,标准库为所有 TSync&T 实现了 Send ;这意味着如果 TSend 但不是 Sync ,编译器将不会为 &T 实现 Send

自动 特型 也可以具有否定实现(在标准库文档中显示为 impl !AutoTrait for T ),这些实现会覆盖自动实现。例如 *mut T 具有 Send 的否定实现,因此即使 TSend*mut T 也不是 Send 。目前还没有稳定且通用的方法来指定额外的否定实现;它们仅存在于标准库中。

自动 特型 可以作为附加约束添加到任何 特型对象 中,即使通常只允许一个 特型 。例如, Box<dyn Debug + Send + UnwindSafe> 是一个有效类型。

Sized

Sized 特型 表示该类型的大小在编译时是已知的;也就是说,它不是 动态大小类型

类型参数 ( 特型 中的 Self 除外)默认是 Sized 的, 关联类型 也是如此。

Sized 总是由编译器自动实现,而不是通过 实现项 实现。

这些隐式的 Sized 约束可以通过使用特殊的 ?Sized 约束来放宽。

名称

一个 实体(entity) 是一种语言结构,它在源程序中可以以某种方式被引用,通常是通过 路径 。实体包括 类型泛型参数变量绑定循环标签生命周期字段属性 以及 lint

一个 声明(declaration) 是一种语法结构,它可以引入一个 名称(name) 来引用实体。实体名称在 作用域 内有效 — 即该名称可以被引用的源文本区域。

某些实体在源码中被 显式声明 ,而某些实体作为语言或编译器扩展的一部分被 隐式声明

路径 用于引用实体,可能是在另一个模块或类型中。

生命周期和循环标签使用一种 专用语法 ,带有一个前导引号。

名称被隔离在不同的 命名空间 中,允许不同命名空间中的实体共享相同的名称而不会发生冲突。

名称解析 是将路径、标识符和标签绑定到实体声明的编译时过程。

对某些名称的访问可能会基于它们的 可见性 受到限制。

显式声明的实体

在源码中显式引入名称的实体有:

此外, 宏调用属性 可以通过展开为上述项之一来引入名称。

隐式声明的实体

以下实体由语言隐式定义,或由编译器选项和扩展引入:

此外,crate 根模块没有名称,但可以使用某些 路径限定符 或别名来引用。

命名空间

一个 命名空间 是已声明 名称 的逻辑分组。名称根据其引用的实体种类被隔离到不同的命名空间中。命名空间允许一个命名空间中出现的名称与另一个命名空间中的相同名称不发生冲突。

存在几个不同的命名空间,每个命名空间包含不同种类的实体。名称的使用将根据上下文在不同的命名空间中寻找该名称的声明,如 名称解析 章节所述。

以下是命名空间列表及其对应的实体:

关于如何在不同命名空间中使用重叠名称而不会产生歧义的示例:

#![allow(unused)]
fn main() {
// Foo 在类型命名空间中引入了一个类型,在值命名空间中引入了一个构造函数。
struct Foo(u32);

// Foo 宏在宏命名空间中声明。
macro_rules! Foo {
    () => {};
}

// f 参数类型中的 Foo 指向类型命名空间中的 Foo。
// 'Foo 在生命周期命名空间中引入了一个新的生命周期。
fn example<'Foo>(f: Foo) {
    // Foo 指向值命名空间中的 Foo 构造函数。
    let ctor = Foo;
    // Foo 指向宏命名空间中的 Foo 宏。
    Foo!{}
    // 'Foo 在标签命名空间中引入了一个标签。
    'Foo: loop {
        // 'Foo 指向 'Foo 生命周期参数,而 Foo 指向类型命名空间。
        let x: &'Foo Foo;
        // 'Foo 指向该标签。
        break 'Foo;
    }
}
}

无命名空间的具名实体

以下实体具有显式名称,但这些名称不属于任何特定的命名空间。

字段

尽管结构体、枚举和联合体字段是具名的,但这些具名字段并不存在于显式命名空间中。它们只能通过 字段表达式 访问,该表达式仅检查正在访问的特定类型的字段名称。

Use声明

一个 use声明 具有导入到作用域中的具名别名,但 use 项本身并不属于特定命名空间。相反,它可以根据正在导入的项种类,将别名引入到多个命名空间中。

子命名空间

宏命名空间分为两个子命名空间:一个用于 感叹号风格宏 ,另一个用于 属性 。 当解析属性时,作用域内的任何感叹号风格宏都将被忽略。 反之,解析感叹号风格宏将忽略作用域内的属性宏。 这可以防止一种风格遮蔽另一种风格。

例如, cfg 属性cfg 是宏命名空间中具有相同名称的两个不同实体,但它们仍然可以在各自的上下文中使用。

注意

use 导入仍然不能在模块或块中创建相同名称的重复绑定,无论子命名空间如何。

#[macro_export]
macro_rules! mymac {
    () => {};
}

use myattr::mymac; // error[E0252]: 名称 `mymac` 被多次定义。

作用域

一个 作用域 是源代码中一个被命名的 实体 可以通过该名称被引用的区域。 以下部分提供了有关作用域规则和行为的详细信息,这些规则和行为取决于实体的种类及其声明位置。 名称如何解析为实体的过程在 名称解析 章节中描述。 有关用于运行析构函数的 “drop scopes” 的更多信息,可以在 析构函数 章节中找到。

项作用域

直接在 模块 中声明的 的名称,其作用域从模块开始延伸到模块结束。这些项也是模块的成员,可以通过从其模块开始的 路径 来引用。

作为 语句 声明的项的名称,其作用域从该项语句所在的块开始延伸到该块结束。

在同一个模块或块中,在同一个 命名空间 内引入与另一个项同名的项是错误的。 星号通配符导入 在处理重复名称和遮蔽方面具有特殊行为,详见链接章节。

模块中的项可能会遮蔽 预导入 中的项。

外部模块中的项名称在嵌套模块的作用域内不可见。 路径 可用于引用另一个模块中的项。

关联项作用域

关联项 没有作用域,只能通过使用从它们关联的类型或 特型 开始的 路径 来引用。 方法 也可以通过 调用表达式 来引用。

与模块或块内的项类似,在 特型 或实现中引入与该 特型 或实现中同一命名空间内的另一个项重复的项是错误的。

模式绑定作用域

局部变量 模式 绑定的作用域取决于它的使用位置:

  • let 语句 绑定的范围从 let 语句之后直到声明它的块结束。
  • for 绑定在循环体内部。

局部变量作用域不会延伸到项声明中。

模式绑定遮蔽

模式绑定允许遮蔽作用域内的任何名称,但以下情况除外(属于错误):

以下示例说明了局部绑定如何遮蔽项声明:

#![allow(unused)]
fn main() {
fn shadow_example() {
    // 由于目前作用域内没有局部变量,这会解析为函数。
    foo(); // 打印 `function`
    let foo = || println!("closure");
    fn foo() { println!("function"); }
    // 这会解析为局部闭包,因为它遮蔽了该项。
    foo(); // 打印 `closure`
}
}

泛型参数作用域

泛型参数在 GenericParams 列表中声明。 泛型参数的作用域在声明它的项之内。

所有参数在泛型参数列表中都是可见的,无论其声明顺序如何。 以下展示了一些参数在声明之前被引用的示例:

#![allow(unused)]
fn main() {
// 'b 界限在声明之前就被引用了。
fn params_scope<'a: 'b, 'b>() {}

trait SomeTrait<const Z: usize> {}
// 常量 N 在声明之前就在特型界限中被引用了。
fn f<T: SomeTrait<N>, const N: usize>() {}
}

泛型参数在类型界限和 where 子句中也在作用域内,例如:

#![allow(unused)]
fn main() {
trait SomeTrait<'a, T> {}
// SomeTrait 的 <'a, U> 引用了 bounds_scope 的 'a 和 U 参数。
fn bounds_scope<'a, T: SomeTrait<'a, U>, U>() {}

fn where_scope<'a, T, U>()
    where T: SomeTrait<'a, U>
{}
}

在函数内部声明的 引用其外部作用域的泛型参数是错误的。

#![allow(unused)]
fn main() {
fn example<T>() {
    fn inner(x: T) {} // 错误:不能使用外部函数的泛型参数
}
}

泛型参数遮蔽

遮蔽泛型参数是错误的,但函数内部声明的项允许遮蔽函数的泛型参数名称。

#![allow(unused)]
fn main() {
fn example<'a, T, const N: usize>() {
    // 函数内部的项允许遮蔽作用域内的泛型参数。
    fn inner_lifetime<'a>() {} // OK
    fn inner_type<T>() {} // OK
    fn inner_const<const N: usize>() {} // OK
}
}
#![allow(unused)]
fn main() {
trait SomeTrait<'a, T, const N: usize> {
    fn example_lifetime<'a>() {} // 错误:'a 已经在使用中
    fn example_type<T>() {} // 错误:T 已经在使用中
    fn example_const<const N: usize>() {} // 错误:N 已经在使用中
    fn example_mixed<const T: usize>() {} // 错误:T 已经在使用中
}
}

生命周期作用域

生命周期参数在 GenericParams 列表和 高阶特型界限 中声明。

'static 生命周期和 占位符生命周期 '_ 具有特殊含义,不能声明为参数。

生命周期泛型参数作用域

常量静态 项以及 常量上下文 仅允许 'static 生命周期引用,因此它们内部不能有其他生命周期。 关联常量 确实允许引用其 特型 或实现中声明的生命周期。

高阶特型界限作用域

声明为 高阶特型界限 的生命周期参数的作用域取决于它的使用场景。

  • 作为 TypeBoundWhereClauseItem,声明的生命周期在类型和类型界限中处于作用域内。
  • 作为 TraitBound,声明的生命周期在界限类型路径中处于作用域内。
  • 作为 BareFunctionType,声明的生命周期在函数参数和返回类型中处于作用域内。
#![allow(unused)]
fn main() {
trait Trait<'a>{}

fn where_clause<T>()
    // 'a 在类型和类型界限中都在作用域内。
    where for <'a> &'a T: Trait<'a>
{}

fn bound<T>()
    // 'a 在界限内处于作用域内。
    where T: for <'a> Trait<'a>
{}

struct Example<'a> {
    field: &'a u32
}

// 'a 在参数和返回类型中都在作用域内。
type FnExample = for<'a> fn(x: Example<'a>) -> Example<'a>;
}

Impl trait限制

Impl trait 类型只能引用在函数或实现上声明的生命周期。

#![allow(unused)]
fn main() {
trait Trait1 {
    type Item;
}
trait Trait2<'a> {}

struct Example;

impl Trait1 for Example {
    type Item = Element;
}

struct Element;
impl<'a> Trait2<'a> for Element {}

// 这里的 impl Trait2 不允许引用 'b,但允许引用 'a。
fn foo<'a>() -> impl for<'b> Trait1<Item = impl Trait2<'a> + use<'a>> {
    // ...
   Example
}
}

循环标签作用域

循环标签 可以由 循环表达式 声明。 循环标签的作用域从它被声明的那一点开始,直到循环表达式结束。 该作用域不会延伸到 闭包异步块常量参数常量上下文 以及定义它的 for 循环 的迭代器表达式中。

#![allow(unused)]
fn main() {
'a: for n in 0..3 {
    if n % 2 == 0 {
        break 'a;
    }
    fn inner() {
        // 在这里使用 'a 将是一个错误。
        // break 'a;
    }
}

// 标签在 while 循环的表达式中处于作用域内。
'a: while break 'a {}         // 循环不运行。
'a: while let _ = break 'a {} // 循环不运行。

// 标签在定义它的 for 循环中不在作用域内:
'a: for outer in 0..5 {
    // 这将中断外部循环,跳过内部循环并停止外部循环。
    'a: for inner in { break 'a; 0..1 } {
        println!("{}", inner); // 这不会运行。
    }
    println!("{}", outer); // 这也不会运行。
}

}

循环标签可能会遮蔽外部作用域中同名的标签。 对标签的引用指向最近的定义。

#![allow(unused)]
fn main() {
// 循环标签遮蔽示例。
'a: for outer in 0..5 {
    'a: for inner in 0..5 {
        // 这会终止内部循环,但外部循环继续运行。
        break 'a;
    }
}
}

预导入作用域

预导入 将实体引入每个模块的作用域。 这些实体不是模块的成员,但在 名称解析 期间会被隐式查询。

预导入名称可能会被模块中的声明遮蔽。

预导入是分层的,如果它们包含同名的实体,则其中一个会遮蔽另一个。 预导入可能遮蔽其他预导入的顺序如下,其中前面的条目可以遮蔽后面的条目:

  1. 外部预导入
  2. 工具预导入
  3. macro_use 预导入
  4. 标准库预导入
  5. 语言预导入

macro_rules作用域

macro_rules 宏的作用域在 声明宏 章节中描述。 其行为取决于 macro_usemacro_export 属性的使用。

派生宏辅助属性

派生宏辅助属性 在指定了其相应 derive 属性 的项中处于作用域内。 该作用域从 derive 属性之后延伸到该项结束。

辅助属性会遮蔽作用域内同名的其他属性。

Self作用域

虽然 Self 是一个具有特殊含义的关键字,但它与名称解析的交互方式类似于普通名称。

结构体枚举联合体特型实现 的定义中的隐式 Self 类型被视为类似于 泛型参数,其作用域与泛型类型参数相同。

实现 的值 命名空间 中的隐式 Self 构造函数在实现体(实现的 关联项)内处于作用域内。

#![allow(unused)]
fn main() {
// 结构体定义内的 Self 类型。
struct Recursive {
    f1: Option<Box<Self>>
}

// 泛型参数内的 Self 类型。
struct SelfGeneric<T: Into<Self>>(T);

// 实现内的 Self 值构造函数。
struct ImplExample();
impl ImplExample {
    fn example() -> Self { // Self 类型
        Self() // Self 值构造函数
    }
}
}

预导入

一个 预导入 是自动引入到 crate 中每个模块作用域的名称集合。

这些 预导入 名称本身不是模块的一部分:它们在 名称解析 期间被隐式查询。例如,尽管像 Box 这样的东西在每个模块的作用域内,你也不能将其引用为 self::Box,因为它不是当前模块的成员。

有几种不同的 预导入:

标准库预导入

每个 crate 都有一个标准库预导入,它由单个标准库模块中的名称组成。

所使用的模块取决于 crate 的 版次,以及是否对 crate 应用了 no_std 属性

外部预导入

在根模块中使用 extern crate 导入或提供给编译器(如使用 rustc--extern 标志)的外部 crate 会被添加到 外部预导入 中。如果使用别名导入(如 extern crate orig_name as new_name),那么符号 new_name 将被添加到 预导入 中。

core crate 总是被添加到 外部预导入 中。

只要在 crate 根中没有指定 no_std 属性std crate 就会被添加。

2018 版次差异

在 2015 版次中,外部预导入中的 crate 不能通过 use 声明 引用,因此通常的标准做法是包含 extern crate 声明来将它们引入作用域。

从 2018 版次开始,use 声明 可以引用 外部预导入 中的 crate,因此使用 extern crate 被认为是不符合习惯的。

注意

rustc 一起提供的其他 crate,例如 alloctest,在使用 Cargo 时不会通过 --extern 标志自动包含。即使在 2018 版次中,也必须通过 extern crate 声明将它们引入作用域。

#![allow(unused)]
fn main() {
extern crate alloc;
use alloc::rc::Rc;
}

Cargo 仅会为过程宏 crate 将 proc_macro 引入 外部预导入。

no_std属性

no_std 属性 会导致 std crate 不被自动链接,标准库预导入 转而使用 core 预导入,且 macro_use 预导入 转而使用从 core crate 导出的宏。

例子

#![no_std]

注意

当 crate 的目标平台不支持标准库,或者是有意不使用标准库的功能时,使用 no_std 很有用。这些功能主要是动态内存分配(例如 BoxVec)以及文件和网络功能(例如 std::fsstd::io)。

警告

使用 no_std 并不阻止链接标准库。在 crate 或其依赖项之一中编写 extern crate std 仍然是有效的;这将导致编译器将 std crate 链接到程序中。

no_std 属性使用 MetaWord 语法格式。

no_std 属性只能应用于 crate 根。

no_std 属性可以在一个形式上使用任意次数。

注意

rustc 会对第一次之后的任何使用发出 lint。

no_std 属性将 标准库预导入 更改为使用 core 预导入而不是 std 预导入。

默认情况下,从 std crate 导出的所有宏都会被添加到 macro_use 预导入 中。如果指定了 no_std 属性,那么从 core crate 导出的所有宏将转而被放入 macro_use 预导入 中。

2018 版次差异

在 2018 版次之前,std 默认注入到 crate 根中。如果指定了 no_std,则注入 core 代替。从 2018 版次开始,无论是否指定 no_std,都不会将两者注入到 crate 根中。

语言预导入

语言预导入包含了语言内置的类型和属性名称。语言预导入始终在作用域内。

它包含以下内容:

macro_use预导入

macro_use 预导入包含了从应用了 macro_use 属性extern crate 导入的外部 crate 中的宏。

工具预导入

工具预导入包含了 类型命名空间 中外部工具的工具名称。有关更多详细信息,请参阅 工具属性 部分。

no_implicit_prelude属性

no_implicit_prelude 属性 用于防止隐式 预导入 被引入作用域。

例子

#![allow(unused)]
fn main() {
// 该属性可以应用于 crate 根以影响
// 所有模块。
#![no_implicit_prelude]

// 或者它可以应用于一个模块,仅影响该模块
// 及其后代。
#[no_implicit_prelude]
mod example {
    // ...
}
}

no_implicit_prelude 属性使用 MetaWord 语法格式。

no_implicit_prelude 属性只能应用于 crate 或模块。

注意

rustc 会忽略在其他位置的使用,但会对其发出 lint。这在未来可能会变成一个错误。

no_implicit_prelude 属性可以在一个形式上使用任意次数。

注意

rustc 会对第一次之后的任何使用发出 lint。

no_implicit_prelude 属性阻止 标准库预导入外部预导入macro_use 预导入工具预导入 被引入到该模块及其后代的作用域中。

no_implicit_prelude 属性不影响 语言预导入

2018 版次差异

在 2015 版次中,no_implicit_prelude 属性不影响 macro_use 预导入,并且从标准库导出的所有宏仍包含在 macro_use 预导入中。从 2018 版次开始,该属性确实会移除 macro_use 预导入。

路径

一个 路径 是由 :: 词法单元分隔的一个或多个路径段组成的序列。路径用于引用 、值、 类型属性

两个仅由标识符段组成的简单路径示例:

x;
x::y::z;

路径的类型

简单路径

Syntax
SimplePath
    ::? SimplePathSegment ( :: SimplePathSegment )*

SimplePathSegment
    IDENTIFIER | super | self | crate | $crate

简单路径用于 可见性 标记、 属性声明宏use 项。例如:

#![allow(unused)]
fn main() {
use std::io::{self, Write};
mod m {
    #[clippy::cyclomatic_complexity = "0"]
    pub (in super) fn f1() {}
}
}

表达式中的路径

Syntax
PathInExpression
    ::? PathExprSegment ( :: PathExprSegment )*

PathExprSegment
    PathIdentSegment ( :: GenericArgs )?

PathIdentSegment
    IDENTIFIER | super | self | Self | crate | $crate

GenericArgs
      < >
    | < ( GenericArg , )* GenericArg ,? >

GenericArg
    Lifetime | Type | GenericArgsConst | GenericArgsBinding | GenericArgsBounds

GenericArgsConst
      BlockExpression
    | LiteralExpression
    | - LiteralExpression
    | SimplePathSegment

GenericArgsBinding
    IDENTIFIER GenericArgs? = Type

GenericArgsBounds
    IDENTIFIER GenericArgs? : TypeParamBounds

表达式中的路径允许指定带有泛型参数的路径。它们被用于 表达式模式 的各个地方。

在泛型参数的开口 < 之前需要 :: 词法单元,以避免与小于运算符产生歧义。这被通俗地称为 “turbofish” 语法格式。

#![allow(unused)]
fn main() {
(0..10).collect::<Vec<_>>();
Vec::<u8>::with_capacity(1024);
}

泛型参数的顺序限制为生命周期参数,然后是类型参数,接着是常量参数,最后是等值约束。

常量参数必须用大括号括起来,除非它们是 字面量推断常量 或单段路径。 推断常量 不得用大括号括起来。

#![allow(unused)]
fn main() {
mod m {
    pub const C: usize = 1;
}
const C: usize = m::C;
fn f<const N: usize>() -> [u8; N] { [0; N] }

let _ = f::<1>(); // 字面量。
let _: [_; 1] = f::<_>(); // 推断常量。
let _: [_; 1] = f::<(((_)))>(); // 推断常量。
let _ = f::<C>(); // 单段路径。
let _ = f::<{ m::C }>(); // 多段路径必须用大括号包围。
}
#![allow(unused)]
fn main() {
fn f<const N: usize>() -> [u8; N] { [0; _] }
let _: [_; 1] = f::<{ _ }>();
//                    ^ ERROR `_` not allowed here
}

注意

在泛型参数列表中, 推断常量 被解析为 推断类型,但在语义上被视为一种单独的 常量泛型参数

对应于 impl 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();  // 调用固有实现。
<S as T1>::f();  // 调用 T1 特型函数。
<S as T2>::f();  // 调用 T2 特型函数。
}

类型中的路径

类型路径用于类型定义、特型界限、类型参数界限和限定路径中。

虽然在泛型参数之前允许使用 :: 词法单元,但它不是必需的,因为不像在 PathInExpression 中那样存在歧义。

#![allow(unused)]
fn main() {
mod ops {
    pub struct Range<T> {f1: T}
    pub trait Index<T> {}
    pub struct Example<'a> {f1: &'a i32}
}
struct S;
impl ops::Index<ops::Range<usize>> for S { /*...*/ }
fn i<'a>() -> impl Iterator<Item = ops::Example<'a>> {
    // ...
   const EXAMPLE: Vec<ops::Example<'static>> = Vec::new();
   EXAMPLE.into_iter()
}
type G = std::boxed::Box<dyn std::ops::FnOnce(isize) -> isize>;
}

路径限定符

路径可以用各种前导限定符来表示,以改变其解析方式的含义。

::

:: 开头的路径被认为是 全局路径,其路径段的解析起始位置因版次而异。路径中的每个标识符必须解析为一个项。

2018 版次差异

在 2015 版次中,标识符从 “crate 根”(2018 版次中的 crate::)开始解析,它包含各种不同的项,包括外部 crate、默认 crate(如 stdcore)以及 crate 顶层的项(包括 use 导入)。

从 2018 版次开始,以 :: 开头的路径从 外部预导入 中的 crate 开始解析。也就是说,它们后面必须紧跟一个 crate 的名称。

#![allow(unused)]
fn main() {
pub fn foo() {
    // 在 2018 版次中,这通过外部预导入访问 `std`。
    // 在 2015 版次中,这通过 crate 根访问 `std`。
    let now = ::std::time::Instant::now();
    println!("{:?}", now);
}
}
// 2015 版次
mod a {
    pub fn foo() {}
}
mod b {
    pub fn foo() {
        ::a::foo(); // 调用 a 的 foo 函数
        // 在 Rust 2018 中,`::a` 将被解释为 crate `a`。
    }
}
fn main() {}

self

self 相对于当前模块解析路径。

self 只能用作第一段,且前面不能有 ::

在方法体中,仅由单个 self 段组成的路径解析为该方法的 self 参数。

fn foo() {}
fn bar() {
    self::foo();
}
struct S(bool);
impl S {
  fn baz(self) {
        self.0;
    }
}
fn main() {}

Self

首字母大写的 Self 用于引用当前正在实现或定义的类型。它可以用于以下情况:

  • 特型 定义中,它引用实现该特型的类型。
  • 结构体枚举联合体 的定义中,它引用正在被定义的类型。 定义不允许无限递归(必须有间接寻址)。

Self 的作用域行为类似于泛型参数;有关更多详细信息,请参阅 Self 作用域 部分。

Self 只能用作第一段,且前面不能有 ::

Self 路径不能包含泛型参数(如 Self::<i32>)。

#![allow(unused)]
fn main() {
trait T {
    type Item;
    const C: i32;
    // `Self` 将是实现 `T` 的任何类型。
    fn new() -> Self;
    // `Self::Item` 将是实现中的类型别名。
    fn f(&self) -> Self::Item;
}
struct S;
impl T for S {
    type Item = i32;
    const C: i32 = 9;
    fn new() -> Self {           // `Self` 是类型 `S`。
        S
    }
    fn f(&self) -> Self::Item {  // `Self::Item` 是类型 `i32`。
        Self::C                  // `Self::C` 是常量值 `9`。
    }
}

// `Self` 在特型定义的泛型作用域内,
// 用以引用正在定义的类型。
trait Add<Rhs = Self> {
    type Output;
    // `Self` 也可以引用
    // 正在实现的类型的关联项。
    fn add(self, rhs: Rhs) -> Self::Output;
}

struct NonEmptyList<T> {
    head: T,
    // 结构体可以引用自身(只要它不是
    // 无限递归的)。
    tail: Option<Box<Self>>,
}
}

super

路径中的 super 解析为父模块。

它只能用于路径的前导段,可能在初始的 self 段之后。

mod a {
    pub fn foo() {}
}
mod b {
    pub fn foo() {
        super::a::foo(); // 调用 a 的 foo 函数
    }
}
fn main() {}

super 可以在第一个 superself 之后重复多次,以引用祖先模块。

mod a {
    fn foo() {}

    mod b {
        mod c {
            fn foo() {
                super::super::foo(); // 调用 a 的 foo 函数
                self::super::super::foo(); // 调用 a 的 foo 函数
            }
        }
    }
}
fn main() {}

crate

crate 相对于当前 crate 解析路径。

crate 只能用作第一段,且前面不能有 ::

fn foo() {}
mod a {
    fn bar() {
        crate::foo();
    }
}
fn main() {}

$crate

$crate 仅在 宏转录器 中使用,并且只能用作第一段,且前面不能有 ::

$crate 将展开为访问宏定义所在 crate 顶层项的路径,无论宏在哪个 crate 中被调用。

pub fn increment(x: u32) -> u32 {
    x + 1
}

#[macro_export]
macro_rules! inc {
    ($x:expr) => ( $crate::increment($x) )
}
fn main() { }

规范路径

在模块或实现中定义的项具有一个 规范路径,该路径对应于它在其 crate 中定义的位置。

指向这些项的所有其他路径都是别名。

规范路径被定义为一个 路径前缀 加上项本身定义的路径段。

实现使用声明 没有规范路径,尽管实现定义的项确实有规范路径。在块表达式中定义的项没有规范路径。在没有规范路径的模块中定义的项没有规范路径。在引用了没有规范路径的项(例如作为实现类型、正在实现的特型、类型参数或类型参数界限)的实现中定义的关联项,没有规范路径。

模块的路径前缀是该模块的规范路径。

对于固有实现,它是正在实现的项的规范路径,并用 尖括号 (<>) 包围。

对于 特型实现,它是正在实现的项的规范路径,后跟 as,再后跟该特型的规范路径,全部用 尖括号 (<>) 包围。

规范路径仅在给定的 crate 内有意义。不存在跨 crate 的全局命名空间;项的规范路径仅在其 crate 内标识它。

// 注释显示了项的规范路径。

mod a { // crate::a
    pub struct Struct; // crate::a::Struct

    pub trait Trait { // crate::a::Trait
        fn f(&self); // crate::a::Trait::f
    }

    impl Trait for Struct {
        fn f(&self) {} // <crate::a::Struct as crate::a::Trait>::f
    }

    impl Struct {
        fn g(&self) {} // <crate::a::Struct>::g
    }
}

mod without { // crate::without
    fn canonicals() { // crate::without::canonicals
        struct OtherStruct; // 无

        trait OtherTrait { // 无
            fn g(&self); // 无
        }

        impl OtherTrait for OtherStruct {
            fn g(&self) {} // 无
        }

        impl OtherTrait for crate::a::Struct {
            fn g(&self) {} // 无
        }

        impl crate::a::Trait for OtherStruct {
            fn f(&self) {} // 无
        }
    }
}

fn main() {}

名称解析

名称解析 是将路径和其他标识符绑定到这些实体的声明的过程。名称被分隔到不同的 命名空间 中,允许不同命名空间中的实体在没有冲突的情况下共享相同的名称。每个名称在 作用域(即可以引用该名称的源代码区域)内有效。对名称的访问可能会根据其 可见性 受到限制。

在整个编译过程中,名称解析分为三个阶段。第一个阶段, * 展开时解析 *,解析所有的 use 声明宏调用。第二个阶段, * 初级解析 *,解析所有尚未解析且不依赖于类型信息来解析的名称。最后一个阶段, * 类型相关解析 *,一旦类型信息可用,就解析剩余的名称。

注意

展开时解析 也被称为 * 早期解析 *。 初级解析 也被称为 * 晚期解析 *。

通用

本节中的规则适用于名称解析的所有阶段。

作用域

注意

这是未来关于各种作用域内名称解析扩展的占位符。

展开时名称解析

展开时名称解析是完成宏展开并完全生成 crate 的 AST 所必需的名称解析阶段。此阶段需要解析宏调用和 use 声明。解析 use 声明是解析通过 基于路径的作用域 的宏调用所必需的。为了展开宏调用,必须先解析它们。

在展开时名称解析之后, AST 不得包含任何未展开的宏调用。每个宏调用都必须解析为存在于最终 AST 中或外部 crate 中的有效定义。

#![allow(unused)]
fn main() {
m!(); // 错误:在此作用域内找不到宏 `m`。
}

名称的解析必须是稳定的。展开后,完全展开的 AST 中的名称必须解析为相同的定义,无论宏展开和导入解析的顺序如何。

宏展开期间选择的所有名称解析候选者都被视为推测性的。一旦 crate 被完全展开,所有的推测性导入解析都会被验证,以确保宏展开没有引入任何新的歧义。

注意

由于宏展开的迭代性质,这会导致所谓的时空穿越歧义,例如当宏或通配符导入引入了一个与其自身基路径歧义的项时。

fn main() {}
macro_rules! f {
    () => {
        mod m {
            pub(crate) use f;
        }
    }
}
f!();

const _: () = {
    // 最初,我们将 `m` 推测性地解析为 crate 根中的模块。
    //
    // `f` 的展开在此体内部引入了第二个 `m` 模块。
    //
    // 展开时解析通过重新解析所有导入和宏调用来完成解析,
    // 发现引入的歧义并将其作为错误报告。
    m::f!(); // 错误:`m` 存在歧义。
};

导入

所有的 use 声明都在此解析阶段被完全解析。 类型相关路径 在此阶段无法解析,并将产生错误。

#![allow(unused)]
fn main() {
mod m {
    pub const C: () = ();
    pub enum E { V }
    pub type A = E;
    impl E {
        pub const C: () = ();
    }
}

// 在展开时解析的有效导入:
use m::C; // OK。
use m::E; // OK。
use m::A; // OK。
use m::E::V; // OK。

// 在类型相关解析期间解析的有效表达式:
let _ = m::A::V; // OK。
let _ = m::E::C; // OK。
}
#![allow(unused)]
fn main() {
mod m {
    pub const C: () = ();
    pub enum E { V }
    pub type A = E;
    impl E {
        pub const C: () = ();
    }
}
// 在展开时无法解析的无效类型相关导入:
use m::A::V; // 错误:未解析的导入 `m::A::V`。
use m::E::C; // 错误:未解析的导入 `m::E::C`。
}

除了受 名称解析歧义 限制的情况外,通过 外部作用域 中的 use 声明引入的名称会被内部作用域中同一命名空间内同名的候选者遮蔽。

#![allow(unused)]
fn main() {
pub mod m1 {
    pub mod ambig {
        pub const C: u8 = 1;
    }
}

pub mod m2 {
    pub mod ambig {
        pub const C: u8 = 2;
    }
}

// 这在外部作用域中引入了名称 `ambig`。
use m1::ambig;
const _: () = {
    // 这在内部作用域中遮蔽了 `ambig`。
    use m2::ambig;
    // 这里的 `ambig` 解析选择内部候选者。
    use ambig::C;
    assert!(C == 2);
};
}

在单个作用域内,允许对通过 use 声明引入的名称进行遮蔽的情况如下:

歧义

在展开时解析期间,某些情况下存在多个宏定义、 use 声明或模块,导入或宏调用的名称可能指向这些定义、声明或模块,而编译器无法始终如一地确定哪个候选者应该遮蔽另一个。在这些情况下不允许遮蔽,编译器会发出歧义错误。

名称不能通过有歧义的通配符导入来解析。通配符导入允许在同一命名空间中导入冲突的名称,只要不使用该名称即可。具有来自歧义通配符导入的冲突候选者的名称仍然可以被非通配符导入遮蔽,并在不产生错误的情况下使用。错误发生在使用的时刻,而不是导入的时刻。

#![allow(unused)]
fn main() {
mod m1 {
    pub struct Ambig;
}

mod m2 {
    pub struct Ambig;
}

// OK:这将同一命名空间中冲突的名称引入作用域,但尚未被使用。
use m1::*;
use m2::*;

const _: () = {
    // 当使用具有冲突候选者的名称时发生错误。
    let x = Ambig; // 错误:`Ambig` 存在歧义。
}
}
#![allow(unused)]
fn main() {
mod m1 {
    pub struct Ambig;
}

mod m2 {
    pub struct Ambig;
}

use m1::*;
use m2::*; // OK:没有名称冲突。
const _: () = {
    // 这是允许的,因为解析不是通过歧义通配符进行的。
    struct Ambig;
    let x = Ambig; // OK。
};
}

允许通过多个通配符导入导入相同的名称,如果导入的是同一个项(遵循重导出),则允许使用该名称。该名称的可见性是这些导入中的最大可见性。

mod m1 {
    pub struct Ambig;
}

mod m2 {
    // 这从第二个模块重导出同一个 `Ambig` 项。
    pub use super::m1::Ambig;
}

mod m3 {
    // 这两者都导入同一个 `Ambig`。
    //
    // `Ambig` 的可见性是 `pub`,因为这是这两个 `use` 声明之间的最大可见性。
    pub use super::m1::*;
    use super::m2::*;
}

mod m4 {
    // `Ambig` 可以通过 `m3` 的通配符使用,且仍然具有 `pub` 可见性。
    pub use crate::m3::Ambig;
}

const _: () = {
    // 因此,我们可以在这里使用它。
    let _ = m4::Ambig; // OK。
};
fn main() {}

当在 外部作用域 中有另一个可用候选者时,导入和宏调用中的名称不能通过通配符导入来解析。

#![allow(unused)]
fn main() {
mod glob {
    pub mod ambig {
        pub struct Name;
    }
}

// 外部 `ambig` 候选者。
pub mod ambig {
    pub struct Name;
}

const _: () = {
    // 无法通过此通配符解析 `ambig`,
    // 因为上方有外部 `ambig` 候选者。
    use glob::*;
    use ambig::Name; // 错误:`ambig` 存在歧义。
};
}
#![allow(unused)]
fn main() {
// 如上,但使用宏。
pub mod m {
    macro_rules! f {
        () => {};
    }
    pub(crate) use f;
}
pub mod glob {
    macro_rules! f {
        () => {};
    }
    pub(crate) use f as ambig;
}

use m::f as ambig;

const _: () = {
    use glob::*;
    ambig!(); // 错误:`ambig` 存在歧义。
};
}

注意

这些歧义错误特定于展开时解析。在解析的后续阶段,为一个给定的名称提供多个候选者不被视为错误。只要导入本身没有歧义,就总是会有一个唯一的、无歧义的最近解析。

#![allow(unused)]
fn main() {
mod glob {
    pub const AMBIG: u8 = 1;
}

mod outer {
    pub const AMBIG: u8 = 2;
}

use outer::AMBIG;

const C: () = {
    use glob::*;
    assert!(AMBIG == 1);
    //      ^---- 这个 `AMBIG` 在初级解析期间解析。
};
}

名称不能通过有歧义的宏重导出进行解析。当宏重导出会遮蔽 外部作用域 中同名的文本宏候选者时,宏重导出即具有歧义。

#![allow(unused)]
fn main() {
// 文本宏候选者。
macro_rules! ambig {
    () => {}
}

// 基于路径的宏候选者。
macro_rules! path_based {
    () => {}
}

pub fn f() {
    // 将 `path_based` 宏定义重导出为 `ambig` 
    // 不得遮蔽通过文本宏作用域解析的 `ambig` 宏定义。
    use path_based as ambig;
    ambig!(); // 错误:`ambig` 存在歧义。
}
}

注意

这种限制是由于编译器中的实现细节而需要的,特别是当前的作用域访问逻辑和支持此行为的复杂性。此歧义错误可能会在未来被移除。

宏通过遍历可用作用域来查找可用候选者进行解析。宏被分为两个子命名空间,一个用于类函数宏,另一个用于属性和派生宏。来自错误子命名空间的解析候选者将被忽略。

可用作用域种类的访问顺序如下。这些作用域种类中的每一个都代表一个或多个作用域。

注意

编译器将尝试解析在其关联宏将其引入作用域之前使用的派生辅助属性。此作用域在访问已正确处于作用域内的派生辅助候选者的作用域之后被访问。此行为计划被移除。

更多信息请参见 派生辅助属性作用域

注意

此访问顺序将来可能会改变,例如根据文本和基于路径的作用域候选者的词法作用域来交替访问它们。

2018 版次差异

从 2018 版次开始,当存在 #[no_implicit_prelude] 时,不会访问 #[macro_use] 预导入。

名称 cfgcfg_attr 在宏属性 子命名空间 中被保留。

歧义

名称不能通过宏展开内部的歧义候选者进行解析。当宏展开内部的候选者会遮蔽来自第一个候选者宏展开外部的同名候选者,且正在解析的名称调用也来自第一个候选者宏展开外部时,宏展开内部的候选者即具有歧义。

#![allow(unused)]
fn main() {
macro_rules! define_ambig {
    () => {
        macro_rules! ambig {
            () => {}
        }
    }
}

// 为 `ambig` 宏调用引入外部候选者定义。
macro_rules! ambig {
    () => {}
}

// 在宏展开内部引入第二个 `ambig` 候选者定义。
define_ambig!();

// 来自 `define_ambig` 第二次调用的 `ambig` 定义是最内部的候选者。
//
// 来自 `define_ambig` 第一次调用的 `ambig` 定义是第二个候选者。
//
// 编译器检查第一个候选者是否在宏展开内部,
// 第二个候选者是否不在同一个宏展开内部,
// 以及正在解析的名称是否不在同一个宏展开内部。
ambig!(); // 错误:`ambig` 存在歧义。
}

反过来则不被视为歧义。

#![allow(unused)]
fn main() {
macro_rules! define_ambig {
    () => {
        macro_rules! ambig {
            () => {}
        }
    }
}
// 交换定义顺序。
define_ambig!();
macro_rules! ambig {
    () => {}
}
// 最内部的候选者现在展开程度较低,因此它可以遮蔽其上方的宏展开定义。
ambig!();
}

如果正在解析的调用在最内部候选者的展开范围内,也不是歧义。

#![allow(unused)]
fn main() {
macro_rules! ambig {
    () => {}
}

macro_rules! define_and_invoke_ambig {
    () => {
        // 定义最内部候选者。
        macro_rules! ambig {
            () => {}
        }

        // `ambig` 的调用与最内部候选者在同一个展开中。
        ambig!(); // OK
    }
}

define_and_invoke_ambig!();
}

即使两个定义都来自同一个宏的调用,这也没有关系;最外部的候选者仍然被视为 “展开较少”,因为它不在包含最内部候选者定义的展开范围内。

#![allow(unused)]
fn main() {
macro_rules! define_ambig {
    () => {
        macro_rules! ambig {
            () => {}
        }
    }
}
define_ambig!();
define_ambig!();
ambig!(); // 错误:`ambig` 存在歧义。
}

这也适用于导入,只要名称的最内部候选者来自宏展开。

#![allow(unused)]
fn main() {
macro_rules! define_ambig {
    () => {
        mod ambig {
            pub struct Name;
        }
    }
}

mod ambig {
    pub struct Name;
}

const _: () = {
    // 在此宏展开中引入 `ambig` 模块的最内部候选者。
    define_ambig!();
    use ambig::Name; // 错误:`ambig` 存在歧义。
};
}

用户定义的属性或派生宏不得遮蔽内置的非宏属性(例如 inline)。

// with-helper/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_derive(WithHelperAttr, attributes(non_exhaustive))]
//                                             ^^^^^^^^^^^^^^
//                                   用户定义的属性候选者。
// ...
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}
// src/lib.rs
#[derive(with_helper::WithHelperAttr)]
#[non_exhaustive] // 错误:`non_exhaustive` 存在歧义。
struct S;

注意

无论内置属性是哪个名称的候选者,这都适用:

// with-helper/src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_derive(WithHelperAttr, attributes(helper))]
//                                             ^^^^^^
//                                 用户定义的属性候选者。
// ...
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}
// src/lib.rs
use inline as helper;
//            ^----- 通过重导出的内置属性候选者。

#[derive(with_helper::WithHelperAttr)]
#[helper] // 错误:`helper` 存在歧义。
struct S;

初级名称解析

注意

这是未来关于初级名称解析扩展的占位符。

类型相关解析

注意

这是未来关于类型依赖解析扩展的占位符。

可见性和私有性

Syntax
Visibility
      pub
    | pub ( crate )
    | pub ( self )
    | pub ( super )
    | pub ( in SimplePath )

Visibility pub pub ( crate ) pub ( self ) pub ( super ) pub ( in SimplePath )

这两个术语经常互换使用,它们试图传达的是对 “这个 项 是否可以在此位置使用?” 这一问题的回答。

Rust 的名称解析在命名空间的全局层级结构上运行。层级结构中的每一层都可以被看作是某个 项 。这些 项 是上述提到的那些,但也包括外部 crate 。声明或定义一个新模块可以被看作是在定义所在的位置向层级结构中插入一棵新树。

为了控制接口是否可以跨模块使用,Rust 会检查每个 项 的使用,看它是否应该被允许。这就是生成私有性警告的地方,或者说是 “你使用了另一个模块的私有 项 ,这是不被允许的。”

默认情况下,一切都是 private (私有的),但有两个例外: pub 特型 中的关联 项 默认是公有的; pub 枚举中的枚举 变体 默认也是公有的。当一个 项 被声明为 pub 时,可以认为它是可以被外界访问的。例如:

fn main() {}
// 声明一个私有的结构体
struct Foo;

// 声明一个带有一个私有字段的公有结构体
pub struct Bar {
    field: i32,
}

// 声明一个带有两个公有变体的公有枚举
pub enum State {
    PubliclyAccessibleState,
    PubliclyAccessibleState2,
}

有了 项 是公有或私有这一概念,Rust 在两种情况下允许 项 访问:

  1. 如果一个 项 是公有的,那么如果你可以从模块 m 访问该 项 的所有祖先模块,那么它就可以从模块 m 外部访问。你还可能能够通过重导出为该 项 命名。见下文。
  2. 如果一个 项 是私有的,它可以被当前模块及其后代访问。

这两条规则在创建暴露公有 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 必须是一个简单路径,解析为声明其可见性的 项 的祖先模块。 path 中的每个标识符必须直接引用一个模块(而不是由 use 语句引入的名称)。
  • pub(crate) 使一个 项 在当前 crate 内可见。
  • pub(super) 使一个 项 对父模块可见。这等同于 pub(in super)
  • pub(self) 使一个 项 对当前模块可见。这等同于 pub(in self) 或根本不使用 pub

2018 版次差异

从 2018 版次开始, pub(in path) 的路径必须以 crateself ,或 super 开头。2015 版次还可以使用以 :: 开头的路径或来自 crate 根部的模块。

这里有一个例子:

pub mod outer_mod {
    pub mod inner_mod {
        // 这个函数在 `outer_mod` 内可见
        pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
        // 与上面相同,这仅在 2015 版次中有效。
        pub(in outer_mod) fn outer_mod_visible_fn_2015() {}

        // 这个函数对整个 crate 可见
        pub(crate) fn crate_visible_fn() {}

        // 这个函数在 `outer_mod` 内可见
        pub(super) fn super_mod_visible_fn() {
            // 这个函数是可见的,因为我们在同一个 `mod` 中
            inner_mod_visible_fn();
        }

        // 这个函数仅在 `inner_mod` 内可见,
        // 这与保持其私有是一样的。
        pub(self) fn inner_mod_visible_fn() {}
    }
    pub fn foo() {
        inner_mod::outer_mod_visible_fn();
        inner_mod::crate_visible_fn();
        inner_mod::super_mod_visible_fn();

        // 这个函数不再可见,因为我们在 `inner_mod` 之外
        // 错误!`inner_mod_visible_fn` 是私有的
        //inner_mod::inner_mod_visible_fn();
    }
}

fn bar() {
    // 这个函数仍然可见,因为我们在同一个 crate 中
    outer_mod::inner_mod::crate_visible_fn();

    // 这个函数不再可见,因为我们在 `outer_mod` 之外
    // 错误!`super_mod_visible_fn` 是私有的
    //outer_mod::inner_mod::super_mod_visible_fn();

    // 这个函数不再可见,因为我们在 `outer_mod` 之外
    // 错误!`outer_mod_visible_fn` 是私有的
    //outer_mod::inner_mod::outer_mod_visible_fn();

    outer_mod::foo();
}

fn main() { bar() }

注意

此语法格式仅为 项 的可见性添加了另一个限制。它并不保证该 项 在指定作用域的所有部分都可见。要访问一个 项 ,直到当前作用域的所有父 项 必须仍然也是可见的。

重导出和可见性

Rust 允许通过 pub use 指令公开重导出 项 。因为这是一个公开指令,这允许根据上述规则在当前模块中使用该 项 。它本质上允许对重导出的 项 进行公有访问。例如,这个程序是有效的:

pub use self::implementation::api;

mod implementation {
    pub mod api {
        pub fn f() {}
    }
}

fn main() {}

这意味着任何引用 implementation::api::f 的外部 crate 都会收到一个私有性违规,而路径 api::f 将是被允许的。

当重导出私有 项 时,可以将其看作是允许 “私有链” 通过重导出短路,而不是像通常那样通过命名空间层级结构。

内存模型

警告

Rust 的 内存模型 尚未完善且未完全确定。

字节

Rust 中最基本的内存单位是字节。

注意

虽然字节通常被映射到硬件字节,但 Rust 使用一种“抽象”的字节概念,可以做出硬件中不存在的区分,例如未初始化,或存储指针的一部分。这些区分会影响你的程序是否具有未定义行为,因此它们仍然对编译后的 Rust 程序的行为产生实际影响。

每个字节可能具有以下值之一:

  • 一个包含 u8 值和可选的 来源 的已初始化字节,
  • 一个未初始化字节。

注意

以上列表尚未保证是详尽的。

内存分配和生命周期

程序的 项 是那些在编译时计算其值并唯一地存储在 Rust 进程内存映像中的函数、模块和类型。 项 既不动态分配也不释放。

堆 是描述 盒子的通用术语。 堆中分配的生命周期取决于指向它的 盒子 值的生命周期。 由于 盒子 值本身可以在帧之间传递进出,或者存储在 堆中,因此 堆分配 可能会超出它们被分配的帧的生命周期。 堆中的一个分配 在其整个生命周期内保证驻留在 堆中的一个单一位置——它永远不会因为移动 盒子 值而重新定位。

变量

一个 变量 是 栈帧 的一个组成部分,它可以是一个具名函数 参数,一个匿名 临时量,或者一个具名 局部变量。

一个 局部变量 (或 栈-局部 分配)直接持有值,在 栈内存 中分配。该值是 栈帧 的一部分。

局部变量 默认不可变,除非另行声明。例如:let mut x = ...

函数 参数 默认不可变,除非用 mut 声明。mut 关键字仅适用于其后的 参数。例如:|mut x, y|fn f(mut x: Box<i32>, y: Box<i32>) 声明了一个 可变 变量 x 和一个 不可变 变量 y

局部变量 在分配时不会被初始化。相反,整个 栈帧 上的 局部变量 会在 栈帧 进入时以未初始化状态分配。函数内的后续语句可能会或可能不会初始化 局部变量。局部变量 只能在它们通过所有可达的 控制流路径 被初始化后才能使用。

在下一个示例中,init_after_ifif 表达式 之后被初始化,而 uninit_after_if 没有,因为它在 else 分支中未被初始化。

#![allow(unused)]
fn main() {
fn random_bool() -> bool { true }
fn initialization_example() {
    let init_after_if: ();
    let uninit_after_if: ();

    if random_bool() {
        init_after_if = ();
        uninit_after_if = ();
    } else {
        init_after_if = ();
    }

    init_after_if; // ok
    // uninit_after_if; // 错误:使用了可能未初始化的 `uninit_after_if`
}
}

恐慌

Rust 提供了一种机制来防止函数正常返回,而是引发 “恐慌 (panic)”。这是对错误条件的响应,通常在遇到错误的上下文中被认为无法恢复。

一些语言结构,例如越界的 数组索引,会自动引发恐慌。

还有一些语言特性提供了一定程度的恐慌行为控制:

注意

标准库提供了通过 panic! 宏 显式引发恐慌的能力。

panic_handler属性

panic_handler 属性 可以应用于函数以定义恐慌的行为。

panic_handler 属性只能应用于具有签名 fn(&PanicInfo) -> ! 的函数。

注意

PanicInfo 结构体 包含了有关恐慌发生位置的信息。

在依赖图中必须有且仅有一个 panic_handler 函数。

下面展示了一个记录恐慌消息然后停止线程的 panic_handler 函数。

#![no_std]

use core::fmt::{self, Write};
use core::panic::PanicInfo;

struct Sink {
    // ..
   _0: (),
}

impl Sink {
    fn new() -> Sink { Sink { _0: () }}
}

impl fmt::Write for Sink {
    fn write_str(&mut self, _: &str) -> fmt::Result { Ok(()) }
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    let mut sink = Sink::new();

    // 将 "panicked at '$reason', src/main.rs:27:4" 记录到某个 `sink`
    let _ = writeln!(sink, "{}", info);

    loop {}
}

标准行为

std 提供了两种不同的恐慌处理器:

  • unwind — 展开栈,并且是潜在可恢复的。
  • abort –– 中止进程,且是不可恢复的。

并非所有 target 都提供 unwind 处理器。

注意

链接 std 时使用的恐慌处理器可以通过 -C panic 命令行标志设置。大多数 target 的默认值是 unwind

标准库的恐慌行为可以在运行时使用 std::panic::set_hook 函数进行修改。

链接 no_std 二进制文件、dylib、cdylib 或 staticlib 时,需要指定你自己的恐慌处理器。

恐慌策略

恐慌策略 (panic strategy) 定义了一个 crate 构建时支持的恐慌行为类型。

注意

恐慌策略可以在 rustc 中使用 -C panic 命令行标志进行选择。

当生成二进制文件、dylib、cdylib 或 staticlib 且链接 std 时, -C panic 命令行标志也会影响使用哪个 恐慌处理器

注意

当使用 abort 恐慌策略编译代码时,优化器可能会假设跨 Rust 栈帧的展开是不可能的,这可以提高代码大小和运行时速度。

注意

有关链接具有不同恐慌策略的 crate 的限制,请参见 link.unwinding。其中一个暗示是,使用 unwind 策略构建的 crate 可以使用 abort 恐慌处理器,但 abort 策略不能使用 unwind 恐慌处理器。

展开

引发恐慌可能是可恢复的或不可恢复的,尽管可以配置(通过选择不展开的恐慌处理器)为始终不可恢复。(反之则不然:unwind 处理器不保证所有恐慌都是可恢复的,仅保证通过 panic! 宏及类似的标准库机制引发的恐慌是可恢复的。)

当发生恐慌时,unwind 处理器会 “展开 (unwind)” Rust 栈帧,就像 C++ 的 throw 展开 C++ 栈帧一样,直到恐慌到达恢复点(例如在线程边界处)。这意味着当恐慌遍历 Rust 栈帧时,这些栈帧中 实现 Drop 的活跃对象将调用其 drop 方法。因此,当恢复正常执行时,不再可访问的对象将被 “清理” 掉,就像它们正常超出作用域一样。

注意

只要保留了这种资源清理的保证, “展开” 的实现可以不实际使用 target 平台 C++ 所使用的机制。

注意

标准库提供了两种从恐慌中恢复的机制:std::panic::catch_unwind(允许在发生恐慌的线程内恢复)和 std::thread::spawn(自动为生成的线程设置恐慌恢复,以便其他线程可以继续运行)。

跨FFI边界展开

可以使用 适当的 ABI 声明 跨 FFI 边界进行展开。虽然在某些情况下很有用,但这为未定义行为创造了独特的机会,特别是在涉及多个语言运行时时。

使用错误的 ABI 进行展开是未定义行为:

  • 从通过不带展开的 ABI(如 "C""system" 等)声明的函数声明或指针调用的外部函数,引发指向 Rust 代码的展开。(例如,这种情况发生在用 C++ 编写的此类函数抛出未捕获且传播到 Rust 的异常时。)
  • 在不支持展开的代码(例如使用 GCC 或 Clang 并配合 -fno-exceptions 编译的代码)中调用会展开(带有 extern "C-unwind" 或其他允许展开的 ABI)的 Rust extern 函数。

使用 std::panic::catch_unwindstd::thread::JoinHandle::join 或让外部展开操作传播到 Rust main() 函数或线程根部之外来捕获它(例如 C++ 异常),将产生以下两种行为之一,且未指定具体会发生哪种:

  • 进程中止。
  • 函数返回一个包含不透明类型的 Result::Err

注意

就此项保证而言,使用不同的 Rust 标准库实例编译或链接的 Rust 代码算作 “外部异常” 。因此,如果一个使用了 panic! 且链接到一个版本 Rust 标准库的库,被一个使用不同版本标准库的应用程序调用,即使该库仅在子线程中使用,也可能导致整个应用程序中止。

目前对于外部运行时尝试处理或重新抛出 Rust panic 负载时的行为没有任何保证。换句话说,起源于 Rust 运行时的展开必须要么导致进程终止,要么被同一个运行时捕获。

链接

注意

本节更多是从编译器而非语言本身的角度进行描述。

编译器支持静态和动态链接 crate 的多种方法。本节将探讨链接 crate 的各种方法,有关原生库的更多信息可以在 本书的 FFI 章节 中找到。

在一次编译会话中,编译器可以通过使用命令行标志或 crate_type 属性生成多个产物。如果指定了一个或多个命令行标志,则所有的 crate_type 属性都将被忽略,而只构建通过命令行指定的产物。

  • --crate-type=bin, #![crate_type = "bin"] - 将生成一个可运行的可执行文件。这要求 crate 中有一个 main 函数,该函数将在程序开始执行时运行。这将链接所有的 Rust 和原生依赖项,生成一个单一的可分发二进制文件。这是默认的 crate 类型。
  • --crate-type=lib, #![crate_type = "lib"] - 将生成一个 Rust 库。这是一个模棱两可的概念,因为库可以以多种形式表现。这种通用的 lib 选项的目的是生成 “compiler recommended” 风格的库。输出的库始终可以被 rustc 使用,但库的具体类型可能会不时发生变化。其余输出类型都是库的不同变体,lib 类型可以看作是其中之一的别名 (but the actual one is compiler-defined)。
  • --crate-type=dylib, #![crate_type = "dylib"] - 将生成一个动态 Rust 库。这与 lib 输出类型的不同之处在于它强制生成动态库。生成的动态库可以用作其他库和/或可执行文件的依赖项。这种输出类型将在 Linux 上创建 *.so 文件,在 macOS 上创建 *.dylib 文件,在 Windows 上创建 *.dll 文件。
  • --crate-type=staticlib, #![crate_type = "staticlib"] - 将生成一个静态系统库。这与其他库输出的不同之处在于,编译器永远不会尝试链接到 staticlib 输出。这种输出类型的目的是创建一个包含所有本地 crate 代码以及所有上游依赖项的静态库。这种输出类型将在 Linux, macOS 和 Windows (MinGW) 上创建 *.a 文件,并在 Windows (MSVC) 上创建 *.lib 文件。建议在将 Rust 代码链接到现有的非 Rust 应用程序等情况下使用此格式,因为它不会对其他 Rust 代码产生动态依赖。

    请注意,静态库可能具有的任何动态依赖项 (such as dependencies on system libraries, or dependencies on Rust libraries that are compiled as dynamic libraries) 在从某处链接该静态库时必须手动指定。 --print=native-static-libs 标志可能对此有所帮助。

    请注意,由于生成的静态库包含所有依赖项的代码 (including the standard library) ,并且还导出了它们的所有公共符号,因此将静态库链接到可执行文件或共享库可能需要特别注意。在共享库的情况下,必须通过例如链接器或符号版本脚本、导出符号列表 (macOS) 或模块定义文件 (Windows) 来限制导出符号的列表。此外,可以删除未使用的节,以删除未实际使用的所有依赖项代码 (e.g. --gc-sections or -dead_strip for macOS)。

  • --crate-type=cdylib, #![crate_type = "cdylib"] - 将生成一个动态系统库。这用于在编译要从另一种语言加载的动态库时使用。这种输出类型将在 Linux 上创建 *.so 文件,在 macOS 上创建 *.dylib 文件,在 Windows 上创建 *.dll 文件。
  • --crate-type=rlib, #![crate_type = "rlib"] - 将生成一个 “Rust library” 文件。这用作中间产物,可以被认为是 “static Rust library” 。与 staticlib 文件不同,这些 rlib 文件在未来的链接中由编译器解释。这本质上意味着 rustc 将在 rlib 文件中寻找元数据,就像在动态库中寻找元数据一样。这种输出形式用于生成静态链接的可执行文件以及 staticlib 输出。
  • --crate-type=proc-macro, #![crate_type = "proc-macro"] - 生成的输出未指定,但如果为其提供了 -L 路径,则编译器会将输出产物识别为宏,并且可以为程序加载它。使用此 crate 类型编译的 crate 只能导出 过程宏。编译器将自动设置 proc_macro 配置选项。crate 始终使用与编译器本身构建时相同的 target 进行编译。例如,如果您在带有 x86_64 CPU 的 Linux 上执行编译器,则 target 将是 x86_64-unknown-linux-gnu,即使该 crate 是为不同 target 构建的另一个 crate 的依赖项。

请注意,这些输出是可堆叠的,即如果指定了多个,则编译器将生成每种形式的输出,而无需重新编译。但是,这仅适用于通过相同方法指定的输出。如果仅指定了 crate_type 属性,则它们都将被构建,但如果指定了一个或多个 --crate-type 命令行标志,则仅构建那些输出。

对于所有这些不同种类的输出,如果 crate A 依赖于 crate B,则编译器可能会在整个系统中找到各种不同形式的 B。然而,编译器寻找的唯一形式是 rlib 格式和动态库格式。对于依赖库的这两个选项,编译器必须在某个时间点在两种格式之间做出选择。考虑到这一点,编译器在确定将使用哪种格式的依赖项时遵循以下规则:

  1. 如果正在生成静态库,则要求所有上游依赖项都必须以 rlib 格式可用。这一要求源于动态库无法转换为静态格式的原因。

    请注意,无法将原生动态依赖项链接到静态库,在这种情况下,将打印有关所有未链接的原生动态依赖项的警告。

  1. 如果正在生成 rlib 文件,则对上游依赖项可用的格式没有限制。仅要求所有上游依赖项都可用于读取元数据。

    这是因为 rlib 文件不包含其任何上游依赖项。让所有 rlib 文件都包含 libstd.rlib 的副本是非常低效的!

  1. 如果正在生成可执行文件且未指定 -C prefer-dynamic 标志,则首先尝试寻找 rlib 格式的依赖项。如果某些依赖项在 rlib 格式下不可用,则尝试动态链接 (see below)。
  1. 如果正在生成动态库或正在动态链接的可执行文件,则编译器将尝试协调以 rlib 或 dylib 格式提供的依赖项,以创建最终产品。

    编译器的主要目标是确保一个库在任何产物中都不会出现超过一次。例如,如果动态库 B 和 C 都静态链接到库 A,那么一个 crate 就不能同时链接到 B 和 C,因为那样就会有两个 A 的副本。编译器允许混合 rlib 和 dylib 格式,但必须满足这一限制。

    编译器目前没有提供暗示库应以何种格式链接的方法。在动态链接时,编译器将尝试最大化动态依赖项,同时仍允许通过 rlib 链接一些依赖项。

    对于大多数情况,如果进行动态链接,建议所有库都以 dylib 形式可用。对于其他情况,如果编译器无法确定每个库应以哪种格式链接,它将发出警告。

通常,对于所有编译需求,--crate-type=bin--crate-type=lib 应该就足够了,其他选项仅在需要对 crate 输出格式进行更精细控制时可用。

静态和动态C运行时

标准库通常力求根据需要为 target 支持静态链接和动态链接的 C 运行时。例如 x86_64-pc-windows-msvcx86_64-unknown-linux-musl target 通常带有两种运行时,用户可以选择他们喜欢的。编译器中的所有 target 都有链接到 C 运行时的默认模式。通常 target 默认是动态链接的,但也有些例外是默认静态链接的,例如:

  • arm-unknown-linux-musleabi
  • arm-unknown-linux-musleabihf
  • armv7-unknown-linux-musleabihf
  • i686-unknown-linux-musl
  • x86_64-unknown-linux-musl

C 运行时的链接配置为遵循 crt-static target 特性。这些 target 特性通常在命令行中通过编译器的标志进行配置。例如,要启用静态运行时,您可以执行:

rustc -C target-feature=+crt-static foo.rs

而要动态链接到 C 运行时,您可以执行:

rustc -C target-feature=-crt-static foo.rs

不支持在 C 运行时链接方式之间切换的 target 将忽略此标志。建议在编译器成功后检查生成的二进制文件,以确保它按预期链接。

crate 也可以了解 C 运行时是如何被链接的。例如,MSVC 上的代码需要根据链接的运行时进行不同的编译 (e.g. with /MT or /MD) 。这目前通过 cfg 属性 target_feature 选项 导出:

#![allow(unused)]
fn main() {
#[cfg(target_feature = "crt-static")]
fn foo() {
    println!("the C runtime should be statically linked");
}

#[cfg(not(target_feature = "crt-static"))]
fn foo() {
    println!("the C runtime should be dynamically linked");
}
}

另请注意,Cargo 构建脚本可以通过 环境变量 了解此特性。在构建脚本中,您可以通过以下方式检测链接:

use std::env;

fn main() {
    let linkage = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new());

    if linkage.contains("crt-static") {
        println!("the C runtime will be statically linked");
    } else {
        println!("the C runtime will be dynamically linked");
    }
}

要在本地使用此特性,您通常会使用 RUSTFLAGS 环境变量通过 Cargo 为编译器指定标志。例如,要在 MSVC 上编译静态链接的二进制文件,您可以执行:

RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-pc-windows-msvc

混合Rust和外部代码库

如果您混合使用 Rust 和外部代码 (e.g. C, C++) 并希望制作包含两种代码类型的单个二进制文件,则最终二进制链接有两种方法:

  • 使用 rustc。使用 rustc 参数 -L <directory>-l<library> 传递任何非 Rust 库,和/或在 Rust 代码中使用 #[link] 指令。如果您需要链接 *.o 文件,可以使用 -Clink-arg=file.o
  • 使用您的外部链接器。在这种情况下,您首先需要生成一个 Rust staticlib target 并将其传递给您的外部链接器调用。如果您需要链接多个 Rust 子系统,您将需要生成一个 单个 staticlib,可能需要使用大量的 extern crate 语句来包含多个 Rust rlib。多个 Rust staticlib 文件很可能会冲突。

目前不支持将 rlib 直接传递到您的外部链接器中。

注意

对于本节而言,使用不同的 Rust 运行时实例编译或链接的 Rust 代码也算作 “foreign code” 。

禁止的链接和恐慌展开

仅当二进制文件根据以下规则一致地构建时,才能使用恐慌展开。

如果满足以下任何条件,则 Rust 产物被称为 潜在展开的

  • 该产物使用 unwind 恐慌处理器
  • 该产物包含一个使用 unwind 恐慌策略 构建的 crate,该 crate 使用 -unwind ABI 调用函数。
  • 该产物对在另一个拥有独立的 Rust 运行时副本的 Rust 产物中运行的代码进行 “Rust” ABI 调用,并且该另一个产物是潜在展开的。

注意

此定义涵盖了 Rust 产物内部的 “Rust” ABI 调用是否会发生展开。

如果一个 Rust 产物是潜在展开的,那么它的所有 crate 都必须使用 unwind 恐慌策略 构建。否则,展开可能会导致未定义行为。

注意

如果您使用 rustc 进行链接,这些规则会自动执行。如果您 使用 rustc 进行链接,则必须注意确保在整个二进制文件中一致地处理展开。不使用 rustc 链接包括使用 dlopen 或类似设施,其中链接由系统运行时完成,而不涉及 rustc。这种情况仅在混合使用具有不同 -C panic 标志的代码时才会发生,因此大多数用户无需担心这一点。

注意

为了保证无论链接时使用何种恐慌运行时,库都是健全的 (and linkable with rustc) ,可以使用 ffi_unwind_calls lint 。该 lint 会标记对 -unwind 外部函数或函数指针的任何调用。

内联汇编

通过 asm!naked_asm!global_asm! 宏提供对内联汇编的支持。它可以用于在编译器生成的汇编输出中嵌入手写的汇编代码。

内联汇编支持在以下架构上是稳定的:

  • x86 和 x86-64
  • ARM
  • AArch64 和 Arm64EC
  • RISC-V
  • LoongArch
  • s390x

如果在不支持的 target 上使用汇编宏,编译器将报错。

示例

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

// 使用移位和加法将 x 乘以 6
let mut x: u64 = 4;
unsafe {
    asm!(
        "mov {tmp}, {x}",
        "shl {tmp}, 1",
        "shl {x}, 2",
        "add {x}, {tmp}",
        x = inout(reg) x,
        tmp = out(reg) _,
    );
}
assert_eq!(x, 4 * 6);
}
}

语法格式

下面的语法指定了可以传递给 asm!global_asm!naked_asm! 宏的参数。

Syntax
AsmArgsAsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?

FormatStringSTRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation

AsmAttrFormatString → ( OuterAttribute )* FormatString

AsmOperand
      ClobberAbi
    | AsmOptions
    | RegOperand

AsmAttrOperand → ( OuterAttribute )* AsmOperand

ClobberAbiclobber_abi ( Abi ( , Abi )* ,? )

AsmOptions
    options ( ( AsmOption ( , AsmOption )* ,? )? )

AsmOption
      pure
    | nomem
    | readonly
    | preserves_flags
    | noreturn
    | nostack
    | att_syntax
    | raw

RegOperand → ( ParamName = )?
    (
          DirSpec ( RegSpec ) Expression
        | DualDirSpec ( RegSpec ) DualDirSpecExpression
        | sym PathExpression
        | const Expression
        | label { Statements? }
    )

ParamNameIDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER

DualDirSpecExpression
      Expression
    | Expression => Expression

RegSpecRegisterClass | ExplicitRegister

RegisterClassIDENTIFIER_OR_KEYWORD

ExplicitRegisterSTRING_LITERAL

DirSpec
      in
    | out
    | lateout

DualDirSpec
      inout
    | inlateout

作用域

内联汇编可以以三种方式之一使用。

使用 asm! 宏,汇编代码在函数作用域内发出,并集成到编译器生成的函数汇编代码中。这些汇编代码必须遵守 严格的规则 以避免未定义行为。请注意,在某些情况下,编译器可能会选择将汇编代码作为单独的函数发出并生成对它的调用。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
unsafe { core::arch::asm!("/* {} */", in(reg) 0); }
}
}

使用 naked_asm! 宏,汇编代码在函数作用域内发出,并构成函数的完整汇编代码。naked_asm! 宏仅允许在 裸函数 中使用。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "C" fn wrapper() {
core::arch::naked_asm!("/* {} */", const 0);
}
}
}

使用 global_asm! 宏,汇编代码在全局作用域内发出,位于函数之外。这可用于使用汇编代码手写整个函数,通常可以更自由地使用任意寄存器和汇编指令。

fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("/* {} */", const 0);

模板字符串参数

汇编器模板使用与 格式化字符串 相同的语法格式(即占位符通过大括号指定)。

对应的参数按顺序、按索引或按名称访问。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
let z: i64;
// 这个
unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); }
// ... 这个
unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); }
// ... 以及这个
unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); }
// 都有相同的行为
assert_eq!(x, y);
assert_eq!(y, z);
}
}

但是,不支持(由 RFC #2795 引入的)隐式命名参数。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5;
// 我们不能直接从作用域引用 `x`,我们需要一个类似 `in(reg) x` 的操作数
unsafe { core::arch::asm!("/* {x} */"); } // ERROR: no argument named x
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

一个 asm! 调用可以有一个或多个模板字符串参数;具有多个模板字符串参数的 asm! 被处理为所有字符串都连接在一起,且它们之间有一个 \n。预期用法是每个模板字符串参数对应一行汇编代码。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
// 我们可以将多个字符串分开,就像它们是写在一起的一样
unsafe { core::arch::asm!("mov eax, 5", "mov ecx, eax", out("rax") x, out("rcx") y); }
assert_eq!(x, y);
}
}

所有模板字符串参数必须出现在任何其他参数之前。

#![allow(unused)]
fn main() {
let x = 5;
#[cfg(target_arch = "x86_64")] {
// 模板字符串需要首先出现在 asm 调用中
unsafe { core::arch::asm!("/* {x} */", x = const 5, "ud2"); } // ERROR: unexpected token
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

与格式化字符串一样,位置参数必须出现在命名参数和显式 寄存器操作数 之前。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 命名操作数需要放在位置操作数之后
unsafe { core::arch::asm!("/* {x} {} */", x = const 5, in(reg) 5); }
// ERROR: positional arguments cannot follow named arguments or explicit register arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们也不能将显式寄存器放在位置操作数之前
unsafe { core::arch::asm!("/* {} */", in("eax") 0, in(reg) 5); }
// ERROR: positional arguments cannot follow named arguments or explicit register arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

显式寄存器操作数不能被模板字符串中的占位符使用。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 显式寄存器操作数不会被替换,请在字符串中显式使用 `eax`
unsafe { core::arch::asm!("/* {} */", in("eax") 5); }
// ERROR: invalid reference to argument at index 0
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

所有其他命名和位置操作数必须在模板字符串中至少出现一次,否则会产生编译器错误。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们必须在格式化字符串中命名所有的操作数
unsafe { core::arch::asm!("", in(reg) 5, x = const 5); }
// ERROR: multiple unused asm arguments
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

除了将操作数替换到模板字符串中以形成传递给汇编器的代码的方式外,确切的汇编代码语法是特定于 target 的,且对编译器是不透明的。

目前,所有受支持的 target 都遵循 LLVM 内部汇编器使用的汇编代码语法,这通常对应于 GNU 汇编器(GAS)的语法。在 x86 上,默认使用 GAS 的 .intel_syntax noprefix 模式。在 ARM 上,使用 .syntax unified 模式。这些 target 对汇编代码施加了额外的限制:任何汇编器状态(例如可以通过 .section 更改的当前节)必须在汇编字符串结束时恢复为其原始值。不符合 GAS 语法的汇编代码将导致特定于汇编器的行为。有关内联汇编所用指令的进一步约束由 指令支持 指出。

属性

在语义上,只有 cfgcfg_attr 属性被内联汇编模板字符串和操作数接受。其他属性虽然会被解析,但在汇编宏展开时会被拒绝。

fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!(
    #[cfg(not(panic = "abort"))]
    ".cfi_startproc",
    // ...
    "ret",
    #[cfg(not(panic = "abort"))]
    ".cfi_endproc",
);

注意

rustc 中,汇编宏对这些属性的处理与处理语言中类似属性的普通系统是分开实现的。这解释了受支持属性种类有限的原因,并可能导致行为上的细微差异。

在语法上,第一个操作数之前必须至少有一个模板字符串。

#![allow(unused)]
fn main() {
// 这被拒绝是因为 `a = out(reg) x` 不会被解析为
// 模板字符串。
core::arch::asm!(
    #[cfg(false)]
    a = out(reg) x, // ERROR.
    "",
);
}

操作数类型

支持几种类型的操作数:

  • in(<reg>) <expr>
    • <reg> 可以引用一个寄存器类或一个显式寄存器。分配的寄存器名称将被替换到汇编模板字符串中。
    • 在汇编代码开始时,分配的寄存器将包含 <expr> 的值。
    • 分配的寄存器在汇编代码结束时必须包含相同的值(除非 lateout 被分配到同一个寄存器)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `in` 可用于将值传递到内联汇编中...
unsafe { core::arch::asm!("/* {} */", in(reg) 5); }
}
}
  • out(<reg>) <expr>
    • <reg> 可以引用一个寄存器类或一个显式寄存器。分配的寄存器名称将被替换到汇编模板字符串中。
    • 在汇编代码开始时,分配的寄存器将包含一个未定义的值。
    • <expr> 必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
    • 可以指定下划线(_)而不是表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(实际上起到了破坏列表(clobber)的作用)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// 而 `out` 可用于将值传回 rust。
unsafe { core::arch::asm!("/* {} */", out(reg) x); }
}
}
  • lateout(<reg>) <expr>
    • out 相同,除了寄存器分配器可以重用分配给 in 的寄存器。
    • 您应该仅在读取所有输入后才写入寄存器,否则可能会破坏某个输入。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `lateout` 与 `out` 相同
// 但编译器知道在我们要覆盖它的时候,我们并不关心任何输入的值。
unsafe { core::arch::asm!("mov {}, 5", lateout(reg) x); }
assert_eq!(x, 5)
}
}
  • inout(<reg>) <expr>
    • <reg> 可以引用一个寄存器类或一个显式寄存器。分配的寄存器名称将被替换到汇编模板字符串中。
    • 在汇编代码开始时,分配的寄存器将包含 <expr> 的值。
    • <expr> 必须是一个可变的已初始化位置表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inout` 可用于在寄存器内修改值
unsafe { core::arch::asm!("inc {}", inout(reg) x); }
assert_eq!(x, 5);
}
}
  • inout(<reg>) <in expr> => <out expr>
    • inout 相同,除了寄存器的初始值取自 <in expr> 的值。
    • <out expr> 必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
    • 可以为 <out expr> 指定下划线(_)而不是表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(实际上起到了破坏列表的作用)。
    • <in expr><out expr> 可以具有不同的类型。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `inout` 也可以将值移动到不同的地方
unsafe { core::arch::asm!("inc {}", inout(reg) 4u64=>x); }
assert_eq!(x, 5);
}
}
  • inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
    • inout 相同,除了寄存器分配器可以重用分配给 in 的寄存器(如果编译器知道 in 具有与 inlateout 相同的初始值,就会发生这种情况)。
    • 您应该仅在读取所有输入后才写入寄存器,否则可能会破坏某个输入。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64 = 4;
// `inlateout` 是使用 `lateout` 的 `inout`
unsafe { core::arch::asm!("inc {}", inlateout(reg) x); }
assert_eq!(x, 5);
}
}
  • sym <path>
    • <path> 必须引用一个 fnstatic
    • 引用该项的混淆后的符号名称将被替换到汇编模板字符串中。
    • 替换后的字符串不包括任何修饰符(例如 GOT、PLT、重定位等)。
    • <path> 允许指向一个 #[thread_local] 静态变量,在这种情况下,汇编代码可以将该符号与重定位(例如 @plt@TPOFF)结合使用以读取线程局部数据。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {
    println!("Hello from inline assembly")
}
// `sym` 可用于引用函数(即使它没有我们可以直接编写的外部名称)
unsafe { core::arch::asm!("call {}", sym foo, clobber_abi("C")); }
}
}
  • const <expr>
    • <expr> 必须是一个整数常量表达式。此表达式遵循与内联 const 块相同的规则。
    • 表达式的类型可以是任何整数类型,但默认情况下就像整数面值一样是 i32
    • 表达式的值被格式化为字符串,并直接替换到汇编模板字符串中。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// swizzle [0, 1, 2, 3] => [3, 2, 0, 1]
const SHUFFLE: u8 = 0b01_00_10_11;
let x: core::arch::x86_64::__m128 = unsafe { core::mem::transmute([0u32, 1u32, 2u32, 3u32]) };
let y: core::arch::x86_64::__m128;
// 将常量值传递给需要立即数的指令,如 `pshufd`
unsafe {
    core::arch::asm!("pshufd {xmm}, {xmm}, {shuffle}",
        xmm = inlateout(xmm_reg) x=>y,
        shuffle = const SHUFFLE
    );
}
let y: [u32; 4] = unsafe { core::mem::transmute(y) };
assert_eq!(y, [3, 2, 0, 1]);
}
}
  • label <block>
    • 该块的地址将被替换到汇编模板字符串中。汇编代码可以跳转到替换后的地址。
    • 对于区分直接跳转和间接跳转的 target(例如启用了 cf-protection 的 x86-64),汇编代码不得间接跳转到替换后的地址。
    • 执行完该块后,asm! 表达式返回。
    • 块的类型必须是单元类型或 !(从不)。
    • 该块启动了一个新的安全上下文;label 块内的不安全操作必须包装在内部的 unsafe 块中,尽管整个 asm! 表达式已经包装在 unsafe 中了。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
unsafe {
    core::arch::asm!("jmp {}", label {
        println!("Hello from inline assembly label");
    });
}
}

操作数表达式从左到右进行评估,就像函数调用参数一样。在 asm! 执行后,输出按从左到右的顺序写入。如果两个输出指向同一个地方,这一点很重要:该地方将包含最右侧输出的值。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// y 从第二个输出而不是第一个输出获取其值
unsafe { core::arch::asm!("mov {}, 0", "mov {}, 1", out(reg) y, out(reg) y); }
assert_eq!(y, 1);
}
}

由于 naked_asm! 定义了整个函数体,且编译器无法发出任何额外的代码来处理操作数,因此它只能使用 symconst 操作数。

由于 global_asm! 存在于函数之外,它只能使用 symconst 操作数。

fn main() {}
// 不允许寄存器操作数,因为我们不在函数中
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("", in(reg) 5);
// ERROR: the `in` operand cannot be used with `global_asm!`
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
fn main() {}
fn foo() {}

#[cfg(target_arch = "x86_64")]
// 然而,`const` 和 `sym` 都是允许的
core::arch::global_asm!("/* {} {} */", const 0, sym foo);

寄存器操作数

输入和输出操作数既可以指定为显式寄存器,也可以指定为寄存器分配器可以从中选择寄存器的寄存器类。显式寄存器被指定为字符串字面量(例如 "eax"),而寄存器类被指定为标识符(例如 reg)。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// 我们既可以将两者都命名为 `reg`,也可以使用显式寄存器(如 `eax`)来获取一个
// 整数寄存器
unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); }
assert_eq!(y, 5);
}
}

请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14lr)和寄存器的较小视图(例如 eaxrax)视为等同于基本寄存器。

在两个输入操作数或两个输出操作数中使用相同的显式寄存器是一个编译时错误。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们不能两次命名 eax
unsafe { core::arch::asm!("", in("eax") 5, in("eax") 4); }
// ERROR: register `eax` conflicts with register `eax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// ... 即使使用不同的别名
unsafe { core::arch::asm!("", in("ax") 5, in("rax") 4); }
// ERROR: register `rax` conflicts with register `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

此外,在输入操作数或输出操作数中使用重叠的寄存器(例如 ARM VFP)也是一个编译时错误。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// al 与 ax 重叠,所以我们不能同时命名它们。
unsafe { core::arch::asm!("", in("ax") 5, in("al") 4i8); }
// ERROR: register `al` conflicts with register `ax`
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

内联汇编仅允许使用以下类型作为操作数:

  • 整数(有符号和无符号)
  • 浮点数
  • 指针(仅瘦指针)
  • 函数指针
  • SIMD 向量(使用 #[repr(simd)] 定义且实现了 Copy 的结构体)。这包括在 std::arch 中定义的特定架构向量类型,如 __m128 (x86) 或 int8x16_t (ARM)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() {}

// 允许整数...
let y: i64 = 5;
unsafe { core::arch::asm!("/* {} */", in(reg) y); }

// 以及指针...
let py = &raw const y;
unsafe { core::arch::asm!("/* {} */", in(reg) py); }

// 还有浮点数...
let f = 1.0f32;
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); }

// 甚至是函数指针和 simd 向量。
let func: extern "C" fn() = foo;
unsafe { core::arch::asm!("/* {} */", in(reg) func); }

let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) z); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
struct Foo;
let x: Foo = Foo;
// 不允许像结构体这样的复杂类型
unsafe { core::arch::asm!("/* {} */", in(reg) x); }
// ERROR: cannot use value of type `Foo` for inline assembly
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

以下是当前受支持的寄存器类列表:

架构寄存器类寄存器LLVM 约束代码
x86regax, bx, cx, dx, si, di, bp, r[8-15] (仅限 x86-64)r
x86reg_abcdax, bx, cx, dxQ
x86-32reg_byteal, bl, cl, dl, ah, bh, ch, dhq
x86-64reg_byte*al, bl, cl, dl, sil, dil, bpl, r[8-15]bq
x86xmm_regxmm[0-7] (x86) xmm[0-15] (x86-64)x
x86ymm_regymm[0-7] (x86) ymm[0-15] (x86-64)x
x86zmm_regzmm[0-7] (x86) zmm[0-31] (x86-64)v
x86kregk[1-7]Yk
x86kreg0k0仅限破坏列表
x86x87_regst([0-7])仅限破坏列表
x86mmx_regmm[0-7]仅限破坏列表
x86-64tmm_regtmm[0-7]仅限破坏列表
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffr仅限破坏列表
Arm64ECregx[0-12], x[15-22], x[25-27], x30r
Arm64ECvregv[0-15]w
Arm64ECvreg_low16v[0-15]x
ARM (ARM/Thumb2)regr[0-12], r14r
ARM (Thumb1)regr[0-7]r
ARMsregs[0-31]t
ARMsreg_low16s[0-15]x
ARMdregd[0-31]w
ARMdreg_low16d[0-15]t
ARMdreg_low8d[0-8]x
ARMqregq[0-15]w
ARMqreg_low8q[0-7]t
ARMqreg_low4q[0-3]x
RISC-Vregx1, x[5-7], x[9-15], x[16-31] (非 RV32E)r
RISC-Vfregf[0-31]f
RISC-Vvregv[0-31]仅限破坏列表
LoongArchreg$r1, $r[4-20], $r[23,30]r
LoongArchfreg$f[0-31]f
s390xregr[0-10], r[12-14]r
s390xreg_addrr[1-10], r[12-14]a
s390xfregf[0-15]f
s390xvregv[0-31]仅限破坏列表
s390xarega[2-15]仅限破坏列表

注意

  • 在 x86 上,我们将 reg_bytereg 区别对待,因为编译器可以分别分配 alah,而 reg 会保留整个寄存器。
  • 在 x86-64 上,高字节寄存器(例如 ah)在 reg_byte 寄存器类中不可用。
  • 一些寄存器类被标记为 “仅限破坏列表” ,这意味着这些类中的寄存器不能用于输入或输出,只能用于 out(<explicit register>) _lateout(<explicit register>) _ 形式的破坏列表。

每个寄存器类对其可以使用的值类型都有约束。这是必要的,因为将值加载到寄存器的方式取决于其类型。例如,在大端系统上,将 i32x4i8x16 加载到 SIMD 寄存器中可能会导致不同的寄存器内容,即使这两个值的按字节内存表示是相同的。特定寄存器类可用支持类型的可用性可能取决于当前启用了哪些 target 特性。

架构寄存器类target 特性允许的类型
x86-32regNonei16, i32, f32
x86-64regNonei16, i32, f32, i64, f64
x86reg_byteNonei8
x86xmm_regssei32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
x86ymm_regavxi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
x86zmm_regavx512fi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
i8x64, i16x32, i32x16, i64x8, f32x16, f64x8
x86kregavx512fi8, i16
x86kregavx512bwi32, i64
x86mmx_regN/A仅限破坏列表
x86x87_regN/A仅限破坏列表
x86tmm_regN/A仅限破坏列表
AArch64regNonei8, i16, i32, f32, i64, f64
AArch64vregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
AArch64pregN/A仅限破坏列表
Arm64ECregNonei8, i16, i32, f32, i64, f64
Arm64ECvregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
ARMregNonei8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regNonei8, i16, i32, f32
RISC-V64regNonei8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/A仅限破坏列表
LoongArch32regNonei8, i16, i32, f32
LoongArch64regNonei8, i16, i32, i64, f32, f64
LoongArchfregff32
LoongArchfregdf64
s390xreg, reg_addrNonei8, i16, i32, i64
s390xfregNonef32, f64
s390xvregN/A仅限破坏列表
s390xaregN/A仅限破坏列表

注意

就上表而言,指针、函数指针和 isize/usize 被视为等效的整数类型(取决于 target,为 i16/i32/i64)。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5i32;
let y = -1i8;
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };

// reg 对 `i32` 有效,reg_byte 对 `i8` 有效,而 xmm_reg 对 `__m128i` 有效
// 我们不能使用 `tmm0` 作为输入或输出,但我们可以将其加入破坏列表。
unsafe { core::arch::asm!("/* {} {} {} */", in(reg) x, in(reg_byte) y, in(xmm_reg) z, out("tmm0") _); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) };
// 我们不能将 `__m128i` 传递给 `reg` 输入
unsafe { core::arch::asm!("/* {} */", in(reg) z); }
// ERROR: type `__m128i` cannot be used with this register class
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

如果值的大小小于它所分配到的寄存器的大小,那么该寄存器的高位对于输入将具有未定义的值,而对于输出将被忽略。唯一的例外是 RISC-V 上的 freg 寄存器类,其中 f32 值按照 RISC-V 架构的要求被 NaN 装箱(NaN-boxed)在 f64 中。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64;
// 将 32 位值移动到 64 位值中,哎呀。
#[allow(asm_sub_register)] // rustc 对此行为发出警告
unsafe { core::arch::asm!("mov {}, {}", lateout(reg) x, in(reg) 4i32); }
// 高 32 位是不确定的
assert_eq!(x, 4); // 不保证此断言成功
assert_eq!(x & 0xFFFFFFFF, 4); // 但是,这个断言会成功
}
}

当为 inout 操作数指定单独的输入和输出表达式时,两个表达式必须具有相同的类型。唯一的例外是如果两个操作数都是指针或整数,在这种情况下,它们仅要求具有相同的大小。存在此限制是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的绑定操作数。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 指针和整数可以混用(只要它们大小相同)
let x: isize = 0;
let y: *mut ();
// 使用内联汇编魔术将 `isize` 转换为 `*mut ()`
unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); }
assert!(y.is_null()); // 一种极其曲折的创建空指针的方法
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let y: f32;
// 但我们不能像这样将 `i32` 重新解释为 `f32`
unsafe { core::arch::asm!("/* {} */", inout(reg) x=>y); }
// ERROR: incompatible types for asm inout argument
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

寄存器名称

有些寄存器有多个名称。编译器将这些名称都视为与基本寄存器名称相同。以下是所有受支持的寄存器别名列表:

架构基本寄存器别名
x86axeax, rax
x86bxebx, rbx
x86cxecx, rcx
x86dxedx, rdx
x86siesi, rsi
x86diedi, rdi
x86bpbpl, ebp, rbp
x86spspl, esp, rsp
x86ipeip, rip
x86st(0)st
x86r[8-15]r[8-15]b, r[8-15]w, r[8-15]d
x86xmm[0-31]ymm[0-31], zmm[0-31]
AArch64x[0-30]w[0-30]
AArch64x29fp
AArch64x30lr
AArch64spwsp
AArch64xzrwzr
AArch64v[0-31]b[0-31], h[0-31], s[0-31], d[0-31], q[0-31]
Arm64ECx[0-30]w[0-30]
Arm64ECx29fp
Arm64ECx30lr
Arm64ECspwsp
Arm64ECxzrwzr
Arm64ECv[0-15]b[0-15], h[0-15], s[0-15], d[0-15], q[0-15]
ARMr[0-3]a[1-4]
ARMr[4-9]v[1-6]
ARMr9rfp
ARMr10sl
ARMr11fp
ARMr12ip
ARMr13sp
ARMr14lr
ARMr15pc
RISC-Vx0zero
RISC-Vx1ra
RISC-Vx2sp
RISC-Vx3gp
RISC-Vx4tp
RISC-Vx[5-7]t[0-2]
RISC-Vx8fp, s0
RISC-Vx9s1
RISC-Vx[10-17]a[0-7]
RISC-Vx[18-27]s[2-11]
RISC-Vx[28-31]t[3-6]
RISC-Vf[0-7]ft[0-7]
RISC-Vf[8-9]fs[0-1]
RISC-Vf[10-17]fa[0-7]
RISC-Vf[18-27]fs[2-11]
RISC-Vf[28-31]ft[8-11]
LoongArch$r0$zero
LoongArch$r1$ra
LoongArch$r2$tp
LoongArch$r3$sp
LoongArch$r[4-11]$a[0-7]
LoongArch$r[12-20]$t[0-8]
LoongArch$r21
LoongArch$r22$fp, $s9
LoongArch$r[23-31]$s[0-8]
LoongArch$f[0-7]$fa[0-7]
LoongArch$f[8-23]$ft[0-15]
LoongArch$f[24-31]$fs[0-7]
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z = 0i64;
// rax 是 eax 和 ax 的别名
unsafe { core::arch::asm!("", in("rax") z); }
}
}

某些寄存器不能用于输入或输出操作数:

架构不受支持的寄存器原因
Allsp, r15 (s390x)栈指针必须在汇编代码结束时或跳转到 label 块之前恢复其原始值。
Allbp (x86), x29 (AArch64 和 Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x)帧指针不能用作输入或输出。
ARMr7r11在 ARM 上,帧指针可以是 r7r11,具体取决于 target。帧指针不能用作输入或输出。
Allsi (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 和 Arm64EC), x9 (RISC-V), $s8 (LoongArch)LLVM 内部将其用作具有复杂栈帧的函数的 “base pointer” 。
x86ip这是程序计数器,不是真实的寄存器。
AArch64xzr这是一个常量零寄存器,无法修改。
AArch64x18在某些 AArch64 target 上,这是操作系统保留的寄存器。
Arm64ECxzr这是一个常量零寄存器,无法修改。
Arm64ECx18这是一个操作系统保留的寄存器。
Arm64ECx13, x14, x23, x24, x28, v[16-31], p[0-15], ffr这些是 Arm64EC 不支持的 AArch64 寄存器。
ARMpc这是程序计数器,不是真实的寄存器。
ARMr9在某些 ARM target 上,这是操作系统保留的寄存器。
RISC-Vx0这是一个常量零寄存器,无法修改。
RISC-Vgp, tp这些寄存器是保留的,不能用作输入或输出。
LoongArch$r0$zero这是一个常量零寄存器,无法修改。
LoongArch$r2$tp这是为 TLS 保留的。
LoongArch$r21这是由 ABI 保留的。
s390xc[0-15]由内核保留。
s390xa[0-1]保留供系统使用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// bp 是保留的
unsafe { core::arch::asm!("", in("bp") 5i32); }
// ERROR: invalid register `bp`: the frame pointer cannot be used as an operand for inline asm
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

帧指针和基指针寄存器保留供 LLVM 内部使用。虽然 asm! 语句不能显式指定使用保留寄存器,但在某些情况下,LLVM 会为 reg 操作数分配这些保留寄存器之一。使用保留寄存器的汇编代码应当小心,因为 reg 操作数可能会使用相同的寄存器。

模板修饰符

占位符可以通过修饰符进行增强,修饰符在花括号中的 : 之后指定。这些修饰符不影响寄存器分配,但会改变操作数在插入模板字符串时的格式化方式。

每个模板占位符仅允许使用一个修饰符。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们不能同时指定 `r` 和 `e`。
unsafe { core::arch::asm!("/* {:er}", in(reg) 5i32); }
// ERROR: asm template modifier must be a single character
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

支持的修饰符是 LLVM(和 GCC)汇编模板参数修饰符 的子集,但不使用相同的字母代码。

架构寄存器类修饰符示例输出LLVM 修饰符
x86-32regNoneeaxk
x86-64regNoneraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteNoneal / ahNone
x86xmm_regNonexmm0x
x86ymm_regNoneymm0t
x86zmm_regNonezmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregNonek1None
AArch64/Arm64ECregNonex0x
AArch64/Arm64ECregww0w
AArch64/Arm64ECregxx0x
AArch64/Arm64ECvregNonev0None
AArch64/Arm64ECvregvv0None
AArch64/Arm64ECvregbb0b
AArch64/Arm64ECvreghh0h
AArch64/Arm64ECvregss0s
AArch64/Arm64ECvregdd0d
AArch64/Arm64ECvregqq0q
ARMregNoner0None
ARMsregNones0None
ARMdregNoned0P
ARMqregNoneq0q
ARMqrege / fd0 / d1e / f
RISC-VregNonex1None
RISC-VfregNonef0None
LoongArchregNone$r1None
LoongArchfregNone$f0None
s390xregNone%r0None
s390xreg_addrNone%r1None
s390xfregNone%f0None

注意

  • 在 ARM 上 e / f:这会打印 NEON quad (128-bit) 寄存器的低双字或高双字寄存器名称。
  • 在 x86 上:我们对不带修饰符的 reg 的行为与 GCC 所做的不同。GCC 将根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。
  • 在 x86 xmm_reg 上:xtg LLVM 修饰符尚未在 LLVM 中实现(它们仅受 GCC 支持),但这应该是一个简单的改动。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0x10u16;

// 使用 `xchg` 的 u16::swap_bytes
// `{x}` 的低半部分由 `{x:l}` 引用,高半部分由 `{x:h}` 引用
unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); }
assert_eq!(x, 0x1000u16);
}
}

如上一节所述,传递小于寄存器宽度的输入值将导致寄存器的高位包含未定义的值。如果内联汇编仅访问寄存器的低位,这就不是问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称(例如使用 ax 而不是 rax)来实现。由于这是一个容易掉进去的陷阱,编译器将根据输入类型建议在适当的地方使用模板修饰符。如果对操作数的所有引用都已经有了修饰符,则会抑制该操作数的警告。

ABI破坏列表

clobber_abi 关键字可用于为汇编代码应用一组默认的破坏列表。这将根据调用具有特定调用约定(calling convention)的函数自动插入必要的破坏约束:如果调用约定不能在调用过程中完全保留寄存器的值,则会隐式地将 lateout("...") _ 添加到操作数列表中(其中 ... 被替换为寄存器的名称)。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() -> i32 { 0 }

let z: i32;
// 要调用函数,我们必须告知编译器我们正在破坏
// 被调用者保存的寄存器
unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); }
assert_eq!(z, 0);
}
}

clobber_abi 可以指定任意次数。它将为所有指定调用约定的并集中的所有唯一寄存器插入一个破坏列表。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "sysv64" fn foo() -> i32 { 0 }
extern "win64" fn bar(x: i32) -> i32 { x + 1}

let z: i32;
// 我们甚至可以调用多个具有不同约定和
// 不同保存寄存器的函数
unsafe {
    core::arch::asm!(
        "call {}",
        "mov ecx, eax",
        "call {}",
        sym foo,
        sym bar,
        out("rax") z,
        clobber_abi("C")
    );
}
assert_eq!(z, 1);
}
}

当使用 clobber_abi 时,编译器不允许泛型寄存器类输出:所有输出必须指定显式寄存器。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo(x: i32) -> i32 { 0 }

let z: i32;
// 必须使用显式寄存器以免意外重叠。
unsafe {
    core::arch::asm!(
        "mov eax, {:e}",
        "call {}",
        out(reg) z,
        sym foo,
        clobber_abi("C")
    );
    // ERROR: asm with `clobber_abi` must specify explicit registers for outputs
}
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}

显式寄存器输出优先于由 clobber_abi 插入的隐式破坏列表:仅当寄存器未用作输出时,才会为该寄存器插入破坏列表。

以下 ABI 可与 clobber_abi 一起使用:

架构ABI 名称被破坏的寄存器
x86-32"C", "system", "efiapi", "cdecl", "stdcall", "fastcall"ax, cx, dx, xmm[0-7], mm[0-7], k[0-7], st([0-7])
x86-64"C", "system" (在 Windows 上), "efiapi", "win64"ax, cx, dx, r[8-11], xmm[0-31], mm[0-7], k[0-7], st([0-7]), tmm[0-7]
x86-64"C", "system" (在非 Windows 上), "sysv64"ax, cx, dx, si, di, r[8-11], xmm[0-31], mm[0-7], k[0-7], st([0-7]), tmm[0-7]
AArch64"C", "system", "efiapi"x[0-17], x18*, x30, v[0-31], p[0-15], ffr
Arm64EC"C", "system"x[0-12], x[15-17], x30, v[0-15]
ARM"C", "system", "efiapi", "aapcs"r[0-3], r12, r14, s[0-15], d[0-7], d[16-31]
RISC-V"C", "system", "efiapi"x1, x[5-7], x[10-17]*, x[28-31]*, f[0-7], f[10-17], f[28-31], v[0-31]
LoongArch"C", "system"$r1, $r[4-20], $f[0-23]
s390x"C", "system"r[0-5], r14, f[0-7], v[0-31], a[2-15]

注意

  • 在 AArch64 上,仅当 x18 在 target 上不被视为保留寄存器时,才将其包含在破坏列表中。
  • 在 RISC-V 上,仅当 x[16-17]x[28-31] 在 target 上不被视为保留寄存器时,才将其包含在破坏列表中。

随着架构获得新寄存器,rustc 会更新每个 ABI 的破坏寄存器列表:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm! 破坏列表将继续保持正确。

选项

标志用于进一步影响内联汇编代码的行为。目前定义了以下选项:

  • pure:汇编代码没有副作用,必须最终返回,并且其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存读取的值(除非还设置了 nomem 选项)。这允许编译器执行汇编代码的次数少于程序中指定的次数(例如通过将其提升到循环之外),或者如果输出未被使用,甚至可以完全消除它。pure 选项必须与 nomemreadonly 选项之一结合使用,否则会发出编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// `pure` 可用于通过假设汇编没有副作用来进行优化
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure, nomem)); }
assert_eq!(z, 1);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// 必须满足 `nomem` 或 `readonly` 之一,以指示是否
// 允许读取内存
unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure)); }
// ERROR: the `pure` option must be combined with either `nomem` or `readonly`
assert_eq!(z, 0);
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
  • nomem:汇编代码不会读取或写入汇编代码之外可访问的任何内存。这允许编译器在执行汇编代码期间将修改后的全局变量的值缓存在寄存器中,因为它知道汇编代码不会读取或写入它们。编译器还假设汇编代码不会与其他线程执行任何形式的同步,例如通过内存屏障(fence)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0i32;
let z: i32;
// 当指定 `nomem` 时,不允许从汇编访问外部内存
unsafe {
    core::arch::asm!("mov {val:e}, dword ptr [{ptr}]",
        ptr = in(reg) &mut x,
        val = lateout(reg) z,
        options(nomem)
    )
}

// 当指定 `nomem` 时,从汇编写入外部内存也是未定义行为
unsafe {
    core::arch::asm!("mov  dword ptr [{ptr}], {val:e}",
        ptr = in(reg) &mut x,
        val = in(reg) z,
        options(nomem)
    )
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// 然而,如果我们分配自己的内存,例如通过 `push`。
// 我们仍然可以使用它
unsafe {
    core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
        x = inout(reg) x => z,
        options(nomem)
    );
}
assert_eq!(z, 1);
}
}
  • readonly:汇编代码不会写入汇编代码之外可访问的任何内存。这允许编译器在执行汇编代码期间将未修改的全局变量的值缓存在寄存器中,因为它知道汇编代码不会写入它们。编译器还假设此汇编代码不会与其他线程执行任何形式的同步,例如通过内存屏障。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0;
// 当指定 `readonly` 时,我们不能修改外部内存
unsafe {
    core::arch::asm!("mov dword ptr[{}], 1", in(reg) &mut x, options(readonly))
}
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// 不过我们仍然可以从中读取
unsafe {
    core::arch::asm!("mov {x}, qword ptr [{x}]",
        x = inout(reg) &x => z,
        options(readonly)
    );
}
assert_eq!(z, 0);
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64 = 0;
let z: i64;
// 与 `nomem` 相同的例外也适用。
unsafe {
    core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}",
        x = inout(reg) x => z,
        options(readonly)
    );
}
assert_eq!(z, 1);
}
}
  • preserves_flags:汇编代码不修改标志寄存器(在下面的规则中定义)。这允许编译器避免在执行汇编代码后重新计算条件标志。
  • noreturn:汇编代码不会顺序执行(fall through);如果它这样做,则行为是未定义的。它仍然可以跳转到 label 块。如果任何 label 块返回单元类型,则 asm! 块将返回单元类型。否则它将返回 !(从不)。与调用不返回的函数一样,作用域内的局部变量在执行汇编代码之前不会被丢弃。
fn main() -> ! {
#[cfg(target_arch = "x86_64")] {
    // 我们可以在 `noreturn` 块内使用一条指令来捕获执行
    unsafe { core::arch::asm!("ud2", options(noreturn)); }
}
#[cfg(not(target_arch = "x86_64"))] panic!("no return");
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 您负责确保执行不会越过 `noreturn` 汇编块的末尾
unsafe { core::arch::asm!("", options(noreturn)); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
let _: () = unsafe {
    // 您仍然可以跳转到 `label` 块
    core::arch::asm!("jmp {}", label {
        println!();
    }, options(noreturn));
};
}
  • nostack:汇编代码不会将数据压入栈,也不会写入栈红区(red-zone)(如果 target 支持)。如果 使用此选项,则编译器保证在汇编代码开始时,栈指针已(根据 target ABI)适当对齐,以便进行函数调用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `push` 和 `pop` 在与 `nostack` 一起使用时是未定义行为
unsafe { core::arch::asm!("push rax", "pop rax", options(nostack)); }
}
}
  • att_syntax:此选项仅在 x86 上有效,并导致汇编器使用 GNU 汇编器的 .att_syntax prefix 模式。替换进来的寄存器操作数带有一个前导 %
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32;
let y = 1i32;
// 我们在这里需要使用 AT&T 语法。操作数的顺序是源、目的
unsafe {
    core::arch::asm!("mov {y:e}, {x:e}",
        x = lateout(reg) x,
        y = in(reg) y,
        options(att_syntax)
    );
}
assert_eq!(x, y);
}
}
  • raw:这导致模板字符串被解析为原始汇编字符串,不对 {} 进行特殊处理。这在通过 include_str! 从外部文件包含原始汇编代码时特别有用。

编译器对选项执行一些额外的检查:

  • nomemreadonly 选项是互斥的:同时指定两者是一个编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `nomem` 严格强于 `readonly` ,它们不能同时指定
unsafe { core::arch::asm!("", options(nomem, readonly)); }
// ERROR: the `nomem` and `readonly` options are mutually exclusive
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
  • 在没有输出或只有被丢弃的输出(_)的汇编块上指定 pure 是一个编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `pure` 块需要至少一个输出
unsafe { core::arch::asm!("", options(pure)); }
// ERROR: asm with the `pure` option must have at least one output
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
  • 在具有输出且没有 label 的汇编块上指定 noreturn 是一个编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z: i32;
// `noreturn` 不能有输出
unsafe { core::arch::asm!("mov {:e}, 1", out(reg) z, options(noreturn)); }
// ERROR: asm outputs are not allowed with the `noreturn` option
}
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
}
  • 在具有输出的汇编块中拥有任何 label 块是一个编译时错误。

naked_asm! 仅支持 att_syntaxraw 选项。其余选项没有意义,因为内联汇编定义了整个函数体。

global_asm! 仅支持 att_syntaxraw 选项。其余选项对于全局作用域内联汇编没有意义。

fn main() {}
#[cfg(target_arch = "x86_64")]
// `nomem` 在 `global_asm!` 上没有用
core::arch::global_asm!("", options(nomem));
#[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");

内联汇编规则

为避免未定义行为,在使用函数作用域内联汇编(asm!)时必须遵守以下规则:

  • 任何未指定为输入的寄存器在进入汇编代码时都将包含一个未定义的值。
    • 在内联汇编的语境下, “未定义的值” 意味着寄存器可以(非确定性地)具有架构所允许的任何一个可能的值。值得注意的是,它与 LLVM 的 undef 不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
  • 任何未指定为输出的寄存器在退出汇编代码时必须具有与进入时相同的值,否则行为是未定义的。
    • 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循特定于 target 的规则。
    • 请注意,lateout 可能会被分配到与 in 相同的寄存器,在这种情况下,此规则不适用。然而,代码不应依赖于此,因为它取决于寄存器分配的结果。
  • 如果执行从汇编代码中展开(unwind),则行为是未定义的。
    • 这也适用于汇编代码调用了一个随后发生展开的函数的情况。
  • 汇编代码允许读取和写入的内存位置集与 FFI 函数所允许的相同。
    • 如果设置了 readonly 选项,则仅允许读取内存。
    • 如果设置了 nomem 选项,则不允许读取或写入内存。
    • 这些规则不适用于汇编代码私有的内存,例如在其内部分配的栈空间。
  • 编译器不能假设汇编代码中的指令就是最终会执行的指令。
    • 这实际上意味着编译器必须将汇编代码视为黑盒,仅考虑接口规范,而不是指令本身。
    • 允许通过特定于 target 的机制进行运行时代码补丁。
    • 然而,不保证源代码中的每个汇编代码块都直接对应于目标文件中的单个指令实例;编译器可以自由地复制或去重 asm! 块中的汇编代码。
  • 除非设置了 nostack 选项,否则允许汇编代码使用栈指针下方的栈空间。
    • 在进入汇编代码时,栈指针保证已(根据 target ABI)适当对齐,以便进行函数调用。
    • 您负责确保不会发生栈溢出(例如使用栈探测以确保触及保护页)。
    • 您应当根据 target ABI 的要求,在分配栈内存时调整栈指针。
    • 栈指针必须在离开汇编代码之前恢复为其原始值。
  • 如果设置了 noreturn 选项,则如果执行顺序执行到汇编代码末尾,则行为是未定义的。
  • 如果设置了 pure 选项,则如果 asm! 除了其直接输出之外还有副作用,则行为是未定义的。如果具有相同输入的 asm! 代码的两次执行导致不同的输出,则行为也是未定义的。
    • 当与 nomem 选项一起使用时, “输入” 只是 asm! 的直接输入。
    • 当与 readonly 选项一起使用时, “输入” 包括汇编代码的直接输入和它允许读取的任何内存。
  • 如果设置了 preserves_flags 选项,则退出汇编代码时必须恢复这些标志寄存器:
    • x86
      • EFLAGS 中的状态标志位(CF, PF, AF, ZF, SF, OF)。
      • 浮点状态字(全部)。
      • MXCSR 中的浮点异常标志(PE, UE, OE, ZF, DE, IE)。
    • ARM
      • CPSR 中的条件标志(N, Z, C, V)
      • CPSR 中的饱和标志(Q)
      • CPSR 中的大于或等于标志(GE)。
      • FPSCR 中的条件标志(N, Z, C, V)
      • FPSCR 中的饱和标志(QC)
      • FPSCR 中的浮点异常标志(IDC, IXC, UFC, OFC, DZC, IOC)。
    • AArch64 和 Arm64EC
      • 条件标志(NZCV 寄存器)。
      • 浮点状态(FPSR 寄存器)。
    • RISC-V
      • fcsr 中的浮点异常标志(fflags)。
      • 向量扩展状态(vtypevlvxsatvxrm)。
    • LoongArch
      • $fcc[0-7] 中的浮点条件标志。
    • s390x
      • 条件代码寄存器 cc
  • 在 x86 上,方向标志位(EFLAGS 中的 DF)在进入汇编代码时为清除状态,且退出时必须为清除状态。
    • 如果退出汇编代码时设置了方向标志位,则行为是未定义的。
  • 在 x86 上,x87 浮点寄存器栈必须保持不变,除非所有 st([0-7]) 寄存器都已标记为使用 out("st(0)") _, out("st(1)") _, ... 进行了破坏。
    • 如果所有 x87 寄存器都被破坏,则保证在进入汇编代码时 x87 寄存器栈为空。汇编代码必须确保在退出汇编代码时 x87 寄存器栈也为空。
#[cfg(target_arch = "x86_64")]
pub fn fadd(x: f64, y: f64) -> f64 {
  let mut out = 0f64;
  let mut top = 0u16;
  // 如果我们破坏了整个 x87 栈,我们可以用 x87 做复杂的事情
  unsafe { core::arch::asm!(
    "fld qword ptr [{x}]",
    "fld qword ptr [{y}])",
    "faddp",
    "fstp qword ptr [{out}]",
    "xor eax, eax",
    "fstsw ax",
    "shl eax, 11",
    x = in(reg) &x,
    y = in(reg) &y,
    out = in(reg) &mut out,
    out("st(0)") _, out("st(1)") _, out("st(2)") _, out("st(3)") _,
    out("st(4)") _, out("st(5)") _, out("st(6)") _, out("st(7)") _,
    out("eax") top
  );}

  assert_eq!(top & 0x7, 0);
  out
}

pub fn main() {
#[cfg(target_arch = "x86_64")]{
  assert_eq!(fadd(1.0, 1.0), 2.0);
}
}
  • 恢复栈指针和非输出寄存器为其原始值的要求仅适用于退出汇编代码时。
    • 这意味着不顺序执行且不跳转到任何 label 块的汇编代码,即使没有标记为 noreturn,也不需要保留这些寄存器。
    • 当返回到与进入时不同的 asm! 块的汇编代码时(例如用于上下文切换),这些寄存器必须包含在进入您正在 退出asm! 块时所具有的值。
      • 您不能退出未进入的 asm! 块的汇编代码。也不能退出其汇编代码已退出的 asm! 块的汇编代码(除非再次进入它)。
      • 您负责切换任何特定于 target 的状态(例如线程局部存储、栈边界)。
      • 您不能从一个 asm! 块中的地址跳转到另一个块中的地址,即使在同一个函数或块内,而不将它们的上下文视为可能不同的并需要进行上下文切换。您不能假设这些上下文中的任何特定值(例如当前栈指针或栈指针下方的临时值)在两个 asm! 块之间将保持不变。
      • 您可以访问的内存位置集是您进入和退出的 asm! 块所允许的位置集的交集。
  • 您不能假设源代码中相邻的两个 asm! 块,即使它们之间没有任何其他代码,最终也会位于二进制文件中连续的地址且它们之间没有任何其他指令。
  • 您不能假设 asm! 块在输出二进制文件中恰好出现一次。编译器允许实例化 asm! 块的多个副本,例如当包含它的函数在多个地方内联时。
  • 在 x86 上,内联汇编不得以会应用于编译器生成的指令的指令前缀(例如 LOCK)结尾。
    • 由于内联汇编的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝这种情况。

注意

作为一般规则,preserves_flags 涵盖的标志是那些在执行函数调用时 被保留的标志。

裸内联汇编规则

为避免未定义行为,在裸函数(naked_asm!)中使用函数作用域内联汇编时必须遵守以下规则:

  • 根据调用约定和函数签名,任何未用于函数输入的寄存器在进入 naked_asm! 块时都将包含一个未定义的值。
    • 在内联汇编的语境下, “未定义的值” 意味着寄存器可以(非确定性地)具有架构所允许的任何一个可能的值。值得注意的是,它与 LLVM 的 undef 不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
  • 所有被调用者保存(callee-saved)的寄存器在返回时必须具有与进入时相同的值。
  • 可以自由使用调用者保存(caller-saved)的寄存器。
  • 如果执行顺序执行过汇编代码末尾,则行为是未定义的。
    • 汇编代码中的每条路径都应以返回指令结束或发散。
  • 汇编代码允许读取和写入的内存位置集与 FFI 函数所允许的相同。
  • 编译器不能假设 naked_asm! 块中的指令就是最终会执行的指令。
    • 这实际上意味着编译器必须将 naked_asm! 视为黑盒,仅考虑接口规范,而不是指令本身。
    • 允许通过特定于 target 的机制进行运行时代码补丁。
  • 允许从 naked_asm! 块中展开。
    • 为了保证行为正确,必须使用发出展开元数据的适当汇编指令。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
    core::arch::naked_asm!(
        // 这里的 "CFI" 代表 "调用栈帧信息" (call frame information)。
        ".cfi_startproc",
        // CFA (规范栈帧地址) 是 `call` 之前 `rsp` 的值,
        // 即在返回地址 `rip` 被压入 `rsp` 之前的值,
        // 所以在进入函数时(在 `rip` 被压入后),
        // 它在内存中比 `rsp` 高 8 个字节。
        //
        // 这是默认设置,所以我们不必写它。
        //".cfi_def_cfa rsp, 8",
        //
        // 传统做法是保留基指针,所以我们就这么做。
        "push rbp",
        // 既然我们现在已在内存中将栈向下扩展了 8 个字节,
        // 我们需要将从 `rsp` 到 CFA 的偏移量再调整 8 个字节。
        ".cfi_adjust_cfa_offset 8",
        // 然后我们还注释了我们将调用者的 `rbp` 值相对于 CFA 存储在哪里,
        // 以便在展开到调用者时可以找到它,以防我们需要它
        // 来计算调用者相对于它的 CFA。
        //
        // 在这里,我们从 CFA 下方 16 字节开始存储调用者的 `rbp`。
        // 即从 CFA 开始,首先是 `rip`(从 CFA 下方 8 字节开始并持续到它),
        // 然后是我们刚刚压入的调用者的 `rbp`。
        ".cfi_offset rbp, -16",
        // 按照传统,我们将基指针设置为栈指针的值。
        // 这样,基指针在整个函数体中保持不变。
        "mov rbp, rsp",
        // 我们现在可以跟踪从基指针到 CFA 的偏移量。
        // 这意味着直到最后我们都不需要再做任何调整,
        // 因为我们不改变 `rbp`。
        ".cfi_def_cfa_register rbp",
        // 我们现在可以调用一个可能产生恐慌的函数。
        "call {f}",
        // 返回后,我们恢复 `rbp` 以准备返回我们自己。
        "pop rbp",
        // 现在我们已经恢复了 `rbp`,我们必须再次根据 `rsp`
        // 指定到 CFA 的偏移量。
        ".cfi_def_cfa rsp, 8",
        // 现在我们可以返回了。
        "ret",
        ".cfi_endproc",
        f = sym may_panic,
    )
}

extern "sysv64-unwind" fn may_panic() {
    panic!("unwind");
}
}
}

注意

有关上述 cfi 汇编指令的更多信息,请参阅以下资源:

正确性和有效性

除了所有先前的规则外,asm! 的字符串参数最终必须变为——在评估了所有其他参数、执行了格式化并转换了操作数之后——在语法上正确且在语义上对于 target 架构有效。格式化规则允许编译器生成具有正确语法的汇编代码。关于操作数的规则允许将 Rust 操作数有效转换进出汇编代码。遵守这些规则是必要的,但对于最终展开的汇编代码既正确又有效是不充分的。例如:

  • 格式化后,参数可能被放置在语法上不正确的位置
  • 指令可能书写正确,但给出了架构上无效的操作数
  • 架构上未指定的指令可能会被汇编成未指定的代码
  • 一组指令,即使每个指令都正确且有效,如果紧接着放置也可能导致未定义行为

因此,这些规则是 非详尽的 。编译器不被要求检查初始字符串或最终生成的汇编代码的正确性和有效性。汇编器可以检查正确性和有效性,但不是必须这样做。使用 asm! 时,一个排版错误可能就足以使程序不健全,而汇编规则可能包括数千页的架构参考手册。程序员应当行使适当的谨慎,因为调用这种 unsafe 能力意味着承担不违反编译器和架构规则的责任。

指令支持

内联汇编支持 GNU AS 和 LLVM 内部汇编器所支持的指令子集,如下所示。使用其他指令的结果是特定于汇编器的(可能会导致错误,也可能会被原样接受)。

如果内联汇编包括任何修改后续汇编处理方式的 “stateful” 指令,则汇编代码必须在内联汇编结束前撤消任何此类指令的影响。

汇编器保证支持以下指令:

  • .2byte
  • .4byte
  • .8byte
  • .align
  • .alt_entry
  • .ascii
  • .asciz
  • .balign
  • .balignl
  • .balignw
  • .bss
  • .byte
  • .comm
  • .data
  • .def
  • .double
  • .endef
  • .equ
  • .equiv
  • .eqv
  • .fill
  • .float
  • .global
  • .globl
  • .inst
  • .insn
  • .lcomm
  • .long
  • .octa
  • .option
  • .p2align
  • .popsection
  • .private_extern
  • .pushsection
  • .quad
  • .scl
  • .section
  • .set
  • .short
  • .size
  • .skip
  • .sleb128
  • .space
  • .string
  • .text
  • .type
  • .uleb128
  • .word
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let bytes: *const u8;
let len: usize;
unsafe {
    core::arch::asm!(
        "jmp 3f", "2: .ascii \"Hello World!\"",
        "3: lea {bytes}, [2b+rip]",
        "mov {len}, 12",
        bytes = out(reg) bytes,
        len = out(reg) len
    );
}

let s = unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(bytes, len)) };

assert_eq!(s, "Hello World!");
}
}

特定于target的指令支持

Dwarf展开

在支持 DWARF 展开信息的 ELF target 上支持以下指令:

  • .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
结构化异常处理

在具有结构化异常处理的 target 上,保证支持以下附加指令:

  • .seh_endproc
  • .seh_endprologue
  • .seh_proc
  • .seh_pushreg
  • .seh_savereg
  • .seh_setframe
  • .seh_stackalloc
x86(32 位和 64 位)

在 x86 target(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 的安全子集中使用:

  • 读取或写入 可变 或不安全的 外部 静态变量。
  • 访问 联合体 的字段,除了对其进行赋值。
  • 调用不安全函数。

  1. 在 2024 版次 之前,允许在不使用 unsafe 的情况下声明外部块。

unsafe关键字

unsafe 关键字用于创建或履行证明某事是安全的义务。具体来说:

  • 它用于标记 定义 了必须在其他地方遵守的额外安全条件的代码。
    • 这包括 unsafe fnunsafe staticunsafe trait
  • 它用于标记程序员 断言 满足了在其他地方定义的安全条件的代码。
    • 这包括 unsafe {}unsafe impl、不带 unsafe_op_in_unsafe_fnunsafe fnunsafe extern#[unsafe(attr)]

下面将讨论这些情况。 请参阅 关键字 文档中的一些示例。

unsafe 关键字可以出现在几种不同的上下文中:

  • 不安全函数 (unsafe fn)
  • 不安全块 (unsafe {})
  • 不安全特型 (unsafe trait)
  • 不安全特型实现 (unsafe impl)
  • 不安全外部块 (unsafe extern)
  • 不安全外部静态项 (unsafe static)
  • 不安全属性 (#[unsafe(attr)])

不安全函数(unsafe fn)

不安全函数是在所有上下文和/或所有可能的输入下都不安全的函数。 我们说它们具有 额外安全条件 ,这些是所有调用者必须遵守的要求,且编译器不会检查。 例如,get_unchecked 具有索引必须在范围内的额外安全条件。 不安全函数应当附带说明这些额外安全条件的文档。

此类函数必须带有 unsafe 关键字前缀,且只能从 unsafe 块内部调用,或者在没有启用 unsafe_op_in_unsafe_fn lint 的 unsafe fn 内部调用。

不安全块(unsafe {})

代码块可以带有 unsafe 关键字前缀,以允许使用 不安全性 一章中定义的各种不安全操作,例如调用其他不安全函数或解引用原始指针。

默认情况下,不安全函数的主体也被认为是一个不安全块; 这可以通过启用 unsafe_op_in_unsafe_fn lint 来更改。

通过将操作放入不安全块中,程序员声明他们已经负责满足该块内所有操作的额外安全条件。

不安全块是不安全函数的逻辑对等物: 不安全函数定义了调用者必须遵守的证明义务,而不安全块则表示块内调用的函数或操作的所有相关证明义务都已履行。 履行证明义务的方法有很多; 例如,可以有运行时检查或数据结构不变性,保证某些属性绝对为真;或者不安全块可能位于 unsafe fn 内部,在这种情况下,该块可以使用该函数的证明义务来履行块内产生的证明义务。

不安全块用于包装外部库、直接使用硬件或实现语言中没有直接提供的功能。 例如,Rust 提供了实现内存安全并发所需的语言特性,但标准库中线程和消息传递的实现使用了不安全块。

Rust 的类型系统是对动态安全要求的保守近似,因此在某些情况下,使用安全代码会有性能代价。 例如,双向链表不是树结构,在安全代码中只能用引用计数指针表示。 通过使用 unsafe 块将反向链接表示为原始指针,可以在不使用引用计数的情况下实现它。 (有关此特定示例的更深入探索,请参阅 “Learn Rust With Entirely Too Many Linked Lists”。)

不安全特型(unsafe trait)

不安全特型是附带额外安全条件的特型,这些条件必须由该特型的 实现 遵守。 不安全特型应当附带说明这些额外安全条件的文档。

此类特型必须带有 unsafe 关键字前缀,且只能由 unsafe impl 块实现。

不安全特型实现(unsafe impl)

在实现不安全特型时,实现需要带有 unsafe 关键字前缀。 通过编写 unsafe impl,程序员声明他们已经负责满足特型所要求的额外安全条件。

不安全特型实现是不安全特型的逻辑对等物:不安全特型定义了实现必须遵守的证明义务,而不安全实现则表示所有相关的证明义务都已履行。

不安全外部块(unsafe extern)

声明 外部块 的程序员必须确保其中包含的项的签名是正确的。如果不这样做,可能会导致未定义行为。通过编写 unsafe extern 来表明这一义务已经履行。

2024 版次差异

在 2024 版次 之前,允许在没有 unsafe 限定的情况下使用 extern 块。

不安全属性(#[unsafe(attr)])

不安全属性 是指在使用该属性时必须遵守额外安全条件的属性。编译器无法检查这些条件是否已得到遵守。为了断言它们已得到遵守,这些属性必须包装在 unsafe(..) 中,例如 #[unsafe(no_mangle)]

被视为未定义的行为

Rust 代码如果表现出以下列表中的任何行为,就是不正确的。这包括 unsafe 块和 unsafe 函数中的代码。unsafe 仅意味着避免未定义行为的责任在程序员身上;它并不会改变 Rust 程序决不能导致未定义行为这一事实。

在编写 unsafe 代码时,程序员有责任确保任何与 unsafe 代码交互的安全代码都不会触发这些行为。对于任何安全客户端都能满足此属性的 unsafe 代码被称为 sound (无害的) ;如果 unsafe 代码可能被安全代码误用而表现出未定义行为,则它是 unsound (有害的)

警告

以下列表并不详尽;它可能会增加或减少。对于在不安全代码中哪些允许、哪些不允许,目前还没有正式的 Rust 语义模型,因此可能还有更多行为被认为是不安全的。我们也保留在未来将列表中某些行为变为“已定义行为”的权利。换句话说,此列表并不代表任何内容在所有未来的 Rust 版本中都 肯定 始终是未定义的(但我们将来可能会对某些列表项做出此类承诺)。

在编写不安全代码之前,请阅读 Rustonomicon

  • 数据竞争。
  • 违反指针别名规则。精确的别名规则尚未确定,但这里有一个通用原则的大纲: &T 必须指向在其存续期间未被修改的内存(除了 UnsafeCell<U> 内部的数据), 且 &mut T 必须指向在其存续期间不被任何非该引用派生的指针读取或写入,且没有其他引用指向的内存。 出于这些规则的目的,Box<T> 的处理方式类似于 &'static mut T。 精确的存续持续时间 (liveness duration) 尚未指定,但存在一些界限:

    • 对于引用,存续持续时间由借用检查器分配的语法生命周期作为上限;它的存续时间不能比该生命周期更
    • 每次解引用或重新借用引用或 Box 时,它都被视为存续。
    • 每次将引用或 Box 传递给函数或从函数返回时,它都被视为存续。
    • 当引用(但不是 Box!)传递给函数时,它的存续时间至少与该函数调用一样长,同样,除非 &T 包含 UnsafeCell<U>

    当这些类型的值作为复合类型的(嵌套)字段传递时,上述规则同样适用,但不适用于指针间接寻址后的情况。

  • 修改不可变字节。 通过 常量提升 表达式可达的所有字节都是不可变的,以及通过 staticconst 初始化器中已 生命周期延长'static 的借用可达的字节也是不可变的。 由不可变绑定或不可变 static 拥有的字节是不可变的,除非这些字节是 UnsafeCell<U> 的一部分。

    此外,由共享引用 指向 的字节(包括通过其他引用(共享和可变)以及 Box 的间接指向)都是不可变的;传递性包括存储在复合类型字段中的那些引用。

    修改 (mutation) 是指与任何相关字节重叠的、多于 0 字节的任何写入(即使该写入没有改变内存内容)。

  • 通过编译器内部函数 (compiler intrinsics) 调用未定义行为。
  • 执行由当前平台不支持的平台特性编译的代码(请参阅 target_feature),除非 平台明确记录这是安全的。
  • 使用错误的 调用 ABI 调用函数,或者回溯 (unwind) 超过一个不允许回溯的栈帧(例如,通过调用一个导入为或转录为 "C" 函数或函数指针的 "C-unwind" 函数)。
  • 产生 无效值。“产生”一个值发生在任何时候:当一个值被赋值给一个位置、从一个位置读取、传递给函数/原始操作或从函数/原始操作返回时。
  • 错误使用内联汇编。有关更多详细信息,请参阅编写使用内联汇编的代码时应遵守的 规则
  • 违反 Rust 运行时的假设。目前大多数 Rust 运行时的假设尚未明确记录。
    • 对于专门与回溯相关的假设,请参阅 恐慌文档
    • 运行时假设 Rust 栈帧在不执行该栈帧拥有的局部变量的析构函数的情况下不会被释放。这一假设可能被像 longjmp 这样的 C 函数违反。

注意

未定义行为会影响整个程序。例如,调用一个表现出 C 语言未定义行为的 C 函数意味着你的整个程序都包含未定义行为,这也可能影响 Rust 代码。反之亦然,Rust 中的未定义行为可能会对通过 FFI 调用其他语言执行的代码产生不利影响。

指向的字节

指针或引用“指向”的字节范围由指针值和被指向类型的类型大小决定(使用 size_of_val)。

基于对齐错误指针的位置

如果在位置计算过程中的最后一个 * 投影是在对其类型未对齐的指针上执行的,则称该位置是“基于对齐错误指针的”。(如果位置表达式中没有 * 投影,那么这就是访问局部变量或 static 的字段,rustc 将保证正确的对齐。如果有多个 * 投影,则其中每一个都会导致从内存中加载待解引用的指针本身,并且这些加载中的每一个都受对齐约束。请注意,由于自动解引用,在 Rust 表面语法格式中可能会省略一些 * 投影;我们在这里考虑的是完全展开后的位置表达式。)

例如,如果 ptr 的类型为 *const S,其中 S 的对齐要求为 8,那么 ptr 必须 8 字节对齐,否则 (*ptr).f 就是“基于对齐错误指针的”。即使字段 f 的类型是 u8(即对齐要求为 1 的类型),这也是成立的。换句话说,对齐要求源自被解引用的指针类型,而不是 正在被访问的字段类型。

请注意,基于对齐错误指针的位置仅在被加载或存储时才会导致未定义行为。

在这样的位置上使用 &raw const/&raw mut 是允许的。

在位置上使用 &/&mut 要求满足字段类型的对齐(否则程序将“产生无效值”),这通常比“基于对齐的指针”的要求更宽松。

在字段类型可能比包含它的类型更对齐的情况下(即 repr(packed)),获取引用将导致编译器错误。这意味着基于对齐的指针始终足以确保新引用是对齐的,但它并不总是必要的。

悬垂指针

如果引用/指针 指向 的字节并不都属于同一个存活的分配(特别地,它们都必须属于 某个 分配),则该引用/指针是“悬垂的”。

如果大小为 0,那么指针在平凡意义上永远不会是“悬垂的”(即使它是空指针)。

请注意,动态大小类型(如切片和字符串)指向它们的整个范围,因此长度元数据绝不能过大非常重要。

特别地,Rust 值的动态大小(由 size_of_val 确定)绝不能超过 isize::MAX,因为单个分配的大小不可能大于 isize::MAX

无效值

Rust 编译器假设程序执行期间产生的所有值都是“有效的”,因此产生无效值会立即导致 UB。

一个值是否有效取决于其类型:

  • 一个 bool 值必须是 false (0) 或 true (1)。
  • 一个 fn 指针值必须非空。
  • 一个 char 值不能是代理对 (surrogate)(即不能在 0xD800..=0xDFFF 范围内),且必须小于或等于 char::MAX
  • ! 值绝不能存在。
  • 整数 (i*/u*)、浮点值 (f*) 或原始指针必须已初始化,即不能从未初始化的内存中获得。
  • str 值被视为类似于 [u8],即它必须已初始化。
  • 一个 enum 必须具有有效的判别值 (discriminant),且该判别值指示的变体 (variant) 的所有字段必须在其各自类型上有效。
  • struct、元组和数组要求所有字段/元素在其各自类型上有效。
  • 对于 union,确切的有效性要求尚未确定。 显然,所有完全在安全代码中创建的值都是有效的。 如果联合体具有零大小字段,则每个可能的值都是有效的。 更多细节 仍处于讨论中
  • 引用或 Box<T> 必须对齐且非空,它不能是 悬垂的,且必须指向一个有效值 (对于动态大小类型,使用由元数据确定的被指向者的实际动态类型)。 请注意,最后一点(关于指向有效值)仍是一个有争议的话题。
  • 宽引用、Box<T> 或原始指针的元数据必须与 unsized tail 的类型匹配:
    • dyn Trait 元数据必须是编译器为 Trait 生成的虚表 (vtable) 指针。 (对于原始指针,这一要求仍是一个有争议的话题。)
    • 切片 ([T]) 元数据必须是一个有效的 usize。 此外,对于宽引用和 Box<T>,如果切片元数据使得被指向值的总大小大于 isize::MAX,则该元数据是无效的。
  • 如果一个类型具有自定义的有效值范围,那么有效值必须在该范围内。 在标准库中,这会影响 NonNull<T>NonZero<T>

    注意

    rustc 通过不稳定的 rustc_layout_scalar_valid_range_* 属性来实现这一点。

  • 常量上下文:除了上述描述之外,常量求值期间还适用进一步的与 物源 (provenance) 相关的要求。任何持有纯整数数据(i*/u*/f* 类型以及 boolchar、枚举判别值和切片元数据)的值都不得携带任何物源。任何持有指针数据(引用、原始指针、函数指针和 dyn Trait 元数据)的值必须要么不携带物源,要么所有字节必须是同一个原始指针值按正确顺序排列的碎片。

    这意味着如果指针具有物源,那么将指针(引用、原始指针或函数指针) transmute 或以其他方式重新解释为非指针类型(如整数)就是未定义行为。

    例子

    以下所有情况都是 UB:

    #![allow(unused)]
    fn main() {
    use core::mem::MaybeUninit;
    use core::ptr;
    // 我们不能将具有物源的指针重新解释为整数,
    // 否则该整数的字节将具有物源。
    const _: usize = {
        let ptr = &0;
        unsafe { (&raw const ptr as *const usize).read() }
    };
    
    // 我们不能重新排列具有物源的指针的字节,
    // 然后将它们解释为引用,因为那样持有
    // 指针数据的值将具有顺序错误的指针碎片。
    const _: &i32 = {
        let mut ptr = &0;
        let ptr_bytes = &raw mut ptr as *mut MaybeUninit::<u8>;
        unsafe { ptr::swap(ptr_bytes.add(1), ptr_bytes.add(2)) };
        ptr
    };
    }

注意: 未初始化内存对于任何具有限制有效值集合的类型也是隐式无效的。换句话说,唯一允许读取未初始化内存的情况是在 union 内部和“填充 (padding)”中(类型字段之间的间隙)。

不被视为 unsafe 的行为

Rust 编译器不认为以下行为是 不安全 的,尽管程序员可能(应该)发现它们是不受欢迎的、意外的或错误的。

  • 死锁
  • 内存和其他资源的泄漏
  • 退出而不调用析构函数
  • 通过指针泄漏暴露随机化的基地址

整数溢出

如果程序包含算术溢出,说明程序员犯了错误。在接下来的讨论中,我们对算术溢出和回绕算术进行区分。前者是错误的,而后者是有意的。

当程序员启用了 debug_assert! 断言时(例如,通过启用非优化构建),实现必须插入在溢出时触发 恐慌 的动态检查。其他类型的构建可能会根据实现的决定,在溢出时导致 恐慌 或静默回绕值。

在隐式回绕溢出的情况下,实现必须通过使用补码溢出惯例提供定义良好的(即使仍被认为是错误的)结果。

整数类型提供了固有方法,允许程序员显式地执行回绕算术。例如,i32::wrapping_add 提供了补码回绕加法。

标准库还提供了一个 Wrapping<T> 新类型,它确保 T 的所有标准算术运算都具有回绕语义。

有关错误条件、基本原理以及关于整数溢出的更多详细信息,请参阅 RFC 560

逻辑错误

安全代码可能会施加额外的逻辑约束,这些约束既不能在编译时也不能在运行时检查。如果程序违反了此类约束,行为可能是未指明的,但不会导致未定义行为。这可能包括 恐慌 、错误结果、中止和非终止。行为在不同运行、构建或构建类型之间也可能有所不同。

例如,同时实现 HashEq 要求被认为相等的值具有相等的哈希值。另一个例子是像 BinaryHeapBTreeMapBTreeSetHashMapHashSet 这样的数据结构,它们描述了当键位于数据结构中时对其进行修改的约束。违反这些约束不被视为不安全,但程序被认为是错误的,其行为是不可预测的。

常量求值

常量求值是在编译过程中计算 表达式 结果的过程。所有表达式中只有一部分可以在编译时求值。

常量表达式

某些形式的表达式,称为常量表达式,可以在编译时求值。

常量上下文 中的表达式必须是常量表达式。

常量上下文中的表达式总是在编译时求值。

在常量上下文之外,常量表达式 可能 会被求值,但不保证一定在编译时求值。

如果一个值必须在编译时(即在常量上下文中)求值,那么诸如越界 数组索引溢出 之类的行为将是编译器错误。否则,这些行为只是警告,但在运行时很可能会产生 恐慌。

只要所有操作数也都是常量表达式,且不会导致任何 Drop::drop 调用被运行,以下表达式就是常量表达式。

  • 指向 静态项 的路径,具有以下限制:

    • 在任何常量求值上下文中都不允许对 static 项进行写入。
    • 在任何常量求值上下文中都不允许从 extern 静态项中读取。
    • 如果求值 不是static 项的初始化器中进行的,那么就不允许从任何可变的 static 中读取。可变的 static 是指 static mut 项,或者具有内部可变类型的 static 项。

    这些要求仅在常量求值时检查。换句话说,只要这些访问从未被执行,它们在语法上出现在常量上下文中是允许的。

  • 所有形式的 借用,包括原始借用,但以下表达式的借用除外(这些表达式的临时作用域会被延长(见 临时变量生命周期延长)至程序结束):

    • 可变借用。
    • 对产生具有 内部可变性 的值的表达式的共享借用。
    #![allow(unused)]
    fn main() {
    // 由于处于尾部位置,此借用将临时变量的作用域延长至程序结束。
    // 由于借用是可变的,这在常量表达式中是不允许的。
    const C: &u8 = &mut 0; // ERROR not allowed
    }
    #![allow(unused)]
    fn main() {
    // 常量块类似于常量项的初始化器。
    let _: &u8 = const { &mut 0 }; // ERROR not allowed
    }
    #![allow(unused)]
    fn main() {
    use core::sync::atomic::AtomicU8;
    // 这是不允许的,因为 1) 临时作用域延长到了程序结束,且 2) 临时变量具有内部可变性。
    const C: &AtomicU8 = &AtomicU8::new(0); // ERROR not allowed
    }
    #![allow(unused)]
    fn main() {
    use core::sync::atomic::AtomicU8;
    // 同上。
    let _: &_ = const { &AtomicU8::new(0) }; // ERROR not allowed
    }
    #![allow(unused)]
    fn main() {
    #![allow(static_mut_refs)]
    // 尽管此借用是可变的,但它不是对临时变量的借用,因此这是允许的。
    const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; // OK
    }
    #![allow(unused)]
    fn main() {
    use core::sync::atomic::AtomicU8;
    // 尽管此借用是对具有内部可变性的值的借用,但它不是对临时变量的借用,因此这是允许的。
    const C: &AtomicU8 = {
        static S: AtomicU8 = AtomicU8::new(0); &S // OK
    };
    }
    #![allow(unused)]
    fn main() {
    use core::sync::atomic::AtomicU8;
    // 这种对内部可变临时变量的共享借用是允许的,因为其作用域没有被延长。
    const C: () = { _ = &AtomicU8::new(0); }; // OK
    }
    #![allow(unused)]
    fn main() {
    // 尽管借用是可变的,且临时变量因提升而存续到程序结束,但这是允许的,因为
    // 借用不在尾部位置,因此临时变量的作用域不会通过临时变量生命周期延长来延长。
    const C: () = { let _: &'static mut [u8] = &mut []; }; // OK
    //                                              ~~
    //                                     提升的临时变量。
    }

    注意

    换句话说 —— 为了关注什么是允许的而不是什么是不允许的 —— 只有当被借用的 位置表达式瞬态的间接的静态的 时,才允许在 常量上下文 中对内部可变数据进行共享借用和可变借用。

    如果位置表达式是当前常量上下文的局部变量,或者是临时作用域包含在当前常量上下文中的表达式,则该位置表达式是 瞬态的

    #![allow(unused)]
    fn main() {
    // 借用是对初始化器局部变量的借用,因此此位置表达式是瞬态的。
    const C: () = { let mut x = 0; _ = &mut x; };
    }
    #![allow(unused)]
    fn main() {
    // 借用是对作用域未延长的临时变量的借用,因此此位置表达式是瞬态的。
    const C: () = { _ = &mut 0u8; };
    }
    #![allow(unused)]
    fn main() {
    // 当临时变量被提升但没有延长生命周期时,其位置表达式仍被视为瞬态的。
    const C: () = { let _: &'static mut [u8] = &mut []; };
    }

    如果位置表达式是 解引用表达式,则该位置表达式是 间接的

    #![allow(unused)]
    fn main() {
    const C: () = { _ = &mut *(&mut 0); };
    }

    如果位置表达式是一个 static 项,则该位置表达式是 静态的

    #![allow(unused)]
    fn main() {
    #![allow(static_mut_refs)]
    const C: &u8 = unsafe { static mut S: u8 = 0; &mut S };
    }

    注意

    这些规则的一个令人惊讶的后果是我们允许这样做:

    #![allow(unused)]
    fn main() {
    const C: &[u8] = { let x: &mut [u8] = &mut []; x }; // OK
    //                                    ~~~~~~~
    // 即使在可变借用之后,空数组也会被提升。
    }

    但我们不允许类似的这段代码:

    #![allow(unused)]
    fn main() {
    const C: &[u8] = &mut []; // ERROR
    //               ~~~~~~~
    //           尾部表达式。
    }

    它们之间的区别在于,在第一种情况中,空数组被 提升 了,但它的作用域没有经历 临时变量生命周期延长,所以我们认为 位置表达式 是瞬态的(即使在提升之后该位置确实存续到程序结束)。在第二种情况中,空数组临时变量的作用域确实经历了生命周期延长,因此它因为是对生命周期延长的临时变量的可变借用(从而借用了非瞬态的位置表达式)而被拒绝。

    这种效果令人惊讶,因为在这种情况下,临时变量生命周期延长导致可编译的代码比没有它时更少。

    issue #143129 了解更多细节。

  • 解引用表达式

    #![allow(unused)]
    fn main() {
    use core::cell::UnsafeCell;
    const _: u8 = unsafe {
        let x: *mut u8 = &raw mut *&mut 0;
        //                        ^^^^^^^
        //             对可变引用的解引用。
        *x = 1; // 对可变指针的解引用。
        *(x as *const u8) // 对常量指针的解引用。
    };
    const _: u8 = unsafe {
        let x = &UnsafeCell::new(0);
        *x.get() = 1; // 对内部可变值的修改。
        *x.get()
    };
    }
  • 转换 表达式,除了
    • 指针到地址转换以及
    • 函数指针到地址转换。

常量上下文

常量上下文 是以下之一:

作为类型一部分使用的常量上下文(数组类型和重复长度表达式以及常量泛型参数)只能限制性地使用周围的泛型参数:此类表达式必须要么是单个裸常量泛型参数,要么是不使用任何泛型的任意表达式。

常量函数

常量函数 是可以从常量上下文中调用的函数。它使用 const 限定符定义,并且还包括 元组结构体元组枚举变体 构造函数。

例子

#![allow(unused)]
fn main() {
const fn square(x: i32) -> i32 { x * x }

const VALUE: i32 = square(12);
square(12);
}

当从常量上下文中调用时,常量函数由编译器在编译时解释。这种解释发生在编译 target 的环境中,而不是宿主环境。因此,如果你针对 32 位系统进行编译,usize 就是 32 位,而不管你是在 64 位还是 32 位系统上进行构建。

当在常量上下文之外调用常量函数时,它的行为与没有 const 限定符时相同。

常量函数的主体只能使用 常量表达式

常量函数不允许是 异步 的。

常量函数的参数类型和返回类型仅限于那些与常量上下文兼容的类型。

应用二进制接口(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 属性

此属性是不安全的,因为不混淆的符号可能会与另一个具有相同名称的符号(或与众所周知的符号)发生冲突,从而导致未定义行为。

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
extern "C" fn foo() {}
}

2024 版次差异

在 2024 版次 之前,允许不带 unsafe 限定地使用 no_mangle 属性。

link_section 属性 指定了 函数static 内容将放置在目标文件的哪个节中。

link_section 属性使用 MetaNameValueStr 语法来指定节的名称。

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
#[unsafe(link_section = ".example_section")]
pub static VAR1: u32 = 1;
}

此属性是不安全的,因为它允许用户将数据和代码放置在不期望它们的内存节中,例如将可变数据放置在只读区域中。

2024 版次差异

在 2024 版次 之前,允许不带 unsafe 限定地使用 link_section 属性。

export_name属性

export_name 属性 指定了将在 函数static 上导出的符号名称。

export_name 属性使用 MetaNameValueStr 语法来指定符号名称。

#![allow(unused)]
fn main() {
#[unsafe(export_name = "exported_symbol_name")]
pub fn name_in_rust() { }
}

此属性是不安全的,因为具有自定义名称的符号可能会与另一个具有相同名称的符号(或与众所周知的符号)发生冲突,从而导致未定义行为。

2024 版次差异

在 2024 版次 之前,允许不带 unsafe 限定地使用 export_name 属性。

Rust 运行时

本节记录了定义 Rust 运行时某些方面的特性。

global_allocator属性

global_allocator 属性 选择一个 内存分配器

例子

#![allow(unused)]
fn main() {
use core::alloc::{GlobalAlloc, Layout};
use std::alloc::System;

struct MyAllocator;

unsafe impl GlobalAlloc for MyAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        unsafe { System.alloc(layout) }
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) }
    }
}

#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;
}

global_allocator 属性使用 MetaWord 语法格式。

global_allocator 属性只能应用于其类型实现了 GlobalAlloc 特型 的 静态项

global_allocator 属性在项上只能使用一次。

global_allocator 属性在 crate 图中只能使用一次。

global_allocator 属性从 标准库预导入 中导出。

windows_subsystem属性

windows_subsystem 属性 在 Windows target 上链接时设置 子系统

例子

#![allow(unused)]
#![windows_subsystem = "windows"]
fn main() {
}

windows_subsystem 属性使用 MetaNameValueStr 语法格式。可接受的值为 "console""windows"

windows_subsystem 属性只能应用于 crate 根。

只有第一次使用的 windows_subsystem 才会生效。

注意

rustc 会对第一次之后的任何使用发出 lint。这在将来可能会变成一个错误。

windows_subsystem 属性在非 Windows target 和非 bin crate 类型 上会被忽略。

"console" 子系统是默认值。如果从现有的控制台运行控制台进程,那么它将附加到该控制台;否则将创建一个新的控制台窗口。

"windows" 子系统将脱离任何现有的控制台运行。

注意

"windows" 子系统通常由不希望在启动时显示控制台窗口的 GUI 应用程序使用。

附录

语法总结

以下是语法生成规则的总结。有关此语法的 语法格式 详情,请参阅 语法格式记法

宏 总结

Syntax
MacroInvocation
    SimplePath ! DelimTokenTree

DelimTokenTree
      ( TokenTree* )
    | [ TokenTree* ]
    | { TokenTree* }

TokenTree
    Tokenexcept delimiters | DelimTokenTree

MacroInvocationSemi
      SimplePath ! ( TokenTree* ) ;
    | SimplePath ! [ TokenTree* ] ;
    | SimplePath ! { TokenTree* }

MacroRulesDefinition
    macro_rules ! IDENTIFIER MacroRulesDef

MacroRulesDef
      ( MacroRules ) ;
    | [ MacroRules ] ;
    | { MacroRules }

MacroRules
    MacroRule ( ; MacroRule )* ;?

MacroRule
    MacroMatcher => MacroTranscriber

MacroMatcher
      ( MacroMatch* )
    | [ MacroMatch* ]
    | { MacroMatch* }

MacroMatch
      Tokenexcept $ and delimiters
    | MacroMatcher
    | $ ( IDENTIFIER_OR_KEYWORDexcept crate | RAW_IDENTIFIER ) : MacroFragSpec
    | $ ( MacroMatch+ ) MacroRepSep? MacroRepOp

MacroFragSpec
      block | expr | expr_2021 | ident | item | lifetime | literal
    | meta | pat | pat_param | path | stmt | tt | ty | vis

MacroRepSepTokenexcept delimiters and MacroRepOp

MacroRepOp* | + | ?

MacroTranscriberDelimTokenTree

项 总结

Syntax
Struct
      StructStruct
    | TupleStruct

StructStruct
    struct IDENTIFIER GenericParams? WhereClause? ( { StructFields? } | ; )

TupleStruct
    struct IDENTIFIER GenericParams? ( TupleFields? ) WhereClause? ;

StructFieldsStructField ( , StructField )* ,?

StructFieldOuterAttribute* Visibility? IDENTIFIER : Type

TupleFieldsTupleField ( , TupleField )* ,?

TupleFieldOuterAttribute* Visibility? Type

ExternBlock
    unsafe? extern Abi? {
        InnerAttribute*
        ExternalItem*
    }

ExternalItem
    OuterAttribute* (
        MacroInvocationSemi
      | Visibility? StaticItem
      | Visibility? Function
    )

StaticItem
    ItemSafety? static mut? IDENTIFIER : Type ( = Expression )? ;

Trait
    unsafe? trait IDENTIFIER GenericParams? ( : TypeParamBounds? )? WhereClause?
    {
        InnerAttribute*
        AssociatedItem*
    }

UseDeclarationuse UseTree ;

UseTree
      ( SimplePath? :: )? *
    | ( SimplePath? :: )? { ( UseTree ( , UseTree )* ,? )? }
    | SimplePath ( as ( IDENTIFIER | _ ) )?

AssociatedItem
    OuterAttribute* (
        MacroInvocationSemi
      | ( Visibility? ( TypeAlias | ConstantItem | Function ) )
    )

GenericParams< ( GenericParam ( , GenericParam )* ,? )? >

GenericParamOuterAttribute* ( LifetimeParam | TypeParam | ConstParam )

LifetimeParamLifetime ( : LifetimeBounds )?

TypeParamIDENTIFIER ( : TypeParamBounds? )? ( = Type )?

ConstParam
    const IDENTIFIER : Type
    ( = ( BlockExpression | IDENTIFIER | -? LiteralExpression ) )?

WhereClausewhere ( WhereClauseItem , )* WhereClauseItem?

WhereClauseItem
      LifetimeWhereClauseItem
    | TypeBoundWhereClauseItem

LifetimeWhereClauseItemLifetime : LifetimeBounds

TypeBoundWhereClauseItemForLifetimes? Type : TypeParamBounds?

Module
      unsafe? mod IDENTIFIER ;
    | unsafe? mod IDENTIFIER {
        InnerAttribute*
        Item*
      }

Enumeration
    enum IDENTIFIER GenericParams? WhereClause? { EnumVariants? }

EnumVariantsEnumVariant ( , EnumVariant )* ,?

EnumVariant
    OuterAttribute* Visibility?
    IDENTIFIER ( EnumVariantTuple | EnumVariantStruct )? EnumVariantDiscriminant?

EnumVariantTuple( TupleFields? )

EnumVariantStruct{ StructFields? }

EnumVariantDiscriminant= Expression

TypeAlias
    type IDENTIFIER GenericParams? ( : TypeParamBounds )?
        WhereClause?
        ( = Type WhereClause? )? ;

ConstantItem
    const ( IDENTIFIER | _ ) : Type ( = Expression )? ;

Function
    FunctionQualifiers fn IDENTIFIER GenericParams?
        ( FunctionParameters? )
        FunctionReturnType? WhereClause?
        ( BlockExpression | ; )

FunctionQualifiersconst? async? ItemSafety? ( extern Abi? )?

ItemSafetysafe | unsafe

AbiSTRING_LITERAL | RAW_STRING_LITERAL

FunctionParameters
      SelfParam ,?
    | ( SelfParam , )? FunctionParam ( , FunctionParam )* ,?

SelfParamOuterAttribute* ( ShorthandSelf | TypedSelf )

ShorthandSelf → ( & | & Lifetime )? mut? self

TypedSelfmut? self : Type

FunctionParamOuterAttribute* ( FunctionParamPattern | ... | Type )

FunctionParamPatternPatternNoTopAlt : ( Type | ... )

FunctionReturnType-> Type

ImplementationInherentImpl | TraitImpl

InherentImpl
    impl GenericParams? Type WhereClause? {
        InnerAttribute*
        AssociatedItem*
    }

TraitImpl
    unsafe? impl GenericParams? !? TypePath for Type
    WhereClause?
    {
        InnerAttribute*
        AssociatedItem*
    }

ExternCrateextern crate CrateRef AsClause? ;

CrateRefIDENTIFIER | self

AsClauseas ( IDENTIFIER | _ )

Union
    union IDENTIFIER GenericParams? WhereClause? { StructFields? }

Visibility
      pub
    | pub ( crate )
    | pub ( self )
    | pub ( super )
    | pub ( in SimplePath )

Item
    OuterAttribute* ( VisItem | MacroItem )

VisItem
    Visibility?
    (
        Module
      | ExternCrate
      | UseDeclaration
      | Function
      | TypeAlias
      | Struct
      | Enumeration
      | Union
      | ConstantItem
      | StaticItem
      | Trait
      | Implementation
      | ExternBlock
    )

MacroItem
      MacroInvocationSemi
    | MacroRulesDefinition

Crate
    InnerAttribute*
    Item*

ItemSafety safe unsafe
Visibility pub pub ( crate ) pub ( self ) pub ( super ) pub ( in SimplePath )

词法分析器 总结

Syntax
Token
      RESERVED_TOKEN
    | RAW_IDENTIFIER
    | CHAR_LITERAL
    | STRING_LITERAL
    | RAW_STRING_LITERAL
    | BYTE_LITERAL
    | BYTE_STRING_LITERAL
    | RAW_BYTE_STRING_LITERAL
    | C_STRING_LITERAL
    | RAW_C_STRING_LITERAL
    | FLOAT_LITERAL
    | INTEGER_LITERAL
    | LIFETIME_TOKEN
    | PUNCTUATION
    | IDENTIFIER_OR_KEYWORD

SUFFIXIDENTIFIER_OR_KEYWORDexcept _

SUFFIX_NO_ESUFFIXnot beginning with e or E

CHAR_LITERAL
    '
        ( ~[' \ LF CR TAB] | QUOTE_ESCAPE | ASCII_ESCAPE | UNICODE_ESCAPE )
    ' SUFFIX?

QUOTE_ESCAPE\' | \"

ASCII_ESCAPE
      \x OCT_DIGIT HEX_DIGIT
    | \n | \r | \t | \\ | \0

UNICODE_ESCAPE
    \u{ ( HEX_DIGIT _* )1..6valid hex char value }

STRING_LITERAL
    " (
        ~[" \ CR]
      | QUOTE_ESCAPE
      | ASCII_ESCAPE
      | UNICODE_ESCAPE
      | STRING_CONTINUE
    )* " SUFFIX?

STRING_CONTINUE\ LF

RAW_STRING_LITERALr RAW_STRING_CONTENT SUFFIX?

RAW_STRING_CONTENT
      " ( ~CR )* (non-greedy) "
    | # RAW_STRING_CONTENT #

BYTE_LITERAL
    b' ( ASCII_FOR_CHAR | BYTE_ESCAPE ) ' SUFFIX?

ASCII_FOR_CHAR
    <any ASCII (i.e. 0x00 to 0x7F) except ', \, LF, CR, or TAB>

BYTE_ESCAPE
      \x HEX_DIGIT HEX_DIGIT
    | \n | \r | \t | \\ | \0 | \' | \"

BYTE_STRING_LITERAL
    b" ( ASCII_FOR_STRING | BYTE_ESCAPE | STRING_CONTINUE )* " SUFFIX?

ASCII_FOR_STRING
    <any ASCII (i.e 0x00 to 0x7F) except ", \, or CR>

RAW_BYTE_STRING_LITERAL
    br RAW_BYTE_STRING_CONTENT SUFFIX?

RAW_BYTE_STRING_CONTENT
      " ASCII_FOR_RAW* (non-greedy) "
    | # RAW_BYTE_STRING_CONTENT #

ASCII_FOR_RAW
    <any ASCII (i.e. 0x00 to 0x7F) except CR>

C_STRING_LITERAL
    c" (
        ~[" \ CR NUL]
      | BYTE_ESCAPEexcept \0 or \x00
      | UNICODE_ESCAPEexcept \u{0}, \u{00}, …, \u{000000}
      | STRING_CONTINUE
    )* " SUFFIX?

RAW_C_STRING_LITERAL
    cr RAW_C_STRING_CONTENT SUFFIX?

RAW_C_STRING_CONTENT
      " ( ~[CR NUL] )* (non-greedy) "
    | # RAW_C_STRING_CONTENT #

INTEGER_LITERAL
    ( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL | DEC_LITERAL ) SUFFIX_NO_E?

DEC_LITERALDEC_DIGIT ( DEC_DIGIT | _ )*

BIN_LITERAL0b _* BIN_DIGIT ( BIN_DIGIT | _ )*

OCT_LITERAL0o _* OCT_DIGIT ( OCT_DIGIT | _ )*

HEX_LITERAL0x _* HEX_DIGIT ( HEX_DIGIT | _ )*

BIN_DIGIT → [0-1]

OCT_DIGIT → [0-7]

DEC_DIGIT → [0-9]

HEX_DIGIT → [0-9 a-f A-F]

TUPLE_INDEXDEC_LITERAL | BIN_LITERAL | OCT_LITERAL | HEX_LITERAL

FLOAT_LITERAL
      DEC_LITERAL ( . DEC_LITERAL )? FLOAT_EXPONENT SUFFIX?
    | DEC_LITERAL . DEC_LITERAL SUFFIX_NO_E?
    | DEC_LITERAL .not immediately followed by ., _ or an XID_Start character

FLOAT_EXPONENT
    ( e | E ) ( + | - )? _* DEC_DIGIT ( DEC_DIGIT | _ )*

RESERVED_NUMBER
      BIN_LITERAL [2-9]
    | OCT_LITERAL [8-9]
    | ( BIN_LITERAL | OCT_LITERAL | HEX_LITERAL ) .not immediately followed by ., _ or an XID_Start character
    | ( BIN_LITERAL | OCT_LITERAL ) ( e | E )
    | 0b _* <end of input or not BIN_DIGIT>
    | 0o _* <end of input or not OCT_DIGIT>
    | 0x _* <end of input or not HEX_DIGIT>
    | DEC_LITERAL ( . DEC_LITERAL )? ( e | E ) ( + | - )? <end of input or not DEC_DIGIT>

LIFETIME_TOKEN
      RAW_LIFETIME
    | ' IDENTIFIER_OR_KEYWORDnot immediately followed by '

LIFETIME_OR_LABEL
      RAW_LIFETIME
    | ' NON_KEYWORD_IDENTIFIERnot immediately followed by '

RAW_LIFETIME
    'r# IDENTIFIER_OR_KEYWORDnot immediately followed by '

RESERVED_RAW_LIFETIME'r# ( _ | crate | self | Self | super )not immediately followed by '

PUNCTUATION
      ...
    | ..=
    | <<=
    | >>=
    | !=
    | %=
    | &&
    | &=
    | *=
    | +=
    | -=
    | ->
    | ..
    | /=
    | ::
    | <-
    | <<
    | <=
    | ==
    | =>
    | >=
    | >>
    | >
    | ^=
    | |=
    | ||
    | !
    | #
    | $
    | %
    | &
    | (
    | )
    | *
    | +
    | ,
    | -
    | .
    | /
    | :
    | ;
    | <
    | =
    | ?
    | @
    | [
    | ]
    | ^
    | {
    | |
    | }
    | ~

RESERVED_TOKEN
      RESERVED_GUARDED_STRING_LITERAL
    | RESERVED_NUMBER
    | RESERVED_POUNDS
    | RESERVED_RAW_IDENTIFIER
    | RESERVED_RAW_LIFETIME
    | RESERVED_TOKEN_DOUBLE_QUOTE
    | RESERVED_TOKEN_LIFETIME
    | RESERVED_TOKEN_POUND
    | RESERVED_TOKEN_SINGLE_QUOTE

RESERVED_TOKEN_DOUBLE_QUOTE
    IDENTIFIER_OR_KEYWORDexcept b or c or r or br or cr "

RESERVED_TOKEN_SINGLE_QUOTE
    IDENTIFIER_OR_KEYWORDexcept b '

RESERVED_TOKEN_POUND
    IDENTIFIER_OR_KEYWORDexcept r or br or cr #

RESERVED_TOKEN_LIFETIME
    ' IDENTIFIER_OR_KEYWORDexcept r #

RESERVED_GUARDED_STRING_LITERAL#+ STRING_LITERAL

RESERVED_POUNDS#2..

WHITESPACE
      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 // 段落分隔符

TAB → U+0009 // 水平制表符,'\t'

LF → U+000A // 换行符,'\n'

CR → U+000D // 回车符,'\r'

IDENTIFIER_OR_KEYWORD → ( XID_Start | _ ) XID_Continue*

XID_Start<XID_Start defined by Unicode>

XID_Continue<XID_Continue defined by Unicode>

RAW_IDENTIFIERr# IDENTIFIER_OR_KEYWORD

NON_KEYWORD_IDENTIFIERIDENTIFIER_OR_KEYWORDexcept a strict or reserved keyword

IDENTIFIERNON_KEYWORD_IDENTIFIER | RAW_IDENTIFIER

RESERVED_RAW_IDENTIFIER
    r# ( _ | crate | self | Self | super )not immediately followed by XID_Continue

LINE_COMMENT
      // ( ~[/ ! LF] | // ) ~LF*
    | //

BLOCK_COMMENT
      /*
        ( ~[* !] | ** | BLOCK_COMMENT_OR_DOC )
        ( BLOCK_COMMENT_OR_DOC | ~*/ )*
      */
    | /**/
    | /***/

INNER_LINE_DOC
    //! ~[LF CR]*

INNER_BLOCK_DOC
    /*! ( BLOCK_COMMENT_OR_DOC | ~[*/ CR] )* */

OUTER_LINE_DOC
    /// ( ~/ ~[LF CR]* )?

OUTER_BLOCK_DOC
    /**
      ( ~* | BLOCK_COMMENT_OR_DOC )
      ( BLOCK_COMMENT_OR_DOC | ~[*/ CR] )*
    */

BLOCK_COMMENT_OR_DOC
      BLOCK_COMMENT
    | OUTER_BLOCK_DOC
    | INNER_BLOCK_DOC

CHAR<a Unicode scalar value>

NUL → U+0000

SUFFIX_NO_E not beginning with `e` or `E` SUFFIX
UNICODE_ESCAPE \u{ valid hex char value at most 5 more times HEX_DIGIT _ }
RAW_STRING_CONTENT " non-greedy ⚠️ with the exception of CR CHAR " # RAW_STRING_CONTENT #
ASCII_FOR_CHAR any ASCII (i.e. 0x00 to 0x7F) except `'`, `\`, LF, CR, or TAB
ASCII_FOR_STRING any ASCII (i.e 0x00 to 0x7F) except `"`, `\`, or CR
ASCII_FOR_RAW any ASCII (i.e. 0x00 to 0x7F) except CR
C_STRING_LITERAL c" ⚠️ with the exception of " \ CR NUL CHAR except `\0` or `\x00` BYTE_ESCAPE except `\u{0}`, `\u{00}`, …, `\u{000000}` UNICODE_ESCAPE STRING_CONTINUE " SUFFIX
RAW_C_STRING_CONTENT " non-greedy ⚠️ with the exception of CR NUL CHAR " # RAW_C_STRING_CONTENT #
HEX_DIGIT 0-9 a-f A-F
RESERVED_NUMBER BIN_LITERAL 2-9 OCT_LITERAL 8-9 BIN_LITERAL OCT_LITERAL HEX_LITERAL not immediately followed by `.`, `_` or an XID_Start character . BIN_LITERAL OCT_LITERAL e E 0b _ end of input or not BIN_DIGIT 0o _ end of input or not OCT_DIGIT 0x _ end of input or not HEX_DIGIT DEC_LITERAL . DEC_LITERAL e E + - end of input or not DEC_DIGIT
RAW_LIFETIME 'r# not immediately followed by `'` IDENTIFIER_OR_KEYWORD
RESERVED_RAW_LIFETIME 'r# not immediately followed by `'` _ crate self Self super
PUNCTUATION ... ..= <<= >>= != %= && &= *= += -= -> .. /= :: <- << <= == => >= >> > ^= |= || ! # $ % & ( ) * + , - . / : ; < = ? @ [ ] ^ { | } ~
RESERVED_TOKEN_DOUBLE_QUOTE except `b` or `c` or `r` or `br` or `cr` IDENTIFIER_OR_KEYWORD "
WHITESPACE U+0009 U+000A U+000B U+000C U+000D U+0020 U+0085 U+200E U+200F U+2028 U+2029
TAB U+0009
LF U+000A
CR U+000D
XID_Start `XID_Start` defined by Unicode
XID_Continue `XID_Continue` defined by Unicode
NON_KEYWORD_IDENTIFIER except a strict or reserved keyword IDENTIFIER_OR_KEYWORD
RESERVED_RAW_IDENTIFIER r# not immediately followed by XID_Continue _ crate self Self super
LINE_COMMENT // ⚠️ with the exception of / ! LF CHAR // ⚠️ with the exception of LF CHAR //
BLOCK_COMMENT /* ⚠️ with the exception of * ! CHAR ** BLOCK_COMMENT_OR_DOC BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CHAR */ /**/ /***/
INNER_LINE_DOC //! ⚠️ with the exception of LF CR CHAR
INNER_BLOCK_DOC /*! BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CR CHAR */
OUTER_LINE_DOC /// ⚠️ with the exception of / CHAR ⚠️ with the exception of LF CR CHAR
OUTER_BLOCK_DOC /** ⚠️ with the exception of * CHAR BLOCK_COMMENT_OR_DOC BLOCK_COMMENT_OR_DOC ⚠️ with the exception of */ CR CHAR */
CHAR a Unicode scalar value
NUL U+0000

模式 总结

Syntax
Pattern|? PatternNoTopAlt ( | PatternNoTopAlt )*

PatternNoTopAlt
      PatternWithoutRange
    | RangePattern

PatternWithoutRange
      LiteralPattern
    | IdentifierPattern
    | WildcardPattern
    | RestPattern
    | ReferencePattern
    | StructPattern
    | TupleStructPattern
    | TuplePattern
    | GroupedPattern
    | SlicePattern
    | PathPattern
    | MacroInvocation

LiteralPattern-? LiteralExpression

IdentifierPatternref? mut? IDENTIFIER ( @ PatternNoTopAlt )?

WildcardPattern_

RestPattern..

RangePattern
      RangeExclusivePattern
    | RangeInclusivePattern
    | RangeFromPattern
    | RangeToExclusivePattern
    | RangeToInclusivePattern
    | ObsoleteRangePattern

RangeExclusivePattern
      RangePatternBound .. RangePatternBound

RangeInclusivePattern
      RangePatternBound ..= RangePatternBound

RangeFromPattern
      RangePatternBound ..

RangeToExclusivePattern
      .. RangePatternBound

RangeToInclusivePattern
      ..= RangePatternBound

ObsoleteRangePattern
    RangePatternBound ... RangePatternBound

RangePatternBound
      LiteralPattern
    | PathExpression

ReferencePattern → ( & | && ) mut? PatternWithoutRange

StructPattern
    PathInExpression {
        StructPatternElements?
    }

StructPatternElements
      StructPatternFields ( , | , StructPatternEtCetera )?
    | StructPatternEtCetera

StructPatternFields
    StructPatternField ( , StructPatternField )*

StructPatternField
    OuterAttribute*
    (
        TUPLE_INDEX : Pattern
      | IDENTIFIER : Pattern
      | ref? mut? IDENTIFIER
    )

StructPatternEtCetera..

TupleStructPatternPathInExpression ( TupleStructItems? )

TupleStructItemsPattern ( , Pattern )* ,?

TuplePattern( TuplePatternItems? )

TuplePatternItems
      Pattern ,
    | RestPattern
    | Pattern ( , Pattern )+ ,?

GroupedPattern( Pattern )

SlicePattern[ SlicePatternItems? ]

SlicePatternItemsPattern ( , Pattern )* ,?

PathPatternPathExpression

表达式 总结

Syntax
CallExpressionExpression ( CallParams? )

CallParamsExpression ( , Expression )* ,?

UnderscoreExpression_

PathExpression
      PathInExpression
    | QualifiedPathInExpression

ClosureExpression
    async?
    move?
    ( || | | ClosureParameters? | )
    ( Expression | -> TypeNoBounds BlockExpression )

ClosureParametersClosureParam ( , ClosureParam )* ,?

ClosureParamOuterAttribute* PatternNoTopAlt ( : Type )?

TupleExpression( TupleElements? )

TupleElements → ( Expression , )+ Expression?

TupleIndexingExpressionExpression . TUPLE_INDEX

ArrayExpression[ ArrayElements? ]

ArrayElements
      Expression ( , Expression )* ,?
    | Expression ; Expression

IndexExpressionExpression [ Expression ]

IfExpression
    if Conditions BlockExpression
    ( else ( BlockExpression | IfExpression ) )?

Conditions
      Expressionexcept StructExpression
    | LetChain

LetChainLetChainCondition ( && LetChainCondition )*

LetChainCondition
      Expressionexcept ExcludedConditions
    | OuterAttribute* let Pattern = Scrutineeexcept ExcludedConditions

ExcludedConditions
      StructExpression
    | LazyBooleanExpression
    | RangeExpr
    | RangeFromExpr
    | RangeInclusiveExpr
    | AssignmentExpression
    | CompoundAssignmentExpression

MethodCallExpressionExpression . PathExprSegment ( CallParams? )

ReturnExpressionreturn Expression?

RangeExpression
      RangeExpr
    | RangeFromExpr
    | RangeToExpr
    | RangeFullExpr
    | RangeInclusiveExpr
    | RangeToInclusiveExpr

RangeExprExpression .. Expression

RangeFromExprExpression ..

RangeToExpr.. Expression

RangeFullExpr..

RangeInclusiveExprExpression ..= Expression

RangeToInclusiveExpr..= Expression

StructExpression
    PathInExpression { ( StructExprFields | StructBase )? }

StructExprFields
    StructExprField ( , StructExprField )* ( , StructBase | ,? )

StructExprField
    OuterAttribute*
    (
        IDENTIFIER
      | ( IDENTIFIER | TUPLE_INDEX ) : Expression
    )

StructBase.. Expression

FieldExpressionExpression . IDENTIFIER

OperatorExpression
      BorrowExpression
    | DereferenceExpression
    | TryPropagationExpression
    | NegationExpression
    | ArithmeticOrLogicalExpression
    | ComparisonExpression
    | LazyBooleanExpression
    | TypeCastExpression
    | AssignmentExpression
    | CompoundAssignmentExpression

BorrowExpression
      ( & | && ) Expression
    | ( & | && ) mut Expression
    | ( & | && ) raw const Expression
    | ( & | && ) raw mut Expression

DereferenceExpression* Expression

TryPropagationExpressionExpression ?

NegationExpression
      - Expression
    | ! Expression

ArithmeticOrLogicalExpression
      Expression + Expression
    | Expression - Expression
    | Expression * Expression
    | Expression / Expression
    | Expression % Expression
    | Expression & Expression
    | Expression | Expression
    | Expression ^ Expression
    | Expression << Expression
    | Expression >> Expression

ComparisonExpression
      Expression == Expression
    | Expression != Expression
    | Expression > Expression
    | Expression < Expression
    | Expression >= Expression
    | Expression <= Expression

LazyBooleanExpression
      Expression || Expression
    | Expression && Expression

TypeCastExpressionExpression as TypeNoBounds

AssignmentExpressionExpression = Expression

CompoundAssignmentExpression
      Expression += Expression
    | Expression -= Expression
    | Expression *= Expression
    | Expression /= Expression
    | Expression %= Expression
    | Expression &= Expression
    | Expression |= Expression
    | Expression ^= Expression
    | Expression <<= Expression
    | Expression >>= Expression

LoopExpression
    LoopLabel? (
        InfiniteLoopExpression
      | PredicateLoopExpression
      | IteratorLoopExpression
      | LabelBlockExpression
    )

InfiniteLoopExpressionloop BlockExpression

PredicateLoopExpressionwhile Conditions BlockExpression

IteratorLoopExpression
    for Pattern in Expressionexcept StructExpression BlockExpression

LoopLabelLIFETIME_OR_LABEL :

BreakExpressionbreak LIFETIME_OR_LABEL? Expression?

LabelBlockExpressionBlockExpression

ContinueExpressioncontinue LIFETIME_OR_LABEL?

GroupedExpression( Expression )

LiteralExpression
      CHAR_LITERAL
    | STRING_LITERAL
    | RAW_STRING_LITERAL
    | BYTE_LITERAL
    | BYTE_STRING_LITERAL
    | RAW_BYTE_STRING_LITERAL
    | C_STRING_LITERAL
    | RAW_C_STRING_LITERAL
    | INTEGER_LITERAL
    | FLOAT_LITERAL
    | true
    | false

AwaitExpressionExpression . await

MatchExpression
    match Scrutinee {
        InnerAttribute*
        MatchArms?
    }

ScrutineeExpressionexcept StructExpression

MatchArms
    ( MatchArm => ( ExpressionWithoutBlock , | ExpressionWithBlock ,? ) )*
    MatchArm => Expression ,?

MatchArmOuterAttribute* Pattern MatchArmGuard?

MatchArmGuardif Expression

BlockExpression
    {
        InnerAttribute*
        Statements?
    }

Statements
      Statement+
    | Statement+ ExpressionWithoutBlock
    | ExpressionWithoutBlock

AsyncBlockExpressionasync move? BlockExpression

ConstBlockExpressionconst BlockExpression

UnsafeBlockExpressionunsafe BlockExpression

Expression
      ExpressionWithoutBlock
    | ExpressionWithBlock

ExpressionWithoutBlock
    OuterAttribute*
    (
        LiteralExpression
      | PathExpression
      | OperatorExpression
      | GroupedExpression
      | ArrayExpression
      | AwaitExpression
      | IndexExpression
      | TupleExpression
      | TupleIndexingExpression
      | StructExpression
      | CallExpression
      | MethodCallExpression
      | FieldExpression
      | ClosureExpression
      | AsyncBlockExpression
      | ContinueExpression
      | BreakExpression
      | RangeExpression
      | ReturnExpression
      | UnderscoreExpression
      | MacroInvocation
    )

ExpressionWithBlock
    OuterAttribute*
    (
        BlockExpression
      | ConstBlockExpression
      | UnsafeBlockExpression
      | LoopExpression
      | IfExpression
      | MatchExpression
    )

LetChainCondition except ExcludedConditions Expression OuterAttribute let Pattern = except ExcludedConditions Scrutinee
Scrutinee except StructExpression Expression

属性 总结

Syntax
ProcMacroDeriveAttribute
    proc_macro_derive ( DeriveMacroName ( , DeriveMacroAttributes )? ,? )

DeriveMacroNameIDENTIFIER

DeriveMacroAttributes
    attributes ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )

InnerAttribute# ! [ Attr ]

OuterAttribute# [ Attr ]

Attr
      SimplePath AttrInput?
    | unsafe ( SimplePath AttrInput? )

AttrInput
      DelimTokenTree
    | = Expression

MetaItem
      SimplePath
    | SimplePath = Expression
    | SimplePath ( MetaSeq? )

MetaSeq
    MetaItemInner ( , MetaItemInner )* ,?

MetaItemInner
      MetaItem
    | Expression

MetaWord
    IDENTIFIER

MetaNameValueStr
    IDENTIFIER = ( STRING_LITERAL | RAW_STRING_LITERAL )

MetaListPaths
    IDENTIFIER ( ( SimplePath ( , SimplePath )* ,? )? )

MetaListIdents
    IDENTIFIER ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )

MetaListNameValueStr
    IDENTIFIER ( ( MetaNameValueStr ( , MetaNameValueStr )* ,? )? )

CollapseDebuginfoAttributecollapse_debuginfo ( CollapseDebuginfoOption )

CollapseDebuginfoOption
      yes
    | no
    | external

InlineAttribute
      inline ( always )
    | inline ( never )
    | inline

配置 总结

路径 总结

语句 总结

Syntax
Statement
      ;
    | Item
    | LetStatement
    | ExpressionStatement
    | OuterAttribute* MacroInvocationSemi

LetStatement
    OuterAttribute* let PatternNoTopAlt ( : Type )?
    (
          = Expression
        | = Expressionexcept LazyBooleanExpression or end with a } else BlockExpression
    )? ;

ExpressionStatement
      ExpressionWithoutBlock ;
    | ExpressionWithBlock ;?

类型 总结

Syntax
Type
      TypeNoBounds
    | ImplTraitType
    | TraitObjectType

TypeNoBounds
      ParenthesizedType
    | ImplTraitTypeOneBound
    | TraitObjectTypeOneBound
    | TypePath
    | TupleType
    | NeverType
    | RawPointerType
    | ReferenceType
    | ArrayType
    | SliceType
    | InferredType
    | QualifiedPathInType
    | BareFunctionType
    | MacroInvocation

ParenthesizedType( Type )

NeverType!

ImplTraitTypeimpl TypeParamBounds

ImplTraitTypeOneBoundimpl TraitBound

BareFunctionType
    ForLifetimes? FunctionTypeQualifiers fn
       ( FunctionParametersMaybeNamedVariadic? ) BareFunctionReturnType?

FunctionTypeQualifiersunsafe? ( extern Abi? )?

BareFunctionReturnType-> TypeNoBounds

FunctionParametersMaybeNamedVariadic
    MaybeNamedFunctionParameters | MaybeNamedFunctionParametersVariadic

MaybeNamedFunctionParameters
    MaybeNamedParam ( , MaybeNamedParam )* ,?

MaybeNamedParam
    OuterAttribute* ( ( IDENTIFIER | _ ) : )? Type

MaybeNamedFunctionParametersVariadic
    ( MaybeNamedParam , )* MaybeNamedParam , OuterAttribute* ...

TraitObjectTypedyn? TypeParamBounds

TraitObjectTypeOneBounddyn? TraitBound

TupleType
      ( )
    | ( ( Type , )+ Type? )

SliceType[ Type ]

ReferenceType& Lifetime? mut? TypeNoBounds

RawPointerType* ( mut | const ) TypeNoBounds

InferredType_

ArrayType[ Type ; Expression ]

杂项 总结

Syntax
TypeParamBoundsTypeParamBound ( + TypeParamBound )* +?

TypeParamBoundLifetime | TraitBound | UseBound

TraitBound
      ( ? | ForLifetimes )? TypePath
    | ( ( ? | ForLifetimes )? TypePath )

LifetimeBounds → ( Lifetime + )* Lifetime?

Lifetime
      LIFETIME_OR_LABEL
    | 'static
    | '_

UseBounduse UseBoundGenericArgs

UseBoundGenericArgs
      < >
    | < ( UseBoundGenericArg , )* UseBoundGenericArg ,? >

UseBoundGenericArg
      Lifetime
    | IDENTIFIER
    | Self

ForLifetimesfor GenericParams

汇编 总结

Syntax
AsmArgsAsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?

FormatStringSTRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation

AsmAttrFormatString → ( OuterAttribute )* FormatString

AsmOperand
      ClobberAbi
    | AsmOptions
    | RegOperand

AsmAttrOperand → ( OuterAttribute )* AsmOperand

ClobberAbiclobber_abi ( Abi ( , Abi )* ,? )

AsmOptions
    options ( ( AsmOption ( , AsmOption )* ,? )? )

AsmOption
      pure
    | nomem
    | readonly
    | preserves_flags
    | noreturn
    | nostack
    | att_syntax
    | raw

RegOperand → ( ParamName = )?
    (
          DirSpec ( RegSpec ) Expression
        | DualDirSpec ( RegSpec ) DualDirSpecExpression
        | sym PathExpression
        | const Expression
        | label { Statements? }
    )

ParamNameIDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER

DualDirSpecExpression
      Expression
    | Expression => Expression

RegSpecRegisterClass | ExplicitRegister

RegisterClassIDENTIFIER_OR_KEYWORD

ExplicitRegisterSTRING_LITERAL

DirSpec
      in
    | out
    | lateout

DualDirSpec
      inout
    | inlateout

语法索引

本附录提供了 词法单元 和常见形式的索引,并附有这些元素定义的链接。

关键字

关键字用途
_通配符模式推断的常量推断类型占位符生命周期常量项外部 crateuse 声明解构赋值
abstract保留关键字
as外部 crateuse 声明类型转换表达式限定路径
async异步函数异步块异步闭包
awaitawait 表达式
become保留关键字
box保留关键字
breakbreak 表达式
const常量函数常量项const 泛型常量块裸借用运算符裸指针类型常量汇编操作数
continuecontinue 表达式
crate外部 crate可见性路径
do保留关键字
dyn特型对象
elselet 语句if 表达式
enum枚举
extern外部 crate外部函数限定符外部块外部函数指针类型
false布尔类型布尔表达式配置断言
final保留关键字
fn函数函数指针类型
for特型实现迭代器循环高阶特型界限
gen保留关键字
ifif 表达式match 守卫
impl固有实现特型实现impl trait 类型匿名类型参数
in可见性迭代器循环汇编操作数
letlet 语句if let 模式
loop无限循环
macro_rules声明宏
macro保留关键字
matchmatch 表达式
mod模块
move闭包表达式异步块
mut借用表达式标识符模式引用模式结构体模式引用类型裸指针类型self 参数静态项
override保留关键字
priv保留关键字
pub可见性
raw借用表达式原始汇编
ref标识符模式结构体模式
returnreturn 表达式
safe外部块函数外部块静态变量
self外部 crateself 参数可见性self 路径
SelfSelf 类型路径use 界限
static静态项'static 生命周期
struct结构体
supersuper 路径可见性
trait特型项
true布尔类型布尔表达式配置断言
try保留关键字
type类型别名
typeof保留关键字
union联合体项
unsafeunsafe 块unsafe 属性unsafe 模块unsafe 函数unsafe 外部块unsafe 外部函数unsafe 外部静态变量unsafe 特型unsafe 特型实现
unsized保留关键字
useuse 项use 界限
virtual保留关键字
wherewhere 子句
while断言循环
yield保留关键字

运算符和标点符号

符号名称用途
+加号加法特型界限宏克莱尼匹配器
-减号减法求反
*星号乘法解引用裸指针宏克莱尼匹配器glob 导入
/斜杠除法
%百分号取余
^插入符位与逻辑异或
!位与逻辑非宏调用内部属性never 类型负实现
&位与逻辑与借用引用引用模式
|位与逻辑或闭包或模式if letwhile let
&&逻辑与惰性与借用引用引用模式
||逻辑或惰性或闭包
<<左移左移嵌套泛型
>>右移右移嵌套泛型
+=加法赋值加法赋值
-=减法赋值减法赋值
*=乘法赋值乘法赋值
/=除法赋值除法赋值
%=取余赋值取余赋值
^=位异或赋值位异或赋值
&=位与赋值位与赋值
|=位或赋值位或赋值
<<=左移赋值左移赋值
>>=右移赋值右移赋值嵌套泛型
=等于赋值let 语句属性,各种类型定义
==等于等于等于
!=不等于不等于
>大于大于泛型路径use 界限
<小于小于泛型路径use 界限
>=大于等于大于或等于泛型
<=小于等于小于或等于
@at 符号子模式绑定
.字段访问元组索引
..点点范围表达式结构体表达式剩余模式范围模式结构体模式
...点点点可变参数函数范围模式
..=点点等于包含范围表达式范围模式
,逗号各种分隔符
;分号各种 项 和 语句的终止符,数组表达式数组类型
:冒号各种分隔符
::路径分隔符路径分隔符
->右箭头函数闭包函数指针类型
=>胖箭头match 分支
<-左箭头左箭头符号在 Rust 1.0 之前就已停用,但仍被视为单个 词法单元。
#井号属性原始字符串字面量原始字节字符串字面量原始 C 字符串字面量
$美元符号
?问号try 传播表达式宽松特型界限宏克莱尼匹配器
~波浪号波浪号运算符在 Rust 1.0 之前就已停用,但其 词法单元 仍可能被使用。

注释

其他词法单元

词法单元用途
ident标识符
r#ident原始标识符
'ident生命周期和循环标签
'r#ident原始生命周期和循环标签
…u8, …i32, …f64, …usize, …数字字面量
"…"字符串字面量
r"…", r#"…"#, r##"…"##, …原始字符串字面量
b"…"字节字符串字面量
br"…", br#"…"#, br##"…"##, …原始字节字符串字面量
'…'字符字面量
b'…'字节字面量
c"…"C 字符串字面量
cr"…", cr#"…"#, cr##"…"##, …原始 C 字符串字面量

语法格式用途
ident!(…)
ident! {…}
ident![…]
宏调用
$ident宏元变量
$ident:kind宏匹配器片段指定符
$(…)…宏重复

属性

语法格式用途
#[meta]外部属性
#![meta]内部属性

表达式

表达式用途
|…| expr
|…| -> Type { … }
闭包
ident::…路径
::crate_name::…显式 crate 路径
crate::…crate 相对路径
self::…模块相对路径
super::…父模块路径
Type::…
<Type as Trait>::ident
关联项
<Type>::…限定路径,可用于没有名称的类型,例如 <&T>::…<[T]>::… 等。
Trait::method(…)
Type::method(…)
<Type as Trait>::method(…)
消歧方法调用
method::<…>(…)
path::<…>
泛型参数,又称 涡轮鱼
()单元
(expr)带括号的表达式
(expr,)单元素元组表达式
(expr, …)元组表达式
expr(expr, …)调用表达式
expr.0, expr.1, …元组索引表达式
expr.ident字段访问表达式
{…}块表达式
Type {…}结构体表达式
Type(…)元组结构体构造器
[…]数组表达式
[expr; len]重复数组表达式
expr[..], expr[a..], expr[..b], expr[a..b], expr[a..=b], expr[..=b]数组和切片索引表达式
if expr {…} else {…}if 表达式
match expr { pattern => {…} }match 表达式
loop {…}无限循环表达式
while expr {…}断言循环表达式
for pattern in expr {…}迭代器循环
&expr
&mut expr
借用表达式
&raw const expr
&raw mut expr
裸借用表达式
*expr解引用表达式
expr?try 传播表达式
-expr求反表达式
!expr位与逻辑非表达式
expr as Type类型转换表达式

是 crate 的组成部分。

用途
mod ident;
mod ident {…}
模块
use path;use 声明
fn ident(…) {…}函数
type Type = Type;类型别名
struct ident {…}结构体
enum ident {…}枚举
union ident {…}联合体
trait ident {…}特型
impl Type {…}
impl Type for Trait {…}
实现
const ident = expr;常量项
static ident = expr;静态项
extern "C" {…}外部块
fn ident<…>(…) …
struct ident<…> {…}
enum ident<…> {…}
impl<…> Type<…> {…}
泛型定义

类型表达式

类型表达式 用于引用 类型。

类型用途
bool, u8, f64, str, …原始类型
for<…>高阶特型界限
T: TraitA + TraitB特型界限
T: 'a + 'b生命周期界限
T: TraitA + 'a特型和生命周期界限
T: ?Sized宽松特型界限
[Type; len]数组类型
(Type, …)元组类型
[Type]切片类型
(Type)带括号的类型
impl Traitimpl trait 类型匿名类型参数
dyn Trait特型对象类型
ident
ident::…
类型路径 (可引用 结构体枚举联合体类型别名特型泛型 等)
Type<…>
Trait<…>
泛型参数 (例如 Vec<u8>)
Trait<ident = Type>关联类型绑定 (例如 Iterator<Item = T>)
Trait<ident: …>关联类型界限 (例如 Iterator<Item: Send>)
&Type
&mut Type
引用类型
*mut Type
*const Type
裸指针类型
fn(…) -> Type函数指针类型
_推断类型推断的常量
'_占位符生命周期
!never 类型

模式

模式 用于匹配值。

模式用途
"foo", 'a', 123, 2.4, …字面量模式
ident标识符模式
_通配符模式
..剩余模式
a.., ..b, a..b, a..=b, ..=b范围模式
&pattern
&mut pattern
引用模式
path {…}结构体模式
path(…)元组结构体模式
(pattern, …)元组模式
(pattern)分组模式
[pattern, …]切片模式
CONST, Enum::Variant, …路径模式

附录:宏后继集歧义形式化规范

本页记录了 声明宏 后继规则的形式化规范。它们最初在 RFC 550 中指定,本文的大部分内容即复制自该 RFC,并在后续的 RFC 中进行了扩展。

定义和约定

  • macro: 源代码中可调用为 foo!(...) 的任何内容。
  • MBE: 声明宏,由 macro_rules 定义的宏。
  • matcher: macro_rules 调用中规则的左侧,或其子部分。
  • macro 解析器: Rust 解析器中用于使用所有匹配器派生出的语法格式来解析输入的代码片段。
  • fragment: 给定 匹配器 将接受(或“匹配”)的 Rust 语法类别。
  • repetition: 遵循常规重复模式的片段。
  • NT: 非终结符,出现在 匹配器 中的各种“元变量”或重复匹配器,在 MBE 语法格式中以 $ 字符开头。
  • simple NT: “元变量”非终结符(下文将进一步讨论)。
  • complex NT: 重复匹配非终结符,通过重复运算符(*+?)指定。
  • token: 匹配器的原子元素;即 标识符、运算符、开/闭分隔符,以及 简单 NT。
  • token tree: 由 词法单元(叶子)、复杂 NT 和有限 词法单元树 序列组成的树结构。
  • delimiter token: 用于分隔一个片段的结尾和下一个片段的开头的 词法单元。
  • separator token: 复杂 NT 中可选的 分隔符 词法单元,用于分隔匹配重复中的每对元素。
  • separated complex NT: 具有自己 分隔符 词法单元 的复杂 NT。
  • delimited sequence: 以适当的开闭分隔符开始和结束的 词法单元树 序列。
  • empty fragment: 分隔 词法单元 的不可见 Rust 语法格式类别,即 空白符,或(在某些词法上下文中)空 词法单元 序列。
  • fragment specifier: 简单 NT 中指定 NT 接受哪个片段的 标识符。
  • language: 上下文无关语言。

示例:

#![allow(unused)]
fn main() {
macro_rules! i_am_an_mbe {
    (start $foo:expr $($i:ident),* end) => ($foo)
}
}

(start $foo:expr $($i:ident),* end) 是一个 匹配器。整个 匹配器 是一个 分隔序列(带有开闭分隔符 ()),$foo$i 是带有 exprident 作为其各自 片段指定符 的 简单 NT。

$(i:ident),* 也是 一个 NT;它是一个 复杂 NT,匹配逗号分隔的 标识符 重复。, 是 复杂 NT 的 分隔符 词法单元;它出现在匹配片段的每对元素(如果存在)之间。

复杂 NT 的另一个例子是 $(hi $e:expr ;)+,它匹配 hi <expr>; hi <expr>; ... 形式的任何片段,其中 hi <expr>; 至少出现一次。请注意,此 复杂 NT 没有专用的 分隔符 词法单元。

(请注意,Rust 的 解析器 确保 分隔序列 总是以适当嵌套的 词法单元树 结构和正确的开闭分隔符匹配出现。)

我们将倾向于使用变量 “M” 代表 匹配器,变量 “t” 和 “u” 代表任意单个 词法单元,变量 “tt” 和 “uu” 代表任意 词法单元树。(“tt” 的使用确实可能与它作为 片段指定符 的额外角色产生歧义;但从上下文中可以清楚地看出其意图。)

“SEP” 将表示 分隔符 词法单元,“OP” 表示重复运算符 *+?,“OPEN”/“CLOSE” 表示围绕 分隔序列 的匹配 词法单元 对(例如 [])。

希腊字母 “α”、“β”、“γ”、“δ” 代表可能为空的 词法单元树 序列。(但是,希腊字母 “ε”(epsilon)在表达中具有特殊作用,不代表 词法单元树 序列。)

  • 这种希腊字母约定通常仅在序列的存在是技术细节时使用;特别是,当我们希望 强调 我们正在对 词法单元树 序列进行操作时,我们将使用符号 “tt …” 代表该序列,而不是希腊字母。

请注意,匹配器 仅仅是 词法单元树。如上所述,“简单 NT” 是一个元变量 NT;因此它不是一个 重复。例如,$foo:ty 是一个 简单 NT,但 $($foo:ty)+ 是一个 复杂 NT。

另请注意,在此形式化上下文中,“词法单元” 一词通常 包含 简单 NT。

最后,读者需要记住,根据此形式化的定义,没有 简单 NT 匹配 空片段,同样也没有 词法单元 匹配 Rust 语法格式的 空片段。(因此,唯一 可以匹配 空片段 的 NT 是 复杂 NT。)这实际上不是真的,因为 vis 匹配器 可以匹配一个 空片段。因此,为了形式化的目的,我们将把 $v:vis 视为 $($v:vis)?,并要求 匹配器 匹配一个 空片段。

匹配器不变式

要有效,匹配器 必须满足以下三个 不变式。FIRST 和 FOLLOW 的定义将在后面描述。

  1. 在 匹配器 M 中,对于任意两个连续的 词法单元树 序列(即 M = ... tt uu ...),且 uu ... 非空,我们必须有 FOLLOW(... tt) ∪ {ε} ⊇ FIRST(uu ...)。
  2. 在 匹配器 中,对于任何 分隔复杂 NT,M = ... $(tt ...) SEP OP ...,我们必须有 SEP ∈ FOLLOW(tt ...)。
  3. 在 匹配器 中,对于未分隔的 复杂 NT,M = ... $(tt ...) OP ...,如果 OP = *+,我们必须有 FOLLOW(tt ...) ⊇ FIRST(tt ...)。

第一个 不变式 表示,无论 匹配器 之后实际出现什么 词法单元(如果存在),它都必须在预定的 后继集 中。这确保了合法的宏定义将继续分配相同的确定,即 ... tt 在哪里结束,uu ... 在哪里开始,即使在语言中添加了新的 语法格式 形式。

第二个 不变式 表示,分隔复杂 NT 必须使用一个 分隔符 词法单元,该 词法单元 是 NT 内部内容的预定 后继集 的一部分。这确保了合法的宏定义将继续将输入片段解析为相同的 tt ... 分隔序列,即使在语言中添加了新的 语法格式 形式。

第三个 不变式 表示,当我们有一个 复杂 NT 可以匹配两个或多个相同且没有分隔符 的实体时,必须允许它们根据第一个 不变式 放置在一起。这个 不变式 还要求它们非空,这消除了可能的歧义。

注意:由于历史遗留问题和对行为的严重依赖,第三个 不变式 目前未强制执行。目前尚未决定下一步如何处理。不遵守该行为的宏在未来的 Rust 版次 中可能会变得无效。参见 跟踪 issue

FIRST和FOLLOW,非正式地

给定 匹配器 M 映射到三个集合:FIRST(M)、LAST(M) 和 FOLLOW(M)。

这三个集合中的每一个都由 词法单元 组成。FIRST(M) 和 LAST(M) 还可以包含一个特殊的非 词法单元 元素 ε (“epsilon”),它表示 M 可以匹配 空片段。(但 FOLLOW(M) 始终只是 词法单元 集合。)

非正式地:

  • FIRST(M): 收集在将片段匹配到 M 时可能首先使用的 词法单元。
  • LAST(M): 收集在将片段匹配到 M 时可能最后使用的 词法单元。
  • FOLLOW(M): 允许紧跟在 M 匹配的某个片段之后的 词法单元 集合。

    换句话说:当且仅当存在(可能为空) 词法单元 序列 α、β、γ、δ,其中:

    • M 匹配 β,

    • t 匹配 γ,并且

    • 拼接 α β γ δ 是一个可解析的 Rust 程序,

    则 t ∈ FOLLOW(M)。

我们使用简写 ANYTOKEN 表示所有 词法单元(包括 简单 NT)的集合。例如,如果任何 词法单元 在 匹配器 M 之后都是合法的,那么 FOLLOW(M) = ANYTOKEN。

(为了回顾对上述非正式描述的理解,读者此时可能希望跳到 FIRST/LAST 示例 之前阅读它们的正式定义。)

FIRST,LAST

以下是 FIRST 和 LAST 的形式化归纳定义。

“A ∪ B” 表示集合并集,“A ∩ B” 表示集合交集,“A \ B” 表示集合差集(即 A 中所有不在 B 中的元素)。

FIRST

FIRST(M) 根据序列 M 及其第一个 词法单元树(如果存在)的结构进行案例分析定义:

  • 如果 M 是空序列,则 FIRST(M) = { ε },
  • 如果 M 以 词法单元 t 开头,则 FIRST(M) = { t },

    (注意:这涵盖了 M 以 分隔词法单元树 序列 M = OPEN tt ... CLOSE ... 开头的情况,在这种情况下 t = OPEN,因此 FIRST(M) = { OPEN }。)

    (注意:这严重依赖于没有 简单 NT 匹配 空片段 的属性。)

  • 否则,M 是以 复杂 NT 开头的 词法单元树 序列:M = $( tt ... ) OP α,或 M = $( tt ... ) SEP OP α(其中 α 是 匹配器 剩余部分的(可能为空) 词法单元树 序列)。

    • 如果 SEP 存在且 ε ∈ FIRST(tt ...),则令 SEP_SET(M) = { SEP };否则 SEP_SET(M) = {}。
  • 如果 OP = *?,则令 ALPHA_SET(M) = FIRST(α);如果 OP = +,则 ALPHA_SET(M) = {}。

  • FIRST(M) = (FIRST(tt ...) \ {ε}) ∪ SEP_SET(M) ∪ ALPHA_SET(M)。

复杂 NT 的定义值得一些解释。SEP_SET(M) 定义了分隔符可能作为 M 的有效第一个 词法单元 的可能性,当存在分隔符且重复片段可能为空时,就会发生这种情况。ALPHA_SET(M) 定义了 复杂 NT 可能为空的可能性,这意味着 M 的有效第一个 词法单元 是紧随其后的 词法单元树 序列 α 的。这发生在使用了 *? 的情况下,此时可能存在零次重复。理论上,如果使用 + 且重复片段可能为空,也可能发生这种情况,但这被第三个 不变式 禁止。

从那里,显然 FIRST(M) 可以包含来自 SEP_SET(M) 或 ALPHA_SET(M) 的任何 词法单元,如果 复杂 NT 匹配非空,那么任何以 FIRST(tt ...) 开头的 词法单元 也可以。最后要考虑的是 ε。SEP_SET(M) 和 FIRST(tt ...) \ {ε} 不能包含 ε,但 ALPHA_SET(M) 可以。因此,此定义允许 M 接受 ε 当且仅当 ε ∈ ALPHA_SET(M) 时。这是正确的,因为对于 复杂 NT 情况下的 M 要接受 ε,复杂 NT 和 α 都必须接受它。如果 OP = +,这意味着 复杂 NT 不能为空,那么根据定义 ε ∉ ALPHA_SET(M)。否则,复杂 NT 可以接受零次重复,然后 ALPHA_SET(M) = FOLLOW(α)。因此,此定义对于 \varepsilon 也是正确的。

LAST

LAST(M) 根据 M 本身( 词法单元树 序列)进行案例分析定义:

  • 如果 M 是空序列,则 LAST(M) = { ε }
  • 如果 M 是单例 词法单元 t,则 LAST(M) = { t }
  • 如果 M 是重复零次或多次的单例 复杂 NT,M = $( tt ... ) *,或 M = $( tt ... ) SEP *

    • 如果 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 };否则 sep_set = {}。

    • 如果 ε ∈ LAST(tt ...),则 LAST(M) = LAST(tt ...) ∪ sep_set

    • 否则,序列 tt ... 必须非空;LAST(M) = LAST(tt ...)

  • 如果 M 是重复零次或一次的单例 复杂 NT,M = $( tt ...) ?,则 LAST(M) = LAST(tt ...) ∪ {ε}。
  • 如果 M 是 分隔词法单元树 序列 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(expr_2021) = FOLLOW(stmt) = {=>, ,, ;}。
  • FOLLOW(ty) = FOLLOW(path) = {{, [, ,, =>, :, =, >, >>, ;, |, as, where, 块 非终结符}。
  • FOLLOW(vis) = {,l 任何 关键字 或 标识符,除了非原始 priv;任何可以开始一个 类型 的 词法单元;标识符, 类型 和 路径 非终结符}。
  • FOLLOW(t) = ANYTOKEN 对于任何其他 简单 词法单元,包括 块,标识符, tt, 项,生命周期,字面量 和 元 简单 非终结符,以及所有 终结符。
  • FOLLOW(M),对于任何其他 M,定义为 FOLLOW(t) 的交集,其中 t 遍历 (LAST(M) \ {ε})。

可以开始一个 类型 的 词法单元,截至本文撰写之时,有 {(, [, !, *, &, &&, ?, 生命周期,>, >>, ::, 任何非 关键字 标识符,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 <) = { , } ⊆ FOLLOW(ty)。

  • ($pa:pat $pb:pat $ty:ty ,):非法,因为 FIRST($pb:pat $ty:ty ,) = { $pb:pat } ⊈ FOLLOW(pat),并且 FIRST($ty:ty ,) = { $ty:ty } ⊈ FOLLOW(pat)。

  • ( $($a:tt $b:tt)* ; ):合法,因为 FIRST($b:tt) = { $b:tt } ⊆ FOLLOW(tt) = ANYTOKEN,FIRST(;) = { ; } 也是。

  • ( $($t:tt),* , $(t:tt),* ):合法(尽管任何实际使用此宏的尝试都会在展开期间发出本地歧义错误)。

  • ($ty:ty $(; not sep)* -):非法,因为 FIRST($(; not sep)* -) = { ;, - } 不在 FOLLOW(ty) 中。

  • ($($ty:ty)-+):非法,因为分隔符 - 不在 FOLLOW(ty) 中。

  • ($($e:expr)*):非法,因为 expr NT 不在 FOLLOW(expr NT) 中。

影响

Rust 并非一种特别原创的语言,其设计元素来自广泛的来源。其中一些列举如下(包括后来已被移除的元素):

  • SML, OCaml: 代数数据类型,模式匹配,类型推断,分号语句分隔
  • C++: 引用,RAII,智能指针,移动语义,单态化,内存模型
  • ML Kit, Cyclone: 基于区域的内存管理
  • Haskell (GHC): 类型类,类型家族
  • Newsqueak, Alef, Limbo: 通道,并发
  • Erlang: 消息传递,线程失败,关联线程失败轻量级并发
  • Swift: 可选绑定
  • Scheme: 卫生宏
  • C#: 属性
  • Ruby: 闭包 语法格式,块 语法格式
  • NIL, Hermes: 类型状态
  • Unicode 附录 #31: 标识符 和 模式 语法格式

测试总结

以下是引用中链接到各个规则标识符的测试总数的总结。

Rules Tests Uncovered Rules Coverage
引言 1 0 0.0%
1. 记法 5 0 0.0%
2. 词法结构 0
      2.1. 输入格式 12 0 0.0%
      2.2. 关键字 18 0 0.0%
      2.3. 标识符 12 0 0.0%
      2.4. 注释 10 0 0.0%
      2.5. 空白 5 0 0.0%
      2.6. 词法单元 116 0 0.0%
3. 宏 13 0 0.0%
      3.1. 声明宏 64 0 0.0%
      3.2. 过程宏 45 0 0.0%
4. crate和源文件 19 0 0.0%
5. 条件编译 75 0 0.0%
6. 项 10 0 0.0%
      6.1. 模块 19 0 0.0%
      6.2. 外部crate 15 0 0.0%
      6.3. use声明 41 0 0.0%
      6.4. 函数 49 0 0.0%
      6.5. 类型别名 8 0 0.0%
      6.6. 结构体 7 0 0.0%
      6.7. 枚举 33 0 0.0%
      6.8. 联合体 31 0 0.0%
      6.9. 常量项 13 0 0.0%
      6.10. 静态项 19 0 0.0%
      6.11. 特型 32 0 0.0%
      6.12. 实现 31 0 0.0%
      6.13. 外部块 91 0 0.0%
      6.14. 泛型参数 26 0 0.0%
      6.15. 关联项 45 0 0.0%
7. 属性 24 0 0.0%
      7.1. 测试 23 0 0.0%
      7.2. 派生 15 0 0.0%
      7.3. 诊断 51 0 0.0%
      7.4. 代码生成 67 0 0.0%
      7.5. 限制 0
      7.6. 类型系统 9 0 0.0%
      7.7. 调试器 20 0 0.0%
8. 语句与表达式 1 0 0.0%
      8.1. 语句 23 0 0.0%
      8.2. 表达式 46 0 0.0%
            8.2.1. 字面量表达式 87 0 0.0%
            8.2.2. 路径表达式 6 0 0.0%
            8.2.3. 块表达式 40 0 0.0%
            8.2.4. 运算符表达式 108 0 0.0%
            8.2.5. 分组表达式 6 0 0.0%
            8.2.6. 数组与索引表达式 22 0 0.0%
            8.2.7. 元组与索引表达式 16 0 0.0%
            8.2.8. 结构体表达式 14 0 0.0%
            8.2.9. 调用表达式 11 0 0.0%
            8.2.10. 方法调用表达式 12 0 0.0%
            8.2.11. 字段访问表达式 8 0 0.0%
            8.2.12. 闭包表达式 17 0 0.0%
            8.2.13. 循环表达式 56 0 0.0%
            8.2.14. 范围表达式 5 0 0.0%
            8.2.15. if表达式 18 0 0.0%
            8.2.16. match表达式 25 0 0.0%
            8.2.17. return表达式 4 0 0.0%
            8.2.18. await表达式 9 0 0.0%
            8.2.19. 下划线表达式 5 0 0.0%
9. 模式 140 0 0.0%
10. 类型系统 0
      10.1. 类型 22 0 0.0%
            10.1.1. 布尔类型 23 0 0.0%
            10.1.2. 数值类型 10 0 0.0%
            10.1.3. 文本类型 9 0 0.0%
            10.1.4. 永不类型 5 0 0.0%
            10.1.5. 元组类型 9 0 0.0%
            10.1.6. 数组类型 5 0 0.0%
            10.1.7. 切片类型 5 0 0.0%
            10.1.8. 结构体类型 7 0 0.0%
            10.1.9. 枚举类型 6 0 0.0%
            10.1.10. 联合体类型 6 0 0.0%
            10.1.11. 函数项类型 6 0 0.0%
            10.1.12. 闭包类型 55 0 0.0%
            10.1.13. 指针类型 22 0 0.0%
            10.1.14. 函数指针类型 7 0 0.0%
            10.1.15. 特型对象类型 11 0 0.0%
            10.1.16. impl trait类型 23 0 0.0%
            10.1.17. 类型参数 1 0 0.0%
            10.1.18. 推断类型 4 0 0.0%
      10.2. 动态大小类型 7 0 0.0%
      10.3. 类型布局 67 0 0.0%
      10.4. 内部可变性 9 0 0.0%
      10.5. 子类型与变型 12 0 0.0%
      10.6. 特型与生命周期界限 22 0 0.0%
      10.7. 类型隐式转换 44 0 0.0%
      10.8. 析构函数 49 0 0.0%
      10.9. 生命周期省略 24 0 0.0%
11. 特殊类型与特型 54 0 0.0%
12. 名称 24 0 0.0%
      12.1. 命名空间 8 0 0.0%
      12.2. 作用域 46 0 0.0%
      12.3. 预导入 34 0 0.0%
      12.4. 路径 54 0 0.0%
      12.5. 名称解析 29 0 0.0%
      12.6. 可见性与私有性 18 0 0.0%
13. 内存模型 6 0 0.0%
      13.1. 内存分配与生命周期 3 0 0.0%
      13.2. 变量 6 0 0.0%
14. 恐慌 21 0 0.0%
15. 链接 27 0 0.0%
16. 内联汇编 120 0 0.0%
17. 不安全性 11 0 0.0%
      17.1. unsafe关键字 16 0 0.0%
      17.2. 被视为未定义的行为 42 0 0.0%
      17.3. 不被视为不安全的行为 0
18. 常量求值 44 0 0.0%
19. 应用程序二进制接口(ABI) 19 0 0.0%
20. Rust运行时 16 0 0.0%
21. 附录 0
      21.1. 语法总结 0
      21.2. 语法索引 0
      21.3. 宏后继集歧义形式化规范 45 0 0.0%
      21.4. 影响 0
      21.5. 测试总结 0
      21.6. 词汇表 2 0 0.0%
Total: 2868 0 2868 0.0%

词汇表

抽象语法树

“抽象语法树”,或“AST”,是编译器编译程序时,程序结构的中间表示。

对齐

值的 对齐 方式指定了值倾向于从哪个地址开始。始终是 2 的幂。对值的引用必须对齐。 更多

应用程序二进制接口(ABI)

一个 应用程序二进制接口 (ABI) 定义了编译后的代码如何与其它编译后的代码交互。通过 externextern fnABI 字符串 影响:

  • 调用约定:函数参数如何传递,返回值如何返回(例如,在寄存器中或在栈上),以及谁负责清理栈。
  • 展开:是否允许栈展开。例如,"C-unwind" ABI 允许跨 FFI 边界展开,而 "C" ABI 不允许。

元数

元数 指的是函数或运算符接受的参数数量。例如,f(2, 3)g(4, 6) 的 元数 是 2,而 h(8, 2, 6) 的 元数 是 3。! 运算符的 元数 是 1。

数组

数组,有时也称为 固定大小数组 或 内联数组,是一个描述元素集合的值,每个元素通过程序在运行时计算的索引来选择。它占据一片连续的内存区域。

关联项

关联项 是与另一个 关联的 。关联项 在 实现 中定义,并在 特型 中声明。只有函数、常量和类型别名可以关联。与 自由项 形成对比。

全局实现

任何类型以 未覆盖类型 方式出现的实现。impl<T> Foo for Timpl<T> Bar<T> for Timpl<T> Bar<Vec<T>> for Timpl<T> Bar<T> for Vec<T> 都被认为是 全局实现。然而,impl<T> Bar<Vec<T>> for Vec<T> 不是 全局实现,因为出现在此 impl 中的所有 T 实例都被 Vec 覆盖。

界限

界限 是对类型或 特型 的约束。例如,如果对函数接受的参数设置了 界限,则传递给该函数的类型必须遵守该约束。

组合器

组合器 是高阶函数,它们只应用函数和先前定义的 组合器,以从其参数中提供结果。它们可以用于以模块化的方式管理控制流。

crate

crate 是编译和链接的单元。有不同的 crate 类型,例如库或可执行文件。crate 可以链接并引用其他库 crate,称为 外部 crate。一个 crate 拥有一个自包含的 模块 树,从一个名为 crate 根 的未命名根模块开始。 可以通过在 crate 根 中将其标记为公共来对其他 crate 可见,包括通过公共模块的 路径更多

调度

调度 是一种机制,用于在涉及 多态 时确定实际运行哪个特定版本的代码。调度的主要形式有两种:静态调度 和 动态调度。Rust 通过使用 特型对象 支持 动态调度。

动态大小类型(DST)

动态大小类型 (DST) 是一种没有静态已知大小或对齐的 类型。

实体

一个 实体 是一种语言构造,可以在源程序中以某种方式引用,通常通过 路径 来引用。实体 包括 类型泛型参数变量绑定循环标签生命周期字段属性lint

表达式

表达式 是值、常量、变量、运算符和函数的组合,它们评估为一个单一的值,有或没有副作用。

例如,2 + (3 * 4) 是一个返回 14 的 表达式。

自由项

一个不属于 实现,例如 自由函数自由常量。与 关联项 形成对比。

基本特型

基本特型 是指为现有类型添加它的实现会造成 破坏性变更 的 特型。Fn 特型和 Sized 是 基本特型。

基本类型构造器

基本类型构造器 是一种类型,对其实现 全局实现 会造成 破坏性变更。&&mutBoxPin 是 基本的。

任何时候,如果类型 T 被认为是 局部 的,那么 &T&mut TBox<T>Pin<T> 也被认为是 局部 的。基本类型构造器 不能 覆盖 其他类型。任何时候使用术语“覆盖类型”时,&T&mut TBox<T>Pin<T> 中的 T 都不被认为是 覆盖的。

可居住的

如果一个类型有构造器,从而可以被 实例化,那么它就是 可居住的。一个 可居住的 类型在“空”的意义上并非“空”,即可以存在该类型的值。与 不可居住的 相反。

固有实现

一个适用于名义类型而非 特型-类型对 的 实现更多

固有方法

固有实现 中定义而不是在 特型实现 中定义的 方法

已初始化

如果一个变量已被 赋值 且之后未被 移出,则它就是 已初始化 的。所有其他内存位置都被认为是 未初始化 的。只有 非安全 Rust 才能创建未初始化的内存位置。

局部特型

在当前 crate 中定义的 trait。特型定义 的 局部性 独立于应用的 类型参数。给定 trait Foo<T, U>Foo 始终是 局部 的,无论为 TU 替换什么类型。

局部类型

在当前 crate 中定义的 structenumunion。这不受应用的 类型参数 影响。struct Foo 被认为是 局部 的,但 Vec<Foo> 不是。LocalType<ForeignType> 是 局部 的。类型别名 不影响 局部性。

模块

模块 是一个包含零个或多个 的容器。模块 以树形结构组织,从一个未命名的根模块开始,该模块称为 crate 根 或 根模块。路径 可用于引用其他模块中的 项,这可能受到 可见性规则 的限制。 更多

名称

一个 名称 是一个 标识符生命周期或循环标签,它引用一个 实体名称绑定 是指一个 实体声明 引入了一个与该 实体 关联的 标识符 或 标签。路径、标识符 和 标签 用于引用一个 实体。

名称解析

名称解析 是将 路径标识符标签实体 声明 关联起来的 编译时过程。

命名空间

一个 命名空间 是根据名称所引用的 实体 类型,对已声明的 名称 进行的 逻辑分组。命名空间 允许一个 名称 在一个 命名空间 中出现,而不会与另一个 命名空间 中的同名 名称 冲突。

在 命名空间 内部,名称 以 层次结构 的方式组织,其中 层次结构 的每个级别都有自己的一组 命名实体。

名义类型

可以直接通过 路径 引用的 类型。具体来说是 枚举结构体联合体特型对象类型

dyn兼容特型

可以在 特型对象类型 (dyn Trait) 中使用的 特型 。 只有遵循特定 规则 的 特型 才是 dyn 兼容 的。

这些以前被称为 对象安全特型

路径

一个 路径 是一系列一个或多个 路径段,用于引用当前 作用域命名空间 层次结构 其它级别中的 实体

预导入

预导入,或称 Rust 预导入,是一个小型的 项 集合——主要是 特型——它们被导入到每个 crate 的每个 模块 中。预导入 中的 特型 是普遍存在的。

作用域

一个 作用域 是源代码中一个 命名实体 可以通过该名称被引用的区域。

审查值

审查值 是在 match 表达式和类似的 模式匹配构造 中被匹配的 表达式。例如,在 match x { A => 1, B => 2 } 中,表达式 x 是 审查值。

大小

值 的 大小 有两种定义。

第一种是存储该 值 必须分配多少 内存。

第二种是具有该 项类型 的 数组 中连续 元素 之间的 字节 偏移量。

它是 对齐 的倍数,包括零。大小 可能因 编译器版本(随着新 优化 的出现)和 目标平台(类似于 usize 在不同平台上的差异)而变化。

更多

切片

切片 是一个对 连续序列 的 动态大小视图,写作 [T]

它通常以其借用形式出现,可以是 可变 的或 共享 的。共享 切片类型 是 &[T],而 可变 切片类型 是 &mut [T],其中 T 代表 元素类型。

语句

语句 是编程语言中最小的独立元素,它命令计算机执行一个 动作。

字符串字面量

字符串字面量 是直接存储在 最终二进制文件 中的 字符串,因此在 'static 持续时间 内有效。

它的类型是 'static 持续时间 借用字符串切片,&'static str

字符串切片

字符串切片 是 Rust 中最原始的 字符串类型,写作 str。它通常以其借用形式出现,可以是 可变 的或 共享 的。共享 字符串切片类型 是 &str,而 可变 字符串切片类型 是 &mut str

字符串切片 始终是有效的 UTF-8。

特型

特型 是一种语言项,用于描述 类型 必须提供的 功能。它允许 类型 对其 行为 做出某些承诺。

泛型函数 和 泛型结构体 可以使用 特型 来 约束 或 限定它们接受的 类型。

涡轮鱼

表达式 中带有 泛型参数 的 路径 必须在开头的尖括号前加上 ::。结合 泛型 的尖括号,这看起来像一条鱼 ::<>。因此,这种 语法格式 被俗称为 涡轮鱼 语法格式。

示例:

#![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 是 覆盖的。这仅与 类型参数 相关。

未定义行为

未定义行为 是未指定 的 编译时 或 运行时行为。这可能导致(但不限于):进程终止或损坏;不适当、不正确或意外的计算;或平台特定结果。 更多

不可居住的

如果一个 类型 没有 构造器,从而永远不能被 实例化,那么它就是 不可居住的。一个 不可居住的 类型在“空”的意义上是“空”的,即该 类型 没有 值。一个 不可居住的 类型 的典型例子是 never 类型 !,或者一个没有 变体 的枚举 enum Never { }。与 可居住的 相反。