深入浅出Rust
上QQ阅读APP看书,第一时间看更新

第1章 与君初相见

Rust编程语言的官方网站是https://www.rust-lang.org/。在官网主页上,我们可以看到,在最显眼的位置,写着Rust语言最重要的特点:

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.

Rust语言是一门系统编程语言,它有三大特点:运行快、防止段错误、保证线程安全

系统级编程是相对于应用级编程而言。一般来说,系统级编程意味着更底层的位置,它更接近于硬件层次,并为上层的应用软件提供支持。系统级编程语言一般具有以下特点:

❏ 可以在资源非常受限的环境下执行;

❏ 运行时开销很小,非常高效;

❏ 很小的运行库,甚至于没有;

❏ 可以允许直接的内存操作。

目前,C和C++应该是业界最流行的系统编程语言。Rust的定位与它们类似,但是增加了安全性。C和C++都是编译型语言,无须规模庞大的运行时(runtime)支持,也没有自动内存回收(Garbage Collection)机制。

本章主要对Rust做一个简单的介绍,准备好一些基本概念以及开发环境。

1.1 版本和发布策略

Rust编程语言是开源的,编译器的源码位于https://github.com/rust-lang/rust项目中,语言设计和相关讨论位于https://github.com/rust-lang/rfcs项目中。对于想深入研究这门语言的读者来说,这是一个非常好的消息,大家可以通过研读开放的源代码和技术文档了解到很多书本上没有讲解过的知识。任何一个开发者都可以直接给这个项目提bug,或者直接贡献代码。Rust项目是完全由开源社区管理和驱动的,社区的氛围非常友好。

Rust编译器的版本号采用了“语义化版本号”(Semantic Versioning)规划。在这个规则之下,版本格式为:主版本号.次版本号.修订号。版本号递增规则如下。

❏ 主版本号:当你做了不兼容的API修改

❏ 次版本号:当你做了向下兼容的功能性新增

❏ 修订号:当你做了向下兼容的问题修正

Rust的第一个正式版本号是1.0,是2015年5月发布的。从那以后,只要版本没有出现大规模的不兼容的升级,大版本号就一直维持在“1”,而次版本号会逐步升级。Rust一般以6个星期更新一个正式版本的速度进行迭代。

为了兼顾更新速度以及稳定性,Rust使用了多渠道发布的策略:

❏ nightly版本

❏ beta版本

❏ stable版本

nightly版本是每天在主版本上自动创建出来的版本,这个版本上的功能最多,更新最快,但是某些功能存在问题的可能性也更大。因为新功能会首先在这个版本上开启,供用户试用。beta版本是每隔一段时间,将一些在nightly版本中验证过的功能开放给用户使用。它可以被看作stable版本的“预发布”版本。而stable版本则是正式版,它每隔6个星期发布一个新版本,一些实验性质的新功能在此版本上无法使用。它也是最稳定、最可靠的版本。stable版本是保证向前兼容的。

在nightly版本中使用试验性质的功能,必须手动开启feature gate。也就是说要在当前项目的入口文件中加入一条#! [feature(…name…)]语句。否则是编译不过的。等到这个功能最终被稳定了,再用新版编译器编译的时候,它会警告你这个feature gate现在是多余的了,可以去掉了。

Rust语言相对重大的设计,必须经过RFC(Request For Comments)设计步骤。这个步骤主要是用于讨论如何“设计”语言。这个项目存在于https://github.com/rust-lang/rfcs。所有大功能必须先写好设计文档,讲清楚设计的目标、实现方式、优缺点等,让整个社区参与讨论,然后由“核心组”(Core Team)的成员参与定夺是否接受这个设计。笔者强烈建议各位读者多读一下RFC文档,许多深层次的设计思想问题可以在这个项目中找到答案。在Rust社区,我们不仅可以看到最终的设计结果,还能看到每一步设计的过程,对我们来说非常有教育意义。

Rust语言每个相对复杂一点的新功能,都要经历如下步骤才算真正稳定可用:

RFC→Nightly→Beta→Stable

先编写一份RFC,其中包括这个功能的目的、详细设计方案、优缺点探讨等。如果这个RFC被接受了,下一步就是在编译器中实现这个功能,在nightly版本中开启。经过几个星期甚至几个月的试用之后,根据反馈结果来决定撤销、修改或者接受这个功能。如果表现不错,它就会进入beta版本,继续过几个星期后,如果确实没发现什么问题,最终会进入stable版本。至此,这个功能才会被官方正式定为“稳定的”功能,在后续版本中要确保兼容性的。

