LISP程序员知道一切的价值,却对代价一无所知. --Alan Perlis, epigram #55
原文
LISP programmers know the value of everything, but the cost of nothing. --Alan Perlis, epigram #55
在本章中,我们将介绍Rust的 表达式(expressions) ,它是构成Rust函数体的积木.一些概念(如闭包和迭代器)足够深入,我们稍后将专门用一章来介绍它们.现在,我们的目标是在几页中涵盖尽可能多的语法.
Rust在视觉上类似于C语言家族,但这有点像诡计.在C中, 表达式(expressions) ,看起来像这样的代码:
5 * (fahr-32) / 9
和 语句(statements) ,看起来更像这样:
for (; begin != end; ++begin) {
if (*begin == target)
break;
}
之间有明显的区别.
表达式有值.语句没有.
Rust是所谓的 表达式语言(expression language).这意味着它遵循较古老的传统,可追溯到Lisp,其中表达式完成所有工作.
在C中,if
和switch
是语句.它们不会产生值,也不能在表达式中使用它们.在Rust中,if
和match
可以(can) 产生值.我们已经在第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中,它们都是表达式.
块也是表达式.块生成一个值,可以在需要值的任何地方使用:
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说的:
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假定你故意省略了这个分号;它没有考虑它只是一个错字的可能性.结果是一个混乱的错误消息.当你看到expected type `()`
时,首先查找缺少的分号.
块中也允许 空语句(Empty statements) .空语句由一个游离的分号组成,就是它本身:
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
,但无论哪种方式,它都只将被初始化一次,因此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
后缀,以便所有变量都具有不同的名称.
块也可以包含 项目声明(item declarations) .项只是可以在程序或模块中全局出现的任何声明,例如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
时,其作用域是整个块--也就是说它可以在整个封闭块中 使用(used) .但是嵌套的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
块一样.
match
表达式类似于C语言switch
语句,但更灵活. 一个简单的例子:
match code {
0 => println!("OK"),
1 => println!("Wires Tangled"),
2 => println!("User Asleep"),
_ => println!("Unrecognized Error {}", code)
}
这是switch
语句可以做的事情.根据代码的值,这个match
表达式的四个分支中的一个将执行.通配符模式_
匹配所有内容,因此它用作dafault
:情况.
编译器可以使用跳转表(jump table)来优化这种match
,就像C++中的switch
语句一样.当匹配的每个分支产生常量值时,应用类似的优化.在这种情况下,编译器构建这些值的数组,并将匹配编译为数组访问.除了边界检查之外,编译后的代码中根本没有分支.
mathc
的多功能性源于各种支持的 模式(patterns
) ,这些模式可用于每个分支的=>
的左侧.上面,每个模式只是一个常数整数.我们还展示了区分两种Option
值的match
表达式:
match params.get("name") {
Some(name) => println!("Hello, {}!", name),
None => println!("Greetings, stranger.")
}
这只是模式能做什么的一个提示.模式可以匹配一系列值.它可以解构元组.它可以匹配结构的各个字段.它可以追逐引用,借用值的一部分,等等.Rust的模式自己就是一个微型语言.我们将在第10章中用几页的篇幅来介绍它们.
match
表达式的一般形式是:
match value {
pattern => expr,
...
}
如果 expr
是块,则可以删除分支之后的逗号.
Rust会检查给定的值依次比对每个模式,从第一个模式开始.当模式匹配时,计算对应的 expr
并完成match
表达式;不再检查其他模式.至少有一个模式必须匹配.Rust禁止不覆盖所有可能值的match
表达式:
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
(最后一个例子是个错误,因为在七月,结果将是()
.)
同样,match
表达式的所有分支必须具有相同的类型:
let suggested_pet =
match favorites.element {
Fire => Pet::RedPanda,
Air => Pet::Buffalo,
Water => Pet::Orca,
_ => None // error: incompatible types
};
还有一种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) = present_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
if let
绝对不是 必须(necessary) 使用的,因为match
可以做任何if let
能做的事情.if let
表达式是只有一个模式match
的简写:
match expr{
pattern => { block1 }
_ => { block2 }
}
有4种循环表达式:
while condition {
block
}
while let pattern = expr {
block
}
loop{
block
}
for pattern in collection {
block
}
在Rust中,循环是表达式,但它们不会产生有用的值.循环的值是()
.
while
循环的行为与C等价物完全相同,但同样, condition
必须是精确类型bool
.
while let
循环类似于if let
.在每次循环迭代开始时, expr
的值或者匹配给定的 pattern
,在这种情况下块运行,否则不运行,在这种情况下循环退出.
使用loop
编写无限循环.它会一直重复执行该 块
(或者直到达到break
或return
,或者线程发生panics).
for
循环计算 collection
表达式,然后为集合中的每个值计算一次*
block*
.支持许多集合类型. 标准Cfor
循环:
for (int i = 0; i < 20; i++) {
printf("%d\n", i);
}
在Rust中这样写:
for i in 0..20 {
println!("{}", i);
}
与在C中一样,最后打印的数字是19.
..
运算符生成一个 range ,一个带有两个字段的简单结构:start
和end
.0..20
与std::ops::Range { start: 0, end: 20 }
相同. Range可以与for
循环一起使用,因为Range
是一个可迭代类型:它实现了std::iter::IntoIterator
trait,我们将在第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);
}
这里&strings
的类型是&Vec<String>
,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
仅在循环中起作用.在match
表达式中没有必要,在这方面与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
循环中,continue
前进到集合中的下一个值.如果没有更多值,则循环退出.同样,在while
循环中,continue
重新检查循环条件.如果它现在为假,则循环退出.
循环可以用生命周期 标记(labeled) .在下面的示例中,'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;
}
}
}
标签(label)也可以和continue
一起使用.
return
表达式退出当前函数,返回一个值给调用者.
return
不带值是return()
的简写:
fn f() { // return type omitted: defaults to ()
return; // return value omitted: defaults to ()
}
就像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)
,那么整个匹配表达式的计算结果为f
,因此f
存储在output
中,我们继续跟着match
的下一行代码.
否则,我们将匹配Err(err)
并点击返回表达式.当发生这种情况时,我们正在计算match
表达式以确定变量output
的值,这并不重要.我们放弃所有这些,并退出封闭函数,返回从File::create()
中得到的任何错误.
我们会在第152页的"传播错误(Propagating Errors)"中的更完整地涵盖?
操作符.
Rust编译器分析通过你程序的控制流的几个部分.
-
Rust检查函数的每个路径都返回预期返回类型的值. 要正确执行此操作,需要知道是否可以到达函数的末尾.
-
Rust检查局部变量从未未初始化就使用.这需要检查函数中的每个路径,以确保无法到达使用变量的位置,而没有经过初始化它的代码.
-
Rust警告无法访问的代码.如果 没有(no) 通过该函数的路径到达代码,代码就是无法访问的.
这些被称为 流敏感(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();
}
}
}
// error: not all control paths return a value
这里的错误是假的.实际上,如果不返回值,则无法到达函数的末尾.
loop
表达式对这个问题提供"说出你的意思(say-what-you-mean)"解决方案.
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函数,方法和运算符.
调用函数和调用方法的语法在Rust中与在许多其他语言中相同:
let x = gcd(1302, 462); // function call
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()
.
let mut numbers = Vec::new(); // static method call
静态和非静态方法之间的区别与面向对象语言相同:非静态方法在值上调用(如my_vec.len()
),静态方法在类型上调用(如Vec::new()
).
当然,方法调用可以链式的:
Iron::new(router).http("localhost:3000".unwrap();
Rust语法的一个怪癖是在函数调用或方法调用中,泛型类型Vec<T>
的通常语法不起作用:
return Vec<i32>::with_capacity(1000); // error: something about chained comparisons
let ramp = (0 .. n).collect<Vec<i32>>(); // same error
问题是在表达式中,<
是小于运算符.在这种情况下,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, 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
括号左侧的值将自动解引用.
像这三个表达式称为 左值(lvalues) ,因为它们可以出现在赋值的左侧:
game.black_pawns = 0x00ff0000_00000000_u64;
coords.1 = 0;
pieces[2] = Some(Piece::new(Black, Knight, coords));
当然m只有当game
,coords
和pieces
被声明为mut
变量时才允许这样做.
从数组或向量中提取切片很简单:
let second_half = &game_moves[midpoint .. end];
这里game_moves
可以是数组,切片或向量;无论如何,结果是一个借用的切片,长度为end .. midpoint]
.game_moves
被认为是在second_half
的生命周期中被借用的.
..
运算符允许省略任一操作数;它根据存在的操作数产生最多四种不同类型的对象:
.. // RangeFull
a .. // RangeFrom { start: a }
.. b // RangeTo { end: b }
a .. b // Range { start: a, end: b }
Rust范围是 半开放的(half-open) :它们包括起始值(如果有),但不包括结束值.范围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章所述,在调试版本中检测到整数溢出,并导致恐慌(panic).标准库为未经检查的算术提供了a.wrapping_add(b)
等方法.
将整数除以零即使在发布版本中也会触发恐慌(panic).整数有一个方法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中一样,a % b
计算除法的余数或模数.结果与左操作数具有相同的符号.请注意,%
可用于浮点数和整数:
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
.
=
运算符可用于给mut
变量及其字段或元素赋值.但是,在Rust中,赋值并不像在其他语言中那样常见,因为默认情况下变量是不可变的.
如第4章所述,赋值 移动(moves) 不可复制类型的值,而不是隐式复制它们.
支持复合赋值:
total += item.price;
这相当于total = total + item.price;
.也支持其他运算符:-=
,*=
,等等.完整列表在本章末尾的表6-1中给出.
与C不同,Rust不支持链式赋值:你不能写a = b = 3
来为a
和b
赋值3
.Rust中的赋值很少见,你不会错过这个简写.
Rust没有C的递增和递减运算符++
和--
.
将值从一种类型转换为另一种类型通常需要在Rust中使用显式强制转换.强制转换使用as
关键字:
let x = 17; // x is type i32
let index = x asusize; // convert to usize
允许的几种强制转换:
- 数字可以从任何内置数字类型强制转换为任何其他数字类型.
将整数强制转换为另一个整数类型始终是明确定义的.转换为较窄的类型会导致截断.有符号整数强制转换为更宽的类型是符号扩展;无符号整数是零扩展;等等.简而言之,没有惊喜.
但是,在撰写本书时,将一个大的浮点值转换为一个太小而不能表示它的整数类型会导致未定义的行为.即使在安全的Rust中,这也可能导致崩溃.这是编译器中的一个bug,github.com/rust-lang/prog/issues/10184.
- 类型为
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代码点,用于保存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
一样.
用户定义的类型也可以实现Deref
trait.当你需要编写自己的智能指针类型时,请参见第289页的"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章说明.
表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` |
小于 | 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 | |
范围(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` |
闭包 | ` | x, y |
所有可以有用地链接的运算符都是左关联的.也就是说,诸如a-b-c
之类的操作链被分组为(a – b) – c
,而不是a – (b – c)
.可以用这种方式链接的运算符是你可能期望的所有运算符:
* / % + – << >> & ^ | && || as
比较运算符,赋值运算符和范围运算符. .
不能链接.
表达式是我们所认为的"运行代码".它们是Rust程序的一部分,编译为机器指令.然而,它们只占整个语言的一小部分.
在大多数编程语言中也是如此.程序的第一项工作是运行,但这不是它唯一的工作.程序必须沟通.它们必须是可测试的.它们必须保持组织性和灵活性,这样才能继续发展.它们必须与其他团队构建的代码和服务进行互操作. 即使只是为了运行,像Rust这样的静态类型语言的程序需要更多的工具来组织数据而不仅仅是元组和数组.
接下来,我们将花几个章节讨论这个领域的特性:模块和crates,它们提供程序结构,然后是结构和枚举,它们对你的数据做同样的事情.
首先,我们将用几页来讨论当事情出错时该做些什么的重要话题.