表达式(Expressions)

LISP程序员知道一切的价值,却对代价一无所知. —Alan Perlis, epigram #55

原文

LISP programmers know the value of everything, but the cost of nothing. —Alan Perlis, epigram #55

在本章中,我们将介绍Rust的 表达式(expressions) ,它是构成Rust函数体的积木.一些概念(如闭包和迭代器)足够深入,我们稍后将专门用一章来介绍它们.现在,我们的目标是在几页中涵盖尽可能多的语法.

一个表达式语言(An Expression Language)

Rust在视觉上类似于C语言家族,但这有点像诡计.在C中, 表达式(expressions) ,看起来像这样的代码:

  1. 5 * (fahr-32) / 9

语句(statements) ,看起来更像这样:

  1. for (; begin != end; ++begin) {
  2. if (*begin == target)
  3. break;
  4. }

之间有明显的区别.

表达式有值.语句没有.

Rust是所谓的 表达式语言(expression language).这意味着它遵循较古老的传统,可追溯到Lisp,其中表达式完成所有工作.

在C中,ifswitch是语句.它们不会产生值,也不能在表达式中使用它们.在Rust中,ifmatch 可以(can) 产生值.我们已经在第2章中看到了一个产生数值的match表达式:

  1. pixels[r * bounds.0 + c] =
  2. match escapes(Complex { re: point.0, im: point.1 }, 255) {
  3. None => 0,
  4. Some(count) => 255 - count as u8
  5. };

if表达式可用于初始化变量:

  1. let status =
  2. if cpu.temperature <= MAX_TEMP {
  3. HttpStatus::Ok
  4. } else {
  5. HttpStatus::ServerError // server melted
  6. };

match表达式可以作为参数传递给函数或宏:

  1. println!("Inside the vat, you see {}.",
  2. match vat.contents {
  3. Some(brain) => brain.desc(),
  4. None => "nothing of interest"
  5. });

这解释了为什么Rust没有C的三元运算符(expr1 ? expr2: expr3).在C语言中,它是if语句的一个方便的表达式类似物.在Rust中它是多余的:if表达式处理两种情况.

C中的大多数控制流工具都是语句.在Rust中,它们都是表达式.

块和分号(Blocks and Semicolons)

块也是表达式.块生成一个值,可以在需要值的任何地方使用:

  1. let display_name = match post.author() {
  2. Some(author) => author.name(),
  3. None => {
  4. let network_info = post.get_network_metadata()?;
  5. let ip = network_info.client_address();
  6. ip.to_string()
  7. }
  8. };

Some(author) =>之后的代码是简单表达式author.name().None =>之后的代码是块表达式.这对Rust来说没什么区别.块的值是其最后一个表达式ip.to_string()的值.

请注意,该表达式后面没有分号.大多数Rust代码行都以分号或大括号结尾,就像C或Java一样.如果一个块看起来像C代码,在所有熟悉的地方都有分号,那么它将像C块一样运行,其值将是().正如我们在第2章中提到的,当你将分号放在块的最后一行时,你正在使该块产生一个值—最终表达式的值.

在某些语言中,特别是JavaScript,你可以省略分号,而语言就为你填写—这是一个小小的便利.这是不同的.在Rust中,分号实际上意味着什么.

  1. let msg = {
  2. // let-declaration: semicolon is always required
  3. let dandelion_control = puffball.open();
  4. // expression + semicolon: method is called, return value dropped
  5. dandelion_control.release_all_seeds(launch_codes);
  6. // expression with no semicolon: method is called,
  7. // return value stored in `msg`
  8. dandelion_control.get_status()
  9. };

块的这种能力,包含声明并在最后产生一个值是一个很好的功能,很快就会感觉很自然.一个缺点是,当你意外地遗漏分号时,它会导致奇怪的错误消息.

  1. ...
  2. if preferences.changed() {
  3. page.compute_size() // oops, missing semicolon
  4. }
  5. ...

