Programming Rust Fast, Safe Systems Development(译) 表达式(第六章 完)
LISP programmers know the value of everything, but the cost of nothing.
—Alan Perlis, epigram #55
在本章中,我们将介绍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章中看到了一个产生数值的匹配表达式:
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
};
匹配表达式可以作为参数传递给函数或宏:
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中,它们都是表达式。
块和分号
块也是表达式。块生成一个值,可以在需要值的任何地方使用:
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()的值。
请注意,该表达式后没有分号。大多数Rust代码行都以分号或大括号结尾,就像C或Java一样。如果一个块看起来像C代码,在所有熟悉的地方都有分号,那么它将像C块一样运行,它的值将是()。正如我们在第2章中提到的,当你将分号留在块的最后一行时,你正在使该块产生一个值 - 最终表达式的值。
在某些语言中,特别是JavaScript,你可以省略分号,而语言只是为你填写它们 - 这是一个小小的便利。这是不同的。在Rust中,分号实际上意味着什么。
let msg = {
// let-declaration: semicolon is always required
let dandelion_control = puffball.open();
// expression + semicolon: method is called, return value dropped
dandelion_control.release_all_seeds(launch_codes);
// expression with no semicolon: method is called,
// return value stored in `msg`
dandelion_control.get_status()
};
块的这种能力包含声明并在最后产生一个值是一个很好的功能,很快就会感觉很自然。一个缺点是,当您意外地遗漏分号时,它会导致奇怪的错误消息。
...
if preferences.changed() {
page.compute_size() // oops, missing semicolon
}
...
如果你在C或Java程序中犯了这个错误,编译器只会指出你错过了一个分号。这是Rust说的话:
t you’re missing a semicolon. Here’s what Rust says:
error[E0308]: mismatched types
--> expressions_missing_semicolon.rs:19:9
|
19 | page.compute_size() // oops, missing semicolon
| ^^^^^^^^^^^^^^^^^^^ expected (), found tuple
|
= note: expected type `()`
found type `(u32, u32)`
Rust假定你故意省略了这个分号;它没有考虑它只是一个错字的可能性。结果是一个混乱的错误消息。当你看到期望的类型()
时,首先查找缺少的分号。块中也允许空语句。一个空语句由一个独立的分号组成:
loop {
work();
play();
; // <-- empty statement
}
Rust允许这样做遵循C的传统。除了传达轻微的忧郁感之外,空的陈述什么都不做。我们只是为了完整性而提到它们。
声明
除了表达式和分号之外,块还可以包含任意数量的声明。最常见的是let声明,它声明了局部变量:
let name: type = expr;
类型和初始化程序是可选的。分号是必需的。 let声明可以在不初始化的情况下声明变量。然后可以使用稍后的赋值初始化变量。这偶尔会有用,因为有时变量应该从某种控制流构造的中间初始化:
let name;
if user.has_nickname() {
name = user.nickname();
} else {
name = generate_unique_name();
user.register(&name);
}
这里有两种不同的方式可以初始化局部变量名,但无论哪种方式,它都将被初始化一次,因此name不需要声明为mut。在变量初始化之前使用变量是错误的。 (这与移动后使用值的错误密切相关.Rust真的希望你只在它们存在时使用值!)你可能偶尔会看到似乎重新声明现有变量的代码,如下所示:
for line in file.lines() {
let line = line?;
...
}
这相当于:
for line_result in file.lines() {
let line = line_result?;
...
}
let声明创建一个不同类型的新的第二个变量。 line_result的类型是Result <String,io :: Error>。第二个变量line是String。赋予第二个变量与第一个变量相同的名称是合法的。在本书中,我们将坚持在这种情况下使用_result后缀,以便所有变量都具有不同的名称。
块也可以包含项目声明。项只是可以在程序或模块中全局出现的任何声明,例如fn,struct或use。
后面的章节将详细介绍项目。就目前而言,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) // first, compare timestamps
.reverse() // newest file first
.then(a.path.cmp(&b.path)) // compare paths to break ties
}
v.sort_by(cmp_by_timestamp_then_name);
...
}
当在块内声明fn时,其范围是整个块 - 也就是说,它可以在整个封闭块中使用。但是嵌套的fn无法访问恰好在范围内的局部变量或参数。例如,函数cmp_by_timestamp_then_name无法直接使用v。 (Rust也有封闭,可以看到封闭的范围。见第14章。)
块甚至可以包含整个模块。这似乎有点像我们真的需要能够将每个语言嵌套在其他所有部分中吗? - 但是程序员(尤其是使用宏的程序员)可以找到语言提供的每个正交性片段的用途。
if and match
if表达式的形式很熟悉:
he form of an if expression is familiar:
if condition1 {
block1
} else if condition2 {
block2
} else {
block_n
}
每个条件必须是bool类型的表达式;如果为true,则Rust不会隐式地将数字或指针转换为布尔值。
与C不同,条件不需要括号。事实上,如果存在不必要的括号,rustc将发出警告。然而,花括号是必需的。
else if块以及最后的else是可选的。没有else块的if表达式就像它有一个空的else块一样。匹配表达式类似于C语言切换语句,但更灵活。一个简单的例子:
match code {
0 => println!("OK"),
1 => println!("Wires Tangled"),
2 => println!("User Asleep"),
_ => println!("Unrecognized Error {}", code)
}
这是switch语句可以做的事情。这个匹配表达式的四个臂中的一个将执行,具体取决于代码的值。通配符模式_匹配所有内容,因此它用作默认值:case。
编译器可以使用跳转表来优化这种匹配,就像C ++中的switch语句一样。当匹配的每个臂产生恒定值时,应用类似的优化。在这种情况下,编译器构建这些值的数组,并将匹配编译为数组访问。除了边界检查之外,编译代码中根本没有分支。
匹配的多功能性源于各种支撑模式,可用于每个臂的=>左侧。上面,每个模式只是一个常数整数。我们还展示了用于区分两种Option值的匹配表达式:
match params.get("name") {
Some(name) => println!("Hello, {}!", name),
None => println!("Greetings, stranger.")
}
这只是模式可以做什么的暗示。模式可以匹配一系列值。它可以解压缩元组。它可以匹配结构的各个字段。它可以追逐引用,借用值的一部分等等。 Rust的模式是他们自己的迷你语言。我们将在第10章中为它们分配几页。匹配表达式的一般形式是:
match value {
pattern => expr,
...
}
如果expr是块,则可以删除arm之后的逗号
Rust从第一个开始依次检查每个模式的给定值。当模式匹配时,将评估相应的expr并完成匹配表达式;没有检查进一步的模式。至少有一个模式必须匹配。
Rust禁止不包含所有可能值的匹配表达式:
let score = match card.rank {
Jack => 10,
Queen => 10,
Ace => 11
}; // error: nonexhaustive patterns
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
(最后一个例子是错误,因为在七月,结果将是()。)
同样,匹配表达式的所有臂必须具有相同的类型:
let suggested_pet =
match favorites.element {
Fire => Pet::RedPanda,
Air => Pet::Buffalo,
Water => Pet::Orca,
_ => None // error: incompatible types
};
if let
还有一个if表单,if let表达式:
if let pattern = expr {
block1
} else {
block2
}
给定的expr与模式匹配,在这种情况下block1运行,或者不运行,并且block2运行。有时这是从选项或结果中获取数据的好方法:
if let Some(cookie) = request.session_cookie {
return restore_session(cookie);
}
if let Err(err) = present_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
如果let,那么使用它绝对不是必需的,因为如果我们可以做的话,匹配可以做任何事情。 if let表达式只是一个模式匹配的简写:
match expr {
pattern => { block1 }
_ => { block2 }
}
Loops
有四个循环表达式:
while condition {
block
}
while let pattern = expr {
block
}
loop {
block
}
for pattern in collection {
block
}
循环是Rust中的表达式,但它们不会产生有用的值。循环的值是()。
while循环的行为与C等价物完全相同,但同样,条件必须是精确类型bool。
while循环类似于let。在每次循环迭代开始时,expr的值或者匹配给定的模式,在这种情况下块运行,或者不运行,在这种情况下循环退出。
使用循环写入无限循环。它会一直重复执行该块(或者直到达到中断或返回,或者线程发生混乱)。
for循环计算集合表达式,然后为集合中的每个值计算一次块。支持许多集合类型。循环的标准C:
for (int i = 0; i < 20; i++) {
printf("%d\n", i);
}
在Rust中这样写的:
for i in 0..20 {
println!("{}", i);
}
与在C中一样,最后打印的数字是19。
…运算符生成一个范围,一个带有两个字段的简单结构:开始和结束。 0…20与std :: ops :: Range {start:0,end:20}相同。范围可以与for循环一起使用,因为Range是一个可迭代类型:它实现了std :: iter :: IntoIterator特征,我们将在第15章讨论。标准集合都是可迭代的,数组和切片也是如此。为了与Rust的移动语义保持一致,对值的for循环会消耗该值:
let strings: Vec<String> = error_messages();
for s in strings { // each String is moved into s here...
println!("{}", s);
} // ...and dropped here
println!("{} error(s)", strings.len()); // error: use of moved value
这可能不方便。简单的补救措施是循环遍历对集合的引用。然后,循环变量将是对集合中每个项目的引用:
for rs in &strings {
println!("String {:?} is at address {:p}.", *rs, rs);
}
这里&string的类型是&Vec ,rs的类型是&String。迭代mut引用提供了对每个元素的mut引用:
for rs in &mut strings { // the type of rs is &mut String
rs.push('\n'); // add a newline to each string
}
第15章更详细地介绍了for循环,并展示了使用迭代器的许多其他方法。
break表达式退出封闭循环。 (在Rust中,break仅在循环中起作用。在匹配表达式中没有必要,这与这方面的switch语句不同。)
continue表达式跳转到下一个循环迭代:
// Read some data, one line at a time.
for line in input_lines {
let trimmed = trim_comments_and_whitespace(line);
if trimmed.is_empty() {
// Jump back to the top of the loop and
// move on to the next line of input.
continue;
}
...
}
在for循环中,继续前进到集合中的下一个值。如果没有更多值,则循环退出。同样,在while循环中,继续重新检查循环条件。如果它现在为假,则循环退出。
循环可以标记为生命周期。在以下示例中,'search:是外部for循环的标签。因此break’搜索退出该循环,而不是内循环。
'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;
}
}
}
标签也可以继续使用。
返回表达式
返回表达式退出当前函数,向调用者返回一个值。没有值的return是return()的简写:
fn f() { // return type omitted: defaults to ()
return; // return value omitted: defaults to ()
}
就像休息表达一样,返回可以放弃正在进行的工作。例如,在第2章中,我们使用了?调用可能失败的函数后,运算符检查错误:
let output = File::create(filename)?;
我们解释说这是匹配表达式的简写:
let output = match File::create(filename) {
Ok(f) => f,
Err(err) => return Err(err)
};
此代码首先调用File :: create(filename)。如果返回Ok(f),则整个匹配表达式的计算结果为f,因此f存储在输出中,我们继续匹配后的下一行代码。
否则,我们将匹配Err(错误)并点击返回表达式。当发生这种情况时,我们正在评估匹配表达式以确定变量输出的值并不重要。我们放弃所有这些并退出封闭函数,返回我们从File :: create()得到的任何错误。
我们会报道?第152页的“传播错误”中的运算符更完整
为什么Rust有 loop
Rust编译器的几个部分通过您的程序分析控制流。
•Rust检查函数的每个路径都返回预期返回类型的值。要正确执行此操作,需要知道是否可以到达函数的末尾。
•Rust检查从未使用未初始化的局部变量。这需要检查函数中的每个路径,以确保无法到达使用变量的位置,而无需通过初始化它的代码。
•Rust警告无法访问的代码。如果没有通过该功能的路径到达,则代码无法访问。这些被称为流敏分析。
它们并不新鲜;多年来,Java已经进行了“明确的分配”分析,类似于Rust的分析。
在强制执行这种规则时,语言必须在简单性之间取得平衡,这使程序员更容易弄清楚编译器有时会讨论什么 - 以及聪明,这可以帮助消除错误警告和编译器拒绝完美的情况安全计划。 Rust很简单。它的流敏感分析根本不检查循环条件,而只是假设程序中的任何条件都可以是真或假。这会导致Rust拒绝一些安全的程序:
fn wait_for_process(process: &mut Process) -> i32 {
while true {
if process.wait() {
return process.exit_code();
}
}
} // error: not all control paths return a value
这里的错误是假的。实际上,如果不返回值,则无法到达函数的末尾。
循环表达式作为这个问题的“说出你的意思”解决方案提供。
Rust的类型系统也受控制流的影响。之前我们说if表达式的所有分支都必须具有相同的类型。但是对于以中断或返回表达式,无限循环或对panic!()或std :: process:exit()的调用结束的块强制执行此规则将是愚蠢的。所有这些表达的共同点是它们永远不会以通常的方式完成,产生价值。中断或返回突然退出当前块;无限循环永远不会完成;等等。
所以在Rust中,这些表达式没有普通类型。没有正常完成的表达式被赋予特殊类型!,并且它们不受关于必须匹配的类型的规则的约束。你可以看到!在std :: process :: exit()的函数签名中:
fn exit(code: i32) -> !
的!表示exit()永远不会返回。这是一个不同的功能。
您可以使用相同的语法编写自己的不同函数,这在某些情况下非常自然:
fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {
socket.listen();
loop {
let s = socket.accept();
handler.handle(s);
}
当然,如果函数可以正常返回,Rust会认为它是一个错误。
本章的部分内容是关注控制流程。其余部分包括Rust函数,方法和运算符。
函数和方法调用
调用函数和方法的语法在Rust中与在许多其他语言中一样:
let x = gcd(1302, 462); // function call
let room = player.location(); // method call
在这里的第二个例子中,player是make-up类型Player的变量,它有一个伪造的.location()方法。 (我们将在第9章开始讨论用户定义的类型时展示如何定义自己的方法。)
Rust通常会在引用和它们引用的值之间做出明显的区分。如果将&i32传递给期望i32的函数,那就是类型错误。你会注意到的。运营商放松了一些规则。在方法调用player.location()中,播放器可能是播放器,类型为&Player的引用,或类型为Box 或Rc 的智能指针。 .location()方法可以通过值或引用来获取播放器。相同的.location()语法适用于所有情况,因为Rust的。运算符会根据需要自动取消引用播放器或借用它。
第三种语法用于调用静态方法,如Vec :: new()。
let mut numbers = Vec::new(); // static method call
静态和非静态方法之间的区别与面向对象语言相同:非静态方法在值上调用(如my_vec.len()),静态方法在类型上调用(如Vec :: new())。当然,方法调用可以链接:
Iron::new(router).http("localhost:3000").unwrap();
Rust语法的一个怪癖是在函数调用或方法调用中,泛型类型的通常语法Vec 不起作用:
return Vec<i32>::with_capacity(1000); // error: something about chained comparisons
let ramp = (0 .. n).collect<Vec<i32>>(); // same error
问题是在表达式中,<是小于运算符。 Rust编译器有助于建议在这种情况下编写:而不是,这解决了问题:
return Vec::<i32>::with_capacity(1000); // ok, using ::<
let ramp = (0 .. n).collect::<Vec<i32>>(); // ok, using ::<
符号:: <…>在Rust社区中被亲切地称为涡轮机。
或者,通常可以删除类型参数并让Rust推断它们:
return Vec::with_capacity(10); // ok, if the fn return type is Vec<i32>
let ramp: Vec<i32> = (0 .. n).collect(); // ok, variable's type is given
无论什么时候推断它们都可以省略类型。
字段和元素
使用熟悉的语法访问结构的字段。元组是相同的,除了它们的字段有数字而不是名字:
game.black_pawns // struct field
coords.1 // tuple element
如果点左边的值是引用或智能指针类型,则会自动解除引用,就像方法调用一样。
方括号访问数组,切片或向量的元素:
pieces[i] // array element
括号左侧的值将自动取消引用。
像这三个表达式称为左值,因为它们可以出现在赋值的左侧:
game.black_pawns = 0x00ff0000_00000000_u64;
coords.1 = 0;
pieces[2] = Some(Piece::new(Black, Knight, coords));
当然,只有当游戏,坐标和棋子被声明为mut变量时才允许这样做。
从数组或向量中提取切片很简单:
let second_half = &game_moves[midpoint .. end];
这里game_moves可以是数组,切片或向量;无论如何,结果是一个借用的长度末端 - 中点。 game_moves被认为是在second_half的生命周期中借用的。
…运算符允许省略任一操作数;它根据存在的操作数产生最多四种不同类型的对象:
ypes of object depending on which operands are present:
.. // RangeFull
a .. // RangeFrom { start: a }
.. b // RangeTo { end: b }
a .. b // Range { start: a, end: b }
生锈范围是半开放的:它们包括起始值(如果有),但不包括结束值。范围0 … 4包括数字0,1,2和3。
只有包含起始值的范围才是可迭代的,因为循环必须具有某个起始位置。但是在数组切片中,所有四种形式都很有用。如果省略范围的开头或结尾,则默认为要切片的数据的开头或结尾。
因此,快速排序(一种经典的分而治之的排序算法)的实现可能部分地看起来像这样:
fn quicksort<T: Ord>(slice: &mut [T]) {
if slice.len() <= 1 {
return; // Nothing to sort.
}
// Partition the slice into two parts, front and back.
let pivot_index = partition(slice);
// Recursively sort the front half of `slice`.
quicksort(&mut slice[.. pivot_index]);
// And the back half.
quicksort(&mut slice[pivot_index + 1 ..]);
}
引用运算符
第5章介绍了运算符地址&和&mut。
一元运算符用于访问引用指向的值。正如我们所见,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章所述,在调试版本中检测到整数溢出,并导致混乱。标准库为未经检查的算术提供了a.wrapping_add(b)等方法。
将整数除以零会触发即使在发布版本中也会出现混乱。整数有一个方法a.checked_div(b)返回一个Option(如果b为零则为None)并且永远不会发生恐慌。
一元 - 否定一个数字。除了无符号整数之外,所有数字类型都支持它。没有一元+运算符。
println!("{}", -100); // -100
println!("{}", -100u32); // error: can't apply unary `-` to type `u32`
println!("{}", +100); // error: expected expression, found `+`
与C中一样,%b计算除法的余数或模数。结果与左操作数具有相同的符号。请注意,%可以用于浮点数和整数:
let x = 1234.567 % 10.0; // approximately 4.567
Rust还继承了C的按位整数运算符,&,|,^,<<和>>。但是,鲁斯特
用途!代替〜为按位NOT:
let hi: u8 = 0xe0;
let lo = !hi; // 0x1f
这意味着!n不能在整数n上用来表示“n为零。”为此,写n == 0。
位移始终在有符号整数类型上进行符号扩展,在无符号整数类型上进行零扩展。由于Rust具有无符号整数,因此它不需要Java的>>>运算符。
与C不同,按位运算具有比比较更高的优先级,因此如果你写x&BIT!= 0,那意味着(x&BIT)!= 0,正如你可能想要的那样。这比C的解释更有用,x&(BIT!= 0),它测试错误的位!
Rust的比较运算符是==,!=,<,<=,>和> =。要比较的两个值必须具有相同的类型。
Rust还有两个短路逻辑运算符&&和||。两个操作数必须具有确切类型bool。
赋值
=运算符可用于分配mut变量及其字段或元素。但是,在Rust中,赋值并不像在其他语言中那样常见,因为默认情况下变量是不可变的。
如第4章所述,赋值移动不可复制类型的值,而不是隐式复制它们。
支持复合赋值:
total += item.price;
这相当于total = total + item.price;。也支持其他运算符: - =,* =,等等。完整列表在本章末尾的表6-1中给出。
与C不同,Rust不支持链接赋值:您不能写a = b = 3将值3分配给a和b。 Rust中的作业很少见,你不会错过这个简写。
Rust没有C的递增和递减运算符++和 - 。
Type Casts类型转换
将值从一种类型转换为另一种类型通常需要在Rust中进行显式转换。
强制转换使用as关键字:
let x = 17; // x is type i32
let index = x as usize; // convert to usize
允许使用几种演员表:
•数字可以从任何内置数字类型转换为任何其他数字类型。将整数转换为另一个整数类型始终是明确定义的。转换为较窄的类型会导致截断。转换为更宽类型的有符号整数是signextended;无符号整数是零扩展的;等等。简而言之,没有任何意外。
但是,在撰写本文时,将大浮点值转换为太小而无法表示它的整数类型会导致未定义的行为。即使在安全的Rust中,这也可能导致崩溃。这是编译器中的一个错误,github.com/rust-lang/rust/ issues / 10184。
•类型为bool,char或类似枚举类型的值的值可以强制转换为任何整数类型。 (我们将在第10章介绍枚举。)
不允许在另一个方向上进行转换,因为bool,char和enum类型都对它们的值有限制,这些限制必须通过运行时检查来强制执行。例如,禁止将u16转换为char类型,因为某些u16值(如0xd800)对应于Unicode代理代码点,因此不会生成有效的char值。有一个标准方法std :: char :: from_u32(),它执行运行时检查并返回一个Option ;但更重要的是,对这种转换的需求变得越来越少。我们通常一次转换整个字符串或流,而Unicode文本上的算法通常是非常重要的,最好留给库。作为例外,可以将u8强制转换为char类型,因为0到255之间的所有整数都是有效的Unicode代码点,用于保存char。
•还允许一些涉及不安全指针类型的强制类型转换。请参见第538页的“原始指针”。
我们说转换通常需要演员。涉及引用类型的一些转换非常简单,即使没有强制转换,语言也会执行它们。一个简单的例子是将mut引用转换为非mut引用。
但是,可能会发生几个更重要的自动转换:
•类型和字符串的值自动转换为类型&str而不进行强制转换。
•类型&Vec 的值自动转换为&[i32]。
•类型&Box 的值自动转换为&Chessboard。
这些被称为deref强制,因为它们适用于实现Deref内置特征的类型。 Deref强制的目的是使智能指针类型(如Box)的行为尽可能与基础值相似。感谢Deref,使用Box 大致就像使用普通Chessboard一样。用户定义的类型也可以实现Deref特征。如果需要编写自己的智能指针类型,请参见“Deref和DerefMut”(第289页)。
闭包
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章中说明。
优先级和相关性
表6-1给出了Rust表达式语法的摘要。运算符按优先顺序列出,从最高到最低。 (与大多数编程语言一样,当表达式包含多个相邻运算符时,Rust具有运算符优先级来确定运算的顺序。例如,在limit <2 * broom.size + 1中,。运算符具有最高优先级,因此字段访问先发生。)
Table 6-1. Expressions
可以有用地链接的所有运算符都是左关联的。也就是说,诸如a-b-c之类的操作链被分组为(a-b)-c,而不是a-(b-c)。可以以这种方式链接的运算符是您可能期望的所有运算符:
- /%+ - << >>&^ | && ||如
比较运算符,赋值运算符和范围运算符…根本无法链接。
Onward
表达式是我们所认为的“运行代码”。它们是Rust程序的一部分,它编译为机器指令。然而,它们只是整个语言的一小部分。
在大多数编程语言中也是如此。程序的第一项工作是运行,但这不是它唯一的工作。程序必须沟通。它们必须是可测试的。他们必须保持井井有条和灵活,以便他们能够继续发展。他们必须与其他团队构建的代码和服务进行互操作。甚至只是运行,像Rust这样的静态类型语言的程序需要更多的工具来组织数据而不仅仅是元组和数组。
接下来,我们将花几个章节讨论这个领域的特性:模块和板条箱,它们提供程序结构,然后是结构和枚举,它们对你的数据做同样的事情。
首先,我们将专门针对出现问题时要做的重要主题。