第9章 语句和表达式
Rust 基本上是一种表达式语言。这意味着大多数形式的求值或生成表达效果的计算的都是由表达式的统一句法类别来指导的。每一种表达式通常都可以内嵌到另一种表达式中,表达式的求值规则包括指定表达式产生的值和指定其各个子表达式的求值顺序。
对比之下,Rust 中的语句则主要用于包含表达式求值,以及显式地安排表达式的求值顺序。
语句
句法
Statement :
;
| Item
| LetStatement
| ExpressionStatement
| MacroInvocationSemi
语句是块(block)1的一个组件,反过来,块又是其外层表达式或函数的组件。
Rust 有两种语句:声明语句(declaration statements)和表达式语句(expression statements)。
声明语句
声明语句是在它自己封闭的语句块的内部引入一个或多个名称的语句。声明的名称可以表示新变量或新的程序项。
这两种声明语句就是程序项声明语句和 let声明语句。
程序项声明语句
程序项声明语句的句法形式与模块中的程序项声明的句法形式相同。在语句块中声明的程序项会将其作用域限制为包含该语句的块。这类程序项以及在其内声明子项(sub-items)都没有给定的规范路径。例外的是,只要程序项和 trait(如果有的话)的可见性允许,在(程序项声明语句内定义的和此程序项或 trait 关联的)实现中定义的关联项在外层作用域内仍然是可访问的。除了这些区别外,它与在模块中声明的程序项的意义也是相同的。
程序项声明语句不会隐式捕获包含它的函数的泛型参数、参数和局部变量。如下,inner 不能访问 outer_var。
outer_var.
#![allow(unused)] fn main() { fn outer() { let outer_var = true; fn inner() { /* outer_var 的作用域不包括这里 */ } inner(); } }
let语句
句法
LetStatement :
OuterAttribute*letPatternNoTopAlt (:Type )? (=Expression )?;
let语句通过一个不可反驳型模式引入了一组新的变量,变量由该模式给定。模式后面有一个可选的类型标注(annotation),再后面是一个可选的初始化表达式。当没有给出类型标注时,编译器将自行推断类型,如果没有足够的信息来执行有限次的类型推断,则将触发编译器报错。由变量声明引入的任何变量从声明开始直到封闭块作用域结束都是可见的。
表达式语句
句法
ExpressionStatement :
ExpressionWithoutBlock;
| ExpressionWithBlock;?
表达式语句是对表达式求值并忽略其结果的语句。通常,表达式语句存在的目的是触发对其内部的表达式的求值时的效果。
仅由块表达式或控制流表达式组成的表达式,如果它们在允许使用语句的上下文中使用时,是可以省略其后面的分号的。这有可能会导致解析歧义,因为它可以被解析为独立语句,也可以被解析为另一个表达式的一部分;下例中的控制流表达式被解析为一个语句。注意 ExpressionWithBlock 形式的表达式用作语句时,其类型必须是单元类型(())。
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3]; v.pop(); // 忽略从 pop 返回的元素 if v.is_empty() { v.push(5); } else { v.remove(0); } // 分号可以省略。 [1]; // 单独的表达式语句,而不是索引表达式。 }
当省略后面的分号时,结果必须是 () 类型。
#![allow(unused)] fn main() { // bad: 下面块的类型是i32,而不是 `()` // Error: 预期表达式语句的返回值是 `()` // if true { // 1 // } // good: 下面块的类型是i32,(加`;`后的语句的返回值就是 `()`了) if true { 1 } else { 2 }; }
语句上的属性
语句可以有外部属性。在语句中有意义的属性是 cfg 和 lint检查类属性。
本书原文还有 block of code 的写法,这种有些类似于我们口语中说的那种任意的代码段的“代码块”。原文中 block of code 的典型情况是非安全(unsafe)块。
表达式
句法
Expression :
ExpressionWithoutBlock
| ExpressionWithBlockExpressionWithoutBlock :
OuterAttribute*†
(
LiteralExpression
| PathExpression
| OperatorExpression
| GroupedExpression
| ArrayExpression
| AwaitExpression
| IndexExpression
| TupleExpression
| TupleIndexingExpression
| StructExpression
| CallExpression
| MethodCallExpression
| FieldExpression
| ClosureExpression
| ContinueExpression
| BreakExpression
| RangeExpression
| ReturnExpression
| MacroInvocation
)ExpressionWithBlock :
OuterAttribute*†
(
BlockExpression
| AsyncBlockExpression
| UnsafeBlockExpression
| LoopExpression
| IfExpression
| IfLetExpression
| MatchExpression
)
一个表达式可能有两个角色:它总是能产生1一个值;它还有可能表达出效果(effects)(也被称为“副作用(side effects)”)。表达式 求值/计算为(evaluates to) 值,并在 求值(evaluation) 期间表达出效果。
许多表达式包含子表达式,此子表达式也被称为此表达式的操作数。每种表达式都表达了以下几点含义:
- 在对表达式求值时是否对操作数求值
- 对操作数求值的顺序
- 如何组合操作数的值来获取表达式的值
基于对这几种含义的实现要求,表达式通过其内在结构规定了其执行结构。
块只是另一种表达式,所以块、语句和表达式可以递归地彼此嵌套到任意深度。
注意:我们给表达式的操作数做了(如上)命名,以便于我们去讨论它们,但这些名称并没有最终稳定下来,以后可能还会更改。
表达式的优先级
Rust 运算符和表达式的优先级顺序如下,从强到弱。具有相同优先级的二元运算符按其结合(associativity)顺序做了分组。
| 运算符/表达式 | 结合性 |
|---|---|
| Paths(路径) | |
| Method calls(方法调用) | |
| Field expressions (字段表达式) | 从左向右 |
| Function calls, array indexing(函数调用,数组索引) | |
? | |
Unary(一元运算符) - * ! & &mut | |
as | 从左向右 |
* / % | 从左向右 |
+ - | 从左向右 |
<< >> | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
| | 从左向右 |
== != < > <= >= | 需要圆括号 |
&& | 从左向右 |
|| | 从左向右 |
.. ..= | 需要圆括号 |
= += -= *= /= %= &= |= ^= <<= >>= | 从右向左 |
return break closures(返回、中断、闭包) |
操作数的求值顺序
下面的表达式列表都以相同的方式计算它们的操作数,具体列表后面也有详述。其他表达式要么不接受操作数,要么按照各自约定(后续章节会有讲述)的条件进行求值。
- 解引用表达式(Dereference expression)
- 错误传播表达式(Error propagation expression)
- 取反表达式(Negation expression)
- 算术和二进制逻辑运算(Arithmetic and logical binary operators)
- 比较运算(Comparison operators)
- 类型转换表达式(Type cast expression)
- 分组表达式(Grouped expression)
- 数组表达式(Array expression)
- 等待表达式(Await expression)
- 索引表达式(Index expression)
- 元组表达式(Tuple expression)
- 元组索引表达式(Tuple index expression)
- 结构体表达式(Struct expression)
- 调用表达式(Call expression)
- 方法调用表达式(Method call expression)
- 字段表达式(Field expression)
- 中断表达式(Break expression)
- 区间表达式(Range expression)
- 返回表达式(Return expression)
在实现执行这些表达式的效果之前,会先对这些表达式的操作数进行求值。拥有多个操作数的表达式会按照源代码书写的顺序从左到右计算。
注意:子表达式是一个表达式的操作数时,此子表达式内部的求值顺序是由根据前面的章节规定的优先级来确定的。
例如,下面两个 next方法的调用总是以相同的顺序调用:
#![allow(unused)] fn main() { // 使用 vec 代替 array 来避免引用,因为在编写本例时,拥有内部元素的所有权的数组迭代器还没有稳定下来。 let mut one_two = vec![1, 2].into_iter(); assert_eq!( (1, 2), (one_two.next().unwrap(), one_two.next().unwrap()) ); }
注意:由于表达式是递归执行的,那这些表达式也会从最内层到最外层逐层求值,忽略兄弟表达式,直到没有(未求值的)内部子表达式为止。
位置表达式和值表达式
表达式分为两大类:位置表达式和值表达式(,它们各自形成了自己的上下文)。因此,在每个表达式中,操作数可以出现在位置上下文,也可出现在值上下文中。表达式的求值既依赖于它自己的类别,也依赖于它所在的上下文。
位置表达式是表示内存位置的表达式。语言中表现为指向局部变量、静态变量、解引用(*expr)、数组索引表达式(expr[expr])、字段引用(expr.f)、圆括号括起来的位置表达式的路径。所有其他形式的表达式都是值表达式。
值表达式是代表实际值的表达式。
下面的上下文是位置表达式上下文:
- 赋值或复合赋值表达式的左操作数。
- 一元运算符借用或解引用的操作数。
- 字段表达式的操作数。
- 数组索引表达式的被索引操作数。
- 任何隐式借用的操作数。
- let语句的初始化器(initializer)。
if let表达式、while let表达式或匹配(match)表达式的检验对象(scrutinee)。- 结构体表达式里的函数式更新(functional update)的基(base)。
注意:历史上,位置表达式被称为 左值/lvalues,值表达式被称为 右值/rvalues。
移动语义类型和复制语义类型
当位置表达式在值表达式上下文中被求值时,或在模式中被值绑定时,这表示此值会*保存进(held in)*当前表达式代表的内存地址。如果该值的类型实现了 Copy,那么该值将被从原来的位置复制一份过来。如果该值的类型没有实现 Copy,但实现了 Sized,那么就有可能把该值从原来的位置移动(move)过来。从位置表达式里移出值对位置表达式也有要求,只有如下的位置表达式才可能把值从其中移出(move out):
- 变量当前未被借用。
- 临时值(Temporary values)。
- 可以移出且没实现
Drop的位置表达式的字段。 - 对可移出且类型为
Box<T>的表达式作解引用的结果。
把值从一个位置表达式里移出到一个局部变量,那此(表达式代表的)地址将被去初始化(deinitialized),并且该地址在重新初始化之前无法被再次读取。
除以上列出的情况外,任何在值表达式上下文中使用位置表达式都是错误的。
可变性
如果一个位置表达式将会被赋值、可变借出、隐式可变借出或被绑定到包含 ref mut 模式上,则该位置表达式必须是可变的(mutable)。我们称这类位置表达式为可变位置表达式。与之相对,其他位置表达式称为不可变位置表达式。
下面的表达式可以是可变位置表达式上下文:
- 当前未被借出的可变变量。
- 可变静态(
static)项。 - 临时值。
- 字段,在可变位置表达式上下文中,可以对此子表达式求值。
- 对
*mut T指针的解引用。 - 对类型为
&mut T的变量或变量的字段的解引用。注意:这条是下一条规则的必要条件的例外情况。2 - 对实现
DerefMut的类型的解引用,那对这里解出的表达式求值就需要在一个可变位置表达式上下文中进行。 - 对实现
IndexMut的类型做索引,那对此检索出的表达式求值就需要在一个可变位置表达式上下文进行。注意对索引(index)本身的求值不用。
临时位置/临时变量
在大多数位置表达式上下文中使用值表达式时,会创建一个临时的未命名内存位置,并将该位置初始为该值,然后这个表达式的求值结果就为该内存位置。此过程也有例外,就是把此临时表达式提升为一个静态项(static)。(译者注:这种情况下表达式将直接在编译时就求值了,求值的结果会根据编译器要求重新选择地址存储)。临时位置/临时变量的销毁作用域(drop scope)通常在包含它的最内层语句的结尾处。
隐式借用
某些特定的表达式可以通过隐式借用一个表达式来将其视为位置表达式。例如,可以直接比较两个非固定尺寸的切片是否相等,因为 == 操作符隐式借用了它自身的操作数:
#![allow(unused)] fn main() { let c = [1, 2, 3]; let d = vec![1, 2, 3]; let a: &[i32]; let b: &[i32]; a = &c; b = &d; // ... *a == *b; //译者注:&[i32] 解引用后是一个动态尺寸类型,理论上两个动态尺寸类型上无法比较大小的,但这里因为隐式借用此成为可能 // 等价于下面的形式: ::std::cmp::PartialEq::eq(&*a, &*b); }
隐式借用可能会被以下表达式采用:
- 方法调用表达式中的左操作数。
- 字段表达式中的左操作数。
- 调用表达式中的左操作数。
- 数组索引表达式中的左操作数。
- 解引用操作符(
*)的操作数。 - 比较运算的操作数。
- 复合赋值(compound assignment)的左操作数。
重载 trait
本书后续章节的许多操作符和表达式都可以通过使用 std::ops 或 std::cmp 中的 trait 被其他类型重载。这些 trait 也存在于同名的 core::ops 和 core::cmp 中。
表达式属性
只有在少数特定情况下,才允许在表达式之前使用外部属性:
在下面情形之前是不允许的:
- 区间(Range)表达式。
- 二元运算符表达式(ArithmeticOrLogicalExpression、ComparisonExpression、LazyBooleanExpression、TypeCastExpression、AssignmentExpression、CompoundAssignmentExpression)。
通俗理解“求值”,可以理解为:一方面可以从表达式求出值,另一方面可以为表达式赋值。
译者暂时还未用代码测试这种例外情况。
字面量表达式
句法
LiteralExpression :
CHAR_LITERAL
| STRING_LITERAL
| RAW_STRING_LITERAL
| BYTE_LITERAL
| BYTE_STRING_LITERAL
| RAW_BYTE_STRING_LITERAL
| INTEGER_LITERAL
| FLOAT_LITERAL
| BOOLEAN_LITERAL
字面量表达式由字面量里讲过的任一形式组成。 它直接描述一个数字、字符、字符串或布尔值。
#![allow(unused)] fn main() { "hello"; // 字符串类型 '5'; // 字符类型 5; // 整型 }
路径表达式
句法
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; }
块表达式
句法
BlockExpression :
{
InnerAttribute*
Statements?
}Statements :
Statement+
| Statement+ ExpressionWithoutBlock
| ExpressionWithoutBlock
块表达式或块是一个控制流表达式(control flow expression),同时也是程序项声明和变量声明的匿名空间作用域。
作为控制流表达式,块按顺序执行其非程序项声明的语句组件,最后执行可选的最终表达式(final expression)。
作为一个匿名空间作用域,在本块内声明的程序项只在块本身围成的作用域内有效,而块内由 let语句声明的变量的作用域为下一条语句到块尾。
块的句法规则为:先是一个 {,后跟内部属性,再后是任意条语句,再后是一个被称为最终操作数(final operand)的可选表达式,最后是一个 }。
语句之间通常需要后跟分号,但有两个例外: 1、程序项声明语句不需要后跟分号。 2、表达式语句通常需要后面的分号,但它的外层表达式是控制流表达式时不需要。
此外,允许在语句之间使用额外的分号,但是这些分号并不影响语义。
在对块表达式进行求值时,除了程序项声明语句外,每个语句都是按顺序执行的。 如果给出了块尾的可选的最终操作数(final operand),则最后会执行它。
块的类型是最此块的最终操作数(final operand)的类型,但如果省略了最终操作数,则块的类型为 ()。
#![allow(unused)] fn main() { fn fn_call() {} let _: () = { fn_call(); }; let five: i32 = { fn_call(); 5 }; assert_eq!(5, five); }
注意:作为控制流表达式,如果块表达式是一个表达式语句的外层表达式,则该块表达式的预期类型为
(),除非该块后面紧跟着一个分号。
块总是值表达式,并会在值表达式上下文中对最后的那个操作数进行求值。
注意:如果确实有需要,块可以用于强制移动值。 例如,下面的示例在调用
consume_self时失败,因为结构体已经在之前的块表达式里被从s里移出了。#![allow(unused)] fn main() { struct Struct; impl Struct { fn consume_self(self) {} fn borrow_self(&self) {} } fn move_by_block_expression() { let s = Struct; // 将值从块表达式里的 `s` 里移出。 (&{ s }).borrow_self(); // 执行失败,因为 `s` 里的值已经被移出。 s.consume_self(); } }
async块
句法
AsyncBlockExpression :
asyncmove? BlockExpression
*异步块(async block)*是求值为 future 的块表达式的一个变体。 块的最终表达式(如果存在)决定了 future 的结果值。(译者注:单词 future 对应中文为“未来”。原文可能为了双关的行文效果,经常把作为类型的 future 和字面意义上的 future 经常混用,所以译者基本保留此单词不翻译,特别强调“未来”的意义时也会加上其英文单词。)
执行一个异步块类似于执行一个闭包表达式:它的即时效果是生成并返回一个匿名类型。
类似闭包返回的类型实现了一个或多个 std::ops::Fn trait,异步块返回的类型实现了 std::future::Future trait。
此类型的实际数据格式规范还未确定下来。
注意: rustc 生成的 future类型大致相当于一个枚举,rustc 为这个 future 的每个
await点生成一个此枚举的变体,其中每个变体都存储了对应点再次恢复执行时需要的数据。
版本差异: 异步块从 Rust 2018 版才开始可用。
捕获方式
异步块使用与闭包相同的捕获方式从其环境中捕获变量。
跟闭包一样,当编写 async { .. } 时,每个变量的捕获方式将从该块里的内容中推断出来。
而 async move { .. } 类型的异步块将把所有需要捕获的变量使用移动语义移入(move)到相应的结果 future 中。
异步上下文
因为异步块构造了一个 future,所以它们定义了一个async上下文,这个上下文可以相应地包含 await表达式。
异步上下文是由异步块和异步函数的函数体建立的,它们的语义是依照异步块定义的。
控制流操作符
异步块的作用类似于函数边界,或者更类似于闭包。
因此 ?操作符和 返回(return)表达式也都能影响 future 的输出,且不会影响封闭它的函数或其他上下文。
也就是说,future 的输出跟闭包将其中的 return <expr> 的表达式 <expr> 的计算结果作为未来的输出的做法是一样的。
类似地,如果 <expr>? 传播(propagate)一个错误,这个错误也会被 future 在未来的某个时候作为返回结果被传播出去。
最后,关键字 break 和 continue 不能用于从异步块中跳出分支。
因此,以下内容是非法的:
#![allow(unused)] fn main() { loop { async move { break; // 这将打破循环。 } } }
非安全(unsafe)块
句法\23672971a16c69ea894bef24992b74912cfe5d25e` 的信息_
可以在代码块前面加上关键字 unsafe 以允许非安全操作。
例如:
#![allow(unused)] fn main() { 在以下上下文中,允许在块表达式的左括号之后直接使用[内部属性][inner attributes]: * [函数][function]和[方法][method]的代码体。 * 循环体([`loop`], [`while`], [`while let`], 和 [`for`])。 * 被用作[语句][statement]的块表达式。 * 块表达式作为[数组表达式][array expressions]、[元组表达式][tuple expressions]、[调用表达式][call expressions]、[元组结构体][struct]表达式和[枚举变体][enum variant]表达式的元素。 * 作为另一个块表达式的尾部表达式(tail expression)的块表达式。 <!-- 本列表需要和 expressions.md 保持同步 --> 在块表达式上有意义的属性有 [`cfg`] 和 [lint检查类属性][the lint check attributes]。 例如,下面这个函数在 unix 平台上返回 `true`,在其他平台上返回 `false`。 ```rust fn is_unix_platform() -> bool { #[cfg(unix)] { true } #[cfg(not(unix))] { false } } }
操作符/运算符表达式
句法
OperatorExpression :
BorrowExpression
| DereferenceExpression
| ErrorPropagationExpression
| NegationExpression
| ArithmeticOrLogicalExpression
| ComparisonExpression
| LazyBooleanExpression
| TypeCastExpression
| AssignmentExpression
| CompoundAssignmentExpression
操作符是 Rust 语言为其内建类型定义的。
本文后面的许多操作符都可以使用 std::ops 或 std::cmp 中的 trait 进行重载。
溢出
在 debug模式下编译整数运算时,如果发生溢出,会触发 panic。
可以使用命令行参数 -C debug-assertions 和 -C overflow-checks 设置编译器标志位来更直接地控制这个溢出过程。
以下情况被认为是溢出:
- 当
+、*或-创建的值大于当前类型可存储的最大值或小于最小值。这包括任何有符号整型的最小值上的一元运算符-。 - 使用
/或%,其中左操作数是某类有符号整型的最小整数,右操作数是-1。 - 使用
<<或>>,其中右操作数大于或等于左操作数类型的 bit 数,或右操作数为负数。
借用/引用操作符/运算符
句法
BorrowExpression :
(&|&&) Expression
| (&|&&)mutExpression
&(共享借用)和 &mut(可变借用)运算符是一元前缀运算符。
当应用于位置表达式上时,此表达式生成指向值所在的内存位置的引用(指针)。
在引用存续期间,该内存位置也被置于借出状态。
对于共享借用(&),这意味着该位置可能不会发生变化,但可能会被再次读取或共享。
对于可变借用(&mut),在借用到期之前,不能以任何方式访问该位置。&mut 在可变位置表达式上下文中会对其操作数求值。
如果 & 或 &mut 运算符应用于值表达式上,则会创建一个临时值。
这类操作符不能重载。
#![allow(unused)] fn main() { { // 将创建一个存值为7的临时位置,该该临时位置在此作用域内持续存在 let shared_reference = &7; } let mut array = [-2, 3, 9]; { // 在当前作用域内可变借用了 `array`。那 `array` 就只能通过 `mutable_reference` 来使用。 let mutable_reference = &mut array; } }
尽管 && 是一个单一 token(惰性与(and)操作符),但在借用表达式(borrow expressions)上下文中使用时,它是作为两个借用操作符用的:
#![allow(unused)] fn main() { // 意义相同: let a = && 10; let a = & & 10; // 意义相同: let a = &&&& mut 10; let a = && && mut 10; let a = & & & & mut 10; }
解引用操作符
句法
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); }
问号操作符
句法
ErrorPropagationExpression :
Expression?
问号操作符(?)解包(unwrap)有效值或返回错误值,并将它们传播(propagate)给调用函数。
问号操作符(?)是一个一元后缀操作符,只能应用于类型 Result<T, E> 和 Option<T>。
当应用在 Result<T, E> 类型的值上时,它可以传播错误。
如果值是 Err(e),那么它实际上将从此操作符所在的函数体或闭包中返回 Err(From::from(e))。
如果应用到 Ok(x),那么它将解包此值以求得 x。
#![allow(unused)] fn main() { use std::num::ParseIntError; fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // 立即返回一个 Err() Ok(x + y) // 不会执行到这里 } let res = try_to_parse(); println!("{:?}", res); assert!(res.is_err()) }
当应用到 Option<T> 类型的值时,它向调用者传播错误 None。
如果它应用的值是 None,那么它将返回 None。
如果应用的值是 Some(x),那么它将解包此值以求得 x。
#![allow(unused)] fn main() { fn try_option_some() -> Option<u8> { let val = Some(1)?; Some(val) } assert_eq!(try_option_some(), Some(1)); fn try_option_none() -> Option<u8> { let val = None?; Some(val) } assert_eq!(try_option_none(), None); }
操作符 ? 不能被重载。
取反运算符
语法
NegationExpression :
-Expression
|!Expression
这是最后两个一元运算符。 下表总结了它们用在基本类型上的表现,同时指出其他类型要重载这些操作符需要实现的 trait。 记住,有符号整数总是用二进制补码形式表示。 所有这些运算符的操作数都在值表达式上下文中被求值,所以这些操作数的值会被移走或复制。
| 符号 | 整数 | bool | 浮点数 | 用于重载的 trait |
|---|---|---|---|---|
- | 符号取反* | 符号取反 | std::ops::Neg | |
! | 按位取反 | 逻辑非 | std::ops::Not |
* 仅适用于有符号整数类型。
下面是这些运算符的一些示例:
#![allow(unused)] fn main() { let x = 6; assert_eq!(-x, -6); assert_eq!(!x, -7); assert_eq!(true, !false); }
算术和逻辑二元运算符
句法
ArithmeticOrLogicalExpression :
Expression+Expression
| Expression-Expression
| Expression*Expression
| Expression/Expression
| Expression%Expression
| Expression&Expression
| Expression|Expression
| Expression^Expression
| Expression<<Expression
| Expression>>Expression
二元运算符表达式都用中缀表示法(infix notation)书写。 下表总结了算术和逻辑二元运算符在原生类型(primitive type)上的行为,同时指出其他类型要重载这些操作符需要实现的 trait。 记住,有符号整数总是用二进制补码形式表示。 所有这些运算符的操作数都在值表达式上下文中求值,因此这些操作数的值会被移走或复制。
| 符号 | 整数 | bool | 浮点数 | 用于重载此运算符的 trait | 用于重载此运算符的复合赋值(Compound Assignment) Trait |
|---|---|---|---|---|---|
+ | 加法 | 加法 | std::ops::Add | std::ops::AddAssign | |
- | 减法 | 减法 | std::ops::Sub | std::ops::SubAssign | |
* | 乘法 | 乘法 | std::ops::Mul | std::ops::MulAssign | |
/ | 除法* | 取余 | std::ops::Div | std::ops::DivAssign | |
% | 取余 | Remainder | std::ops::Rem | std::ops::RemAssign | |
& | 按位与 | 逻辑与 | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位或 | 逻辑或 | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位异或 | 逻辑异或 | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移位 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移位** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法趋零取整。
** 有符号整数类型算术右移位,无符号整数类型逻辑右移位。
下面是使用这些操作符的示例:
#![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); }
比较运算符
句法
ComparisonExpression :
Expression==Expression
| Expression!=Expression
| Expression>Expression
| Expression<Expression
| Expression>=Expression
| Expression<=Expression
Rust 还为原生类型以及标准库中的多种类型都定义了比较运算符。
链式比较运算时需要借助圆括号,例如,表达式 a == b == c 是无效的,(但如果逻辑允许)可以写成 (a == b) == c。
与算术运算符和逻辑运算符不同,重载这些运算符的 trait 通常用于显示/约定如何比较一个类型,并且还很可能会假定使用这些 trait 作为约束条件的函数定义了实际的比较逻辑。 其实标准库中的许多函数和宏都使用了这个假定(尽管不能确保这些假定的安全性)。 与上面的算术和逻辑运算符不同,这些运算符会隐式地对它们的操作数执行共享借用,并在位置表达式上下文中对它们进行求值:
#![allow(unused)] fn main() { let a = 1; let b = 1; a == b; // 等价于: ::std::cmp::PartialEq::eq(&a, &b); }
这意味着不需要将值从操作数移出(moved out of)。
| 符号 | 含义 | 须重载方法 |
|---|---|---|
== | 等于 | std::cmp::PartialEq::eq |
!= | 不等于 | std::cmp::PartialEq::ne |
> | 大于 | std::cmp::PartialOrd::gt |
< | 小于 | std::cmp::PartialOrd::lt |
>= | 大于或等于 | std::cmp::PartialOrd::ge |
<= | 小于或等于 | std::cmp::PartialOrd::le |
下面是使用比较运算符的示例:
#![allow(unused)] fn main() { assert!(123 == 123); assert!(23 != -12); assert!(12.5 > 12.2); assert!([1, 2, 3] < [1, 3, 4]); assert!('A' <= 'B'); assert!("World" >= "Hello"); }
短路布尔运算符
句法
LazyBooleanExpression :
Expression||Expression
| Expression&&Expression
运算符 || 和 && 可以应用在布尔类型的操作数上。
运算符 || 表示逻辑“或”,运算符 && 表示逻辑“与”。
它们与 | 和 & 的不同之处在于,只有在左操作数尚未确定表达式的结果时,才计算右操作数。
也就是说,|| 只在左操作数的计算结果为 false 时才计算其右操作数,而只有在计算结果为 true 时才计算 && 的操作数。
#![allow(unused)] fn main() { let x = false || true; // true let y = false && panic!(); // false, 不会计算 `panic!()` }
类型转换表达式
句法
TypeCastExpression :
ExpressionasTypeNoBounds
类型转换表达式用二元运算符 as 表示。
执行类型转换(as)表达式将左侧的值显式转换为右侧的类型。
类型转换(as)表达式的一个例子:
#![allow(unused)] fn main() { fn sum(values: &[f64]) -> f64 { 0.0 } fn len(values: &[f64]) -> i32 { 0 } fn average(values: &[f64]) -> f64 { let sum: f64 = sum(values); let size: f64 = len(values) as f64; sum / size } }
as 可用于显式执行自动强转(coercions),以及下列形式的强制转换。
任何不符合强转规则或不在下表中的转换都会导致编译器报错。
下表中 *T 代表 *const T 或 *mut T。m 引用类型中代表可选的 mut 或指针类型中的 mut 或 const。
e 的类型 | U | 通过 e as U 执行转换 |
|---|---|---|
| 整型或浮点型 | 整型或浮点型 | 数字转换 |
| 类C(C-like)枚举 | 整型 | 枚举转换 |
bool 或 char | 整型 | 原生类型到整型的转换 |
u8 | char | u8 到 char 的转换 |
*T | *V where V: Sized * | 指针到指针的转换 |
*T where T: Sized | 数字型(Numeric type) | 指针到地址的转换 |
| 整型 | *V where V: Sized | 地址到指针的转换 |
&m₁ T | *m₂ T ** | 引用到指针的转换 |
&m₁ [T; n] | *m₂ T ** | 数组到指针的转换 |
| 函数项 | 函数指针 | 函数到函数指针的转换 |
| 函数项 | *V where V: Sized | 函数到指针的转换 |
| 函数项 | 整型 | 函数到地址的转换 |
| 函数指针 | *V where V: Sized | 函数指针到指针的转换 |
| 函数指针 | 整型 | 函数指针到地址的转换 |
| 闭包 *** | 函数指针 | 闭包到函数指针的转换 |
* 或者 T和V 也可以都是兼容的 unsized 类型,例如,两个都是切片,或者都是同一种 trait对象。
** 仅当 m₁ 是 mut 或 m₂ 是 const时, 可变(mut)引用到 const指针才会被允许。
*** 仅适用于不捕获(遮蔽(close over))任何环境变量的闭包。
语义
- 数字转换(Numeric cast)
- 在两个尺寸(size)相同的整型数值(例如 i32 -> u32)之间进行转换是一个空操作(no-op)
- 从一个较大尺寸的整型转换为较小尺寸的整型(例如 u32 -> u8)将会采用截断(truncate)算法 1
- 从较小尺寸的整型转换为较大尺寸的整型(例如 u8 -> u32)将
- 如果源数据是无符号的,则进行零扩展(zero-extend)
- 如果源数据是有符号的,则进行符号扩展(sign-extend)
- 从浮点数转换为整型将使浮点数趋零取整(round the float towards zero)
NaN将返回0- 大于转换到的整型类型的最大值时,取该整型类型的最大值。
- 小于转换到的整型类型的最小值时,取该整型类型的最小值。
- 从整数强制转换为浮点数将产生最接近的浮点数 *
- 如有必要,舍入采用
roundTiesToEven模式 *** - 在溢出时,将会产生该浮点型的常量 Infinity(∞)(与输入符号相同)
- 注意:对于当前的数值类型集,溢出只会发生在
u128 as f32这种转换形式,且数字大于或等于f32::MAX + (0.5 ULP)时。
- 如有必要,舍入采用
- 从 f32 到 f64 的转换是无损转换
- 从 f64 到 f32 的转换将产生最接近的 f32 **
- 如有必要,舍入采用
roundTiesToEven模式 *** - 在溢出时,将会产生 f32 的常量 Infinity(∞)(与输入符号相同)
- 如有必要,舍入采用
- 枚举转换(Enum cast)
- 先将枚举转换为它的判别值(discriminant),然后在需要时使用数值转换。
- 原生类型到整型的转换
false转换为0,true转换为1char会先强制转换为代码点的值,然后在需要时使用数值转换。
u8到char的转换- 转换为具有相应代码点的
char值。
- 转换为具有相应代码点的
* 如果硬件本身不支持这种舍入模式和溢出行为,那么这些整数到浮点型的转换可能会比预期的要慢。
** 如果硬件本身不支持这种舍入模式和溢出行为,那么这些 f64 到 f32 的转换可能会比预期的要慢。
*** 按照 IEEE 754-2008§4.3.1 的定义:选择最接近的浮点数,如果恰好在两个浮点数中间,则优先选择最低有效位为偶数的那个。
赋值表达式
句法
AssignmentExpression :
Expression=Expression
赋值表达式会把某个值移入到一个特定的位置。
赋值表达式由一个可变 位置表达式(就是被赋值的位置操作数)后跟等号(=)和值表达式(就是被赋值的值操作数)组成。
与其他位置操作数不同,赋值位置操作数必须是一个位置表达式。 试图使用值表达式将导致编译器报错,而不是将其提升转换为临时位置。
赋值表达式要先计算它的操作数。 赋值的值操作数先被求值,然后是赋值的位置操作数。
注意:此表达式与其他表达式的求值顺序不同,此表达式的右操作数在左操作数之前被求值。
对赋值表达的位置表达式求值时会先销毁(drop)此位置(如果是未初始化的局部变量或未初始化的局部变量的字段则不会启动这步析构操作),然后将赋值值复制(copy)或移动(move)到此位置中。
赋值表达式总是会生成单元类型值。
示例:
#![allow(unused)] fn main() { let mut x = 0; let y = 0; x = y; }
复合赋值表达式
句法
CompoundAssignmentExpression :
Expression+=Expression
| Expression-=Expression
| Expression*=Expression
| Expression/=Expression
| Expression%=Expression
| Expression&=Expression
| Expression|=Expression
| Expression^=Expression
| Expression<<=Expression
| Expression>>=Expression
复合赋值表达式将算术符(以及二进制逻辑操作符)与赋值表达式相结合在一起使用。
比如:
#![allow(unused)] fn main() { let mut x = 5; x += 1; assert!(x == 6); }
复合赋值的句法是可变 位置表达式(被赋值操作数),然后是一个操作符再后跟一个 =(这两个符号共同作为一个单独的 token),最后是一个值表达式(也叫被复合修改操作数(modifying operand))。
与其他位置操作数不同,被赋值的位置操作数必须是一个位置表达式。 试图使用值表达式将导致编译器报错,而不是将其提升转换为临时位置。
复合赋值表达式的求值取决于操作符的类型。
如果复合赋值表达式了两个操作数的类型都是原生类型,则首先对被复合修改操作数进行求值,然后再对被赋值操作数求值。 最后将被赋值操作数的位置值设置为原被赋值操作数的值和复合修改操作数执行运算后的值。
注意:此表达式与其他表达式的求值顺序不同,此表达式的右操作数在左操作数之前被求值。
此外,这个表达式是调用操作符重载复合赋值trait 的函数的语法糖(见本章前面的表格)。 被赋值操作数必须是可变的。
例如,下面 example函数中的两个表达式语句是等价的:
#![allow(unused)] fn main() { struct Addable; use std::ops::AddAssign; impl AddAssign<Addable> for Addable { /* */ fn add_assign(&mut self, other: Addable) {} } fn example() { let (mut a1, a2) = (Addable, Addable); a1 += a2; let (mut a1, a2) = (Addable, Addable); AddAssign::add_assign(&mut a1, a2); } }
与赋值表达式一样,复合赋值表达式也总是会生成单元类型值。
警告:复合赋值表达式的操作数的求值顺序取决于操作数的类型:对于原生类型,右边操作数将首先被求值,而对于非原生类型,左边操作数将首先被求值。 建议尽量不要编写依赖于复合赋值表达式中操作数的求值顺序的代码。请参阅这里的测试以获得使用此依赖项的示例。
截断,即一个值范围较大的变量A转换为值范围较小的变量B,如果超出范围,则将A减去B的区间长度。例如,128超出了i8类型的范围(-128,127),截断之后的值等于128-256=-128。
圆括号表达式(分组表达式)
句法
GroupedExpression :
(InnerAttribute* Expression)
由圆括号封闭的表达式的求值结果就是在其内的表达式的求值结果。 在表达式内部,圆括号可用于显式地指定表达式内部的求值顺序。
*圆括号表达式(parenthesized expression)包装单个表达式,并对该表达式求值。 圆括号表达式的句法规则就是一对圆括号封闭一个被称为封闭操作数(enclosed operand)*的表达式。
圆括号表达式被求值为其封闭操作数的值。 与其他表达式不同,圆括号表达式可以是[位置表达式或值表达式][place]。 当封闭操作数是位置表达式时,它是一个位置表达式;当封闭操作数是一个值表达式是,它是一个值表达式。
圆括号可用于显式修改表达式中的子表达式的优先顺序。
圆括号表达式的一个例子:
#![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"); }
分组表达式上的属性
在允许块表达式上的属性存在的那几种表达式上下文中,可以在分组表达式的左括号后直接使用内部属性。
md#attributes-on-block-expressions [place]: ../expressions.md#place-expressions-and-value-expressions
数组和数组索引表达式
数组表达式
句法
ArrayExpression :
[InnerAttribute* ArrayElements?]ArrayElements :
Expression (,Expression )*,?
| Expression;Expression
数组表达式用来构建数组。 数组表达式有两种形式。
第一种形式是在数组中列举出所有的元素值。 这种形式的句法规则通过在方括号中放置统一类型的、逗号分隔的表达式来表现。 这样编写将生成一个包含这些表达式的值的数组,其中数组元素的顺序就是这些表达式写入的时顺序。
第二种形式的句法规则通过在方括号内放置两用个用分号(;)分隔的表达式来表现。
分号(;)前的表达式被称为重复体操作数(repeat operand)
分号(;)后的表达式被称为数组长度操作数(length operand)
其中,数组长度操作数必须是 usize 类型的,并且必须是常量表达式,比如是字面量或常量项。
也就是说,[a; b] 这种形式会创建包含 b 个 a 值的数组。
如果数组长度操作数的值大于 1,则要求 a 的类型实现了 Copy,或 a 自己是一个常量项的路径。
当 [a; b] 形式的重复体操作数 a 是一个常量项时,其将被计算求值数组长度操作数 b 次。
如果数组长度操作数 b 为 0,则常量项根本不会被求值。
对于非常量项的表达式,只计算求值一次,然后将结果复制数组长度操作数 b 次。
警告:当数组长度操作数为 0 时,而重复体操作数是一个非常量项的情况下,目前在 rustc 中存在一个 bug,即值 a 会被求值,但不会被销毁而导致的内存泄漏。参见 issue#74836。
#![allow(unused)] fn main() { [1, 2, 3, 4]; ["a", "b", "c", "d"]; [0; 128]; // 内含128个0的数组 [0u8, 0u8, 0u8, 0u8,]; [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 二维数组 const EMPTY: Vec<i32> = Vec::new(); [EMPTY; 2]; }
数组表达式上的属性
在允许块表达式上的属性存在的那几种表达式上下文中,可以在数组表达式的左括号后直接使用内部属性。
数组和切片索引表达式
句法
IndexExpression :
Expression[Expression]
数组和切片类型的值(slice-typed values)可以通过后跟一个由方括号封闭一个类型为 usize 的表达式(索引)的方式来对此数组或切片进行索引检索。
如果数组是可变的,则其检索出的内存位置还可以被赋值。
对于数组和切片类型之外的索引表达式 a[b] 其实相当于执行 *std::ops::Index::index(&a, b),或者在可变位置表达式上下文中相当于执行 *std::ops::IndexMut::index_mut(&mut a, b)。
与普通方法一样,Rust 也将在 a 上反复插入解引用操作,直到查找到对上述方法的实现。
数组和切片的索引是从零开始的。数组访问是一个常量表达式,因此数组索引的越界检查可以在编译时通过检查常量索引值本身进行。 否则,越界检查将在运行时执行,如果此时越界检查未通过,那将把当前线程置于 panicked 状态。
#![allow(unused)] fn main() { // 默认情况下,`unconditional_panic` lint检查会执行 deny 级别的设置, // 即 crate 在默认情况下会有外部属性设置 `#[deny(unconditional_panic)]` // 而像 `(["a", "b"])[n]` 这样的简单动态索引检索会被该 lint 检查出来,而提前报错,导致程序被拒绝编译。 // 因此这里调低 `unconditional_panic` 的 lint 级别以通过编译。 #![warn(unconditional_panic)] ([1, 2, 3, 4])[2]; // 3 let b = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; b[1][2]; // 多维数组索引 let x = (["a", "b"])[10]; // 告警:索引越界 let n = 10; // 译者注:上行可以在 `#![warn(unconditional_panic)]` 被注释的情况下换成 // let n = if true {10} else {0}; // 试试,那下行就不会被 unconditional_panic lint 检查到了 let y = (["a", "b"])[n]; // panic let arr = ["a", "b"]; arr[10]; // 告警:索引越界 }
数组和切片以外的类型可以通过实现 Index trait 和 IndexMut trait 来达成数组索引表达式的效果。
元组和元组索引表达式
元组表达式
句法
TupleExpression :
(InnerAttribute* TupleElements?)TupleElements :
( Expression,)+ Expression?
元组表达式用来构建元组值。
元组表达式的句法规则为:一对圆括号封闭的以逗号分隔的表达式列表,这些表达式被称为元组初始化操作数(tuple initializer operands)。 为了避免和圆括号表达式混淆,一元元组表达式的元组初始化操作数后的逗号不能省略。
元组表达式是一个值表达式,它会被求值计算成一个元组类型的新值。
元组初始化操作数的数量构成元组的元数(arity)。
没有元组初始化操作数的元组表达式生成单元元组(unit tuple)。
对于其他元组表达式,第一个被写入的元组初始化操作数初始化第 0 个元素,随后的操作数依次初始化下一个开始的元素。
例如,在元组表达式 ('a', 'b', 'c') 中,'a' 初始化第 0 个元素的值,'b' 初始化第 1 个元素,'c' 初始化第2个元素。
元组表达式和相应类型的示例:
| 表达式 | 类型 |
|---|---|
() | () (unit) |
(0.0, 4.5) | (f64, f64) |
("x".to_string(), ) | (String, ) |
("a", 4usize, true) | (&'static str, usize, bool) |
元组表达式上的属性
在允许块表达式上的属性存在的那几种表达式上下文中,可以在元组表达式的左括号后直接使用内部属性。
元组索引表达式
句法
TupleIndexingExpression :
Expression.TUPLE_INDEX
元组索引表达式被用来存取元组或[元组结构体][tuple structs]的字段。
元组索引表达式的句法规则为:一个被称为*元组操作数(tuple operand)*的表达式后跟一个 .,最后再后跟一个元组索引。
元组索引的句法规则要求该索引必须写成一个不能有前导零、下划线和后缀的十进制字面量的形式。
例如 0 和 2 是合法的元祖索引,但 01、0_、0i32 这些不行。
元组操作数的类型必须是元组类型或[元组结构体][tuple structs]。 元组索引必须是元组操作数类型的字段的名称。(译者注:这句感觉原文表达有问题,这里也给出原文 The tuple index must be a name of a field of the type of the tuple operand.)
对元组索引表达式的求值计算除了能求取其元组操作数的对应位置的值之外没有其他作用。 作为位置表达式,元组索引表达式的求值结果是元组操作数字段的位置,该字段与元组索引同名。
元组索引表达式示例:
#![allow(unused)] fn main() { // 索引检索一个元组 let pair = ("a string", 2); assert_eq!(pair.1, 2); // 索引检索一个元组结构体 struct Point(f32, f32); let point = Point(1.0, 0.0); assert_eq!(point.0, 1.0); assert_eq!(point.1, 0.0); }
注意:与字段访问表达式不同,元组索引表达式可以是调用表达式的函数操作数。 (这之所以可行,)因为元组索引表达式不会与方法调用相混淆,因为方法名不可能是数字。
注意:虽然数组和切片也有元素,但它们必须使用数组或切片索引表达式或切片模式去访问它们的元素。
结构体表达式
句法
StructExpression :
StructExprStruct
| StructExprTuple
| StructExprUnitStructExprStruct :
PathInExpression{InnerAttribute* (StructExprFields | StructBase)?}StructExprFields :
StructExprField (,StructExprField)* (,StructBase |,?)StructExprField :
IDENTIFIER
| (IDENTIFIER | TUPLE_INDEX):ExpressionStructBase :
..ExpressionStructExprTuple :
PathInExpression(
InnerAttribute*
( Expression (,Expression)*,? )?
)StructExprUnit : PathInExpression
结构体表达式创建结构体或联合体的值。它由指向结构体程序项、枚举变体、联合体程序项的路径,以及与此程序项的字段对应的值组成。 结构体表达式有三种形式:结构体(struct)、元组结构体(tuple)和单元结构体(unit)。
下面是结构体表达式的示例:
#![allow(unused)] fn main() { struct Point { x: f64, y: f64 } struct NothingInMe { } struct TuplePoint(f64, f64); mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } } struct Cookie; fn some_fn<T>(t: T) {} Point {x: 10.0, y: 20.0}; NothingInMe {}; TuplePoint(10.0, 20.0); TuplePoint { 0: 10.0, 1: 20.0 }; // 效果和上一行一样 let u = game::User {name: "Joe", age: 35, score: 100_000}; some_fn::<Cookie>(Cookie); }
结构体表达式的字段设置
用花括号把字段括起来的结构体表达式允许以任意顺序指定每个字段的值。字段名与值之间用冒号分隔。
联合体类型的值只能使用此句法创建,并且只能指定一个字段。
函数式更新句法
结构体表达式构建一个结构体类型的值时可以以 .. 后跟一个表达式的句法结尾,这种句法表示这是一种函数式更新(functional update)。
.. 后跟的表达式(此表达式被称为此函数式更新的基(base))必须与正在构造的新结构体值是同一种结构体类型的。
整个结构体表达式先为已指定的字段使用已给定的值,然后再从基表达式(base expression)里为剩余未指定的字段移动或复制值。 与所有结构体表达式一样,此结构体类型的所有字段必须是可见的,甚至那些没有显式命名的字段也是如此。
#![allow(unused)] fn main() { struct Point3d { x: i32, y: i32, z: i32 } let mut base = Point3d {x: 1, y: 2, z: 3}; let y_ref = &mut base.y; Point3d {y: 0, z: 10, .. base}; // OK, 只有 base.x 获取进来了 drop(y_ref); }
带花括号的结构体表达式不能直接用在循环表达式或 if表达式的头部,也不能直接用在 if let或匹配表达式的检验对象(scrutinee)上。 但是,如果结构体表达式在另一个表达式内(例如在圆括号内),则可以用在这些情况下。
构造元组结构体时其字段名可以是代表索引的十进制整数数值。 这中表达方法还可以与基结构体一起使用来填充其余未指定的索引:
#![allow(unused)] fn main() { struct Color(u8, u8, u8); let c1 = Color(0, 0, 0); // 创建元组结构体的典型方法。 let c2 = Color{0: 255, 1: 127, 2: 0}; // 按索引来指定字段。 let c3 = Color{1: 0, ..c2}; // 使用基的字段值来填写结构体的所有其他字段。 }
初始化结构体字段的快捷方法
当使用字段的名字(注意不是位置索引数字)初始化某数据结构(结构体、枚举、联合体)时,允许将 fieldname: fieldname 写成 fieldname 这样的简化形式。
这种句法让代码更少重复,更加紧凑。
例如:
For example: For example:
#![allow(unused)] fn main() { struct Point3d { x: i32, y: i32, z: i32 } let x = 0; let y_value = 0; let z = 0; Point3d { x: x, y: y_value, z: z }; Point3d { x, y: y_value, z }; }
元组结构体表达式
用圆括号括起字段的结构体表达式构造出来的结构体为元组结构体。 虽然为了完整起见,也把它作为一个特定的(结构体)表达式列在这里,但实际上它等价于执行元组结构体构造器的调用表达式。例如:
#![allow(unused)] fn main() { struct Position(i32, i32, i32); Position(0, 0, 0); // 创建元组结构体的典型方法。 let c = Position; // `c` 是一个接收3个参数的函数。 let pos = c(8, 6, 7); // 创建一个 `Position` 值。 }
单元结构体表达式
单元结构体表达式只是单元结构体程序项(unit struct item)的路径。 也是指向此单元结构体的值的隐式常量。 单元结构体的值也可以用无字段结构体表达式来构造。例如:
#![allow(unused)] fn main() { struct Gamma; let a = Gamma; // Gamma的值。 let b = Gamma{}; // 和`a`的值完全一样。 }
结构体表达式上的属性
在允许块表达式上的属性存在的那几种表达式上下文中,可以在结构体表达式的左括号后直接使用内部属性。
调用表达式
句法
CallExpression :
Expression(CallParams?)CallParams :
Expression (,Expression )*,?
调用表达式用来调用函数。
调用表达式的句法规则为:一个被称作*函数操作数(function operand)的表达式,后跟一个圆括号封闭的逗号分割的被称为参数操作数(argument operands)*的表达式列表。
如果函数最终返回,则此调用表达式执行完成。
对于非函数类型,表达式 f(...) 会使用 std::ops::Fn、std::ops::FnMut 或 std::ops::FnOnce 这些 trait 上的某一方法,选择使用哪个要看 f 如何获取其输入的参数,具体就是看是通过引用、可变引用、还是通过获取所有权来获取的。
如有需要,也可通过自动借用。
Rust 也会根据需要自动对 f 作解引用处理。
下面是一些调用表达式的示例:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { 0 } let three: i32 = add(1i32, 2i32); let name: &'static str = (|| "Rust")(); }
函数调用的消歧
为获得更直观的完全限定的句法规则,Rust 对所有函数调都作了糖化(sugar)处理。 根据当前作用域内的程序项调用的二义性,函数调用有可能需要完全限定。
注意:过去,Rust 社区在文档、议题、RFC 和其他社区文章中使用了术语“确定性函数调用句法(Unambiguous Function Call Syntax)”、“通用函数调用句法(Universal Function Call Syntax)” 或 “UFCS”。 但是,这个术语缺乏描述力,可能还会混淆当前的议题。 我们在这里提起这个词是为了便于搜索。
少数几种情况下经常会出现一些导致方法调用或关联函数调用的接受者或引用对象不明确的情况。这些情况可包括:
- 作用域内的多个 trait 为同一类型定义了相同名称的方法
- 自动解引(Auto-
deref)用搞不定的情况;例如,区分智能指针本身的方法和指针所指对象上的方法 - 不带参数的方法,就像
default()这样的和返回类型的属性(properties)的,如size_of()
为了解决这种二义性,程序员可以使用更具体的路径、类型或 trait 来明确指代他们想要的方法或函数。
例如:
trait Pretty { fn print(&self); } trait Ugly { fn print(&self); } struct Foo; impl Pretty for Foo { fn print(&self) {} } struct Bar; impl Pretty for Bar { fn print(&self) {} } impl Ugly for Bar { fn print(&self) {} } fn main() { let f = Foo; let b = Bar; // 我们可以这样做,因为对于`Foo`,我们只有一个名为 `print` 的程序项 f.print(); // 对于 `Foo`来说,这样是更明确了,但没必要 Foo::print(&f); // 如果你不喜欢简洁的话,那,也可以这样 <Foo as Pretty>::print(&f); // b.print(); // 错误: 发现多个 `print` // Bar::print(&b); // 仍错: 发现多个 `print` // 必要,因为作用域内的多个程序项定义了 `print` <Bar as Pretty>::print(&b); }
更多细节和动机说明请参考RFC 132。
方法调用表达式
句法
MethodCallExpression :
Expression.PathExprSegment(CallParams?)
方法调用由一个表达式(接受者(receiver))后跟一个单点号(.)、一个表达式路径段(path segment)和一个圆括号封闭的的表达式列表组成。
方法调用被解析为特定 trait 上的关联方法时,如果点号左边的表达式有确切的已知的 self类型,则会静态地分发(statically dispatch)给在此类型下查找到的某个同名方法来执行;如果点号左边的表达式是间接的 trait对象,则会采用动态分发(dynamically dispatch)的方式。
#![allow(unused)] fn main() { let pi: Result<f32, _> = "3.14".parse(); let log_pi = pi.unwrap_or(1.0).log(2.72); assert!(1.14 < log_pi && log_pi < 1.15) }
在查找方法调用时,为了调用某个方法,可能会自动对接受者做解引用或借用。 这需要比其他函数更复杂的查找流程,因为这可能需要调用许多可能的方法。具体会用到下述步骤:
第一步是构建候选接受者类型的列表。通过重复对接受者表达式的类型作解引用,将遇到的每个类型添加到列表中,然后在最后再尝试进行一次非固定尺寸类型自动强转(unsized coercion),如果成功,则将结果类型也添加到此类型列表里。
然后,再在这个列表中的每个候选类型 T 后紧跟着添加 &T 和 &mut T 候选项。
例如,接受者的类型为 Box<[i32;2]>,则候选类型为 Box<[i32;2]>,&Box<[i32;2]>,&mut Box<[i32;2]>,[i32; 2](通过解引用得到),&[i32; 2],&mut [i32; 2],[i32](通过非固定尺寸类型自动强转得到),&[i32],最后是 &mut [i32]。
然后,对每个候选类型 T,编译器会它的以下位置上搜索一个可见的同名方法,找到后还会把此方法所属的类型当做接受者:
T的固有方法(直接在T上实现的方法)。- 由
T已实现的可见的 trait 所提供的任何方法。如果T是一个类型参数,则首先查找由T上的 trait约束所提供的方法。然后查找作用域内所有其他的方法。
注意:查找是按顺序进行的,这有时会导致出现不太符合直觉的结果。 下面的代码将打印 “In trait impl!”,因为首先会查找
&self上的方法,在找到结构体Foo的(接受者类型为)&mut self的(固有)方法(Foo::bar)之前先找到(接受者类型为&self的)trait方法(Bar::bar)。struct Foo {} trait Bar { fn bar(&self); } impl Foo { fn bar(&mut self) { println!("In struct impl!") } } impl Bar for Foo { fn bar(&self) { println!("In trait impl!") } } fn main() { let mut f = Foo{}; f.bar(); }
如果上面这第二步查找导致了多个可能的候选类型1,就会导致报错,此时必须将接受者转换为适当的接受者类型再来进行方法调用。
此方法过程不考虑接受者的可变性或生存期,也不考虑方法是否为非安全(unsafe)方法。
一旦查找到了一个方法,如果由于这些(可变性、生存期或健全性)原因中的一个(或多个)而不能调用,则会报编译错误。
如果某步碰到了存在多个可能性方法的情况,比如泛型方法之间或 trait方法之间被认为是相同的,那么它就会导致编译错误。 这些情况就需要使用函数调用的消歧句法来为方法调用或函数调用消除歧义。
警告: 对于 trait对象,如果有一个与 trait方法同名的固有方法,那么当尝试在方法调用表达式(method call expression)中调用该方法时,将编译报错。 此时,可以使用消除函数调用歧义的句法来明确调用语义。 在 trait对象上使用消除函数调用歧义的句法,将只能调用 trait方法,无法调用固有方法。 所以只要不在 trait对象上定义和 trait方法同名的固有方法就不会碰到这种麻烦。
1:这个应该跟后面说的方法名歧义了一样,如果类型 T 的两个 trait 都有调用的那个方法,那此方法的调用者类型就不能确定了,就需要消歧。
字段访问表达式
句法
FieldExpression :
Expression.IDENTIFIER
*字段表达式(field expression)*是计算求取结构体或联合体的字段的内存位置的位置表达式。 当操作数可变时,其字段表达式也是可变的。
*字段表达式(field expression)的句法规则为:一个被称为容器操作数(container operand)*的表达式后跟一个单点号(.),最后是一个标识符。
字段表达式后面不能再紧跟着一个被圆括号封闭起来的逗号分割的表达式列表(这种表示这是一个方法调用表达式)。
因此字段表达式不能是调用表达式的函数调用者。
字段表达式代表结构体(struct)或联合体(union)的字段。要调用存储在结构体的字段中的函数,需要在此字段表达式外加上圆括号。
注意:如果要在调用表达式中使用它(来调用函数),要把此字段表达式先用圆括号包装成一个圆括号表达式。
#![allow(unused)] fn main() { struct HoldsCallable<F: Fn()> { callable: F } let holds_callable = HoldsCallable { callable: || () }; // 非法: 会被解析为调用 "callable"方法 // holds_callable.callable(); // 合法 (holds_callable.callable)(); }
示例:
mystruct.myfield;
foo().x;
(Struct {a: 10, b: 20}).a;
(mystruct.function_field)() // 调用表达式里包含一个字段表达式
自动解引用
如果容器操作数的类型实现了 Deref 或 DerefMut(这取决于该操作数是否为可变),则会尽可能多次地自动解引用(automatically dereferenced),以使字段访问成为可能。
这个过程也被简称为自动解引用(autoderef)。
借用
当借用时,结构体的各个字段以及对结构体的整体引用都被视为彼此分离的实体。
如果结构体没有实现 Drop,同时该结构体又存储在局部变量中,(这种各个字段被视为彼此分离的单独实体的逻辑)还适用于每个字段的移出(move out)。
但如果对 Box化之外的用户自定义类型执行自动解引用,这(种各个字段被视为彼此分离的单独实体的逻辑)就不适用了。
#![allow(unused)] fn main() { struct A { f1: String, f2: String, f3: String } let mut x: A; x = A { f1: "f1".to_string(), f2: "f2".to_string(), f3: "f3".to_string() }; let a: &mut String = &mut x.f1; // x.f1 被可变借用 let b: &String = &x.f2; // x.f2 被不可变借用 let c: &String = &x.f2; // 可以被再次借用 let d: String = x.f3; // 从 x.f3 中移出 }
闭包表达式
句法
ClosureExpression :
move?
(||||ClosureParameters?|)
(Expression |->TypeNoBounds BlockExpression)ClosureParameters :
ClosureParam (,ClosureParam)*,?ClosureParam :
OuterAttribute* PatternNoTopAlt (:Type )?
闭包表达式,也被称为 lambda表达式或 lambda,它定义了一个闭包类型,并把此表达式求值计算为该类型的值。
闭包表达式的句法规则为:先是一个可选的 move关键字,后跟一对管道定界符(|)封闭的逗号分割的被称为闭包参数(closure parameters)的模式列表(每个闭包参数都可选地通过 : 后跟其类型),再可选地通过 -> 后跟一个返回类型,最后是被称为闭包体操作数(closure body operand) 的表达式。
代表闭包参数的每个模式后面的可选类型是该模式的类型标注(type annotations)。
如果存在返回类型,则闭包体表达式必须是一个普通的块(表达式)。
闭包表达式本质是将一组参数映射到参数后面的表达式的函数。
与 let绑定一样,闭包参数也是不可反驳型模式的,其类型标注是可选的,如果没有给出,则从上下文推断。
每个闭包表达式都有一个唯一的匿名类型。
特别值得注意的是闭包表达式能捕获它们被定义时的环境中的变量(capture their environment),而普通的函数定义则不能。
如果没有关键字 move,闭包表达式将[推断它该如何从其环境中捕获每个变量][infers how it captures each variable from its environment],它倾向于通过共享引用来捕获,从而有效地借用闭包体中用到的所有外部变量。
如果有必要,编译器会推断出应该采用可变引用,还是应该从环境中移动或复制值(取决于这些变量的类型)。
闭包可以通过前缀关键字 move 来强制通过复制值或移动值的方式捕获其环境变量。
这通常是为了确保当前闭包的生存期类型为 'static。
编译器将通过闭包对其捕获的变量的处置方式来确定此闭包类型将实现的[闭包trait][closure traits]。
如果所有捕获的类型都实现了 Send 和/或 Sync,那么此闭包类型也实现了 Send 和/或 Sync。
这些存在这些 trait,函数可以通过泛型的方式接受各种闭包,即便闭包的类型名无法被确切指定。
闭包 trait 的实现
当前闭包类型实现哪一个闭包trait 依赖于该闭包如何捕获变量和这些变量的类型。
了解闭包如何和何时实现 Fn、FnMut 和 FnOnce 这三类 trait,请参看调用trait 和自动强转那一章。
如果所有捕获的类型都实现了 Send 和/或 Sync,那么此闭包类型也实现了 Send 和/或 Sync。
Example
示例
在下面例子中,我们定义了一个名为 ten_times 的函数,它接受高阶函数参数,然后我们传给它一个闭包表达式作为实参并调用它。
之后又定义了一个使用移动语义从环境中捕获变量的闭包表达式来供该函数调用。
#![allow(unused)] fn main() { fn ten_times<F>(f: F) where F: Fn(i32) { for index in 0..10 { f(index); } } ten_times(|j| println!("hello, {}", j)); // 带类型标注 i32 ten_times(|j: i32| -> () { println!("hello, {}", j) }); let word = "konnichiwa".to_owned(); ten_times(move |j| println!("{}, {}", word, j)); }
闭包参数上的属性
闭包参数上的属性遵循与常规函数参数上相同的规则和限制。
循环
句法
LoopExpression :
LoopLabel? (
InfiniteLoopExpression
| PredicateLoopExpression
| PredicatePatternLoopExpression
| IteratorLoopExpression
)
Rust支持四种循环表达式:
loop表达式表示一个无限循环。while表达式不断循环,直到谓词为假。while let表达式循环测试给定模式。for表达式从迭代器中循环取值,直到迭代器为空。
所有四种类型的循环都支持 break表达式、continue表达式和循环标签(label)。
只有 loop循环支持对循环体非平凡求值(evaluation to non-trivial values)1。
无限循环
句法
InfiniteLoopExpression :
loopBlockExpression
loop表达式会不断地重复地执行它代码体内的代码:loop { println!("I live."); }。
没有包含关联的 break表达式的 loop表达式是发散的,并且具有类型 !。
包含相应 break表达式的 loop表达式可以结束循环,并且此表达式的类型必须与 break表达式的类型兼容。
谓词循环
句法
PredicateLoopExpression :
whileExpression排除结构体表达式 BlockExpression
while循环从对布尔型的循环条件操作数求值开始。
如果循环条件操作数的求值结果为 true,则执行循环体块,然后控制流返回到循环条件操作数。如果循环条件操作数的求值结果为 false,则 while表达式完成。
举个例子:
#![allow(unused)] fn main() { let mut i = 0; while i < 10 { println!("hello"); i = i + 1; } }
谓词模式循环
句法
PredicatePatternLoopExpression :
whileletPattern=Expression排除结构体表达式和惰性布尔运算符表达式 BlockExpression
while let循环在语义上类似于 while循环,但它用 let关键字后紧跟着一个模式、一个 =、一个检验对象(scrutinee)表达式和一个块表达式,来替代原来的条件表达式。
如果检验对象表达式的值与模式匹配,则执行循环体块,然后控制流再返回到模式匹配语句。如果不匹配,则 while表达式执行完成。
#![allow(unused)] fn main() { let mut x = vec![1, 2, 3]; while let Some(y) = x.pop() { println!("y = {}", y); } while let _ = 5 { println!("不可反驳模式总是会匹配成功"); break; } }
while let循环等价于包含匹配(match)表达式的 loop表达式。
如下:
'label: while let PATS = EXPR {
/* loop body */
}
等价于
'label: loop {
match EXPR {
PATS => { /* loop body */ },
_ => break,
}
}
可以使用操作符 | 指定多个模式。
这与匹配(match)表达式中的 | 具有相同的语义:
#![allow(unused)] fn main() { let mut vals = vec![2, 3, 1, 2, 2]; while let Some(v @ 1) | Some(v @ 2) = vals.pop() { // 打印 2, 2, 然后 1 println!("{}", v); } }
与 if let表达式的情况一样,检验表达式不能是一个懒惰布尔运算符表达式。
迭代器循环
句法
IteratorLoopExpression :
forPatterninExpression排除结构体表达式 BlockExpression
for表达式是一个用于在 std::iter::IntoIterator 的某个迭代器实现提供的元素上进行循环的语法结构。
如果迭代器生成一个值,该值将与此 for表达式提供的不可反驳型模式进行匹配,执行循环体,然后控制流返回到 for循环的头部。
如果迭代器为空了,则 for表达式执行完成。
for循环遍历数组内容的示例:
#![allow(unused)] fn main() { let v = &["apples", "cake", "coffee"]; for text in v { println!("I like {}.", text); } }
for循环遍历一个整数序列的例子:
#![allow(unused)] fn main() { let mut sum = 0; for n in 1..11 { sum += n; } assert_eq!(sum, 55); }
for循环等价于后面的块表达式。
'label: for PATTERN in iter_expr {
/* loop body */
}
等价于:
{
let result = match IntoIterator::into_iter(iter_expr) {
mut iter => 'label: loop {
let mut next;
match Iterator::next(&mut iter) {
Option::Some(val) => next = val,
Option::None => break,
};
let PATTERN = next;
let () = { /* loop body */ };
},
};
result
}
这里的 IntoIterator、Iterator 和 Option 是标准库的程序项(standard library item),不是当前作用域中解析的的任何名称。
变量名 next、iter 和 val 也仅用于表述需要,实际上它们不是用户可以输入的名称。
注意:上面代码里使用外层
matche来确保iter_expr中的任何临时值在循环结束前不会被销毁。next先声明后赋值是因为这样能让编译器更准确地推断出类型。
循环标签
句法
LoopLabel :
LIFETIME_OR_LABEL:
一个循环表达式可以选择设置一个标签。
这类标签被标记为循环表达式之前的生存期(标签),如 'foo: loop { break 'foo; }、'bar: while false {}、'humbug: for _ in 0..0 {}。
如果循环存在标签,则嵌套在该循环中的带此标签的 break表达式和 continue表达式可以退出此标签标记的循环层或将控制流返回至此标签标记的循环层的头部。
具体请参见后面的 break表达式和 continue表达式。
break表达式
句法
BreakExpression :
breakLIFETIME_OR_LABEL? Expression?
当遇到 break 时,相关的循环体的执行将立即结束,例如:
#![allow(unused)] fn main() { let mut last = 0; for x in 1..100 { if x > 12 { break; } last = x; } assert_eq!(last, 12); }
break表达式通常与包含 break表达式的最内层 loop、for或 while循环相关联,但是可以使用循环标签来指定受影响的循环层(此循环层必须是封闭该 break表达式的循环之一)。
例如:
#![allow(unused)] fn main() { 'outer: loop { while true { break 'outer; } } }
break表达式只允许在循环体内使用,它有 break、break 'label 或(参见后面)break EXPR 或 break 'label EXPR 这四种形式。
continue表达式
句法
ContinueExpression :
continueLIFETIME_OR_LABEL?
当遇到 continue 时,相关的循环体的当前迭代将立即结束,并将控制流返回到循环头。
在 while循环的情况下,循环头是控制循环的条件表达式。
在 for循环的情况下,循环头是控制循环的调用表达式。
与 break 一样,continue 通常与最内层的循环相关联,但可以使用 continue 'label 来指定受影响的循环层。
continue表达式只允许在循环体内部使用。
break和loop返回值
当使用 loop循环时,可以使用 break表达式从循环中返回一个值,通过形如 break EXPR 或 break 'label EXPR 来返回,其中 EXPR 是一个表达式,它的结果被从 loop循环中返回。
例如:
#![allow(unused)] fn main() { let (mut a, mut b) = (1, 1); let result = loop { if b > 10 { break b; } let c = a + b; a = b; b = c; }; // 斐波那契数列中第一个大于10的值: assert_eq!(result, 13); }
如果 loop 有关联的 break,则不认为该循环是发散的,并且 loop表达式的类型必须与每个 break表达式的类型兼容。
其后不跟表达式的 break 被认为与后跟 () 的break表达式的效果相同。
求得 () 类型以外的值。
区间表达式
句法
RangeExpression :
RangeExpr
| RangeFromExpr
| RangeToExpr
| RangeFullExpr
| RangeInclusiveExpr
| RangeToInclusiveExprRangeExpr :
Expression..ExpressionRangeFromExpr :
Expression..RangeToExpr :
..ExpressionRangeFullExpr :
..RangeInclusiveExpr :
Expression..=ExpressionRangeToInclusiveExpr :
..=Expression
.. 和 ..= 操作符会根据下表中的规则构造 std::ops::Range(或 core::ops::Range)的某一变体类型的对象:
| 产生式/句法规则 | 句法 | 类型 | 区间语义 |
|---|---|---|---|
| RangeExpr | start..end | std::ops::Range | start ≤ x < end |
| RangeFromExpr | start.. | std::ops::RangeFrom | start ≤ x |
| RangeToExpr | ..end | std::ops::RangeTo | x < end |
| RangeFullExpr | .. | std::ops::RangeFull | - |
| RangeInclusiveExpr | start..=end | std::ops::RangeInclusive | start ≤ x ≤ end |
| RangeToInclusiveExpr | ..=end | std::ops::RangeToInclusive | x ≤ end |
举例:
#![allow(unused)] fn main() { 1..2; // std::ops::Range 3..; // std::ops::RangeFrom ..4; // std::ops::RangeTo ..; // std::ops::RangeFull 5..=6; // std::ops::RangeInclusive ..=7; // std::ops::RangeToInclusive }
下面的表达式是等价的。
#![allow(unused)] fn main() { let x = std::ops::Range {start: 0, end: 10}; let y = 0..10; assert_eq!(x, y); }
区间能在 for循环里使用:
#![allow(unused)] fn main() { for i in 1..11 { println!("{}", i); } }
if和 if let表达式
if表达式
句法
IfExpression :
ifExpression排除结构体表达式 BlockExpression
(else( BlockExpression | IfExpression | IfLetExpression ) )?
if表达式是程序控制中的一个条件分支。if表达式的句法是一个条件操作数(operand)后紧跟一个块,再后面是任意数量的 else if条件表达式和块,最后是一个可选的尾部 else块。
条件操作数的类型必须是布尔型。如果条件操作数的求值结果为 true,则执行紧跟的块,并跳过后续的 else if块或 else块。
如果条件操作数的求值结果为 false,则跳过紧跟的块,并按顺序求值后续的 else if条件表达式。
如果所有 if条件表达式和 else if条件表达式的求值结果均为 false,则执行 else块。
if表达式的求值结果就是所执行的块的返回值,或者如果没有块被求值那 if表达式的求值结果就是 ()。
if表达式在所有情况下的类型必须一致。
#![allow(unused)] fn main() { let x = 3; if x == 4 { println!("x is four"); } else if x == 3 { println!("x is three"); } else { println!("x is something else"); } let y = if 12 * 15 > 150 { "Bigger" } else { "Smaller" }; assert_eq!(y, "Bigger"); }
if let表达式
句法
IfLetExpression :
ifletPattern=Expression排除结构体表达式和惰性布尔运算符表达式 BlockExpression
(else( BlockExpression | IfExpression | IfLetExpression ) )?
if let表达式在语义上类似于 if表达式,但是代替条件操作数的是一个关键字 let,再后面是一个模式、一个 = 和一个检验对象(scrutinee)操作数。
如果检验对象操作数的值与模式匹配,则执行相应的块。
否则,如果存在 else块,则继续处理后面的 else块。和 if表达式一样,if let表达式也可以有返回值,这个返回值是由被求值的块确定。
#![allow(unused)] fn main() { let dish = ("Ham", "Eggs"); // 此主体代码将被跳过,因为该模式被反驳 if let ("Bacon", b) = dish { println!("Bacon is served with {}", b); } else { // 这个块将被执行。 println!("No bacon will be served"); } // 此主体代码将被执行 if let ("Ham", b) = dish { println!("Ham is served with {}", b); } if let _ = 5 { println!("不可反驳型的模式总是会匹配成功的"); } }
if表达式和 if let表达式能混合使用:
#![allow(unused)] fn main() { let x = Some(3); let a = if let Some(1) = x { 1 } else if x == Some(2) { 2 } else if let Some(y) = x { y } else { -1 }; assert_eq!(a, 3); }
if let表达式等价于match表达式,例如:
if let PATS = EXPR {
/* body */
} else {
/*else */
}
is equivalent to
match EXPR {
PATS => { /* body */ },
_ => { /* else */ }, // 如果没有 else块,这相当于 `()`
}
可以使用操作符 | 指定多个模式。
这与匹配(match)表达式中的 | 具有相同的语义:
#![allow(unused)] fn main() { enum E { X(u8), Y(u8), Z(u8), } let v = E::Y(12); if let E::X(n) | E::Y(n) = v { assert_eq!(n, 12); } }
if let表达式不能是惰性布尔运算符表达式。
使用惰性布尔运算符的效果是不明确的,因为 Rust 里一个新特性(if-let执行链(if-let chains)的实现-请参阅eRFC 2947)正被提上日程。
当确实需要惰性布尔运算符表达式时,可以像下面一样使用圆括号来实现:
// Before...
if let PAT = EXPR && EXPR { .. }
// After...
if let PAT = ( EXPR && EXPR ) { .. }
// Before...
if let PAT = EXPR || EXPR { .. }
// After...
if let PAT = ( EXPR || EXPR ) { .. }
匹配(match)表达式
句法
MatchExpression :
matchExpression排除结构体表达式{
InnerAttribute*
MatchArms?
}MatchArms :
( MatchArm=>( ExpressionWithoutBlock,| ExpressionWithBlock,? ) )*
MatchArm=>Expression,?MatchArm :
OuterAttribute* Pattern MatchArmGuard?MatchArmGuard :
ifExpression
匹配(match)表达式在模式(pattern)上建立代码逻辑分支(branch)。
匹配的确切形式取决于其应用的模式。
一个匹配(match)表达式带有一个要与模式进行比较的 检验对象(scrutinee)表达式。
检验对象表达式和模式必须具有相同的类型。
根据检验对象表达式是位置表达式或值表达式,匹配(match)的行为表现会有所不同。
如果检验对象表达式是一个值表达式,则这个表达式首先会在被求值到一个临时内存位置,然后将这个结果值按顺序与匹配臂(arms)中的模式进行比较,直到找到一个成功的匹配。
第一个匹配成功的模式所在的匹配臂会被选中为当前匹配(match)的分支目标,然后以该模式绑定的变量为中介,(把它从检验对象那里匹配到的变量值)转赋值给该匹配臂的块中的局部变量,然后控制流进入该块。
当检验对象表达式是一个位置表达式时,此匹配(match)表达式不用先去内存上分配一个临时位置;但是,按值匹配的绑定方式(by-value binding)会复制或移动这个(位置表达式代表的)内存位置里面的值。
如果可能,最好还是在位置表达式上进行匹配,因为这种匹配的生存期继承了该位置表达式的生存期,而不会(让其生存期仅)局限于此匹配的内部。
匹配(match)表达式的一个示例:
#![allow(unused)] fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), 4 => println!("four"), 5 => println!("five"), _ => println!("something else"), } }
模式中绑定到的变量的作用域可以覆盖到匹配守卫(match guard)和匹配臂的表达式里。 变量绑定方式(移动、复制或引用)取决于使用的具体模式。
可以使用操作符 | 连接多个匹配模式。
每个模式将按照从左到右的顺序进行测试,直到找到一个成功的匹配。
#![allow(unused)] fn main() { let x = 9; let message = match x { 0 | 1 => "not many", 2 ..= 9 => "a few", _ => "lots" }; assert_eq!(message, "a few"); // 演示模式匹配顺序。 struct S(i32, i32); match S(1, 2) { S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1), _ => panic!(), } }
注意:
2..=9是一个区间(Range)模式,不是一个区间表达式。 因此,只有区间模式支持的区间类型才能在匹配臂中使用。
每个 | 分隔的模式里出现的变量绑定必须出现在匹配臂的所有模式里。
相同名称的绑定变量必须具有相同的类型和相同的变量绑定模式。
匹配守卫
匹配臂可以接受*匹配守卫(Pattern guard)*来进一步改进匹配标准。
模式守卫出现在模式的后面,由关键字 if 后面的布尔类型表达式组成。
当模式匹配成功时,将执行匹配守卫表达式。
如果此表达式的计算结果为真,则此模式将进一步被确认为匹配成功。
否则,匹配将测试下一个模式,包括测试同一匹配臂中运算符 | 分割的后续匹配模式。
#![allow(unused)] fn main() { let maybe_digit = Some(0); fn process_digit(i: i32) { } fn process_other(i: i32) { } let message = match maybe_digit { Some(x) if x < 10 => process_digit(x), Some(x) => process_other(x), None => panic!(), }; }
注意:使用操作符
|的多次匹配可能会导致后跟的匹配守卫必须多次执行的副作用。 例如:#![allow(unused)] fn main() { use std::cell::Cell; let i : Cell<i32> = Cell::new(0); match 1 { 1 | _ if { i.set(i.get() + 1); false } => {} _ => {} } assert_eq!(i.get(), 2); }
匹配守卫可以引用绑定在它们前面的模式里的变量。 在对匹配守卫进行计算之前,将对检验对象内部被模式的变量匹配上的那部分进行共享引用。 在对匹配守卫进行计算时,访问守卫里的这些变量就会使用这个共享引用。只有当匹配守卫最终计算为真时,此共享引用的值才会从检验对象内部移动或复制到相应的匹配臂的变量中。 这使得共享借用可以在守卫内部使用,还不会在守卫不匹配的情况下将值移出检验对象。 此外,通过在计算匹配守卫的同时持有共享引用,也可以防止匹配守卫内部意外修改检验对象。
匹配臂上的属性
在匹配臂上允许使用外部属性,但在匹配臂上只有 cfg、cold 和 lint检查类属性这些属性才有意义。
在允许块表达式上的属性存在的那几种表达式上下文中,可以在匹配表达式的左括号后直接使用内部属性。
返回(return)表达式
句法
ReturnExpression :
returnExpression?
返回(return)表达式使用关键字 return 来标识。
对返回(return)表达式求值会将其参数移动到当前函数调用的指定输出位置,然后销毁当前函数的激活帧(activation frame),并将控制权转移到此函数的调用帧(caller frame)。
一个返回(return)表达式的例子:
#![allow(unused)] fn main() { fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } }
等待(await)表达式
句法
AwaitExpression :
Expression.await
等待(await)表达式挂起当前计算,直到给定的 future 准备好生成值。
等待(await)表达式的句法格式为:一个其类型实现了 Future trait 的表达式(此表达式本身被称为 future操作数)后跟一 .标记,再后跟一个 await关键字。
等待(await)表达式仅在异步上下文中才能使用,例如 异步函数(async fn) 或 异步(async)块。
更具体地说,等待(await)表达式具有以下效果:
- 把 future操作数求值计算到一个 future类型的
tmp中; - 使用
Pin::new_unchecked固定住(Pin)这个tmp; - 然后通过调用
Future::poll方法对这个固定住的 future 进行轮询,同事将当前任务上下文传递给它; - 如果轮询(
poll)调用返回Poll::Pending,那么这个 future 就也返回 `Expression
版本差异: 等待(await)表达式只能从 Rust 2018 版开始才可用
任务上下文
任务上下文是指在对异步上下文本身进行轮询时提供给当前异步上下文的上下文(Context)。
因为等待(await)表达式只能在异步上下文中才能使用,所以此时必须有一些任务上下文可用。
近似脱糖
实际上,一个等待(await)表达式大致相当于如下这个非正规的脱糖过程:
match future_operand {
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 是指从异步环境中获取的上下文。