如果你在C或Java程序中犯了这个错误,编译器只会指出你漏掉了一个分号.这是Rust说的:

  1. error[E0308]: mismatched types
  2. --> expressions_missing_semicolon.rs:19:9
  3. |
  4. 19 | page.compute_size() // oops, missing semicolon
  5. | ^^^^^^^^^^^^^^^^^^^ expected (), found tuple
  6. |
  7. = note: expected type `()`
  8. found type `(u32, u32)`

Rust假定你故意省略了这个分号;它没有考虑它只是一个错字的可能性.结果是一个混乱的错误消息.当你看到expected type `()` 时,首先查找缺少的分号.

块中也允许 空语句(Empty statements) .空语句由一个游离的分号组成,就是它本身:

  1. loop {
  2. work();
  3. play();
  4. ; // <-- empty statement
  5. }

Rust允许这样做,遵循C的传统.除了传达轻微的忧郁感之外,空语句什么都不做.我们只是为了完整性而提到它们.

声明(Declarations)

除了表达式和分号之外,块还可以包含任意数量的声明.最常见的是let声明,它声明了局部变量:

  1. let name: type = expr;

类型和初始化器是可选的.分号是必需的.

let声明可以在不初始化的情况下声明变量.然后可以使用稍后的赋值初始化变量.这偶尔会有用,因为有时变量应该从某种控制流结构的中间初始化:

  1. let name;
  2. if user.has_nickname() {
  3. name = user.nickname();
  4. } else {
  5. name = generate_unique_name();
  6. user.register(&name);
  7. }

这里有两种不同的方式可以初始化局部变量name,但无论哪种方式,它都只将被初始化一次,因此name不需要声明为mut.

在变量初始化之前使用变量是错误的.(这与移动后使用值的错误密切相关.Rust真的希望你只在它们存在时使用值!)

你可能偶尔会看到似乎重新声明现在变量的代码,如下所示:

  1. for line in file.lines() {
  2. let line = line?;
  3. ...
  4. }

这相当于:

  1. for line_result in file.lines() {
  2. let line = line_result?;
  3. ...
  4. }

let声明创建一个不同类型的新的第二个变量.line_result的类型是Result<String, io::Error>.第二个变量lineString.赋予第二个变量与第一个变量相同的名称是合法的.在本书中,我们将坚持在这种情况下使用_result后缀,以便所有变量都具有不同的名称.

块也可以包含 项目声明(item declarations) .项只是可以在程序或模块中全局出现的任何声明,例如fn,structuse.

后面的章节将详细介绍项目.就目前而言,fn就是一个充分的例子.任何块都可能包含一个fn:

  1. use std::io;
  2. use std::cmp::Ordering;
  3. fn show_files() -> io::Result<()> {
  4. let mut v = vec![];
  5. ...
  6. fn cmp_by_timestamp_then_name(a: &FileInfo, b: &FileInfo) -> Ordering {
  7. a.timestamp.cmp(&b.timestamp) // first, compare timestamps
  8. .reverse() // newest file first
  9. .then(a.path.cmp(&b.path)) // compare paths to break ties
  10. }
  11. v.sort_by(cmp_by_timestamp_then_name);
  12. ...
  13. }

当在块内声明fn时,其作用域是整个块—也就是说它可以在整个封闭块中 使用(used) .但是嵌套的fn无法访问在作用域内的局部变量或参数.例如,函数cmp_by_timestamp_then_name无法直接使用v.(Rust也有闭包,它可以看到封闭的作用域.见第14章.)

块甚至可以包含整个模块.这似乎有点多—我们真的需要能够将每一段语言嵌套在其他所有部分中吗?—但是程序员(尤其是使用宏的程序员)有一种方法可以找到语言提供的每一个正交性的用途.

if和match(if and match)

if表达式的形式很熟悉:

  1. if condition1 {
  2. block1
  3. } else if condition2 {
  4. block2
  5. } else {
  6. block_n
  7. }

每个 condition 必须是bool类型的表达式;一如既往,Rust不会隐式地将数字或指针转换为布尔值.

与C不同,在条件周围不需要括号.实际上,如果存在不必要的括号,rustc将发出警告.然而,花括号是必需的.

