【Rust】表达式
Rust
被称作表达式语言,在 C
中,if
和 switch
是语句,它们不会产生值,也不能在表达式中间使用。在 Rust
中,if
和 match
可以产生值。例如:
1 | let status = if cpu.temperature <= MAX_TEMP { |
这解释了为什么 Rust
没有 C
的三元运算符 (expr1: Expr2: expr3)
,在 C
中,它类似 if
语句,而在 Rust
中,if
完全可以代替。另外大多数控制流在 C
中是语句,而在 Rust
中是表达式(语句都会以 ;
结束,而表达式没有)。
下面是 Rust
中所有支持的表达式列表:
下面的运算符都是左联运算符。例如:a - b - c
被分组成 (a-b)-c
,而不是 a - (b - c)
:
* / % + - << >> & ^ | && || as
比较运算符,赋值运算符以及范围运算符 ..
不能被链在一起使用。
代码块和分号
块,也就是一个大括号,是最通用的表达方式,它能产生一个值,可以在任何需要值的地方使用:
1 | fn main() { |
Some(author) =>
后面是简单的表达式 author.name()
,而 None
后面是一个块表达式,它的值是其中的最后一个表达式 ip.to_string()
的值,并且要注意它的后面没有分号。
确实大多数 Rust
代码行都是以 ;
分号结束的,如果一个代码块以 ;
结束,那么它的值是 ()
。在 javascript
中,允许省略 ;
,但是语言会自动填充。在 Rust
中有没有分号是有不同的意义的:
1 | fn main() { |
代码块内可以做一些声明,并且在最后返回一个值,能够使代码看起来比较整洁,用多了会觉得很爽。缺点是当忘记加分号时,可能会引发错误。但一般情况下是编译器都会提示我们。
声明
let
语句的形式如下,其中的 type
和 expr
是可以省略的:
let name: type = expr;
let
语句可以只声明一个变量而不用初始化,可以在后面的代码中用赋值语句初始化它。这有时候很有用,我们可以先声明一个变量,然后在下面的控制流代中初始化它:
1 | let name; |
这里局部变量有两种不同的方式初始化,但无论哪种方式,name
仅被初始化一次,所以无需声明为 mut
类型,在没有初始化之前使用变量是不允许的。
Rust
代码中允许重新二次定义同名变量,它会在这个二次定义的变量存在期间,将之前的变量屏蔽。在这里,line
开始的类型是 Result<String, io::Error>
,后面又是 String
,这在代码中是非常常见的,具有同一个语义的变量具有不同的类型。
1 | for line in file.lines() { |
我们甚至可以在代码块中声明一个 fn
或者结构体,但是它们的作用域仅限于这个代码块。当我们在代码块中定义函数时,它是不能访问代码块中的局部变量的。例如,下面的 cmp_by_timestamp_then_name
不能访问变量 v
:
1 | use std::io; |
if
和 match
if
表达式比较简单,形式如下:
1 | if condition1 { |
每个 condition
必须是一个 bool
类型的表达式,Rust
不会对数字或者指针进行隐式转换。condition
两边的括号不是必须的,如果添加了,rustc
会给一个告警。
match
语句很像 C
语言中的 switch
,但是更加灵活,下面是一个简单的例子。这很像 switch
语句根据 code
的值具体执行某个分支的表达式,通配符 _
就像 switch
中的 default
,能匹配任何东西,只是它必须放在最后面。将 _
放在之前,意味着它的优先级更高,在它的之后匹配都不可达。
1 | match code { |
match
表达式经常用于去区分 Option
的两种类型:Some(v)
和 None
:
1 | match params.get("name") { |
match
的通用形式如下:
1 | match value { |
如果 expr
是一个代码块,那么逗号 ,
是可以省略的的。Rust
从头开始检查 value
和哪个 pattern
匹配,一旦匹配,表达式 expr
就会被执行,后面的 pattern
就不会被检查了,所以如果我们将通配符 _
放在最前面,那么在它后面的 pattern
都不会被检查了。rust
中,match
表达式必须包含所有可能的情况,例如下面的代码会编译失败:
1 |
|
编译器提示我们有未覆盖的情况,建议我们使用通配符:
error[E0004]: non-exhaustive patterns: `i32::MIN..=-1_i32` and `3_i32..=i32::MAX` not covered
--> src/main.rs:4:11
|
4 | match code {
| ^^^^ patterns `i32::MIN..=-1_i32` and `3_i32..=i32::MAX` not covered
|
= note: the matched value is of type `i32`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms
|
7 ~ 2 => println!("User Asleep"),
8 ~ i32::MIN..=-1_i32 | 3_i32..=i32::MAX => todo!(),
所有的 if
分支返回的值类型必须是相同的:
1 | let suggested_pet = |
同理,match
表达式也是,所有的分支必须返回相同类型的值:
1 | let suggested_pet = |
更多关于 match
的用法可以看 【Rust】实战突破 或者 模式匹配。
if let
这里还有一个 if
的形式,if let
表达式:
1 | if let pattern = expr { |
如果给定的表达式 expr
匹配 pattern
,那么 block1
将会运行;如果不匹配,block2
就会运行。这是一个从 Option
或者 Result
获取数据比较好的方式:
1 | if let Some(cookie) = request.session_cookie { |
if let
可以做的事情 match
都可以做,所以说 if let
只是 match
的一种简写方式:
1 | match expr { |
循环
这里有四种循环表达式:
1 | while condition { |
Rust
中的循环语句都是表达式,但是 while
和 for
的值永远是 ()
,所以它们不是很有用,loop
倒是可以返回一个值,当然只有在你声明的时候。
while
循环和 C
语言很像,但是 Rust
中的 condition
必须是精确的 bool
类型。
while let
类似于 if let
。在每次循环迭代开始的时候,expr
的值如果匹配 pattern
,那么 block
就会运行,负责循环就会退出。
loop
经常用于去写无限循环,它会一直重复执行 block
,直到遇到 return
,break
或者 panic
。
for
循环会计算 iterable
表达式获得一个值,然后运行 block
依次。这里有许多可以迭代的类型,包括标准集合中所有类型,例如: vec
和 HashMap
。
标准的 C
循环:
1 | for(int i = 0;i < 20; i++) { |
在 rust
中写作如下的形式:
1 | for i in 0..20 { |
..
运算符可以生成一个 range
,它是一个具有两个字段(start
和 end
)的简单结构体。0..20
很像标准库中的 std::ops::Range { start: 0, end: 20 }
。Range
可以被用于 for
循环,是因为它实现了 std::iter::IntoIterator
。
有一点需要记住的是 for
循环会 move
值得所有权并且它包含的元素,所以下面这段代码编译失败:
1 | fn main() { |
编译器提示我们,由于隐式调用 .into_iter()
方法,strings
包含的值的所有权已经被转移,他已经处于未初始化状态:
error[E0382]: borrow of moved value: `strings`
--> src/main.rs:7:29
|
2 | let strings = vec!["hello", "world"];
| ------- move occurs because `strings` has type `Vec<&str>`, which does not implement the `Copy` trait
3 | for s in strings {
| ------- `strings` moved due to this implicit call to `.into_iter()`
...
7 | println!("{} error(s)", strings.len()); // error: use of moved value
| ^^^^^^^^^^^^^ value borrowed here after move
|
这看起来很不方便,改进的方式是使用引用迭代集合,例如:
1 | for rs in &strings { |
如果我们在迭代过程中需要对它进行更改,可以获取 strings
的 muteable reference
:
1 | fn main() { |
运行成功:
/Users/fudenglong/.cargo/bin/cargo run --color=always --package mandelbrot --bin mandelbrot
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/mandelbrot`
hello
world
2 error(s)
Process finished with exit code 0
break
和 continue
可以使用 break
退出 loop
循环,在 loop
的循环体中,可以给 break
一个表达式,它的值变成 loop
的值,loop
中所有 break
的表达式都必须要有相同的类型:
1 | // Each call to `next_line` returns either `Some(line)`, where |
continue
表达式用于跳到下次迭代:
1 | // Read some data, one line at a time. |
对于嵌套的循环,我们如何直接从内部退出。在 Rust
中,我们可以给循环一个 label
,用于在 break
时退出到哪层循环。例如:
1 | 'search: |
当然,break
语句也可以将表达式和 label
一起使用:
1 | // Find the square root of the first perfect square // in the series. |
label
也可以配合 continue
使用。
return
return
语句用于退出当前的函数,返回值给调用者,特殊情况,return;
其实就是 return ();
的简写。 函数一般可能没有显示的 return
语句,函数体很像一个 block
,如果最后一个表达式没有以 ;
结尾,那么它就是函数的返回值,一般情况下,这是 Rust
函数中用于返回值得首选方式。
但这并不意味着 return
是没用的,就像 break
一样,return
可以提前结束函数的运行。例如,下面的示例,当函数调用返回错误时,我们可以提前返回:
1 | let output = match File::create(filename) { |
never
类型 !
!
表示 never
类型。在 Rust
中,有些函数,可能包含死循环,panic!()
或者类似 std::process::exit()
,这些函数都无法正常完成,它们的返回值难以确定是什么类型,例如,标准库中的 std::process::exit()
,它的源码是这样的:
1 | pub fn exit(code: i32) -> ! { |
在 Rust
中,这些函数没有正常类型,未正常完成的表达式被分配到特殊类型 !
,并且它们不受类型必须匹配的规则的约束。例如我们编写下面这样的函数:
1 | fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! { |
函数和方法调用
函数调用和方法调用同其他的语言比较类似:
1 | let x = gcd(1302, 462); // function call |
Rust
在引用和值之间有明显的区分,所以在传递参数时精确的类型,如果函数需要 i32
类型,你传入的是 &i32
类型就会报错。但是 .
运算符放宽了这些规则,在 player.location()
的方法调用中,player
可能是 Player
,&Player
,Box<Player>
或者 Rc<Player>
。.location()
方法可以通过值或引用来获取 player
,因为 Rust
的 .
运算符能够自动解引用或根据需要创建引用。
另外一种语法是和类型关联的函数,例如 Vec::new()
,类似于面向对象语言中的静态方法
1 | let mut numbers = Vec::new(); // type-associated function call |
方法调用可以串联起来:
1 | server |
Rust
语法的一个怪癖是,在函数调用或方法调用中,泛型类型的常用语法 Vec<T>
不起作用:
1 | return Vec<i32>::with_capacity(1000); // error: something about chained comparisons |
问题是表达式中的 <
被当做小于运算符,正确的语法是:
1 | return Vec::<i32>::with_capacity(1000); // ok, using ::< |
Rust
社区将 ::<...>
叫做 turbofish
,但是我们也可以省略它们,改由 Rust
进行推断:
1 | return Vec::with_capacity(10); // ok, if the fn return type is Vec<i32> |
字段和索引
结构体字段的访问和其他语言比较类似,tuple
采用相同的语法,只是它只能使用数字作为索引。如果 .
左边是个引用或者智能指针,会自动进行解引用:
1 | game.black_pawns // struct field |
[]
用于访问数组,slice
或者 vector
的元素:
1 | pieces[i] |
这些变量可以被当做左值表达式,如果它们被声明为 muteable
,例如:
1 | game.black_pawns = 0x00ff0000_00000000_u64; |
可以使用 ..
运算符从一个数组,slice
或者 vector
获取一个 slice
,例如:
1 | let second_half = &game_moves[midpoint .. end]; |
..
运算符可以省略一些操作数,总共有下面这些操作类型,区间是左闭右开类型的,例如:0 .. 3
是 0, 1, 2
:
1 | .. // RangeFull |
..=
运算符可以包含右边的结束值,例如 0 ..= 3
是 0, 1, 2, 3
:
1 | ..= b // RangeToInclusive { end: b } |
但是在循环中,必须要有起始位置,因为循环必须要有个起始点。不过在数组切片中,六种形式都是有用的,如果 start
和 end
被省略,就会指向 slice
全部。
下面是一个分值算法的示例,用于实现快速排序:
1 | fn quicksort<T: Ord>(slice: &mut [T]) { |
解引用操作符
一元 *
操作符被用于访问引用指向的值,由于 .
在访问结构体字段或者方法时会自动解引用,所以 *
没有太多发挥的场景。
1 | let padovan: Vec<u64> = compute_padovan_sequence(n); |
算数,位运算,比较和逻辑运算符
大多数适合是和 C
语言比较相似的,我们来看一些特别的例子。-
可以用于表示负数,但是没有对应的 +
。
1 | println!("{}", -100); // -100 |
与 C
中一样, a % b
计算除法向零舍入的有符号余数或模数。结果与左操作数的符号相同。请注意,%
可用于浮点数和整数:
1 | let x = 1234.567 % 10.0; // approximately 4.567 |
Rust
也继承了 C
的位运算符,&, |, ^, <<, >>
,只是 Rust
中使用 !
表示 NOT
而不是 ~
:
1 | let hi: u8 = 0xe0; let lo = !hi; // 0x1f |
移位运算符在处理有符号数时进行符号扩展,在处理无符号整数时进行 0
扩展。
位运算符比比较运算符有更高的优先级,这点和 C
语言不太一样。x & BIT != 0
表示 (x & BIT) != 0
。
比较运算符 ==, !=, <, <=, >, >=
中的两个操作数必须要有相同的类型。
逻辑运算符 ||
和 &&
两个操作数必须都是 bool
类型。
赋值
=
赋值运算符用于变量的初始化,或者对可变变量,或者它们的字段,内部元素进行赋值。Rust
不同与其他语言,默认情况下,变量都是不可变的,也就是不能修改。
另外,如果值是 non-copy
类型,那么赋值运算符将会转移它的所有权,值原来的所有者就会变成未初始化状态。
除了基本的赋值运算符之外,还支持组合赋值,例如:+=
,*=
,-=
等等:
1 | total += item.price; |
要注意的是,Rust
不支持 C
中的链式赋值,所以 a = b = 3
是不允许的,也不支持自增自减运算符 ++
和 --
。
类型转换
Rust
中的类型转换需要显示的使用 as
关键字:
1 | let x = 17; // x is type i32 |
下面是几种允许显示转换的类型:
-
内建的数字类型可以相互转换;将整数转换为另一种整数类型始终是明确定义的。转换为更窄的类型会导致截断。转换为更宽的有符号整数是符号扩展的,无符号整数是零扩展的,依此类推。从浮点类型转换为整数类型会向零舍入:
-1.99 as i32
将会得到-1
。如果该值太大而无法放入整数类型,则强制转换会生成整数类型可以表示的最接近的值:1e6 as u8
将是255
; -
bool
或char
类型或类似C
的枚举类型的值可以转换为任何整数类型,但是反过来转换是不允许的,例如,禁止将u16
强制转换为char
类型,因为某些u16
值(如0xd800
)对应于无效的Unicode
码点,它不是有效的char
值。有一个标准方法,std::char::from_u32()
,它执行运行时检查并返回一个Option<char>
,但这种转换的需求很少。作为一个例外,u8
是唯一可以转换成char
的类型,因为它的范围0-255
都是有效的ASCII
字符;
我们说过转换通常需要强制转换,一些涉及引用类型的转换非常简单,即使没有强制转换,语言也会执行它们。下面是一些自动转换的场景:
String
类型的值可以自动转换为&str
类型;&Vec<i32>
类型的值可以自动转换为&[i32]
类型;&Box<Chessboard>
类型的值可以自动转换为&Chessboard
类型;
闭包
Rust
有闭包,轻量级的类似函数的值。闭包通常由一个参数列表,在竖线之间给出,后跟一个表达式:
1 | let is_even = |x| x % 2 == 0 |
Rust
可以推断参数类型和返回类型,当然也可以向函数那样明确写出来。但是如果指定了返回类型,则为了语法上的完整性,闭包体必须是一个块:
1 | let is_even = |x: u64| -> bool x % 2 == 0; // error |
闭包的调用和函数调用语法一样:
1 | assert_eq!(is_even(14), true); |