这个发布策略非常成功,它保证了新功能可以持续、快速地进入到编译器中。在这个发布策略的支持下,Rust语言以及编译器的进化速度非常了不起,成功实践了快速迭代、敏捷交付以及重视用户反馈的特点,同时也保证了核心设计的稳定性——用户可以根据自己的需要和风险偏好,选择合适的版本。本书假定读者安装的是nightly版本,因为我们的目标是学习,目前有许多重要的功能只存在于nightly版本。

在2017年下半年,Rust设计组又提出了一个基于epoch的演进策略(后来也被称为edition)。它要解决的问题是,如何让Rust更平稳地进化。比如,有时某些新功能确实需要一定程度上破坏兼容性。为了最大化地减少这些变动给用户带来的影响,Rust设计组又设计了一个所谓的edition的方案。简单来说就是让Rust的兼容性保证是一个有时限的长度,而不是永久。Rust设计组很可能会在不久的将来发布一个2018 edition,把之前的版本叫作2015 edition。在这个版本的进化过程中,就可以实施一些不兼容的改变。当然了,Rust设计组不会突然让前一个edition的代码到了后一个edition就不能编译了。他们采用了一种平滑过渡的方案。

我们举个例子。假设我们要添加一个功能,比如增加一个关键字。这件事情肯定是不兼容的改变,因为用户写的代码中很可能包含用这个关键字命名的变量、函数、类型等,直接把这个单词改成关键字会直接导致这些遗留代码出现编译错误。那怎么办呢?首先会在下一个edition中做出警告,提示用户这个单词已经不适合作为变量名了,请用户修改。但是这个阶段代码依然能编译通过。然后到再下一个edition的时候,这个警告就会变成真正的编译错误,此时这个关键字就可以真正启用了。先编译警告,再编译错误,这个过程可能会持续好几年,所以Rust的稳定性还是基本上有保证的。毕竟,如果要维持百分之百的兼容性,Rust语言就很难再继续进化了。如果让极少一部分受影响的遗留代码,完全锁死整个语言的进步空间,对于那些特别需要某些新功能的用户来说也是不公平的。通过这个缓慢过渡的策略,基本可以让所有Rust的使用者平滑、无痛地过渡到新版本。几年的过渡时间也是足够充分的。

Rust的标准库文档位于https://doc.rust-lang.org/std/。学会查阅标准库文档,是每个Rust使用者的必备技能之一。

1.2 安装开发环境

Rust编译器的下载和安装方法在官网上有文档说明,点击官网上的Install链接可以查看。Rust官方已经提供了预编译好的编译器供我们下载,支持Windows平台、Linux平台以及Mac平台。但是一般我们不单独下载Rust的编译器,而是使用一个叫rustup的工具安装Rust相关的一整套工具链,包括编译器、标准库、cargo等。使用这个工具,我们还可以轻易地更新版本、切换渠道、多工具链管理等。

在官网上下载rustup-init程序,打开命令行工具,执行这个程序,按照提示选择合适的选项即可。不论在Windows、Linux还是Mac操作系统上,安装步骤都是差不多的。

在Windows平台下的选项要稍微麻烦一点。在Windows平台上,Rust支持两种形式的ABI(Application Binary Interface),一种是原生的MSVC版本,另一种是GNU版本。如果你需要跟MSVC生成的库打交道,就选择MSVC版本;如果你需要跟MinGW生成的库打交道,就选择GNU版本。一般情况下,我们选择MSVC版本。在这种情况下,Rust编译器还需要依赖MSVC提供的链接器,因此还需要下载VisualC++的工具链。到Visual Studio官网下载VS2015或者VS2017社区版,安装C++开发工具即可。

安装完成之后,在$HOME/.cargo/bin文件夹下可以看到一系列的可执行程序,比如Rust 1.19版本的时候,在Windows平台上安装的程序如图1-1所示。

图1-1

其中,rustc.exe是编译器,cargo.exe是包管理器,cargo-fmt.exe和rustfmt.exe是源代码格式化工具,rust-gdb.exe和rust-lldb.exe是调试器,rustdoc.exe是文档生成器,rls.exe和racer.exe是为编辑器准备的代码提示工具,rustup.exe是管理这套工具链下载更新的工具。