else if以及最后的else是可选的.没有else块的if表达式就像它有一个空的else块一样.

match表达式类似于C语言switch语句,但更灵活. 一个简单的例子:

  1. match code {
  2. 0 => println!("OK"),
  3. 1 => println!("Wires Tangled"),
  4. 2 => println!("User Asleep"),
  5. _ => println!("Unrecognized Error {}", code)
  6. }

这是switch语句可以做的事情.根据代码的值,这个match表达式的四个分支中的一个将执行.通配符模式_匹配所有内容,因此它用作dafault:情况.

编译器可以使用跳转表(jump table)来优化这种match,就像C++中的switch语句一样.当匹配的每个分支产生常量值时,应用类似的优化.在这种情况下,编译器构建这些值的数组,并将匹配编译为数组访问.除了边界检查之外,编译后的代码中根本没有分支.

mathc的多功能性源于各种支持的 模式(patterns ) ,这些模式可用于每个分支的=>的左侧.上面,每个模式只是一个常数整数.我们还展示了区分两种Option值的match表达式:

  1. match params.get("name") {
  2. Some(name) => println!("Hello, {}!", name),
  3. None => println!("Greetings, stranger.")
  4. }

这只是模式能做什么的一个提示.模式可以匹配一系列值.它可以解构元组.它可以匹配结构的各个字段.它可以追逐引用,借用值的一部分,等等.Rust的模式自己就是一个微型语言.我们将在第10章中用几页的篇幅来介绍它们.

match表达式的一般形式是:

  1. match value {
  2. pattern => expr,
  3. ...
  4. }

如果 expr 是块,则可以删除分支之后的逗号.

Rust会检查给定的值依次比对每个模式,从第一个模式开始.当模式匹配时,计算对应的 expr 并完成match表达式;不再检查其他模式.至少有一个模式必须匹配.Rust禁止不覆盖所有可能值的match表达式:

  1. let score = match card.rank {
  2. Jack => 10,
  3. Queen => 10,
  4. Ace => 11
  5. }; // error: nonexhaustive patterns

if表达式的所有块必须产生相同类型的值:

  1. let suggested_pet =
  2. if with_wings { Pet::Buzzard } else { Pet::Hyena }; // ok
  3. let favorite_number =
  4. if user.is_hobbit() { "eleventy-one" } else { 9 }; // error
  5. let best_sports_team =
  6. if is_hockey_season() { "Predators" }; // error

(最后一个例子是个错误,因为在七月,结果将是().)

同样,match表达式的所有分支必须具有相同的类型:

  1. let suggested_pet =
  2. match favorites.element {
  3. Fire => Pet::RedPanda,
  4. Air => Pet::Buffalo,
  5. Water => Pet::Orca,
  6. _ => None // error: incompatible types
  7. };

if let(if let)

还有一种if形式,if let表达式:

  1. if let pattern = expr {
  2. block1
  3. } else {
  4. block2
  5. }

给定的 exprpattern 匹配,在这种情况下 block1 运行,否则它不运行, block2 运行.有时这是从OptionResult中获取数据的好方法:

  1. if let Some(cookie) = request.session_cookie {
  2. return restore_session(cookie);
  3. }
  4. if let Err(err) = present_cheesy_anti_robot_task() {
  5. log_robot_attempt(err);
  6. politely_accuse_user_of_being_a_robot();
  7. } else {
  8. session.mark_as_human();
  9. }

if let绝对不是 必须(necessary) 使用的,因为match可以做任何if let能做的事情.if let表达式是只有一个模式match的简写:

  1. match expr{
  2. pattern => { block1 }
  3. _ => { block2 }
  4. }

循环(Loops)

有4种循环表达式:

  1. while condition {
  2. block
  3. }
  4. while let pattern = expr {
  5. block
  6. }
  7. loop{
  8. block
  9. }
  10. for pattern in collection {
  11. block
  12. }

在Rust中,循环是表达式,但它们不会产生有用的值.循环的值是().

while循环的行为与C等价物完全相同,但同样, condition 必须是精确类型bool.

