LISP programmers know the value of everything, but the cost of nothing
——Alan Perlis, epigram #55
在本章中,我们将介绍Rust的 表达式 ,它是构成Rust函数体和大部分Rust代码的构建块。本章将探索表达式的力量以及如何克服它的局限。我们还将介绍控制流,它在Rust中完全是以表达式为基础的,最后还要介绍Rust中的基本运算符如何单独和组合工作。
还有一些从技术角度应该划入这一类的概念,例如闭包和迭代器。但它们太过重要因此我们之后会用单独的章节介绍它们。现在,我们希望能用尽可能少的页数介绍尽可能多的语法。
Rust表面上看上去像C家族的语言,但这其实是一个误解。在C语言中, 表达式 和 语句 之间有很大的不同。表达式是一些像这样的代码:
5 * (fahr-32) / 9
而语句则是像这样的:
for (; begin != end; ++begin) {
if (*begin == target)
break;
}
表达式有值,但语句没有。
Rust是一种 表达式语言 。这意味着它遵循了起源于Lisp的传统,即表达式负责完成所有工作。
在C中,if
和switch
是语句。它们并不产生值,也不能被用在表达式中间。在Rust中,if
和match
可以 产生值。我们已经在”第2章”中看到过一个产生数字值的match
表达式:
pixels[r * bounds.0 + c] =
match escapes(Complex { re: point.0, im: point.1 }, 255) {
None => 0,
Some(count) => 255 - count as u8
};
一个if
表达式可以用于初始化一个变量:
let status =
if cpu.temperature <= MAX_TEMP {
HttpStatus::Ok
} else {
HttpStatus::ServerError // server melted
};
一个match
表达式可以被用作函数或宏的参数:
println!("Inside the vat, you see {}.",
match vat.contents {
Some(brain) => brain.desc(),
None => "nothing of interest"
});
这解释了Rust为什么没有C的三元运算符(expr1 ? expr2 : expr3)
。在C中,它是一种类似if
语句的表达式。但在Rust中这种写法是多余的,因为if
表达式可以同时实现这两种功能。
C中的大部分控制流工具都是语句,而Rust中的控制流则全是表达式。
”表6-1”总结了Rust的表达式语法。我们将在这一章中介绍所有这些表达式。运算符按照优先级从高到低的顺序列出。(类似于大多数编程语言,Rust使用 运算符优先级 来决定当表达式中含有多个运算符时的运算顺序。例如,在表达式limit < 2 * broom.size + 1
中,.
运算符优先级最高,因此会先访问字段。)
表达式类型 | 示例 | 相关trait |
---|---|---|
数组字面量 | [1, 2, 3] |
|
重复数组字面量 | [0; 50] |
|
元组 | (6, "crullers") |
|
组合 | (2 + 2) |
|
块 | { f(); g() } |
|
控制流表达式 | if ok { f() } |
|
if ok { 1 } else { 0 } |
||
if let Some(x) = f() { x } else { 0 } |
||
match x { None => 0, _ => 1 } |
||
for v in e { f(v); } |
“std::iter::IntoIterator” |
|
while ok { ok = f(); } |
||
while let Some(x) = it.next() { f(x); } |
||
loop { next_event(); } |
||
break |
||
continue |
||
return 0 |
||
宏调用 | println!("ok") |
|
路径 | std::f64::consts::PI |
|
结构体字面量 | Point {x: 0, y: 0} |
|
元组字段访问 | pair.0 |
“Deref” , “DerefMut” |
结构体字段访问 | point.x |
“Deref” , “DerefMut” |
方法调用 | point.translate(50, 50) |
“Deref” , “DerefMut” |
函数调用 | stdin() |
“Fn(Arg0, ...) -> T” , “FnMut(Arg0, ...) -> T” , “FnOnce(Arg0, ...) -> T” |
索引 | arr[0] |
“Index” , “IndexMut” , “Deref” , “DerefMut” |
错误检查 | create_dir("tmp")? |
|
逻辑/位 NOT | !ok |
“Not” |
负 | -num |
“Neg” |
解引用 | *ptr |
“Deref” , “DerefMut” |
借用 | &val |
|
类型转换 | x as u32 |
|
乘法 | n * 2 |
“Mul” |
除法 | n / 2 |
“Div” |
余数(取模) | n % 2 |
“Rem” |
加法 | n + 1 |
“Add” |
减法 | n - 1 |
“Sub” |
左移 | n << 1 |
“Shl” |
右移 | n >> 1 |
“Shr” |
位与 | n & 1 |
“BitAnd” |
位异或 | n ^ 1 |
“BitXor” |
位或 | n | 1 |
“BitOr” |
小于 | n < 1 |
“std::cmp::PartialOrd” |
小于等于 | n <= 1 |
“std::cmp::PartialOrd” |
大于 | n > 1 |
“std::cmp::PartialOrd” |
大于等于 | n >= 1 |
“std::cmp::PartialOrd” |
等于 | n == 1 |
“std::cmp::PartialEq” |
不等于 | n != 1 |
“std::cmp::PartialEq” |
逻辑与 | x.ok && y.ok |
|
逻辑或 | x.ok || backup.ok |
|
左闭右开区间 | start .. stop |
|
左闭右闭区间 | start ..= stop |
|
赋值 | x = val |
|
复合赋值 | x *= 1 |
“MulAssign” |
x /= 1 |
“DivAssign” |
|
x %= 1 |
“RemAssign” |
|
x += 1 |
“AddAssign” |
|
x -= 1 |
“SubAssign” |
|
x <<= 1 |
“ShlAssign” |
|
x >>= 1 |
“ShrAssign” |
|
x &= 1 |
“BitAndAssign” |
|
x ^= 1 |
“BitXorAssign” |
|
x |= 1 |
“BitOrAssign” |
|
闭包 | |x, y| x + y |
所有可以链式使用的运算符都是左结合的。也就是说,一条运算链例如a - b - c
被组合为(a - b) - c
,而不是a - (b - c)
。这些运算符可以被任意组合:
* / % + - << >> & ^ | && || as
比较运算符、赋值运算符、范围运算符..
和..=
不能被链式使用。
块是最通用的表达式。一个块产生一个值,可以被用于任何需要一个值的地方:
let display_name = match post.author() {
Some(author) => author.name(),
None => {
let network_info = post.get_network_metadata()?;
let ip = network_info.client_address();
ip.to_string()
}
};
Some(author) =>
之后的代码是简单的表达式author.name()
,None =>
之后的代码则是一个块表达式。对Rust来种,两种表达式没有区别。块表达式的值是它的最后一条表达式的值,也就是ip.to_string()
。
注意ip.to_string()
后面没有分号。Rust中的大部分代码行都以分号或者花括号结尾,类似于C和Java。如果一个块看起来像C代码一样在所有的表达式后边都有分号,那它的行为就和C块一样,它的值将是()
。正如我们在”第2章”提到的,当你省略了块中最后一个表达式后边的分号,那么块的值将是最后一个表达式的值,而不是通常的()
。
在一些语言中,尤其是Javascript,你可以省略分号,语言会自动为你添加上——这样除了方便一点,并没有任何区别。然而在Rust中,分号通常是有实际意义的:
let msg = {
// let语句:总是需要分号
let dandelion_control = puffball.open();
// 表达式 + 分号:方法被调用,返回值被丢弃
dandelion_control.release_all_seeds(launch_codes);
// 没有分号的表达式:方法被调用,
// 返回值被存储到 `msg`
dandelion_control.get_status()
};
语句块可以包含声明最后还能产生一个值的能力是一个很有用的特性,而且可以很快习惯。它的一个缺陷是如果你偶然忘记了分号会导致一条错误信息:
...
if preferences.changed() {
page.compute_size() // oops, 缺少分号
}
如果你在C或者Java程序中犯了这种错误,编译器会简单地直接指出你少写了一个分号。然而这是Rust的报错:
error[E0308]: mismatched types
22 | page.compute_size() // oops, missing semicolon
| ^^^^^^^^^^^^^^^^^^^- help: try adding a semicolon `;`
| |
| expected (), found tuple
|
= note: expected unit type `()`
found tuple `(u32, u32)`
在缺少分号的情况下,块的值将是page.compute_size()
返回的值,但一个没有else
分支的if
语句必须总是返回()
。幸运的是,Rust知道这种类型的错误并建议加上分号。
除了表达式和分号之外,一个块中可能包含任意数量的声明。最常见的情况是let
声明,它用来声明局部变量:
let name: type = expr;
类型和初始值是可选的,分号是必须的。
一个let
声明可以在不初始化的情况下声明一个变量。这有时很有用,因为有时候一个变量需要在控制流的中途初始化:
let name;
if user.has_nickname() {
name = user.nickname();
} else {
name = generate_unique_name();
user.register(&name);
}
局部变量name
有两种不同的初始化路径,但两条路径上它都只会被初始化一次,所以name
不需要声明为mut
。
在变量初始化之前使用它会导致错误(这和使用被move的值的错误紧密相关,Rust希望你只在变量的值存在时使用它们!)。
你有时可能会看到代码似乎重新声明一个已经存在的变量,例如:
for line in file.lines() {
let line = line?;
...
}
let
声明创建了一个新的、类型不同的、第二个变量line
。第一个line
的类型是Result<String, io::Error>
,第二个line
是一个String
。第二个声明在块的剩余部分会取代第一个。这被称为 遮蔽(shadowing) ,在Rust程序中非常常见。上面的代码等价于:
for line_result in file.lines() {
let line = line_result?;
...
}
在本书中,我们将坚持在这种场景中使用_result
后缀,来保证变量的名字不同。
一个块还可以包含 item declarations 。一个item是一个可以出现在全局或模块中的声明,例如fn
、struct
、use
。
后面的章节将会详细介绍item。现在,fn
足够作为一个例子了。任何块都可以包含fn
声明:
use std::io;
use std::cmp::Ordering;
fn show_files() -> io::Result<()> {
let mut v = vec![];
...
fn cmp_by_timestamp_then_name(a: &FileInfo, b: &FileInfo) -> Ordering {
a.timestamp.cmp(&b.timestamp) // 首先,比较时间戳
.reverse() // 最新的文件优先
.then(a.path.cmp(&b.path)) // 比较路径
}
v.sort_by(cmp_by_timestamp_then_name);
...
}
当一个fn
在块内声明的时候,它的作用域是整个块,它可以在整个块内 使用 。但是一个嵌套的fn
不能访问外围作用域的局部变量和参数。例如,函数cmp_by_timestamp_then_name
不能使用v
。(Rust还有闭包,闭包可以使用外层作用域的变量,见”第14章”。)
一个块甚至可以包含整个模块。这听起来可能有些多余了:我们真的需要把语言的 每一部分 嵌套在其他部分中的能力吗?——但程序员(尤其是使用宏的程序员)可以找到语言提供的每一个正交碎片的用法。
if
表达式的形式大家都很熟悉:
if condition1 {
block1
} else if condition2 {
block2
} else {
block_n
}
每一个condition
都必须是bool
类型的表达式,Rust不会隐式地将数字或者指针类型转换为布尔值。
和C不同的是条件表达式不需要括号,事实上如果有不必要的括号的话,rustc
会发出警告。不过花括号是必须的。
else if
块,和最后的else
都是可选的。一个没有else
块的if
表达式类似于一个else
块为空的if
表达式。
match
表达式有些类似于C的switch
语句,但是更加灵活。一个简单的例子如下:
match code {
0 => println!("OK"),
1 => println!("Wires Tangled"),
2 => println!("User Asleep"),
_ => println!("Unrecognized Error {}", code)
}
这类似于一个switch
语句,match
表达式的四条分支里只有一条会执行,取决于code
的值。通配符模式_
可以匹配任何情况,类似于switch
语句中的default:
标签,不过它会覆盖之后的所有模式,它之后的模式将永远不会匹配到任何东西(出现这种情况时编译器也会警告你)。
编译器可以使用跳转表来优化这种match
表达式,类似于C++中的switch
语句。当match
的每个分支都返回常量值时还会有一个类似的优化,这种情况下,编译器会用那些值构建一个数组,然后match
会被编译为一次数组访问,这种情况下除了边界检查之外,编译出的代码将不会有任何条件分支。
每个分支中=>
左侧支持多种 模式 ,这是match
功能强大的根源。上边的例子中,每种模式都只是一个简单的整数。我们还展示过区分Option
的两种值的match
表达式:
match params.get("name") {
Some(name) => println!("Hello, {}!", name);
None => println!("Greetings, stranger.");
}
这只是模式的一个小应用,一个模式可以匹配很多值。它可以解包元组,可以匹配结构体中的每个字段,可以解引用,借用一个值的一部分,等等。Rust的模式是一种专门的mini语言。我们将在”第10章”中介绍它们。
match
表达式的通用形式是:
match value {
pattern => expr,
...
}
如果expr
是一个块的话,分支最后的逗号可以省略。
Rust会从第一个分支开始,逐个检查value
和给定的模式是否匹配。当有一个模式匹配时,相应的expr
将会被求值,整个match
表达式将完成执行,不会再检查别的模式。必须至少有一个模式可以匹配,Rust会禁止没有覆盖所有可能情况的match
表达式:
let score = match card.rank {
Jack => 10,
Queen => 10,
Ace => 11
}; // 错误:没有穷尽所有模式
if
表达式的所有块必须产生相同类型的值:
let suggested_pet =
if with_wings { Pet::Buzzard } else { Pet::Hyena }; // ok
let favorite_number =
if user.is_hobbit() { "eleventy-one" } else { 9 }; // error
let best_sports_team =
if is_hockey_season() { "Predators" }; // error
(最后一个例子会导致错误,因为在7月结果将是()
。)1
类似的,match
表达式的分支也必须有相同的类型:
let suggested_pet =
match favorite.element {
Fire => Pet::RedPanda,
Air => Pet::Buffalo,
Water => Pet::Orca,
_ => None // 错误:类型不一致
}
if
表达式还有一种形式:if let
表达式:
if let pattern = expr {
block1
} else {
block2
}
expr
如果能匹配pattern
,则执行block1
,如果不能匹配,则执行block2
。有时这是一种获取Option
或Result
的值的好方法:
if let Some(cookie) = request.session_cookie {
return restore_session(cookie);
}
if let Err(err) = show_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
没有任何场景 必须 使用if let
,因为match
可以做到任何if let
可以做的事。if let
表达式类似于如下只有一个模式的match
表达式的缩写:
match expr {
pattern => { block1 }
_ => { block2 }
}
有四种循环表达式:
while condition {
block
}
while let pattern = expr {
block
}
loop {
block
}
for pattern in iterable {
block
}
在Rust中循环也是表达式,不过while
或for
循环的值总是()
,因此它们的值并没有用。如果你指明的话loop
表达式可以产生一个值。
while
循环的行为和C语言中的基本一样,除了condition
必须是精确的bool
类型的表达式。
while let
循环类似于if let
。每一次迭代循环开始时,如果expr
的值可以匹配pattern
,将会运行表达式块,否则循环将结束。
使用loop
循环来编写无限循环。它会永远重复执行block
(直到遇到break
或return
或线程panic)。
一个for
循环会先求值iterable
表达式,然后对表达式返回的迭代器产生的每个值执行一次block
。很多类型都可以被迭代,包括所有标准集合例如Vec
和HashMap
。标准的C语言中的for
循环:
for (int i = 0; i < 20; i++) {
printf("%d\n", i);
}
用Rust来写的话就是:
for i in 0..20 {
println!("{}", i);
}
和C语言一样,最后一个打印出的数字是19
。
..
运算符会产生一个 范围 ,它是一个只有两个字段start
和end
的简单结构体。0..20
等价于std::ops::Range { start: 0, end: 20 }
。范围可以用于for
循环,因为Range
是可以迭代的类型,它实现了std::iter::IntoIterator
trait,我们将在”第15章”中讨论这些。标准集合都是可迭代的,数组和切片也是。
为了保持Rust中的移动语义,for
循环会消耗掉值:
let strings: Vec<String> = error_messages();
for s in strings { // 每一个String被移动进s
println!("{}", s);
} // 并在这里drop
println!("{} error(s)", strings.len()); // 错误:使用了被move的值
这样可能会很不方便,一个简单的方法是让循环获取集合的引用。然后循环变量将会变成集合中每一个元素的引用:
for rs in &strings {
println!("String {:?} is at address {:P}.", *rs, rs);
}
这里&strings
的类型是&Vec<String>
,rs
的类型是&String
。
迭代一个mut
引用会产生每个元素的mut
引用:
for rs in &mut strings { // rs的类型是&mut String
rs.push('\n'); // 每个字符串添加一个换行符
}
”第15章”会更详细的介绍for
循环并展示其他使用迭代器的方式。
break
表达式用来跳出循环(Rust中break
只能在循环中使用。match
表达式中不需要它,这一点和switch
语句不同)。
在一个loop
循环体内,你可以给break
一个表达式,
表达式的值就是整个循环的值:
// 每一次对`next_line`的调用返回`Some(line)`,其中`line`
// 是输入的一行;或者返回`None`,表示已经到达输入结尾。
// 返回第一个以"answer: "开头的行。
// 如果没有,就返回"answer: nothing"。
let answer = loop {
if let Some(line) = next_line() {
if line.starts_with("answer: ") {
break line;
}
} else {
break "answer: nothing";
}
};
自然,loop
表达式中的所有break
表达式必须产生相同类型的值,这个类型也会成为loop
本身的类型。
一个continue
表达式跳转到下一次迭代:
// 读取一些数据,一次读取一行
for line in input_lines {
let trimmed = trim_comments_and_whitespace(line);
if trimmed.is_empty() {
// 跳转到loop的开头并
// 移动到下一行输入
continue;
}
...
}
在一个for
循环中,continue
会跳转到集合中的下一个值,如果没有值了,循环会停止。类似的,在一个while
循环中continue
会重新检查循环条件,如果现在是false,循环会停止。
一个循环可以用一个生命周期 标记 。在下面的例子中,'search
是一个外层for
循环的标签。因此break 'search
会退出外层循环,而不是内层循环:
'search:
for room in apartment {
for spot in room.hiding_spots() {
if spot.contains(keys) {
println!("Your keys are {} in the {}.", spot, room);
break 'search;
}
}
}
一个break
可以同时带有一个标签和一个值表达式:
// 寻找一列数中的第一个完全平方
let sqrt = 'outer: loop {
let n = next_number();
for i in 1.. {
let square = i * i;
if square == n {
// 找到了一个平方根
break 'outer i;
}
if square > n {
// `n`不是完全平方,尝试下一个数
break;
}
}
};
标签也可以和continue
一起使用。
return
表达式退出当前函数,向调用者返回一个值。
没有值的return
相当于return ()
:
fn f() { // 返回类型被省略:默认是()
return; // 返回值被省略:默认是()
}
函数并不一定需要显式的return
表达式。函数体就像是一个块表达式:如果最后一个表达式后边没有分号,它的值将是函数的返回值。事实上,这是在Rust中充当返回值的最佳方法。
但这并不意味着return
就毫无作用,或者仅仅是为了符合不熟悉表达式语言的用户的习惯。类似于break
表达式,return
可以终止当前的工作。例如,在”第2章”中,在调用了一个可能会失败的函数之后,我们使用了?
运算符来检查错误:
let output = File::create(filename)?;
我们解释过这是match
表达式的缩写:
let output = match File::create(filename) {
Ok(f) => f,
Err(err) => return Err(err)
};
这段代码首先调用File::create(filename)
。如果返回Ok(f)
,那么整个match
表达式将求值为f
,因此f
被存储在output
中,然后将继续执行match
之后的代码。
否则, 我们会匹配到Err(err)
,然后触发return
表达式。此时我们正在求值一个match
表达式,来决定变量output
的值。但这都无所谓,我们会放弃所有任务,直接退出函数,并返回我们从File::create()
得到的错误。
我们将在”传播错误”一节中详细介绍?
运算符。
Rust编译器的几个部分会检查整个程序中的控制流:
- Rust会检查一个函数里的所有返回路径是否返回相同类型的值。为了做到这一点,它需要知道控制流是否能到达函数结尾。
- Rust会检查未初始化的局部变量是否绝不会被使用。这需要检查程序中的每一条路径来确保没有路径会到达未初始化的变量被使用的情况。
- Rust会警告不可达的代码。函数中 没有 路径可以到达的代码就是不可达的代码。
这些被称为 控制流敏感(flow-sensitive) 分析。这并不是新的概念,Java很多年前就有一个“确定性赋值(definite assignment)”的分析,和Rust的检查很相似。
当强迫执行这种规则时,一门语言(的编译器)必须在简洁和智能之间权衡。简洁可以让程序员更容易地理解编译器在说什么;智能则可以帮助消除错误警告和程序完全正确但编译器却报错的情况。Rust倾向于简洁,它的控制流敏感分析根本就不检查循环条件,而是假设程序中的所有条件都既可能是真又可能是假。
这导致Rust会拒绝编译这样的安全程序:
fn wait_for_process(process: &mut Process) -> i32 {
while true {
if process.wait() {
return process.exit_code();
}
}
} // 错误:不匹配的类型:期望i32,得到()
这里的错误是假的。函数只可能通过return
表达式返回,因此while
循环不会产生i32
值是无关紧要的。
而loop
表达式作为一种“告诉编译器你的意思”的解决方案来解决这个问题。
Rust的类型系统也会被控制流影响。之前我们说过if
表达式中的所有分支都必须有相同的类型。但如果在以break
、return
、loop
表达式或对panic!()
、std::process::exit()
的调用作为结尾的块中强迫这个规则就会显得很愚蠢。这些表达式的共同点就是它们永远不会像正常的方式一样产生一个值:break
或return
会中断并退出当前的块、无限的loop
循环则永远不会结束,等等。
因此在Rust中,这些表达式并没有通常的类型。不会正常结束的表达式会被赋予特殊类型!
,并且它们不受类型必须匹配的规则的约束。 你可以在std::process::exit()
的函数签名中看到!
:
fn exit(code: i32) -> !
!
意味着exit()
永远不会返回,它是一个 发散函数(divergent function) 。
你可以用相同的语法编写自己的发散函数,在某些情况下这会显得非常自然:
fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {
socket.listen();
loop {
let s = socket.accept();
handler.handle(s);
}
}
当然,如果函数能正常返回的话,Rust会认为这是一个错误。
有了这些大规模控制流的构建块之后,我们可以继续分析常用的更细粒度的表达式,例如函数调用和算术运算。
在Rust中调用函数的方法的语法和许多其它语言一样:
let x = gcd(1302, 462); // 函数调用
let room = player.location(); // 方法调用
在第二个例子中,player
是一个编造的Player
类型的变量,它有一个.location()
方法。(我们将在”第9章”中讨论自定义类型时展示如何定义自己的方法。)
Rust通常会严格区分引用和它指向的值。如果你向接收i32
的函数传递&i32
,将会引发类型错误。不过你会注意到.
运算符放宽了这个限制,在方法调用player.location()
中,player
可以是一个Player
、也可以是一个引用类型&Player
、也可以是智能指针类型Box<Player>
或者Rc<Player>
。.location()
方法可能会以值也可能会以引用获取参数,同样的.location()
语法可以适用于所有情况,因为Rust的.
运算符会根据需要自动解引用player
或者获取它的引用。
第三种语法用于调用类型关联函数,例如Vec::new()
:
let mut numbers = Vec::new(); // 类型关联函数调用
这类似于面向对象语言中的静态方法,方法在值上调用(例如my_vec.len()
),而类型关联函数在类型上调用(例如Vec::new()
)。
当然,方法调用可以被串联起来:
// 取自于第二章的Actix-based web server
server
.bind("127.0.0.1:3000").expect("error binding server to address")
.run().expect("error running server");
Rust语法中的一个怪异之处是,不能按照泛型类型的通常语法例如Vec<T>
来调用函数或者方法:
return Vec<i32>::with_capacity(1000); // error: something about chained comparisons
let ramp = (0 .. n).collect<Vec<i32>>(); // 同样的错误
问题在于这个表达式中的<
被当作小于运算符。Rust编译器会建议使用::<T>
来代替<T>
,这样可以解决这个问题:
return Vec::<i32>::with_capacity(1000); // ok, using ::<
let ramp = (0 .. n).collect::<Vec<i32>>(); // ok, using ::<
符号::<...>
在Rust中被亲切地称为 turbofish 。
另外,通常也可以省略类型参数,让Rust推断它们:
return Vec::with_capacity(10); // ok, 如果函数的返回类型是Vec<i32>
let ramp: Vec<i32> = (0 .. n).collect(); // ok, 指定了变量的类型
在可以推断出类型时省略类型是一种好的风格。
访问结构体中字段的语法大家都很熟悉,元组也一样,不过元组中的字段是数字而不是名称:
game.black_pawns // 结构体字段
coords.1 // 元组元素
如果点左边的值是引用或者智能指针类型,它也会像方法调用一样自动解引用。
方括号可以访问数组、切片或vector中的元素:
pieces[i] // 数组元素
方括号左侧的值也会自动解引用。
类似于这三种的表达式被称为 左值 ,因为它们可以出现在赋值表达式的左边:
game.black_pawns = 0x00ff0000_00000000_u64;
coords.1 = 0;
pieces[2] = Some(Piece::new(Black, Knight, coords));
当然,只有当game
、coords
和pieces
被声明为mut
变量时才可以这么做。
从一个数组或者vector中提取切片非常直观:
let second_half = &game_moves[midpoint .. end];
这里game_moves
可能是一个数组、切片或者是vector,结果将是一个长度为end - midpoint
的切片,在second_half
的生命周期内game_moves
都处于被借用的状态。
..
运算符允许任意一边的操作数被省略,根据两边是否有操作数,它可以被划分为四种形式:
.. // 全部范围
a .. // 起点范围 { start: a }
.. b // 终点范围 { end: b }
a .. b // 范围 { start: a, end: b }
后两种形式是 end-exclusive(右开区间) :终点值将不会被包含。例如,范围0 .. 3
包括数字0
、1
、2
。
..=
运算符产生 end-inclusive(闭区间) 范围,它会包含终点值:
..= b // RangeToInclusive { end: b }
a ..= b // RangeInclusive::new(a, b)
例如,范围0 ..= 3
包含数字0
、1
、2
、3
。
只有包含start值的范围才可以迭代,因为一个循环必须有开始的地方。但在数组切片中,所有的六种形式都是有用的。如果起始值或终点值被省略了,将默认包含数组从起点开始的元素或者直到终点的元素。
因此快速排序的一个实现,经典的分治排序算法,将类似于这样:
fn quicksort<T: Ord>(slice: &mut [T]) {
if slice.len() <= 1 {
return; // 切片为空,不需要排序
}
// 将切片分为两个部分,前半部分和后半部分
let pivot_index = partition(slice);
// 递归排序`slice`的前半部分
quicksort(&mut slice[.. pivot_index]);
// 再排序后半部分
quicksort(&mut slice[pivot_index + 1 ..]);
}
取地址运算符&
和&mut
,已经在”第5章”中介绍过了。
一元*
运算符用于获取引用指向的值。正如我们看到的,当你使用.
运算符访问字段或者方法时,Rust会自动解引用。因此只有在我们想读取或写入引用指向的整个值时,*
运算符才是必须的。
例如,有时迭代器会产生引用,但程序需要底层的类型:
let padovan: Vec<u64> = compute_padovan_sequence(n);
for elem in &padovan {
draw_triangle(turtle, *elem);
}
在这个例子中,elem
的类型是&u64
,因此*elem
是一个u64
值。
Rust的二元运算符和其他语言中的很像。为了节省时间,我们假设你熟悉这些语言中的一种,并只专注于Rust中背离传统的点。
Rust有通常的算术运算符+, -, *, /, %
。正如在”第3章”中说过的一样,整数溢出会被检测到,并在debug模式下造成panic。标准库提供类似a.wrapping_add(b)
的方法来执行不检查溢出的算术。
整数除法会向0舍入,并且整数除以0时即使在release模式也会导致panic。整数有一个a.checked_div(b)
方法会返回Option
(如果b
是0时为None
),而不会panic。
一元的-
运算符用于求负数,它支持除无符号整数外所有的数字类型。没有一元的+
运算符:
println!("{}", -100); // -100
println!("{}", -100u32); // 错误:一元`-`不能用于类型`u32`
println!("{}", +100); // 错误:期望表达式,发现`+`
和在C中一样,a % b
会计算有符号的余数,或者叫取模。结果和左侧的操作数符号一致。注意%
也可以像整数一样用于浮点数:
let x = 1234.567 % 10.0; // 约为4.567
Rust还继承了C的整数的位运算符&, |, ^, <<, >>
。然而,Rust使用!
而不是~
来表示按位取反:
let hi: u8 = 0xe0;
let lo = !hi; // 0x1f
这意味着!n
不能用来判断n
是否为0,必须使用n == 0
。
有符号整数的位移总是符号扩展,无符号整数的位移的位移总是0扩展。因为Rust有无符号整数,所以不需要像Java中的>>>
一样的无符号移位运算符。
和C语言不同的是,位运算符比比较运算的优先级更高,因此如果你写x & BIT != 0
,意味着(x & BIT) != 0
。这比C中的含义更有用,C中将是x & (BIT != 0)
。
Rust的比较运算符是==, !=, <, <=, >, >=
。比较的两个值类型必须相同。
Rust也有短路求值的&&
和||
运算符,所有的操作数必须都是bool
类型。
=
运算符可以用于给mut
变量或它们的字段或元素赋值。但在Rust中赋值行为并不像在其他语言中那么普遍,因为变量默认是不可变的。
正如在”第4章”介绍的那样,如果变量是non-Copy
类型,赋值行为将会把它的值 移动 到目标变量。值的所有权从源对象移动到目的对象,目的对象之前的值会被丢弃。
也可以使用复合赋值:
total += item.price;
这等价于total = total + item.price;
。还支持其他的运算符例如:-=, *=
等。完整的运算符支持在”表6-1”中列出。
和C不同,Rust不支持链式赋值:你不能写a = b = 3
来把3
同时赋给a
和b
。在Rust中赋值很罕见,所以你不会怀念这种缩写的。
Rust不支持C的自增和自减运算符++
和--
。
在Rust中把一个类型转换为另一个类型通常需要显式的转换。使用as
关键字来进行转换:
let x = 17; // x是i32类型
let index = x as usize; // 转换为usize
Rust允许以下几种转换:
-
任何内建的数字类型可以彼此转换。
将一个整数转换为另一个整数总是良定义的。转换为更小的类型会导致截断,有符号数转换为更大的类型会进行符号扩展,无符号数是0扩展,等等。总而言之,没有意外的行为。
浮点数转换为整数会向0舍入:值
-1.99
转换为i32
是-1
。如果值太大不能用整数类型表示,转换会返回整数类型能表示的最接近真实值的值:值1e6
转换为u8
是255
。 -
bool
、char
、或者类似C的enum
类型可以转换为任意整数类型(我们将在”第10章”中介绍枚举)。其他方向的转换是不允许的,因为
bool
、char
和enum
都对它们的值有严格的要求,必须要进行运行时检查。例如,将一个u16
数字转换为char
类型是禁止的,因为一些u16
值例如0xd800
,对应Unicode的代理码点,因此不是有效的char
类型值。有一个标准的方法std::char::from_u32()
来进行运行时检查并返回Option<char>
。但需要指出的是,这种转换的需求非常少见。我们通常一次转换整个字符串或流,有关Unicode文本的算法通常是非平凡的,最好留给库来实现。一个例外是,
u8
可以转换为char
类型,因为0到255之间的所有整数都是有效的Unicode码点。 -
一些涉及unsafe指针类型的转换也是允许的。见”原始指针”一节。
我们说转换 通常 需要显式的转换。但少数涉及引用类型的转换非常直观以至于语言可以自动完成而不需要显式转换。一个小例子是把mut
引用转换为non-mut
引用。
还有一些更重要的自动转换可能发生:
&String
类型的值可以自动转换为&str
类型。&Vec<i32>
类型的值可以自动转换为&[i32]
。&Box<Chessboard>
类型的值可以自动转换为&Chessboard
。
这些被称为 强制解引用(deref coercions) ,因为它们适用于实现了内建的Deref
trait的类型。Deref
是为了智能指针类型设计的,例如Box
,它的行为尽量和和底层的值类型保持一致。得益于Deref
,使用Box<Chessboard>
非常像在使用一个普通的Chessboard
。
用户自定义的类型也可以实现Deref
trait。当你需要编写自己的智能指针类型时,见”Deref和DerefMut”一节。
Rust支持 闭包 :一种轻量的类似函数的值。一个闭包通常由被竖线包围的参数列表和一个表达式组成:
let is_even = |x| x % 2 == 0;
Rust会自动推导参数类型和返回值类型。你可以显式写出它们,就像写函数签名一样。如果你指定了返回值类型,那么为了语法的健全,闭包的主体必须是一个块:
let is_even = |x: u64| -> bool x % 2 == 0; // error
let is_even = |x: u64| -> bool { x % 2 == 0 }; // ok
调用一个闭包和调用函数的语法相同:
assert_eq!(is_even(14), true);
闭包是Rust中最令人愉快的特性之一,关于它有很多内容可以说,我们将在”第14章”中介绍。
表达式是“要运行的代码(running code)”。它们是Rust中要被编译为机器指令的一部分。然而它们只是整个语言的一小部分。
大多数其他语言也是这样。一个程序的第一个任务是运行起来,但这并不是它唯一的任务。程序之间必须交流,它们也必须可以测试,它们还需要保持有序和灵活以便继续改进,它们必须与其它队伍构建的代码和服务进行互操作。而且即使只是为了运行,像Rust这样的典型的静态语言的程序也需要更多的工具来组织数据。
接下来,我们将用数个章节讨论这些领域:让你的程序变得结构化的模块和crate、让你的数据变得结构化的结构体和枚举。
首先,我们将花费一些篇幅来介绍一个非常重要的话题:错误处理。
Footnotes
-
译者注:7月不是曲棍球赛季? ↩