我们可以使用rustup工具管理工具链。

    // 更新rustup本身
    $ rustup self update
    // 卸载rust所有程序
    $ rustup self uninstall
    // 更新工具链
    $ rustup update

我们还可以使用它轻松地在stable/beta/nightly渠道中切换,比如:

    // 安装nightly版本的编译工具链
    $ rustup install nightly
    // 设置默认工具链是nightly版本
    $ rustup default nightly

为了提高访问速度,中国科技大学Linux用户协会(USTC LUG)提供了一个代理服务,官方网址为https://lug.ustc.edu.cn/wiki/mirrors/help/rust-static,建议国内用户设置好以下环境变量再使用rustup:

    export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
    export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup

Rust官方工具链还提供了重要的包管理工具cargo.exe,我们可以通过这个工具轻松导入或者发布开源库。官方的管理仓库在https://crates.io/,大家可以登录这个网站浏览一下Rust社区热门的开源库都有哪些。大型项目往往需要依赖这些开源库,cargo会帮我们自动下载编译。同样,为了解决网络问题,需要利用USTC提供的代理服务,使用方式为:在$HOME/. cargo目录下创建一个名为config的文本文件,其内容为:

    [source.crates-io]
    registry = "https://github.com/rust-lang/crates.io-index"
    replace-with = 'ustc'
    [source.ustc]
    registry = "git://mirrors.ustc.edu.cn/crates.io-index"

这样,在编译需要依赖crates.io的项目时,不会由于网络问题导致依赖库下载失败。

RLS(Rust Language Server)是官方提供的一个标准化的编辑器增强工具。它也是开源的,项目地址在https://github.com/rust-lang-nursery/rls。它是一个单独的进程,通过进程间通信给编辑器或者集成开发环境提供一些信息,实现比较复杂的功能,比如代码自动提示、跳转到定义、显示函数签名等。安装最新的RLS的方法为:

    // 更新rustup到最新
    rustup self update
    // 更新rust编译器到最新的nightly版本
    rustup update nightly
    // 安装RLS
    rustup component add rls --toolchain nightly
    rustup component add rust-analysis --toolchain nightly
    rustup component add rust-src --toolchain nightly

有了这些准备,大家就可以在Visual Studio Code中下载支持Rust的插件,提升编辑体验。理论上来说,RLS可以跟任何编辑器或者集成开发环境配合使用,只要这个编辑器实现了它们之间的通信协议即可。

有了上面这些准备工作,我们就可以正式开始Rust编程之旅了。首先,打开命令行工具,看看rustc编译器能否正常运行,使用-V命令查看rustc的版本:

    $ rustc -V
    rustc 1.20.0-nightly (f85579d4a 2017-07-12)

如果看到类似的输出,说明编译器已经可以正常工作。接下来,请大家探索一下这些工具的简明使用帮助:

1)使用rustc -h命令查看rustc的基本用法;

2)使用cargo -h命令查看cargo的基本用法;

3)使用rustc -C help命令查看rustc的一些跟代码生成相关的选项;

4)使用rustc -W help命令查看rustc的一些跟代码警告相关的选项;

5)使用rustc -Z help命令查看rustc的一些跟编译器内部实现相关的选项;

6)使用rustc -help -V命令查看rustc的更详细的选项说明。

1.3 Hello World

编程语言入门第一课,必须得是hello world程序。我们先来看看Rust的hello world是什么样子:

    // hello_world.rs
    fn main() {
        let s = "hello world! ";
        println!("{}", s);
    }

对于这样一个简单的示例程序,我们并没有使用cargo创建工程,因为没有复杂的依赖关系。编译就直接使用rustc即可,其他所有选项使用默认值:

    rustc hello_world.rs

可看到本地文件夹中生成了一个名为hello_world的可执行程序。执行./hello_world程序,可以看见控制台上输出了hello world!字符串。恭喜读者,第一个Rust程序已经运行成功了!

我们来分析一下这个最简单的程序。

1)一般Rust源代码的后缀名使用.rs表示。源码一定要注意使用utf-8编码。

2)第一行是注释语句,Rust的注释是C语言系列风格的,行注释采用//开头,块注释使用/*和*/包围。它还支持更高级的文档注释,将在后文中详细展开说明。

3)fn是一个关键字(key word),函数定义必须以这个关键字开头。函数体使用大括号来包含。fn是单词function的缩写,在Rust中,设计者比较偏向使用单词缩写,即使是关键字也不例外。在代码风格上,某些读者可能开始会有点不习惯。但总体而言,这只是个审美偏好而已,不必过于纠结,习惯就好。