while let循环类似于if let.在每次循环迭代开始时, expr 的值或者匹配给定的 pattern ,在这种情况下块运行,否则不运行,在这种情况下循环退出.

使用loop编写无限循环.它会一直重复执行该 (或者直到达到breakreturn,或者线程发生panics).

for循环计算 collection 表达式,然后为集合中的每个值计算一次*block*.支持许多集合类型. 标准Cfor循环:

  1. for (int i = 0; i < 20; i++) {
  2. printf("%d\n", i);
  3. }

在Rust中这样写:

  1. for i in 0..20 {
  2. println!("{}", i);
  3. }

与在C中一样,最后打印的数字是19.

..运算符生成一个 range ,一个带有两个字段的简单结构:startend.0..20std::ops::Range { start: 0, end: 20 }相同. Range可以与for循环一起使用,因为Range是一个可迭代类型:它实现了std::iter::IntoIteratortrait,我们将在第15章讨论它.标准集合都是可迭代的,数组和切片也是可迭代的.

为了与Rust的移动语义保持一致,对值的for循环会消耗该值:

  1. let strings: Vec<String> = error_messages();
  2. for s in strings { // each String is moved into s here...
  3. println!("{}", s);
  4. } // ...and dropped here
  5. println!("{} error(s)", strings.len()); // error: use of moved value

这可能不方便.简单的补救措施是循环遍历对集合的引用.然后,循环变量将是对集合中每个项的引用:

  1. for rs in &strings {
  2. println!("String {:?} is at address {:p}.", *rs, rs);
  3. }

这里&strings的类型是&Vec<String>,rs的类型是&String.

迭代mut引用提供对每个元素的mut引用:

  1. for rs in &mut strings { // the type of rs is &mut String
  2. rs.push('\n'); // add a newline to each string
  3. }

第15章更详细地介绍了for循环,并展示了使用迭代器的许多其他方法.

break表达式退出封闭循环.(在Rust中,break仅在循环中起作用.在match表达式中没有必要,在这方面与switch语句不同.)

continue表达式跳转到下一个循环迭代:

  1. // Read some data, one line at a time.
  2. for line in input_lines {
  3. let trimmed = trim_comments_and_whitespace(line);
  4. if trimmed.is_empty() {
  5. // Jump back to the top of the loop and
  6. // move on to the next line of input.
  7. continue;
  8. }
  9. ...
  10. }

for循环中,continue前进到集合中的下一个值.如果没有更多值,则循环退出.同样,在while循环中,continue重新检查循环条件.如果它现在为假,则循环退出.

循环可以用生命周期 标记(labeled) .在下面的示例中,'search:是外部for循环的标签.因此break 'search退出该循环,而不是内循环.

  1. 'search:
  2. for room in apartment {
  3. for spot in room.hiding_spots() {
  4. if spot.contains(keys) {
  5. println!("Your keys are {} in the {}.", spot, room);
  6. break 'search;
  7. }
  8. }
  9. }

标签(label)也可以和continue一起使用.

return表达式(return Expressions)

return表达式退出当前函数,返回一个值给调用者.

return不带值是return()的简写:

  1. fn f() { // return type omitted: defaults to ()
  2. return; // return value omitted: defaults to ()
  3. }

就像break表达一样,return可以放弃正在进行的工作.例如,回顾第2章中,我们使用了?运算符检查错误,调用可能失败的函数后:

  1. let output = File::create(filename)?;

我们解释这是match表达式的简写:

  1. let output = match File::create(filename) {
  2. Ok(f) => f,
  3. Err(err) => return Err(err)
  4. };

此代码首先调用File::create(filename).如果返回Ok(f),那么整个匹配表达式的计算结果为f,因此f存储在output中,我们继续跟着match的下一行代码.

否则,我们将匹配Err(err)并点击返回表达式.当发生这种情况时,我们正在计算match表达式以确定变量output的值,这并不重要.我们放弃所有这些,并退出封闭函数,返回从File::create()中得到的任何错误.

我们会在第152页的”传播错误(Propagating Errors)”中的更完整地涵盖?操作符.

