运算符表达式
语法
运算符表达式 :
借用表达式
| 解引用表达式
| 错误传播表达式
| 取反表达式
| 算术或逻辑表达式
| 比较表达式
| 惰性布尔表达式
| 类型转换表达式
| 赋值表达式
| 复合赋值表达式
运算符由 Rust 语言为内置类型定义。以下许多运算符也可以使用 std::ops
或 std::cmp
中的特征进行重载。
溢出
在调试模式下编译时,整数运算符在溢出时会发生恐慌。可以使用 -C debug-assertions
和 -C overflow-checks
编译器标志更直接地控制这一点。以下情况被认为是溢出
- 当
+
、*
或二元-
创建的值大于可存储的最大值或小于可存储的最小值时。 - 对任何有符号整数类型的最小负值应用一元
-
,除非操作数是字面量表达式(或独立存在于一个或多个分组表达式内的字面量表达式)。 - 使用
/
或%
,其中左侧参数是有符号整数类型的最小整数,右侧参数是-1
。由于历史原因,即使禁用了-C overflow-checks
,也会进行这些检查。 - 使用
<<
或>>
,其中右侧参数大于或等于左侧参数类型的位数,或者为负数。
注意:一元
-
后面的字面量表达式的例外情况意味着-128_i8
或let j: i8 = -(128)
等形式永远不会导致恐慌,并且具有预期值 -128。在这些情况下,字面量表达式已经具有其类型的最小负值(例如,
128_i8
的值为 -128),因为根据整数字面量表达式中的描述,整数字面量会被截断为其类型。由于二进制补码溢出约定,对这些最小负值的取反不会改变值。
在
rustc
中,overflowing_literals
lint 检查也会忽略这些最小负值表达式。
借用运算符
&
(共享借用)和 &mut
(可变借用)运算符是一元前缀运算符。当应用于位置表达式时,此表达式会生成一个指向该值所引用位置的引用(指针)。在引用的持续时间内,内存位置也会被置于借用状态。对于共享借用 (&
),这意味着该位置可能不会被改变,但可以被读取或再次共享。对于可变借用 (&mut
),在借用过期之前,不能以任何方式访问该位置。&mut
在可变位置表达式上下文中对其操作数求值。如果 &
或 &mut
运算符应用于值表达式,则会创建一个临时值。
这些运算符不能被重载。
#![allow(unused)] fn main() { { // a temporary with value 7 is created that lasts for this scope. let shared_reference = &7; } let mut array = [-2, 3, 9]; { // Mutably borrows `array` for this scope. // `array` may only be used through `mutable_reference`. let mutable_reference = &mut array; } }
尽管 &&
是一个单一标记(惰性“与”运算符),但在借用表达式的上下文中,它可以作为两个借用。
#![allow(unused)] fn main() { // same meanings: let a = && 10; let a = & & 10; // same meanings: let a = &&&& mut 10; let a = && && mut 10; let a = & & & & mut 10; }
原始取地址运算符
与借用运算符相关的是*原始取地址运算符*,它们没有一流的语法,但通过宏 ptr::addr_of!(expr)
和 ptr::addr_of_mut!(expr)
公开。表达式 expr
在位置表达式上下文中求值。然后,ptr::addr_of!(expr)
创建一个指向给定位置的类型为 *const T
的常量原始指针,而 ptr::addr_of_mut!(expr)
创建一个指向给定位置的类型为 *mut T
的可变原始指针。
每当位置表达式可能求值为未正确对齐或未存储其类型确定的有效值的位置时,或者每当创建引用会引入不正确的别名假设时,都必须使用原始取地址运算符而不是借用运算符。在这些情况下,使用借用运算符会通过创建无效引用而导致未定义行为,但仍然可以使用取地址运算符构造原始指针。
以下示例演示如何通过 packed
结构创建指向未对齐位置的原始指针
#![allow(unused)] fn main() { use std::ptr; #[repr(packed)] struct Packed { f1: u8, f2: u16, } let packed = Packed { f1: 1, f2: 2 }; // `&packed.f2` would create an unaligned reference, and thus be Undefined Behavior! let raw_f2 = ptr::addr_of!(packed.f2); assert_eq!(unsafe { raw_f2.read_unaligned() }, 2); }
以下示例演示如何创建指向不包含有效值的位置的原始指针
#![allow(unused)] fn main() { use std::{ptr, mem::MaybeUninit}; struct Demo { field: bool, } let mut uninit = MaybeUninit::<Demo>::uninit(); // `&uninit.as_mut().field` would create a reference to an uninitialized `bool`, // and thus be Undefined Behavior! let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) }; unsafe { f1_ptr.write(true); } let init = unsafe { uninit.assume_init() }; }
解引用运算符
语法
解引用表达式 :
*
表达式
*
(解引用)运算符也是一个一元前缀运算符。当应用于指针时,它表示指向的位置。如果表达式的类型为 &mut T
或 *mut T
,并且是局部变量、局部变量的(嵌套)字段或可变位置表达式,则可以为生成的内存位置赋值。解引用原始指针需要 unsafe
。
对于非指针类型,*x
在不可变位置表达式上下文中等效于 *std::ops::Deref::deref(&x)
,在可变位置表达式上下文中等效于 *std::ops::DerefMut::deref_mut(&mut x)
。
#![allow(unused)] fn main() { let x = &7; assert_eq!(*x, 7); let y = &mut 9; *y = 11; assert_eq!(*y, 11); }
问号运算符
语法
错误传播表达式 :
表达式?
问号运算符 (?
) 解包有效值或返回错误值,并将它们传播到调用函数。它是一个一元后缀运算符,只能应用于 Result<T, E>
和 Option<T>
类型。
当应用于 Result<T, E>
类型的值时,它会传播错误。如果值为 Err(e)
,则它将从封闭函数或闭包返回 Err(From::from(e))
。如果应用于 Ok(x)
,则它将解包该值以求值为 x
。
#![allow(unused)] fn main() { use std::num::ParseIntError; fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // returns an Err() immediately Ok(x + y) // Doesn't run. } 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); }
?
不能被重载。
否定运算符
这是最后两个一元运算符。此表总结了它们对基本类型的行为,以及哪些特征用于重载这些运算符以用于其他类型。请记住,带符号整数始终使用二进制补码表示。所有这些运算符的操作数都在值表达式上下文中求值,因此会被移动或复制。
符号 | 整数 | 布尔值 | 浮点数 | 重载特征 |
---|---|---|---|---|
- | 否定* | 否定 | 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); }
算术和逻辑二元运算符
语法
算术或逻辑表达式 :
表达式+
表达式
| 表达式-
表达式
| 表达式*
表达式
| 表达式/
表达式
| 表达式%
表达式
| 表达式&
表达式
| 表达式|
表达式
| 表达式^
表达式
| 表达式<<
表达式
| 表达式>>
表达式
二元运算符表达式都使用中缀符号编写。此表总结了算术和逻辑二元运算符对基本类型的行为,以及哪些特征用于重载这些运算符以用于其他类型。请记住,带符号整数始终使用二进制补码表示。所有这些运算符的操作数都在值表达式上下文中求值,因此会被移动或复制。
符号 | 整数 | 布尔值 | 浮点数 | 重载特征 | 重载复合赋值特征 |
---|---|---|---|---|---|
+ | 加法 | 加法 | std::ops::Add | std::ops::AddAssign | |
- | 减法 | 减法 | std::ops::Sub | std::ops::SubAssign | |
* | 乘法 | 乘法 | std::ops::Mul | std::ops::MulAssign | |
/ | 除法*† | 除法 | std::ops::Div | std::ops::DivAssign | |
% | 取余**† | 取余 | std::ops::Rem | std::ops::RemAssign | |
& | 按位与 | 逻辑与 | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位或 | 逻辑或 | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位异或 | 逻辑异或 | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移*** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法向零舍入。
** Rust 使用截断除法定义的余数。给定 remainder = dividend % divisor
,余数将与被除数具有相同的符号。
*** 带符号整数类型的算术右移,无符号整数类型的逻辑右移。
† 对于整数类型,除以零会发生恐慌。
以下是一些使用这些运算符的示例。
#![allow(unused)] fn main() { assert_eq!(3 + 6, 9); assert_eq!(5.5 - 1.25, 4.25); assert_eq!(-5 * 14, -70); assert_eq!(14 / 3, 4); assert_eq!(100 % 7, 2); assert_eq!(0b1010 & 0b1100, 0b1000); assert_eq!(0b1010 | 0b1100, 0b1110); assert_eq!(0b1010 ^ 0b1100, 0b110); assert_eq!(13 << 3, 104); assert_eq!(-10 >> 2, -3); }
比较运算符
语法
比较表达式 :
表达式==
表达式
| 表达式!=
表达式
| 表达式>
表达式
| 表达式<
表达式
| 表达式>=
表达式
| 表达式<=
表达式
比较运算符也为基本类型和标准库中的许多类型定义。链接比较运算符时需要使用括号。例如,表达式 a == b == c
无效,可以写成 (a == b) == c
。
与算术和逻辑运算符不同,重载这些运算符的特征更普遍地用于显示如何比较类型,并且很可能被假设为由使用这些特征作为边界的函数定义实际比较。然后,标准库中的许多函数和宏可以使用该假设(尽管不能确保安全)。与上面的算术和逻辑运算符不同,这些运算符隐式地对其操作数进行共享借用,并在位置表达式上下文中对其进行求值
#![allow(unused)] fn main() { let a = 1; let b = 1; a == b; // is equivalent to ::std::cmp::PartialEq::eq(&a, &b); }
这意味着不必将操作数移出。
符号 | 含义 | 重载方法 |
---|---|---|
== | 等于 | std::cmp::PartialEq::eq |
!= | 不等于 | std::cmp::PartialEq::ne |
> | 大于 | std::cmp::PartialOrd::gt |
< | 小于 | std::cmp::PartialOrd::lt |
>= | 大于或等于 | std::cmp::PartialOrd::ge |
<= | 小于或等于 | std::cmp::PartialOrd::le |
以下是一些使用比较运算符的示例。
#![allow(unused)] fn main() { assert!(123 == 123); assert!(23 != -12); assert!(12.5 > 12.2); assert!([1, 2, 3] < [1, 3, 4]); assert!('A' <= 'B'); assert!("World" >= "Hello"); }
惰性布尔运算符
运算符 ||
和 &&
可以应用于布尔类型的操作数。||
运算符表示逻辑“或”,&&
运算符表示逻辑“与”。它们与 |
和 &
的不同之处在于,只有当左操作数尚未确定表达式的结果时,才会对右操作数求值。也就是说,||
仅当左操作数求值为 false
时才对其右操作数求值,而 &&
仅当其求值为 true
时才对其右操作数求值。
#![allow(unused)] fn main() { let x = false || true; // true let y = false && panic!(); // false, doesn't evaluate `panic!()` }
类型转换表达式
类型转换表达式用二元运算符 as
表示。
执行 as
表达式会将左侧的值转换为右侧的类型。
as
表达式的示例
#![allow(unused)] fn main() { fn sum(values: &[f64]) -> f64 { 0.0 } fn len(values: &[f64]) -> i32 { 0 } fn average(values: &[f64]) -> f64 { let sum: f64 = sum(values); let size: f64 = len(values) as f64; sum / size } }
as
可用于显式执行强制转换,以及以下附加转换。任何不符合强制转换规则或表中条目的转换都是编译器错误。这里 *T
表示 *const T
或 *mut T
。m
代表引用类型中的可选 mut
和指针类型中的 mut
或 const
。
e 的类型 | U | e as U 执行的转换 |
---|---|---|
整数或浮点数类型 | 整数或浮点数类型 | 数字转换 |
枚举 | 整数类型 | 枚举转换 |
bool 或 char | 整数类型 | 基本类型到整数的转换 |
u8 | char | u8 到 char 的转换 |
*T | *V 其中 V: Sized * | 指针到指针的转换 |
*T 其中 T: Sized | 整数类型 | 指针到地址的转换 |
整数类型 | *V 其中 V: Sized | 地址到指针的转换 |
&m₁ T | *m₂ T ** | 引用到指针的转换 |
&m₁ [T; n] | *m₂ T ** | 数组到指针的转换 |
函数项 | 函数指针 | 函数项到函数指针的转换 |
函数项 | *V 其中 V: Sized | 函数项到指针的转换 |
函数项 | 整数 | 函数项到地址的转换 |
函数指针 | *V 其中 V: Sized | 函数指针到指针的转换 |
函数指针 | 整数 | 函数指针到地址的转换 |
闭包 *** | 函数指针 | 闭包到函数指针的转换 |
* 或 T
和 V
是兼容的无大小类型,例如,都是切片,都是相同的特征对象。
** 仅当 m₁
是 mut
或 m₂
是 const
时。允许将 mut
引用转换为 const
指针。
*** 仅适用于不捕获(闭包)任何局部变量的闭包
语义
数字转换
- 在相同大小的两个整数之间进行转换(例如 i32 -> u32)是无操作的(Rust 对固定整数的负值使用 2 的补码)
- 从较大的整数转换为较小的整数(例如 u32 -> u8)将截断
- 从较小的整数转换为较大的整数(例如 u8 -> u32)将
- 如果源是无符号的,则进行零扩展
- 如果源是有符号的,则进行符号扩展
- 从浮点数转换为整数将向零舍入浮点数
NaN
将返回0
- 大于最大整数值的值(包括
INFINITY
)将饱和为整数类型的最大值。 - 小于最小整数值的值(包括
NEG_INFINITY
)将饱和为整数类型的最小值。
- 从整数转换为浮点数将产生最接近的浮点数 *
- 如有必要,舍入将根据
roundTiesToEven
模式 *** - 发生溢出时,将产生无穷大(与输入符号相同)
- 注意:对于当前的数字类型集,仅当值大于或等于
f32::MAX + (0.5 ULP)
时,才会在u128 as f32
上发生溢出
- 如有必要,舍入将根据
- 从 f32 转换为 f64 是完美且无损的
- 从 f64 转换为 f32 将产生最接近的 f32 **
- 如有必要,舍入将根据
roundTiesToEven
模式 *** - 发生溢出时,将产生无穷大(与输入符号相同)
- 如有必要,舍入将根据
* 如果硬件本身不支持使用此舍入模式和溢出行为的整数到浮点数转换,则这些转换可能会比预期慢。
** 如果硬件本身不支持使用此舍入模式和溢出行为的 f64 到 f32 转换,则这些转换可能会比预期慢。
*** 如 IEEE 754-2008 §4.3.1 中所定义:选择最接近的浮点数,如果恰好位于两个浮点数之间,则优先选择最低有效数字为偶数的浮点数。
枚举转换
将枚举转换为其判别式,然后在需要时使用数字转换。转换仅限于以下几种枚举
基本类型到整数的转换
false
转换为0
,true
转换为1
char
转换为代码点的值,然后在需要时使用数字转换。
u8
到 char
的转换
转换为具有相应代码点的 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
,其行为如下
-
如果
T
和U
都是有大小的,则指针保持不变。 -
如果
T
和U
都是无大小的,则指针也保持不变。特别是,元数据被精确地保留。例如,从
*const [T]
到*const [U]
的转换保留了元素的数量。请注意,因此,此类转换不一定会保留指针所指对象的大小(例如,将*const [u16]
转换为*const [u8]
将导致原始指针指向的对象大小是原始对象的一半)。这同样适用于str
和任何无大小尾部是切片类型的复合类型,例如struct Foo(i32, [u8])
或(u64, Foo)
。 -
如果
T
是无大小的,而U
是有大小的,则转换将丢弃完成宽指针T
的所有元数据,并生成一个由无大小指针的数据部分组成的瘦指针U
。
赋值表达式
赋值表达式将值移动到指定的位置。
赋值表达式由一个 可变的 被赋值表达式(被赋值操作数)组成,后跟一个等号 (=
) 和一个 值表达式(赋值操作数)。在其最基本的形式中,被赋值表达式是一个 位置表达式,我们首先讨论这种情况。下面将讨论更通用的解构赋值情况,但这种情况总是分解为对位置表达式的顺序赋值,这可以被认为是更基本的情况。
基本赋值
对赋值表达式求值首先是对其操作数求值。首先对赋值操作数求值,然后是被赋值表达式。对于解构赋值,被赋值表达式的子表达式从左到右求值。
注意:这与其他表达式不同,因为右操作数在左操作数之前求值。
然后,它会先 删除 赋值位置的值,除非该位置是未初始化的局部变量或局部变量的未初始化字段。接下来,它要么将赋值 复制或移动 到赋值位置。
赋值表达式始终生成 单元值。
示例
#![allow(unused)] fn main() { let mut x = 0; let y = 0; x = y; }
解构赋值
解构赋值与用于变量声明的解构模式匹配相对应,允许赋值给复杂值,例如元组或结构体。例如,我们可以交换两个可变变量
#![allow(unused)] fn main() { let (mut a, mut b) = (0, 1); // Swap `a` and `b` using destructuring assignment. (b, a) = (a, b); }
与使用 let
的解构声明不同,由于语法歧义,模式不能出现在赋值的左侧。相反,与模式相对应的一组表达式被指定为 被赋值表达式,并允许出现在赋值的左侧。然后,被赋值表达式被解构为模式匹配,然后是顺序赋值。解构后的模式必须是不可反驳的:特别是,这意味着只有在编译时已知长度的切片模式和平凡切片 [..]
才允许用于解构赋值。
解构方法很简单,最好通过示例来说明。
#![allow(unused)] fn main() { struct Struct { x: u32, y: u32 } let (mut a, mut b) = (0, 0); (a, b) = (3, 4); [a, b] = [3, 4]; Struct { x: a, y: b } = Struct { x: 3, y: 4}; // desugars to: { let (_a, _b) = (3, 4); a = _a; b = _b; } { let [_a, _b] = [3, 4]; a = _a; b = _b; } { let Struct { x: _a, y: _b } = Struct { x: 3, y: 4}; a = _a; b = _b; } }
不允许在单个被赋值表达式中多次使用标识符。
下划线表达式 和空 范围表达式 可用于忽略某些值,而不绑定它们。
请注意,默认绑定模式不适用于解构后的表达式。
复合赋值表达式
语法
复合赋值表达式 :
表达式+=
表达式
| 表达式-=
表达式
| 表达式*=
表达式
| 表达式/=
表达式
| 表达式%=
表达式
| 表达式&=
表达式
| 表达式|=
表达式
| 表达式^=
表达式
| 表达式<<=
表达式
| 表达式>>=
表达式
复合赋值表达式将算术和逻辑二元运算符与赋值表达式组合在一起。
例如
#![allow(unused)] fn main() { let mut x = 5; x += 1; assert!(x == 6); }
复合赋值的语法是一个 可变的 位置表达式(被赋值操作数),然后是其中一个运算符,后跟一个 =
作为单个标记(没有空格),然后是一个 值表达式(修改操作数)。
与其他位置操作数不同,被赋值位置操作数必须是位置表达式。尝试使用值表达式会导致编译器错误,而不是将其提升为临时值。
复合赋值表达式的求值取决于运算符的类型。
如果两种类型都是基元类型,则将首先对修改操作数求值,然后是被赋值操作数。然后,它会将被赋值操作数位置的值设置为使用被赋值操作数和修改操作数的值执行运算符操作的值。
注意:这与其他表达式不同,因为右操作数在左操作数之前求值。
否则,此表达式是调用运算符的重载复合赋值特征的函数的语法糖(请参阅本章前面的表格)。将自动获取被赋值操作数的可变借用。
例如,example
中的以下表达式语句是等效的
#![allow(unused)] fn main() { struct Addable; use std::ops::AddAssign; impl AddAssign<Addable> for Addable { /* */ fn add_assign(&mut self, other: Addable) {} } fn example() { let (mut a1, a2) = (Addable, Addable); a1 += a2; let (mut a1, a2) = (Addable, Addable); AddAssign::add_assign(&mut a1, a2); } }
与赋值表达式一样,复合赋值表达式始终生成 单元值。
警告:操作数的求值顺序根据操作数的类型而交换:对于基元类型,将首先对右侧求值,而对于非基元类型,将首先对左侧求值。尽量不要编写依赖于复合赋值表达式中操作数求值顺序的代码。有关使用此依赖项的示例,请参阅 此测试。