4)默认情况下,main函数是可执行程序的入口点,它是一个无参数,无返回值的函数。如果我们要定义的函数有参数和返回值,可以使用以下语法(参数列表使用逗号分开,冒号后面是类型,返回值类型使用->符号分隔):

    fn Foo( input1 : i32, input2 : u32) -> i32 {
        ...
    }

5)局部变量声明使用let关键字开头,用双引号包含起来的部分是字符串常量。Rust是静态强类型语言,所有的变量都有严格的编译期语法检查。关于Rust的变量和类型系统将在后文详细说明。

6)每条语句使用分号结尾。语句块使用大括号。空格、换行和缩进不是语法规则的一部分。这都是明显的C语言系列的风格。

最简单的标准输出是使用println!宏来完成。请大家一定注意println后面的感叹号,它代表这是一个宏,而不是一个函数。Rust中的宏与C/C++中的宏是完全不一样的东西。简单点说,可以把它理解为一种安全版的编译期语法扩展。这里之所以使用宏,而不是函数,是因为标准输出宏可以完成编译期格式检查,更加安全。

从这个小程序的惊鸿一瞥中,大家可以看到,Rust的语法主要还是C系列的语法风格。对于熟悉C / C++ / Java / C# / PHP / JavaScript等语言的读者来说,会看到许多熟悉的身影。

1.4 Prelude

Rust的代码从逻辑上是分crate和mod管理的。所谓crate大家可以理解为“项目”。每个crate是一个完整的编译单元,它可以生成为一个lib或者exe可执行文件。而在crate内部,则是由mod这个概念管理的,所谓mod大家可以理解为namespace。我们可以使用use语句把其他模块中的内容引入到当前模块中来。关于Rust模块系统的详细说明,可参见本书第五部分。

Rust有一个极简标准库,叫作std,除了极少数嵌入式系统下无法使用标准库之外,绝大部分情况下,我们都需要用到标准库里面的东西。为了给大家减少麻烦,Rust编译器对标准库有特殊处理。默认情况下,用户不需要手动添加对标准库的依赖,编译器会自动引入对标准库的依赖。除此之外,标准库中的某些type、trait、function、macro等实在是太常用了。每次都写use语句确实非常无聊,因此标准库提供了一个std::prelude模块,在这个模块中导出了一些最常见的类型、trait等东西,编译器会为用户写的每个crate自动插入一句话:

    use std::prelude::*;

这样,标准库里面的这些最重要的类型、trait等名字就可以直接使用,而无须每次都写全称或者use语句。

Prelude模块的源码在src/libstd/prelude/文件夹下。我们可以看到,目前的mod.rs中,直接导出了v1模块中的内容,而v1.rs中,则是编译器为我们自动导入的相关trait和类型。

1.5 Format格式详细说明

在后面的内容中,我们还会大量使用println!宏,因此提前介绍一下这个宏的基本用法。跟C语言的printf函数类似,这个宏也支持各种格式控制,示例如下:

    fn main() {
        println!("{}", 1);                               // 默认用法,打印Display
        println!("{:o}", 9);                             // 八进制
        println!("{:x}", 255);                           // 十六进制 小写
        println!("{:X}", 255);                           // 十六进制 大写
        println!("{:p}", &0);                            // 指针
        println!("{:b}", 15);                            // 二进制
        println!("{:e}", 10000f32);                      // 科学计数(小写)
        println!("{:E}", 10000f32);                      // 科学计数(大写)
        println!("{:? }", "test");                       // 打印Debug
        println!("{:#? }", ("test1", "test2"));          // 带换行和缩进的Debug打印
        println!("{a} {b} {b}", a = "x", b = "y");       // 命名参数
    }

Rust中还有一系列的宏,都是用的同样的格式控制规则,如format! write! writeln!等。详细文档可以参见标准库文档中std::fmt模块中的说明。

Rust标准库中之所以设计了这么一个宏来做标准输出,主要是为了更好地错误检查。大家可以试试,如果出现参数个数、格式等各种原因不匹配会直接导致编译错误。而函数则不具备字符串格式化的静态检查功能,如果出现了不匹配的情况,只能是运行期错误。这个宏最终还是调用了std::io模块内提供的一些函数来完成的。如果用户需要更精细地控制标准输出操作,也可以直接调用标准库来完成。