为什么Rust有循环(Why Rust Has loop)

Rust编译器分析通过你程序的控制流的几个部分.

  • Rust检查函数的每个路径都返回预期返回类型的值. 要正确执行此操作,需要知道是否可以到达函数的末尾.

  • Rust检查局部变量从未未初始化就使用.这需要检查函数中的每个路径,以确保无法到达使用变量的位置,而没有经过初始化它的代码.

  • Rust警告无法访问的代码.如果 没有(no) 通过该函数的路径到达代码,代码就是无法访问的.

这些被称为 流敏感(flow-sensitive) 分析.它们并不是什么新鲜事;多年来,Java已经进行了”明确赋值(definite assignment)”分析,类似于Rust的分析.

当执行这种规则时,语言必须在简单性,这使得程序员更容易弄清楚编译器有时在说什么—和聪明性,这可以帮助消除错误警告和编译器拒绝完全安全的程序的情况,之间取得平衡.Rust选择简单性.它的流敏感分析根本不检查循环条件,而只是假设程序中的任何条件都可以是真或假.

这导致Rust拒绝一些安全程序:

  1. fn wait_for_process(process: &mut Process) -> i32 {
  2. while true {
  3. if process.wait() {
  4. return process.exit_code();
  5. }
  6. }
  7. }
  8. // error: not all control paths return a value

这里的错误是假的.实际上,如果不返回值,则无法到达函数的末尾.

loop表达式对这个问题提供”说出你的意思(say-what-you-mean)”解决方案.

