运算符表达式
语法
运算符表达式 :
借用表达式
| 解引用表达式
| 错误传导表达式
| 取反表达式
| 算术或逻辑表达式
| 比较表达式
| 惰性布尔表达式
| 类型转换表达式
| 赋值表达式
| 复合赋值表达式
在 Rust 语言中,内置类型的运算符已经被定义好了。
许多下面提到的运算符也可以通过 std::ops
或 std::cmp
中的 trait 进行重载。
溢出
调试模式下编译时,整数运算溢出会触发 panic。
编译器可以使用 -C debug-assertions
和 -C overflow-checks
标志控制溢出行为。
下列情况时将触发溢出:
- 当
+
、*
或二元-
创建的值大于可存储的最大值或小于最小值。 - 对任何有符号整数类型的最小值应用一元
-
,除非操作数是 字面值表达式 (或在一个或多个 分组表达式 中单独使用的字面值表达式) 。 /
或%
其左操作数是有符号整数类型的最小整数且右操作数是-1
时。 出于兼容性原因,即使禁用了-C overflow-checks
,此检查也会发生。<<
或>>
其右操作数大于或等于左操作数类型的位数或为负时。
注意: 对于一元
-
后面的字面值表达式,这意味着类似-128_i8
或let j: i8 = -(128)
的形式永远不会导致 panic,并且其期望值为 -128 。在这些情况下,字面值表达式已经具有其类型的最小值 (例如,
128_i8
的值为 -128) ,因为整数字面值按照 整数字面值表达式 中的描述被截断为其类型。这些最小值的否定不会改变值,因为采用了二进制补码的溢出约定。
在
rustc
中,这些最小值表达式也被overflowing_literals
代码分析检查忽略。
借用运算符
&
(共享借用) 和 &mut
(可变借用) 运算符是一元前缀运算符。
当应用于一个 占位表达式 时,该表达式产生一个指向该值引用的位置的引用 (指针) 。
在引用的持续时间内,该内存位置也被置于借用状态。对于共享借用 ( &
) ,这意味着该位置可能不能被修改,但可以被读取或再次共享。
对于可变借用 (&mut
) ,在借用过期之前,该位置可能不能以任何方式访问。
&mut
在可变位置表达式上下文中评估其操作数。如果 &
或 &mut
运算符应用于一个 值表达式 ,则将创建一个 临时值。
这些运算符不能被重载。
#![allow(unused)] fn main() { { // 创建一个值为 7 的临时值,它只在当前作用域有效。 let shared_reference = &7; } let mut array = [-2, 3, 9]; { // 对 `array` 进行可变借用,这个作用域内 `array` 只能通过 `mutable_reference` 使用。 let mutable_reference = &mut array; } }
即使 &&
是单个标记 (惰性 'and' 运算符) ,但在借用表达式的上下文中使,其的作用相当于两个借用:
#![allow(unused)] fn main() { // 同样的含义: let a = && 10; let a = & & 10; // 同样的含义: let a = &&&& mut 10; let a = && && mut 10; let a = & & & & mut 10; }
原始地址运算符
与借用运算符相关的是 原始地址运算符 ,它们没有一级语法,是以宏 ptr::addr_of!(expr)
和 ptr::addr_of_mut!(expr)
的方式公开。
表达式 expr
在占位表达式上下文中进行评估。
ptr::addr_of!(expr)
而后创建类型为 *const T
的常量原始指针指向给定的地址,而 ptr::addr_of_mut!(expr)
则创建类型为 *mut T
的可变原始指针。
每当占位表达式可以评估为未正确对齐或不存储有效值 (根据其类型确定) ,或者每当创建引用会引入不正确的别名假设时,必须使用原始地址运算而不是借用运算符。 在这些情况下,使用借用运算符将创建无效引用导致 未定义行为 ,但仍可以使用地址运算符构造原始指针。
以下是通过 packed
结构体创建指向未对齐地址的原始指针的示例:
#![allow(unused)] fn main() { use std::ptr; // 定义一个 packed 结构体 #[repr(packed)] struct Packed { f1: u8, f2: u16, } // 创建一个 packed 实例 let packed = Packed { f1: 1, f2: 2 }; // `&packed.f2` 会创建一个未对齐的引用,因此是未定义行为! // 使用 `ptr::addr_of!` 创建一个指向 `packed.f2` 的不可变原始指针 let raw_f2 = ptr::addr_of!(packed.f2); // 读取 `raw_f2` 指向的值,并断言其值等于2 assert_eq!(unsafe { raw_f2.read_unaligned() }, 2); }
以下是创建指向不包含有效值的地址的原始指针的示例:
#![allow(unused)] fn main() { use std::{ptr, mem::MaybeUninit}; struct Demo { field: bool, } let mut uninit = MaybeUninit::<Demo>::uninit(); // &uninit.as_mut().field 将创建对未初始化的 `bool` 的引用,因此为未定义行为! let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) }; // 将 'true' 写入先前未初始化的字段。 unsafe { f1_ptr.write(true); } // 因为 'uninit' 已在上面初始化,所以是安全的。 let init = unsafe { uninit.assume_init() }; }
解引用操作符
语法
解引用表达式 :
*
表达式
解引用运算符 *
也是一元前缀运算符。
当应用于 指针 时,表示指向的地址。
如果表达式的类型为 &mut T
或 *mut T
,并且是一个局部变量、局部变量的 (嵌套) 字段或可变的 占位表达式 ,则可以将结果的内存位置分配给其它变量。
对一个裸指针进行解引用需要使用 unsafe
。
对于非指针类型, *x
在不可变的 占位表达式上下文 中等同于 *std::ops::Deref::deref(&x)
,在可变的占位表达式上下文中等同于 *std::ops::DerefMut::deref_mut(&mut x)
。
#![allow(unused)] fn main() { let x = &7; assert_eq!(*x, 7); let y = &mut 9; *y = 11; assert_eq!(*y, 11); }
问号操作符
语法
错误传导表达式 :
表达式?
问号运算符 (?
) 可以展开有效的值或返回错误值,并将它们传播到调用函数中。
它是一元后缀运算符,只能应用于 Result<T, E>
和 Option<T>
类型。
当应用于 Result<T, E>
类型的值时,能够传播错误。
如果该值是 Err(e)
,从包含的函数或闭包返回 Err(From::from(e))
。应用于 Ok(x)
时,将展开该值求值为 x
。
#![allow(unused)] fn main() { use std::num::ParseIntError; fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // 立即返回一个 Err() Ok(x + y) // 不会被执行 } let res = try_to_parse(); println!("{:?}", res); assert!(res.is_err()) }
当应用于类型为 Option<T>
的值时,会传播 None
。如果值为 None
,则返回 None
。如果应用于 Some(x)
,将展开该值以求值为 x
。
#![allow(unused)] fn main() { fn try_option_some() -> Option<u8> { let val = Some(1)?; Some(val) } assert_eq!(try_option_some(), Some(1)); fn try_option_none() -> Option<u8> { let val = None?; Some(val) } assert_eq!(try_option_none(), None); }
?
不能被重载。
取反运算符
这是最后两个一元操作符。 这张表概括了它们在原始类型上的行为,以及用于重载其他类型的这些操作符的 trait 。 请记住,有符号整数总是使用二进制补码表示。所有这些操作符的操作数都在 值表达式上下文 中计算,因此它们会被移动或复制。
符号 | 整数 | 布尔 | 浮点数 | 重载 Trait |
---|---|---|---|---|
- | 取反* | 取反 | std::ops::Neg | |
! | 按位取反 | 逻辑取反 | std::ops::Not |
* 仅适用于有符号整数类型。
下面是这些运算符的一些示例
#![allow(unused)] fn main() { let x = 6; assert_eq!(-x, -6); assert_eq!(!x, -7); assert_eq!(true, !false); }
算术和逻辑二元操作符
语法
算术或逻辑表达式 :
表达式+
表达式
| 表达式-
表达式
| 表达式*
表达式
| 表达式/
表达式
| 表达式%
表达式
| 表达式&
表达式
| 表达式|
表达式
| 表达式^
表达式
| 表达式<<
表达式
| 表达式>>
表达式
二元运算符表达式均以中缀表示。此表总结了原始类型上算术和逻辑二元运算符的行为,以及用于重载其他类型的这些运算符的 trait 。 请记住,有符号整数始终使用二进制补码表示。所有这些运算符的操作数都在 值表达式上下文 中计算,因此会被移动或复制。
符号 | 整数 | 布尔 | 浮点数 | 重载 Trait | 重载复合赋值 Trait |
---|---|---|---|---|---|
+ | 加法 | 加法 | std::ops::Add | std::ops::AddAssign | |
- | 减法 | 减法 | std::ops::Sub | std::ops::SubAssign | |
* | 乘法 | 乘法 | std::ops::Mul | std::ops::MulAssign | |
/ | 除法* | 除法 | std::ops::Div | std::ops::DivAssign | |
% | 取余** | 取余 | std::ops::Rem | std::ops::RemAssign | |
& | 按位与 | 逻辑与 | std::ops::BitAnd | std::ops::BitAndAssign | |
| | 按位或 | 逻辑或 | std::ops::BitOr | std::ops::BitOrAssign | |
^ | 按位异或 | 逻辑异或 | std::ops::BitXor | std::ops::BitXorAssign | |
<< | 左移 | std::ops::Shl | std::ops::ShlAssign | ||
>> | 右移*** | std::ops::Shr | std::ops::ShrAssign |
* 整数除法向零取整。
** Rust 使用的余数采用 截断除法 定义。
给定 remainder = dividend % divisor
,余数将与被除数具有相同的符号。
*** 在有符号整数类型上进行算术右移,无符号整数类型上进行逻辑右移。
以下是使用这些运算符的示例:
#![allow(unused)] fn main() { assert_eq!(3 + 6, 9); assert_eq!(5.5 - 1.25, 4.25); assert_eq!(-5 * 14, -70); assert_eq!(14 / 3, 4); assert_eq!(100 % 7, 2); assert_eq!(0b1010 & 0b1100, 0b1000); assert_eq!(0b1010 | 0b1100, 0b1110); assert_eq!(0b1010 ^ 0b1100, 0b110); assert_eq!(13 << 3, 104); assert_eq!(-10 >> 2, -3); }
比较运算符
语法
比较表达式 :
表达式==
表达式
| 表达式!=
表达式
| 表达式>
表达式
| 表达式<
表达式
| 表达式>=
表达式
| 表达式<=
表达式
在基本类型和标准库的许多类型上都定义了比较运算符。当比较运算符链接时,需要使用括号。
例如,表达式 a == b == c
是无效的,可以写成 (a == b) == c
。
与算术和逻辑运算符不同,用于重载这些运算符的 trait 通常表明类型如何进行比较,并且可能假设使用这些 trait 作为约束的函数定义实际的比较。 标准库中的许多函数和宏可以利用这个假设 (虽然不能确保安全性) 。 与上面的算术和逻辑运算符不同,这些运算符隐式地获取其操作数的共享借用,以 占位表达式上下文 中的方式进行计算:
#![allow(unused)] fn main() { let a = 1; let b = 1; a == b; // 等价于 ::std::cmp::PartialEq::eq(&a, &b); }
这意味着操作数不必移动出去。
符号 | 意义 | 重载方法 |
---|---|---|
== | 等于 | std::cmp::PartialEq::eq |
!= | 不等于 | std::cmp::PartialEq::ne |
> | 大于 | std::cmp::PartialOrd::gt |
< | 小于 | std::cmp::PartialOrd::lt |
>= | 大于等于 | std::cmp::PartialOrd::ge |
<= | 小于等于 | std::cmp::PartialOrd::le |
这里是使用比较运算符的示例:
#![allow(unused)] fn main() { assert!(123 == 123); assert!(23 != -12); assert!(12.5 > 12.2); assert!([1, 2, 3] < [1, 3, 4]); assert!('A' <= 'B'); assert!("World" >= "Hello"); }
惰性布尔运算符
运算符 ||
和 &&
可以用于布尔类型的操作数。 ||
运算符表示逻辑或, &&
运算符表示逻辑与。
它们与 |
和 &
的区别在于,只有当左操作数不能确定表达式结果时才会对右操作数进行求值。
也就是说,只有当左操作数求值结果为 false
时 ||
才会对右操作数进行求值,只有当左操作数求值结果为 true
时 &&
才会对右操作数进行求值。
#![allow(unused)] fn main() { let x = false || true; // true let y = false && panic!(); // false 不会对右侧的操作数进行求值,因此 `panic!()` 表达式不会被执行。 }
类型转换表达式
类型转换表达式使用二元运算符 as
表示。
执行一个 as
表达式会将左侧的值转换为右侧的类型。
以下是一个 as
表达式的例子:
#![allow(unused)] fn main() { fn sum(values: &[f64]) -> f64 { 0.0 } fn len(values: &[f64]) -> i32 { 0 } fn average(values: &[f64]) -> f64 { let sum: f64 = sum(values); let size: f64 = len(values) as f64; sum / size } }
as
可以用于显式执行 强转 ,以及以下附加转换。
任何不符合强转规则或表中条目的转换都是编译器错误。
这里的 *T
表示 *const T
或 *mut T
。
在引用类型中, m
表示可选的 mut
,在指针类型中表示 mut
或 const
。
e 的类型 | U | e as U 执行的类型转换 |
---|---|---|
整型或浮点型 | 整型或浮点型 | 数值类型转换 |
枚举类型 | 整型类型 | 枚举类型转换 |
bool 或 char 类型 | 整型类型 | 基本类型到整型类型的转换 |
u8 类型 | char 类型 | u8 类型到 char 类型的转换 |
*T 类型 | *V where V: Sized * | 指针类型到指针类型的转换 |
*T where T: Sized | 整型类型 | 指针类型到地址类型的转换 |
整型类型 | *V where V: Sized | 地址类型到指针类型的转换 |
&m₁ T 类型 | *m₂ T ** | 引用类型到指针类型的转换 |
&m₁ [T; n] 类型 | *m₂ T ** | 数组类型到指针类型的转换 |
函数条目 类型 | 函数指针 类型 | 函数条目到函数指针类型的转换 |
函数条目 类型 | *V where V: Sized | 函数条目到指针类型的转换 |
函数条目 类型 | 整型类型 | 函数条目到地址类型的转换 |
函数指针 类型 | *V where V: Sized | 函数指针到指针类型的转换 |
函数指针 类型 | 整型类型 | 函数指针到地址类型的转换 |
闭包类型 *** | 函数指针类型 | 闭包类型到函数指针类型的转换 |
* 当 T
和 V
都是不定大小类型但类型相同时,例如都是切片类型,它们之间是兼容的。
** 当且仅当 m₁
为 mut
或 m₂
为 const
时。允许将 mut
引用转换为 const
指针。
*** 仅适用于没有捕获 (关闭) 任何局部变量的闭包。
语义
数值转换
- 两个大小相同的整数之间的转换 (例如 i32 -> u32) 是无操作的 (Rust 对于固定整数的负值使用 2 的补码)
- 从较大的整数转换为较小的整数 (例如 u32 -> u8) 会截断
- 从较小的整数转换为较大的整数 (例如 u8 -> u32) 将:
- 如果源是无符号的,则用零填充
- 如果源是有符号的,则使用符号扩展
- 将浮点数转换为整数会将浮点数向零舍入
NaN
将返回0
- 大于最大整数值的值,包括
INFINITY
,将饱和到整数类型的最大值。 - 小于最小整数值的值,包括
NEG_INFINITY
,将饱和到整数类型的最小值。
- 将整数转换为浮点数将产生可能最接近的浮点数 *
- 如有必要,舍入按照
roundTiesToEven
模式进行 *** - 溢出时,将生成无限大 (与输入相同的符号)
- 注意:对于当前的数字类型集,只有在
u128 as f32
的值大于或等于f32::MAX + (0.5 ULP)
时才会发生溢出
- 如有必要,舍入按照
- 从 f32 转换为 f64 是完美的和无损的
- 从 f64 转换为 f32 将产生可能最接近的 f32 **
- 如有必要,舍入按照
roundTiesToEven
模式进行 *** - 溢出时,将生成无限大 (与输入相同的符号)
- 如有必要,舍入按照
* 如果硬件不支持此舍入模式和溢出行为的整数到浮点数的转换,则此类转换可能比预期的慢。
** 如果硬件不支持此舍入模式和溢出行为的 f64 到 f32 的转换,则此类转换可能比预期的慢。
*** 正如 IEEE 754-2008 §4.3.1: 中所定义的那样:选择最接近的浮点数,如果恰好介于两个浮点数之间,则优先选择带有偶数最低位数字的数字。
枚举转换
将枚举转换为其判别值,然后根据需要使用数值转换。转换仅限于以下种类的枚举:
原始类型到整数的转换
false
转换为0
,true
转换为1
char
转换为其码点值,然后如果需要则使用数字转换。
u8
到 char
的类型转换
将 u8
类型的值转换为相应的 Unicode 编码对应的字符类型 char
。
指针到地址转换
将裸指针转换为整数类型会产生指向内存的机器地址。
如果整数类型比指针类型小,地址可能会被截断;使用 usize
可以避免这种情况。
地址到指针的强制类型转换
将整数强制类型转换为原始指针会将该整数解释为内存地址,并产生一个指向该内存的指针。
警告: 这会与 Rust 内存模型产生交互,而该模型仍在开发中。 即使一个通过此类型转换获得的指针在比特位上等同于一个有效指针,它也可能受到额外的限制。 如果未遵循别名规则,则解引用此类指针可能会导致 未定义行为 。
一些简单正确的地址解算的示例:
#![allow(unused)] fn main() { let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); let first_address = p1 as usize; let second_address = first_address + 4; // 4 == size_of::<i32>() let p2 = second_address as *mut i32; unsafe { *p2 += 1; } assert_eq!(values[1], 3); }
赋值表达式
一个 赋值表达式 将一个值移动到一个指定的位置。
一个赋值表达式由一个 可变的 ,即 赋值运算符 ,后面跟一个等于号 (=
) 和一个 值表达式 ,即 被赋的值操作数 组成。
在最基本的形式中,一个赋值运算符是一个 占位表达式 ,我们首先讨论这种情况。
下面讨论更一般的解构赋值情况,但是这种情况总是分解为对占位表达式的顺序赋值,这可能被当成更为基本的情况。
基本赋值语句
对赋值表达式求值,需要先对操作数进行求值。先对被赋值操作数进行求值,然后是赋值操作数表达式。对于解构赋值,可赋值表达式的子表达式从左到右进行求值。
注意:与其他表达式不同的是,右操作数在左操作数之前进行求值。该表达式首先有一个效果:删除被赋值位置的值,除非被赋值的地方是未初始化的局部变量或局部变量的未初始化字段。然后将赋值的值复制或移动到被赋值的地方。
赋值表达式总是生成 单元值 。
Example:
#![allow(unused)] fn main() { let mut x = 0; let y = 0; x = y; }
解构赋值
解构赋值是变量声明解构模式匹配的一种对应物,允许对复杂的值进行赋值,例如元组或结构体。 例如,我们可以交换两个可变变量:
#![allow(unused)] fn main() { let (mut a, mut b) = (0, 1); // 使用解构赋值交换 `a` 和 `b`。 (b, a) = (a, b); }
与使用 let
进行解构声明不同,由于语法歧义,模式不能出现在赋值语句的左侧。
相反,一组对应模式的表达式被指定为 可赋值表达式 ,并允许出现在赋值语句的左侧。
然后,将赋值表达式展开为模式匹配,然后进行顺序赋值。
展开后的模式必须是不可拒绝的:特别地,这意味着仅允许具有在编译时已知长度的切片模式以及简单的切片 [..]
用于解构赋值。
展开的方法很简单,最好通过示例来说明。
#![allow(unused)] fn main() { struct Struct { x: u32, y: u32 } let (mut a, mut b) = (0, 0); (a, b) = (3, 4); [a, b] = [3, 4]; Struct { x: a, y: b } = Struct { x: 3, y: 4}; // desugars to: { let (_a, _b) = (3, 4); a = _a; b = _b; } { let [_a, _b] = [3, 4]; a = _a; b = _b; } { let Struct { x: _a, y: _b } = Struct { x: 3, y: 4}; a = _a; b = _b; } }
在单个可赋值表达式中,标识符不禁止被多次使用。
下划线表达式 和空 区间表达式 可用于忽略某些值,而不将它们绑定到变量上。
注意,对于展开后的表达式,不适用默认绑定模式。
复合赋值表达式
语法
复合赋值表达式 :
表达式+=
表达式
| 表达式-=
表达式
| 表达式*=
表达式
| 表达式/=
表达式
| 表达式%=
表达式
| 表达式&=
表达式
| 表达式|=
表达式
| 表达式^=
表达式
| 表达式<<=
表达式
| 表达式>>=
表达式
复合赋值表达式 将算术和逻辑二元运算符与赋值表达式结合在一起。
例如:
#![allow(unused)] fn main() { let mut x = 5; x += 1; assert!(x == 6); }
复合赋值语法是一个 可变的 占位表达式,紧接着是 被赋值的操作数 ,然后是其中一个运算符,后面跟着一个 =
符号 (无空格) ,最后是一个 值表达式 ,即 修改操作数 。
与其他占位操作数不同,被赋值的占位操作数必须是一个占位表达式。尝试使用值表达式会导致编译错误,而不是将其升级为临时值。
复合赋值表达式的求值取决于操作符的类型。
如果两个类型都是原始类型,则将先计算修改操作数,然后是被赋值的操作数。 然后它将使用执行该运算符操作的被赋值操作数和修改操作数的值来设置被赋值操作数的占位的值。
注意: 这与其他表达式不同,因为右操作数在左操作数之前被评估。
否则,这个表达式是语法糖,用于调用运算符的重载复合赋值特性的函数 (参见本章前面的表格) 。 被赋值的操作数会自动被可变地借用。
例如,在 example
中,以下表达式语句是等价的:
#![allow(unused)] fn main() { struct Addable; use std::ops::AddAssign; impl AddAssign<Addable> for Addable { /* */ fn add_assign(&mut self, other: Addable) {} } fn example() { let (mut a1, a2) = (Addable, Addable); a1 += a2; let (mut a1, a2) = (Addable, Addable); AddAssign::add_assign(&mut a1, a2); } }
与赋值表达式一样,复合赋值表达式始终生成 单元值 。
警告:操作数的求值顺序根据操作数的类型而交换:对于原始类型,右操作数将首先得到评估,而对于非原始类型,左操作数将首先得到评估。 尽量不要编写依赖于复合赋值表达式中操作数的求值顺序的代码。参见 这个测试 ,了解使用此依赖性的示例。