1.5 使用Rust语言的感受如何?
Rust是Haskell和Java程序员可以用得很顺手的编程语言。在实现了低级的、裸机性能的同时,Rust也提供了接近于Haskell和Java之类的动态语言的高级表达能力。
在1.3节中,我们看到了几个“Hello, world!”的例子。接下来,为了对Rust的一些特性有更好的了解,让我们来尝试一些稍微复杂点儿的东西。清单1.2简单介绍了Rust对于基本的文本处理可以做些什么。此清单的源代码保存在ch1/ch1-penguins/src/main.rs文件中。一些需要关注的语言特性如下。
● 常用的流程控制机制:包括for
循环和continue
关键字。
● 方法语法:虽然Rust不是面向对象的,因为它不支持继承,但是Rust用到了面向对象语言里的方法语法。
● 高阶编程:函数可以接收和返回函数。举例来说,在代码第19行(.map(|field| field.trim())
)中有一个闭包(closure),也叫作匿名函数或lambda函数。
● 类型注解:虽然需要用到类型注解的地方相对是较少的,但有时又必须要用到类型注解,作为给编译器的提示信息,比如,代码中以if let Ok(length)
开头的那一行(第27行)。
● 条件编译:在清单1.2中,第21~24行的代码(if cfg!(...);
)不会被包含到该程序的发布构建(release build)当中。
● 隐式返回:Rust提供了return
关键字,但通常情况下会将其省略。Rust是一门基于表达式的语言。
清单1.2 Rust代码示例,展示了对CSV数据的一些基本处理
1 fn main() { ⇽--- 在可执行的项目中,main() 函数是必需的。
2 let penguin_data = "\ ⇽--- 忽略掉末尾的换行符。
3 common name,length (cm)
4 Little penguin,33
5 Yellow-eyed penguin,65
6 Fiordland penguin,60
7 Invalid,data
8 ";
9
10 let records = penguin_data.lines();
11
12 for (i, record) in records.enumerate() {
13 if i == 0 || record.trim().len() == 0 { ⇽--- 跳过表头行和只含有空白符的行。
14 continue;
15 }
16
17 let fields: Vec <_> = record ⇽--- 从一行文本开始。
18 .split(',') ⇽--- 将record分割(split)为多个子字符串。
19 .map(|field| field.trim()) ⇽--- 修剪(trim)掉每个字段中两端的空白符。
20 .collect(); ⇽--- 构建具有多个字段的集合。
21 if cfg!(debug_assertions) { ⇽--- cfg!用于在编译时检查配置。
22 eprintln!("debug: {:?} -> {:?}",
23 record, fields); ⇽--- eprintln!用于输出到标准错误(stderr)
24 }
25
26 let name = fields[0];
27 if let Ok(length) = fields[1].parse:: <f32>() { ⇽--- 试图把该字段解析为一个浮点数。
28 println!("{}, {}cm", name, length); ⇽--- println!用于输出到标准输出(stdout)。
29 }
30 }
31 }
清单1.2可能会让有些读者感到困惑,尤其是那些以前从未接触过Rust的人。在继续前进之前,我们给出一些简单的说明。
● 第17行变量fields
的类型注解为Vec<_>
。Vec类型是动态数组,是vector的缩写,它是一个可以动态扩展的集合类型。此处的下画线(_)表示,要求Rust推断出此动态数组的元素类型。
● 在第22行和第28行,我们要求Rust把信息输出到控制台上。eprintln!
会输出到标准错误,而println!
会将其参数输出到标准输出。 - 宏类似于函数,但它返回的是代码而不是值。通常,宏用于简化常见的代码模式。 - eprintln!
和println!
都是在其第一个参数中使用一个字符串字面量,并嵌入了一个迷你语言来控制它们的输出。其中的占位符{ }则表示Rust应该使用程序员定义的方法,将该值表示为一个字符串,而{:?}则表示要求使用该值的默认表示形式。
● 第27行包含一些新奇的特性。if let Ok(length) = fields[1].parse::<f32>()
意为“尝试着把fields[1]解析为一个32位浮点数,如果解析成功,则把此浮点数赋值给length变量”。if let
结构是一种有条件地处理数据的简明方法,且具备把该数据赋值给局部变量的功能。如果成功解析字符串,parse()
方法会返回Ok(T)
(这里的T
代表任何类型);反之,如果解析失败,它会返回Err(E)
(这里的E
代表一个错误类型)。if let Ok(T)
的效果就是忽略任何错误的情况,比如在处理Invalid,data
这一行时就会出现错误。 - 如果Rust无法从环境上下文中推断出类型,就会要求你指定这些类型。在这里调用parse()
的代码为parse :: <f32>()
,其中就有一个内嵌的类型注解。
把源代码转换为一个可执行文件的过程叫作编译。要编译Rust代码,我们需要安装Rust编译器并针对此源代码执行编译。编译清单1.2需要采用以下步骤。
(1)打开一个控制台(例如cmd.exe、PowerShell、Terminal或Alacritty)。
(2)找到所下载的源代码,然后进入ch1/ch1-penguins目录(注意:不是ch1/ch1-penguins/src目录)。
(3)执行cargo run
。
输出的结果如下所示:
$ cargo run
Compiling ch1-penguins v0.1.0 (../code/ch1/ch1-penguins)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running 'target/debug/ch1-penguins'
debug: " Little penguin,33" -> ["Little penguin", "33"]
Little penguin, 33cm
debug: " Yellow-eyed penguin,65" -> ["Yellow-eyed penguin", "65"]
Yellow-eyed penguin, 65cm
debug: " Fiordland penguin,60" -> ["Fiordland penguin", "60"]
Fiordland penguin, 60cm
debug: " Invalid,data" -> ["Invalid", "data"]
你会注意到,以debug:开头的这些输出行会带来干扰。我们可以用cargo命令的--release标志项编译出一个发布构建的版本,这样就可以消除这些干扰的输出行了。这个条件编译功能是由cfg
!(debug_assertions){
… }
代码块提供的,如清单1.2的第21~24行所示。发布构建在运行时要快得多,但是需要更长的编译期:
$ cargo run --release
Compiling ch1-penguins v0.1.0 (../code/ch1/ch1-penguins)
Finished release [optimized] target(s) in 0.34s
Running 'target/release/ch1-penguins'
Little penguin, 33cm
Yellow-eyed penguin, 65cm
Fiordland penguin, 60cm
给cargo
命令再添加一个-q
标志项,还能进一步减少输出信息。q
是“quiet”的缩写。具体的用法如下:
$ cargo run -q --release
Little penguin, 33cm
Yellow-eyed penguin, 65cm
Fiordland penguin, 60cm
清单1.1和清单1.2的代码示例,挑选了尽可能多的、有代表性的Rust特性,并把它们打包到易于理解的例子中。希望这些示例能展示出Rust程序既有低级语言的性能,又能给人带来高级语言的编程感受。现在,让我们从具体的语言特性中后退一步,思考Rust语言背后的一些思想,以及这些思想在Rust编程语言的生态系统中的地位。
[11] 参见Chrome OS KVM—A component written in Rust.