Rust的类型系统也受控制流的影响.之前我们说if表达式的所有分支都必须具有相同的类型.但是对于以breakreturn表达式结束的,无限loop或调用panic!()`std::process::exit()的块强制执行此规则将是愚蠢的.所有这些表达的共同之处在于它们永远不会以通常的方式结束,从而产生值.breakreturn突然退出当前块;无限loop永远不会结束;等等.

所以在Rust中,这些表达式没有普通类型.未正常完成的表达式将被指定为特殊类型!,并且他们不受关于必须匹配的类型的规则的约束.你可以看到!std::process::exit()的函数签名中:

  1. fn exit(code: i32) -> !

!表示exit()永远不会返回.这是一个 发散函数(divergent function) .

你可以使用相同的语法编写自己的发散函数,这在某些情况下是完全自然的:

  1. fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {
  2. socket.listen();
  3. loop {
  4. let s = socket.accept();
  5. handler.handle(s);
  6. }
  7. }

当然,如果函数可以正常返回,Rust会认为这是一个错误.

本章的部分内容是关注控制流程.其余部分包括Rust函数,方法和运算符.

函数和方法调用(Function and Method Calls)

调用函数和调用方法的语法在Rust中与在许多其他语言中相同:

  1. let x = gcd(1302, 462); // function call
  2. let room = player.location(); // method call

在这里的第二个例子中,player是虚构的类型Player的变量,它有一个虚构的.location()方法.(我们将在第9章开始讨论用户定义的类型时展示如何定义自己的方法.)

Rust通常会在引用和它们引用的值之间做出明显的区分.如果将&i32传递给需要i32的函数,那就是类型错误.你会注意到.操作符稍稍放松了这些规则.在方法调用player.location()中,play可能是一个Play,一个类型为&Player的引用,或着一个类型为Box<Player>Rc<Player>的智能指针..location()方法可能通过值或通过引用来接受player.相同的.location()语法适用于所有情况,因为Rust的.运算符会根据需要自动解引用player或借用其引用.

第三种语法用于调用静态方法,如Vec::new().

  1. let mut numbers = Vec::new(); // static method call

静态和非静态方法之间的区别与面向对象语言相同:非静态方法在值上调用(如my_vec.len()),静态方法在类型上调用(如Vec::new()).

当然,方法调用可以链式的:

  1. Iron::new(router).http("localhost:3000".unwrap();

Rust语法的一个怪癖是在函数调用或方法调用中,泛型类型Vec<T>的通常语法不起作用:

  1. return Vec<i32>::with_capacity(1000); // error: something about chained comparisons
  2. let ramp = (0 .. n).collect<Vec<i32>>(); // same error

问题是在表达式中,<是小于运算符.在这种情况下,Rust编译器有助于建议编写::<T>而不是<T>,这解决了问题:

  1. return Vec::<i32>::with_capacity(1000); // ok, using ::<
  2. let ramp = (0 .. n).collect::<Vec<i32>>(); // ok, using ::<

符号::<...>在Rust社区中被亲切地称为 涡轮机(turbofish) .

或者,通常可以删除类型参数并让Rust推断它们:

  1. return Vec::with_capacity(10); // ok, if the fn return type is Vec<i32>
  2. let ramp: Vec<i32> = (0 .. n).collect(); // ok, variable's type is given

无论何时可以推断出类型,省略类型都被认为是好的风格.

字段和元素(Fields and Elements)

结构的字段的访问使用熟悉的语法.元组是相同的,除了它们的字段有数字而不是名字:

  1. game.black_pawns // struct field
  2. coords.1 // tuple element

如果点左边的值是引用或智能指针类型,则会自动解引用,就像方法调用一样.

方括号访问数组,切片或向量的元素:

  1. pieces[i] // array element

括号左侧的值将自动解引用.

像这三个表达式称为 左值(lvalues) ,因为它们可以出现在赋值的左侧:

  1. game.black_pawns = 0x00ff0000_00000000_u64;
  2. coords.1 = 0;
  3. pieces[2] = Some(Piece::new(Black, Knight, coords));

当然m只有当game,coordspieces被声明为mut变量时才允许这样做.

从数组或向量中提取切片很简单:

  1. let second_half = &game_moves[midpoint .. end];

这里game_moves可以是数组,切片或向量;无论如何,结果是一个借用的切片,长度为end .. midpoint].game_moves被认为是在second_half的生命周期中被借用的.

..运算符允许省略任一操作数;它根据存在的操作数产生最多四种不同类型的对象:

  1. .. // RangeFull
  2. a .. // RangeFrom { start: a }
  3. .. b // RangeTo { end: b }
  4. a .. b // Range { start: a, end: b }

Rust范围是 半开放的(half-open) :它们包括起始值(如果有),但不包括结束值.范围0 .. 4包括数字0,1,23.

只有包含起始值的范围才是可迭代的,因为循环必须具有某个起始位置.但在数组切片中,所有四种形式都很有用.如果省略范围的开始或结束,则默认数据的开始或结束被切片.

因此,快速排序(一种经典的分而治之排序算法)的实现可能看起来一部分像这样:

  1. fn quicksort<T: Ord>(slice: &mut [T]) {
  2. if slice.len() <= 1 {
  3. return; // Nothing to sort.
  4. }
  5. // Partition the slice into two parts, front and back.
  6. let pivot_index = partition(slice);
  7. // Recursively sort the front half of `slice`.
  8. quicksort(&mut slice[.. pivot_index]);
  9. // And the back half.
  10. quicksort(&mut slice[pivot_index + 1 ..]);
  11. }

引用操作符(Reference Operators)

第5章介绍了取地址运算符&&mut.

一元*运算符用于访问引用指向的值.正如我们所见,你使用.运算符访问字段或方法时,Rust会自动跟随引用,因此只有当我们想要读取或写入引用指向的整个值时才需要*运算符.

例如,有时迭代器会生成引用,但程序需要底层值:

  1. let padovan: Vec<u64> = compute_padovan_sequence(n);
  2. for elem in &padovan {
  3. draw_triangle(turtle, *elem);
  4. }

在这个例子中,elem的类型是&u64,所以*elem的类型是u64.

算术,按位,比较和逻辑运算符(Arithmetic, Bitwise, Comparison, and Logical Operators)

Rust的二元运算符与许多其他语言的运算符类似.为了节省时间,我们假设熟悉其中一种语言,并专注于Rust背离传统的几点.

Rust有通常的算术运算符,+,-,*,/%.如第3章所述,在调试版本中检测到整数溢出,并导致恐慌(panic).标准库为未经检查的算术提供了a.wrapping_add(b)等方法.

将整数除以零即使在发布版本中也会触发恐慌(panic).整数有一个方法a.checked_div(b)返回一个Option(如果b为零则为None)并且永远不会发生恐慌.

一元-运算符取负一个数字.除了无符号整数外,它支持所有数字类型,没有一元+运算符.

  1. println!("{}", -100); // -100
  2. println!("{}", -100u32); // error: can't apply unary `-` to type `u32`
  3. println!("{}", +100); // error: expected expression, found `+`

与在C中一样,a % b计算除法的余数或模数.结果与左操作数具有相同的符号.请注意,%可用于浮点数和整数:

  1. let x = 1234.567 % 10.0; // approximately 4.567

Rust也继承了C的按位整数运算符,&,|,^,<<,>>.但是,Rust使用!而不是~为按位NOT:

这意味着!n不能用在整数n上表示”n等于0.”为此,写n == 0.

位移始终在有符号整数类型上进行符号扩展,对无符号整数类型进行零扩展.由于Rust具有无符号整数,因此它不需要Java的>>>运算符.

与C不同,按位运算具有比比较更高的优先级,因此如果你写x & BIT != 0,那就意味着(x & BIT) != 0,正如你可能想要的那样.这比C的解释更有用,`x & (BIT != 0),测试错误的位!

