Rust实战
上QQ阅读APP看书,第一时间看更新

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.