Rust的比较运算符是==,!=,<,<=,>,和>=.要比较的两个值必须具有相同的类型.

Rust也有两个短路逻辑运算符&&||.两个操作数必须具有确切类型bool.

赋值(Assignment)

=运算符可用于给mut变量及其字段或元素赋值.但是,在Rust中,赋值并不像在其他语言中那样常见,因为默认情况下变量是不可变的.

如第4章所述,赋值 移动(moves) 不可复制类型的值,而不是隐式复制它们.

支持复合赋值:

  1. total += item.price;

这相当于total = total + item.price;.也支持其他运算符:-=,*=,等等.完整列表在本章末尾的表6-1中给出.

与C不同,Rust不支持链式赋值:你不能写a = b = 3来为ab赋值3.Rust中的赋值很少见,你不会错过这个简写.

Rust没有C的递增和递减运算符++--.

类型强制转换(Type Casts)

将值从一种类型转换为另一种类型通常需要在Rust中使用显式强制转换.强制转换使用as关键字:

  1. let x = 17; // x is type i32
  2. let index = x asusize; // convert to usize

允许的几种强制转换:

  • 数字可以从任何内置数字类型强制转换为任何其他数字类型.

将整数强制转换为另一个整数类型始终是明确定义的.转换为较窄的类型会导致截断.有符号整数强制转换为更宽的类型是符号扩展;无符号整数是零扩展;等等.简而言之,没有惊喜.

但是,在撰写本书时,将一个大的浮点值转换为一个太小而不能表示它的整数类型会导致未定义的行为.即使在安全的Rust中,这也可能导致崩溃.这是编译器中的一个bug,github.com/rust-lang/prog/issues/10184.

  • 类型为bool,char或类似C的enum类型的值可以强制转换为任何整数类型.(我们将在第10章中介绍枚举.)

不允许在另一个方向上进行转换,因为bool,charenum类型都对它们的值有限制,这些限制必须通过运行时检查来强制执行.例如,禁止将u16强制转换为char类型,因为某些u16值(如0xd800)对应于Unicode代理代码点,因此不会生成有效的char值.有一个标准方法std::char::from_u32(),它执行运行时检查并返回一个Option<char>;但更重要的是,对这种转换的需求变得越来越少.我们通常一次转换整个字符串或流,Unicode文本上的算法通常是非常重要的,最好留给库.

作为例外,可以将u8强制转换为char类型,因为0到255之间的所有整数都是有效的Unicode代码点,用于保存char.

  • 某些涉及不安全指针类型的强制类型转换也是允许的.请参见第538页的”裸指针(Raw Pointers)”.

我们说转换 通常(usually) 需要强制转换.涉及引用类型的一些转换非常简单,即使没有强制转换,语言也会执行它们.一个简单的例子是将mut引用转换为非mut引用.

不过,还会发生一些更重要的自动转换:

  • 类型&String的值自动转换为类型&str而不使用强制转换.

  • 类型&Vec<i32>的值自动转换为&[i32].

  • 类型&Box<Chessboard>的值自动转换为&Chessboard.

这些被称为 解引用强制多态(deref coercions) ,因为它们适用于实现Deref内置trait的类型.Deref强制多态的目的是使智能指针类型(如Box)的行为尽可能与基础值相似.感谢Deref,使用Box<Chessboard>大致就像使用普通Chessboard一样.

用户定义的类型也可以实现Dereftrait.当你需要编写自己的智能指针类型时,请参见第289页的”Deref和DerefMut”.

闭包(Closures)

Rust有闭包,轻量级的函数类值.闭包通常由一个参数列表组成,在竖条之间给出,后跟一个表达式:

  1. let is_even = |x| x % 2 == 0;

Rust推断出参数类型和返回类型.你也可以显式地将它们写出来,就像对函数一样.如果你确实指定了一个返回类型,那么为了语法的完整性,闭包的主体必须是一个块:

  1. let is_even = |x: u64| -> bool x % 2 == 0; // error
  2. let is_even = |x: u64| -> bool { x % 2 == 0 }; // ok

调用闭包使用与调用函数相同的语法:

  1. assert_eq!(is_even(14), true);

闭包是Rust最令人愉快的特性之一,关于它们还有很多要说的.我们将在第14章说明.

优先级和结合性(Precedence and Associativity)

表6-1总结了Rust表达式语法.运算符按优先级顺序列出,从最高到最低.(与大多数编程语言一样,当表达式包含多个相邻运算符时,Rust用 运算符优先级(operator precedence) 来确定运算的顺序.例如,在limit <2 * broom.size + 1中,.运算符具有最高优先级,因此字段访问首先发生.)

表6-1. 表达式.

表达式类型 示例 相关traits
数组字面量 [1, 2, 3]
重复数组字面量 [0; 50]
元组 (6, "crullers")
组(Grouping) (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); }
while ok { ok = f(); }
while let Some(x) = it.next() { f(x); }
loop { next_event(); }
break
continue
return 0
std::iter::IntoIterator
宏调用 println!("ok")
路径(Path) 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,
Index, IndexMut
Deref,DerefMut
错误检查 create_dir("tmp")?
逻辑/按位非 !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`
范围(Range) start .. stop
赋值 x = val
复合赋值 x *= 1
x /= 1
x %= 1
x += 1
x -= 1
x <<= 1
x >>= 1
x &= 1
x ^= 1
`x
= 1`
MulAssign
DivAssign
RemAssign
AddAssign
SubAssign
ShlAssign
ShrAssign
BitAndAssign
BitXorAssign
BitOrAssign
闭包 ` x, y x + y`

所有可以有用地链接的运算符都是左关联的.也就是说,诸如a-b-c之类的操作链被分组为(a – b) – c ,而不是a – (b – c).可以用这种方式链接的运算符是你可能期望的所有运算符:

  1. * / % + << >> & ^ | && || as

比较运算符,赋值运算符和范围运算符. .不能链接.

继续向前(Onward)

表达式是我们所认为的”运行代码”.它们是Rust程序的一部分,编译为机器指令.然而,它们只占整个语言的一小部分.

在大多数编程语言中也是如此.程序的第一项工作是运行,但这不是它唯一的工作.程序必须沟通.它们必须是可测试的.它们必须保持组织性和灵活性,这样才能继续发展.它们必须与其他团队构建的代码和服务进行互操作. 即使只是为了运行,像Rust这样的静态类型语言的程序需要更多的工具来组织数据而不仅仅是元组和数组.

接下来,我们将花几个章节讨论这个领域的特性:模块和crates,它们提供程序结构,然后是结构和枚举,它们对你的数据做同样的事情.

首先,我们将用几页来讨论当事情出错时该做些什么的重要话题.