Rust实战

978-7-115-59139-5
作者: 蒂姆·麦克纳马拉(Tim McNamara)
译者: 金伟唐刚
编辑: 吴晋瑜

图书目录:

详情

本书通过探索多种系统编程概念和技术引入Rust编程语言,在深入探索计算机工作原理的同时,帮助读者了解Rust的所有权系统、Trait、包管理、错误处理、条件编译等概念,并通过源自现实的示例来帮助读者了解Rust中的内存模型、文件操作、多线程、网络编程等内容。 本书旨在帮助读者理解如何用Rust进行系统编程,并提供了一些使用Rust编写代码的技巧。本书给出了10余个源自现实的示例,让读者不仅能了解Rust语法,还能了解Rust的实际运用。 本书适合所有对Rust感兴趣的读者阅读。要更好地掌握本书涵盖的内容,读者应具备一定的编程经验,至少应对计算机编程的基本概念有所了解。

图书摘要

版权信息

书名:Rust实战

ISBN:978-7-115-59139-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [新西兰] 蒂姆•麦克纳马拉(Tim McNamara)

译    金 伟 唐 刚

审  校 张汉东

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59139”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


Original English language edition published by Manning Publications, USA.Copyright © 2020 by Manning Publications. Simplified Chinese-language edition copyright © 2021 by Posts & Telecom Press Co., LTD. All rights reserved.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社有限公司独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书通过探索多种系统编程概念和技术引入Rust编程语言,在深入探索计算机工作原理的同时,帮助读者了解Rust的所有权系统、Trait、包管理、错误处理、条件编译等概念,并通过源自现实的示例来帮助读者了解Rust中的内存模型、文件操作、多线程、网络编程等内容。

本书旨在帮助读者理解如何用Rust进行系统编程,并提供了一些使用Rust编写代码的技巧。本书给出了10余个源自现实的示例,让读者不仅能了解Rust语法,还能了解Rust的实际运用。

本书适合所有对Rust感兴趣的读者阅读。要更好地掌握本书涵盖的内容,读者应具备一定的编程经验,至少应对计算机编程的基本概念有所了解。

给所有渴望编写更安全软件的你们!


感谢我的妻子Katie在我几近放弃的时候给予我支持和鼓励!感谢我的孩子Florence和Octavia,这段日子里,爸爸因为忙于写作而不能陪你们玩,我感到非常愧疚!感谢你们大大的拥抱和甜甜的微笑。

想感谢的人实在太多了!只字片语不足以表达我诚挚的谢意!Rust社区中的很多成员为本书的成稿贡献了力量。上千名“先读者”通过liveBook提交了勘误、问题和建议。感谢每位给予支持和帮助的小伙伴!谢谢你们!

在此我要特别感谢其中的一小部分“先读者”!他们中的许多人因此结缘,并成了朋友。他们是Aï Maiga、Ana Hobden、Andrew Meredith、Andréy Lesnikóv、Andy Grove、Arturo J. Pérez、Bruce Mitchener、Cecile Tonglet、Daniel Carosone、Eric Ridge、Esteban Kuber、Florian Gilcher、Ian Battersby、Jane Lusby、Javier Viola、Jonathan Turner、Lachezar Lechev、Luciano Mammino、Luke Jones、Natalie Bloomfield、Oleksandr Kaleniuk、Olivia Ifrim、Paul Faria、Paul J.Symonds、Philipp Gniewosz、Rod Elias、Stephen Oates、Steve Klabnik、Tannr Allard、Thomas Lockney和William Brown。在过去的4年中,能与你们互动,让我倍感荣幸!

还要对本书的审稿人表示诚挚的感谢!他们是Afshin Mehrabani、Alastair Smith、Bryce Darling、Christoffer Fink、Christopher Haupt、Damian Esteban、Federico Hernandez、Geert Van Laethem、Jeff Lim、Johan Liseborn、Josh Cohen、Konark Modi、Marc Cooper、Morgan Nelson、Ramnivas Laddad、Riccardo Moschetti、Sanket Naik、Sumant Tambe、Tim van Deurzen、Tom Barber、Wade Johnson、William Brown、William Wheeler和Yves Dorfsman。我阅读了你们提出的所有意见。本书在写作后期的诸多改进都要归功于你们细致入微的反馈。

感谢曼宁出版社的项目编辑Elesha Hyde和Frances Buran!得益于他们的耐心、专业和细致的指导,我才能顺利完成书稿的多次迭代。感谢组稿编辑Bert Bates、Jerry Kuch、Mihaela Batinić、Rebecca Rinehart、René van den Berg和Tim van Deurzen,以及本书的流程编辑Benjamin Berg、Deirdre Hiam、Jennifer Houle和Paul Wells。

本书在MEAP(早期预览版)阶段一共发布了16个版本。得益于众多人的支持和帮助,本书才能顺利付梓。感谢Aleksandar Dragosavljević、Ana Romac、Eleonor Gardner、Ivan Martinović、Lori Weidert、Marko Rajković、Matko Hrvatin、Mehmed Pašić、Melissa Ice、Mihaela Batinić、Owen Roberts、Radmila Ercegovac和Rejhana Markanović。

感谢本书营销团队的成员,他们是Branko Latincic、Candace Gillhoolley、Cody Tankersley、Lucas Weber和Stjepan Jureković。谢谢你们的大力支持!

最后,感谢曼宁出版社的Aira Dučić、Andrew Waldron、Barbara Mirecki、Branko Latinčić、Breckyn Ely、Christopher Kaufmann、Dennis Dalinnik、Erin Twohey、Ian Hough、Josip Maras、Julia Quinn、Lana Klasić、Linda Kotlyarsky、Lori Kehrwald和Melody Dolab!谢谢你们给予我的诸多帮助,谢谢你们的积极回复。感谢Mike Stephens!你曾提醒我写好一本书很难!的确如此!


没人知道阅读某一本技术图书所付出的努力是否值得。这些书可能售价高昂、内容枯燥,并且有可能写得很差。更糟糕的是,很有可能让你什么也学不到。幸运的是,本书的作者十分理解这些情况。

本书的首要目标就是教你使用Rust,为此本书提供了一些较大的、可运行的项目。在学习过程中,你将编写一个数据库、一个CPU模拟器、一个操作系统内核,以及实现一些有趣的项目,甚至会涉足自动生成艺术项目。每个项目的设计都是为了让你能够以自己的节奏来探索Rust编程语言。对不太了解Rust的读者来说,无论你选择哪个方向,都有很多的机会去扩展这些项目。

学习一门编程语言,仅学习相关的语法和语义是不够的,你还需要在相关的社区深入探索。然而,社区中共享的那些知识、术语和实践,对新加入的人而言,很可能就成了无形的障碍。

对许多新的Rust程序员来说,系统编程的那些概念就是这样的障碍—许多刚刚踏入“Rust世界”的程序员并没有这个领域的背景。为了弥补这一点,本书的第二个目标就是帮你掌握系统编程的相关知识。此外,在本书第12章的某些主题中,你还将了解到内存数字时间保持和设备驱动程序是如何工作的。在你成为Rust社区中的一员后,我希望本书的内容能够让你更加游刃有余。期待你的加入!

在人类社会中,各种软件随处可见,而且软件存在一些关键安全漏洞的状况已在人们的可接受范畴之内,甚至被视为正常抑或不可避免的状况。Rust则表明这种状况既不是正常的,也不是不可避免的。此外,计算机中还充斥着各种臃肿的、资源消耗密集型的应用程序。计算机中的资源毕竟是有限的,为了开发出具有较低资源需求的软件,Rust提供了可行的替代方案。

本书旨在为你赋能,让你相信,Rust并不是专门为某些专家预备的,而是一个谁都可以使用的工具。在漫漫学习之旅中,能成为你的“领路人”,我感到荣幸之至!相信你一定能做得很好。


本书主要是为那些可能已经在网上学过Rust开源资料,但是会问自己“接下来该学点什么”的人准备的。本书包含数十个有趣的示例,如果你有自己的想法且时间允许,还可以进一步扩展这些示例。这些示例使得本书12章的内容涵盖了Rust的一个颇为有用的子集,以及许多生态系统会用到的那些最重要的第三方库。

这些代码示例更注重的是易读性,而不是注重如何优雅、地道地使用Rust。如果你是一个很有经验的Rust程序员,那么可能会发现自己并不认同这些例子中的一些风格设定。我希望你能够体谅这是在为初学者考虑。

这并不是一本内容全面的参考书,因此略去了语法和标准库的部分内容。通常情况下,这些省略掉的内容都是高度专业的,应该给予“特殊待遇”。然而,本书旨在为读者提供足够的基础知识和信心,以便在必要时再进一步学习这些特定的主题。从系统编程书的角度来看,本书也是很独特的,因为几乎本书的所有示例都能在微软的Windows系统上运行。

所有对Rust感兴趣的人,喜欢通过实用的示例来学习的人,或者是那些被“Rust是一种系统编程语言”这一事实吓倒的人,都应该会喜欢本书。有编程经验的读者将获益更多,因为本书会假定读者已经了解一些计算机编程的基本概念。

本书的正文分为两部分。第一部分介绍Rust的语法和一些独特的特点,第二部分会应用到在第一部分中介绍的这些知识。每一章都会引入一到两个新的Rust概念。第一部分是对Rust的快速介绍。

第1章解释Rust存在的原因,以及如何开始用它来编程。

第2章提供翔实的Rust语法基础知识。本章示例包括芒德布罗集渲染器和一个grep的克隆。

第3章讲解如何组合Rust的数据类型以及如何使用一些错误处理的工具。

第4章讨论Rust中确保要访问的数据始终有效的机制。

第二部分是将Rust应用于系统编程领域的一些基础性介绍。

第5章介绍在数字计算机中信息是如何表示的,重点介绍数字是如何被近似表示的。本章示例包括实现定点数格式和一个CPU模拟器。

第6章阐释引用、指针、虚拟内存、栈和堆等术语。本章示例包括一个内存扫描器和自动生成艺术项目。

第7章阐释如何将数据结构存储到存储设备中。本章示例包括一个hexdump的克隆和一个可运行的数据库。

第8章通过多次重新实现HTTP讲解计算机是如何进行通信的,每一次实现都会剥离掉一个抽象层。

第9章探索在数字计算机中跟踪时间的过程。本章示例包括一个可运行的NTP客户端。

第10章介绍进程、线程和一些相关的抽象概念。本章示例包括一个海龟绘图应用程序和一个并行解析器。

第11章介绍操作系统的作用和计算机是如何启动的。本章示例包括编译自己的引导加载程序和操作系统内核。

第12章阐释外部世界是如何与CPU和操作系统进行通信的。

请按照本书章节顺序进行阅读。只有掌握了前面章节中的内容,才能更好地学习后续章节中的知识。不过,每一章中的项目是相互独立的。因此,如果本书有某些你特别感兴趣的主题,也欢迎你跳着来阅读。


本书封面上的人物标题为“Le maitre de chausson”或“拳击手”。该插图来自一个由多位艺术家的作品组成的作品集。该作品集由Louis Curmer编辑,于1841年在巴黎出版。该作品集的标题是“Les Français peints par eux-mêmes”,意为“法国画家笔下的法国人”,其中每张插画都是手工精细绘制和着色的。这些丰富多样的插画生动再现了约200年前,世界上各个地区、城镇、村庄和社区在文化上的巨大差异。人们讲着不同的语言和方言,无论是在街上还是在乡下,只要通过着装,就可以很容易地识别出他们住在哪里,甚至他们的职业或者身份。

后来,人们的着装发生了变化,彼时如此丰富的地区多样性逐渐消失了。现在,我们很难区分来自不同大陆的人,更不用说来自不同地区或城镇的了。也许我们以牺牲文化多样性为代价换来了更多样化的个人生活——当然也包括更多样化和快节奏的科技化生活。

在这个很难把一本计算机书与另一本同类书区分开来的时代,曼宁以基于两个世纪前丰富多样的地区生活作为图书封面,以此来颂扬计算机行业的创新性和主动性,并让此类深藏在作品集中的插画重回大众视野。


书的第一部分是对Rust编程语言的基本介绍。学完这一部分的内容,你将会对Rust的语法有较好的理解,并且会了解是什么原因促使人们选择了Rust。你还将了解Rust与其同级语言之间的一些基本差异。


本章主要内容

Rust语言的特点和目标。

Rust的语法简介。

Rust语言的适用场景以及何时应该避免使用Rust。

构建第一个Rust程序。

把Rust语言与面向对象语言以及更广泛的其他语言加以比较。

欢迎走进Rust的世界,这是一种能带给你力量的编程语言。当你逐渐熟悉Rust以后,你会发现,Rust不但是一门超快速、安全的编程语言,而且是让你每天都能愉快地使用的一种语言。

当你开始使用Rust进行编程时,你很可能想要做到上面提到的那样。那么,本书将帮助你建立起作为一名Rust程序员的信心。但是,本书并不适合零编程基础的读者,而是为那些考虑把Rust作为其下一门想要掌握的语言的读者,以及喜欢实现实际的可运行示例的读者准备的。下面所列的是本书涵盖的一些较大示例。

芒德布罗集(Mandelbrot set)渲染器。

一个grep的克隆。

CPU模拟器。

自动生成艺术项目。

一个数据库。

HTTP、NTP以及hexdump客户端。

LOGO语言解释器。

操作系统内核。

正如你可能通过查看此列表所能感觉到的那样,本书会教给你比Rust本身更多的内容。本书还会讲到系统编程和低级编程的内容。在阅读本书的过程中,你会了解操作系统(OS)的作用是什么、CPU是如何工作的、计算机是如何维护时间的、指针是什么,以及数据类型是什么,还将了解计算机内部系统是如何实现交互操作的。除了语法,你还会了解创建Rust的原因是什么,以及它所面临的一些挑战。

Stack Overflow的年度开发者调查结果显示,2016—2020年,Rust年年都荣获“最喜爱的编程语言”的奖项。这可能就是许多大型技术领导企业已经采用Rust的原因所在。

亚马逊云服务(AWS),从2017年开始,在Serverless计算产品、AWS Lambda和AWS Fargate中使用了Rust。在此之后,Rust更是获得了进一步的发展。亚马逊公司已经开发了Bottlerocket操作系统和AWS Nitro系统,以提供其弹性计算云(EC2)服务。[1]

Cloudflare使用Rust开发了多个服务,包括公共DNS、Serverless计算和数据包检查产品等。[2]

Dropbox使用Rust重构了其后端仓库,该仓库管理着EB级数据的存储。[3]

谷歌用Rust开发了安卓系统的某些部分,比如蓝牙模块。Rust还被用于Chrome OS中的crosvm组件,并在谷歌的新操作系统Fuchsia中发挥了重要的作用。[4]

Facebook使用Rust为其Web端、移动端和API服务,还为HHVM中的某些部分赋能,HHVM是HipHop虚拟机,是给Hack编程语言使用的虚拟机。[5]

微软使用Rust编写了Azure云平台中的一些组件,其中包括物联网(IoT)服务的一个安全守护进程。

Mozilla使用Rust强化了火狐浏览器——在火狐浏览器项目中有1500万行代码。在Rust-in-Firefox系列项目的前两个项目中,MP4元数据解析器和文本编/解码器在整体性能和稳定性上都得到了改善。

GitHub的npm公司使用Rust支撑了“每天超过13亿次的软件包下载量”。[6]

Oracle使用Rust开发了一个容器运行时,解决了在使用Go语言的参考实现版时遇到的问题。[7]

三星在其子公司SmartThings的“Hub”项目中使用了Rust。“Hub”是一个智能设备的固件后端,用在物联网服务中。

对快速发展的初创企业来说,Rust也能带来足够的生产力。下面给出几个例子。

Sourcegraph使用Rust为所有的语言提供语法高亮服务。[8]

Figma在多人协作服务器的性能关键型组件中采用了Rust。[9]

Parity使用Rust开发其以太坊区块链的客户端。[10]

[1] 参见How our AWS Rust team will contribute to Rust’s future successes.

[2] 参见Rust at Cloudflare.

[3] 参见The Epic Story of Dropbox’s Exodus From the Amazon Cloud Empire.

[4] 参见Google joins the Rust Foundation.

[5] 参见HHVM 4.20.0 and 4.20.1.

[6] 参见Rust Case Study: Community makes Rust an easy choice for npm.

[7] 参见Building a Container Runtime in Rust.

[8] 参见HTTP code syntax highlighting server written in Rust. 

[9] 参见Rust in Production at Figma.

[10] 参见The fast, light, and robust EVM and WASM client. 

在工作中提倡使用Rust的效果如何?克服了最初的障碍,之后的发展往往就会很顺利。下面转载的2017年的讨论提供了一个很好的轶事。谷歌Chrome OS团队的一名成员讨论了将该语言引入项目中的情况:[11]

2017年9月27日,因迪

Rust是谷歌官方认可的语言吗?

2017年9月27日,萨克斯塞伦

回复作者:Rust在谷歌并没有得到官方认可,但有一些人在使用它。想要在这个组件里使用Rust的技巧就是,让我的同事们相信没有其他语言更适合这项工作,在这个例子中,我相信情况就是如此。

也就是说,要使Rust在Chrome OS的构建环境中发挥出色作用,还有大量工作需要做。不过,使用Rust的同事们为我解答了很多问题,帮了我很大的忙。

2017年9月27日,埃基德

“在这个组件中使用Rust的技巧就是,让我的同事们相信没有其他语言更适合这项工作,在这个例子中,我相信情况就是如此。”

我在自己的一个项目中遇到了类似的情况——一个vobsub字幕解码器,它用于解析复杂的二进制数据,而且有一天我希望将其作为Web服务运行。所以,我当然想确保我的代码里没有漏洞。

我用Rust编写了代码,然后使用“cargo fuzz”模糊测试来尝试发现漏洞。运行了10亿次模糊测试迭代后,我发现了5个错误。

令人高兴的是,实际上,这些错误中没有一个可以真的升级为漏洞。在每种错误的情况下,Rust的各种运行时检查都成功地发现了问题并将其转变为可控的恐慌。(实际上,这将导致彻底重启Web服务器。)

因此,我的主要收获是,每当我想要一种没有GC的语言,同时还想要该语言在安全性至关重要的上下文环境中是可以信任的,这时Rust就是绝佳选择了。此外,实际操作时,我可以静态链接Linux二进制文件(与Go语言类似),这是一个很好的加分项。

2017年9月27日,马尼舍尔思

“令人高兴的是,实际上,这些错误没有一个会真的升级为漏洞。在每种错误的情况下,Rust的各种运行时检查都成功地发现了问题并将其转变为可控的恐慌。”

这或多或少也是我们在浏览器中给Rust代码做模糊测试的经验,仅供参考。模糊测试发现了很多恐慌(以及调试断言/“安全”溢出断言)。在其中的一个测试用例中,它实际上发现了一个在类似Gecko的代码中被忽视了大约10年的错误。

从上面的讨论内容中,我们可以看到,寻求克服技术挑战的工程师们已经“自下而上”地在一些相对较小的项目中采用了Rust语言。从这些成功中获得的经验随后会被用作证据,证明完全可以用于开展更加宏大的工作。

从2017年年底以来,Rust不断成熟和壮大,已成为谷歌技术领域认可的一部分,并且在安卓项目以及Fuchsia操作系统的项目中,现已是官方所认可的一种语言。

在本节内容中,你将初次体验Rust。我们首先要了解如何使用编译器,然后会快速编写一个程序。在接下来的章节中,我们会写一个完整的项目。

注意 要安装Rust,需要使用官方提供的安装器(installer)进行安装。请登录Rust官方网站并下载。

大多数程序员在接触一门新的语言时,要做的第一件事就是学习如何在控制台上输出“Hello,world!”。接下来你也需要这样做。在遇到令人讨厌的语法错误之前,你要先验证所有环境是否已经准备就绪。

如果你使用的是Windows系统,在安装完Rust以后,请通过开始菜单打开命令提示符窗口。然后请执行以下命令:

C:\> cd %TMP%

如果你使用的是Linux或macOS系统,请打开一个终端窗口。打开后,请执行以下命令:

$ cd $TMP

从这里开始,所有操作系统的命令应该是相同的。如果你的Rust环境安装正确,那么执行以下3个命令,将会在屏幕上输出“Hello,world!”(以及一些其他输出)。

$ cargo new hello
$ cd hello
$ cargo run

这是在Windows上执行cmd.exe进入命令行提示符窗口以后,执行整个会话过程的示例:

C:\> cd %TMP%

C:\Users\Tim\AppData\Local\Temp\> cargo new hello
     Created binary (application) 'hello' project

C:\Users\Tim\AppData\Local\Temp\> cd hello

C:\Users\Tim\AppData\Local\Temp\hello\> cargo run
   Compiling hello v0.1.0 (file:///C:/Users/Tim/AppData/Local/Temp/hello)
    Finished debug [unoptimized + debuginfo] target(s) in 0.32s
     Running 'target\debug\hello.exe'
Hello, world!

类似地,在Linux或macOS上,你的控制台上显示的信息应该像下面这样:

$ cd $TMP

$ cargo new hello
     Created binary (application) 'hello' package

$ cd hello

$ cargo run
   Compiling hello v0.1.0 (/tsm/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running 'target/debug/hello'
Hello, world!

如果你走到了这一步,那就太棒了!你已经运行了自己的第一段Rust代码,而且并不需要编写多少Rust代码。接下来,让我们来看看在整个过程中都发生了什么。

Rust的cargo工具既提供了一个构建系统,又提供了包管理器。这意味着cargo知道如何将Rust代码转换成可执行的二进制文件,同时能够管理项目依赖包的下载和编译的过程。

cargo new会遵照标准模板创建一个项目。执行tree命令就能清楚地看到在执行cargo new之后默认的项目目录结构,以及创建出的那些文件:

$ tree hello
hello
├──Cargo.toml
└──src
   └──main.rs

1 directory, 2 files

用cargo创建出来的所有Rust项目有着相同的结构。在项目的根目录中,名为Cargo.toml的文件描述了项目的元数据,例如项目的名称、项目的版本号及其依赖项。源代码保存在src目录中。Rust源文件使用.rs作为它的文件扩展名。要想查看cargo new创建出来的那些文件,可以使用tree命令。

接下来,你要执行的命令是cargo run。这个操作对你来说很容易掌握,然而cargo实际上做的工作比你以为的要多得多。你要求cargo去运行此项目。在调用此命令时,还没有任何实际可执行的程序文件存在,它决定使用调试模式(debug mode)来为你编译代码,这样可以提供最大化的错误信息(error information)。碰巧的是,src/main.rs文件总是会包含一个“Hello,world!”作为初始代码。编译的结果是一个名为hello(或hello.exe)的文件。紧接着它会执行这个文件,并把执行结果输出到屏幕上。

执行cargo run以后,项目还会增加一些新的文件。现在,在项目的根目录中会有一个Cargo.lock文件,还有一个target/目录。这两者都是由cargo管理的。因为它们都是编译过程中的产物,所以可以不予理会。Cargo.lock文件指定了所有依赖项的具体版本号,所以程序总会使用同样的方式,可靠地构建将来的程序版本,直到Cargo.toml的内容被修改才会改变这种构建的方式。

在执行cargo run来编译项目以后,再次执行tree命令来查看新的目录结构:

$ tree --dirsfirst hello
hello
├──src
│  └── main.rs
├──target
│  └── debug
│      ├──build
│      ├──deps
│      ├──examples
│      ├──native
│      └──hello
├──Cargo.lock
└──Cargo.toml

至此,所有步骤能正常运行了,很好!我们已经走捷径直通“Hello, world!”,接下来让我们走一条稍远点儿的路,再次到达这里。

作为第一个Rust程序,我们想编写代码,用于输出以下文本信息:

Hello, world!
Grüß Gott!
ハロー・ワールド

在本书的Rust之旅中,你应该已经见过此输出内容的第一行了。另外的两行是为了展示出Rust的以下特点:易用的迭代和内置对Unicode的支持。与1.3.1节中的程序一样,在这个程序中我们会使用cargo。具体步骤如下。

(1)打开控制台窗口。

(2)如果是在Windows上,就执行cd %TMP%;如果在其他操作系统上,就执行cd $TMP。

(3)执行cargo new hello2命令,创建一个新项目。

(4)执行cd hello2命令,移动到此项目的根目录中。

(5)在一个文本编辑器中打开src/main.rs文件。

(6)用清单1.1中的内容替换该文件中的文本。

清单1.1的源代码参见ch1/ch1-hello2/src/hello2.rs。

清单1.1 用3种语言说“Hello,world!”

 1 fn greet_world() {
 2     println!("Hello, world!");    ⇽---  这里的第一个感叹号表示这是一个宏,这个我们稍后会讨论。
 3     let southern_germany = "Grüß Gott!";    ⇽---  Rust中的赋值,更恰当的说法叫作变量绑定,使用let关键字。
 4     let japan = "ハロー・ワールド";    ⇽---  对Unicode的支持,是“开箱即用”的。
 5     let regions = [southern_germany, japan];    ⇽---  数组字面量使用方括号。
 6     for region in regions.iter() {    ⇽---  很多类型都有iter()方法,此方法会返回一个迭代器。
 7             println!("{}", &region);    ⇽---  此处的和符号(&)表示“借用”region的值,用于只读的访问。
 8     }
 9 }
10
11 fn main() {
12     greet_world();    ⇽---  调用一个函数。要注意紧跟在函数名后面的圆括号。
13 }

现在,代码更新了,只需在hello2/目录里执行cargo run,你应该能看到这3个问候语(它们出现在cargo自己的一些输出的后面),如下所示:

$ cargo run
   Compiling hello2 v0.1.0 (/path/to/ch1/ch1-hello2)
    Finished dev [unoptimized + debuginfo] target(s) in 0.95s
     Running 'target/debug/hello2'
Hello, world!
Grüß Gott!
ハロー・ワールド

让我们花点儿时间来谈谈清单1.1中一些有意思的语言元素。

在这个例子里,你可能最先注意到的就是,Rust中的字符串能够包含许多不同的字符。在Rust中,字符串能够确保是有效的UTF-8编码。这意味着你可以相对轻松地使用非英语的语言。

有一个字符看起来会有点儿奇怪,就是println后面的感叹号。如果你用Ruby编写过程序,可能就会习惯性地认为它表示一个破坏性的操作。然而在Rust中,它表示的是使用一个。就现在来讲,你可以把宏看作一类奇特的函数,其提供了避免“样板代码”(boilerplate code)的能力。对于本例中的println!宏来说,实际上它在底层进行了大量的类型检测工作,所以才能把任意的数据类型输出到屏幕上。

要更好地理解本书中的示例,你需要下载本书清单的源代码。本书所有示例的源代码可以从如下两个地方获得。

异步社区官方网站。

GitHub官方网站。

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.

作为一门编程语言,Rust与众不同的一个特点就是,它能够在编译时就防止对无效数据的访问。微软安全响应中心的研究项目和Chromium浏览器项目都表明了,与无效数据访问相关的问题约占全部严重级安全漏洞(serious security bug)的70%。[12] Rust消除了此类漏洞。它能保证程序是内存安全(memory safe)的,并且不会引入额外的运行时开销。

其他语言可以提供这种级别的安全性(safety),但它们需要在程序的执行期添加额外检查,这无疑会减慢程序的运行速度。Rust设法突破了这种持续已久的状况,开辟出了属于自己的空间,如图1.1所示。

图1.1 Rust兼具安全性和可控性,其他语言则倾向于在这两者之间进行权衡和取舍

就像Rust专业社区所认可的那样,Rust的与众不同之处是“愿意将价值观明确纳入其决策流程中”。这种包容精神无处不在。来自互联网用户的互动消息很受欢迎。Rust社区内的所有互动均受其行为准则约束,甚至Rust编译器的错误信息都是非常有帮助的。

早在2018年年底之前,浏览Rust网站主页的访问者还会看到这样的(更偏向技术性的)宣传语——Rust是一门运行速度极快,能防止出现段错误并能保证线程安全的系统编程语言。后来,社区修改了措辞,从更改后的内容(见表1.1)可以看出,措辞方面已经是以用户(和潜在用户)为中心的了。

表1.1 Rust宣传语的变更。随着对Rust的发展越来越有信心,社区越来越多地接受了这样一种观念,就是可以作为每个希望实现其编程愿望的人的促进者和支持者

2018年年底之前

2018年年底之后

“Rust是一门运行速度极快,能防止出现段错误并能保证线程安全的系统编程语言。

“Rust是一门赋予每个人构建可靠且高效软件能力的语言。

人们给Rust打上了系统编程语言的印记,通常将其视为一个相当专业的、深奥的编程语言分支。但是,许多Rust程序员发现该语言还适用于许多其他领域。安全性、生产力和控制,在软件工程项目中都很有用。Rust社区的“包容性”也意味着,该语言将源源不断地从来自不同利益群体的“新声音”中汲取营养。

接下来,让我们分别来看这3个目标——安全性、生产力和控制,具体指什么,以及为什么它们如此重要。

Rust程序能避免以下几种异常情况出现。

悬垂指针:引用了在程序运行过程中已经变为无效的数据(见清单1.3)。

数据竞争:由于外部因素的变化,无法确定程序在每次运行时的行为(见清单1.4)。

缓冲区溢出:例如一个只有6个元素的数组,试图访问其中的第12个元素(见清单1.5)。

迭代器失效:在迭代的过程中,迭代器中值被更改而导致的问题(见清单1.6)。

如果程序是在调试模式下编译的,那么Rust还可以防止整数溢出。什么是整数溢出呢?整数只能表示数值的一个有限集合,它在内存中具有固定的宽度。比如,整数的上溢出就是指,如果整数的值超出了它的最大值的限制,就会发生溢出,并且它的值会再次变回该整数类型的初始值。

清单1.3所示的是一个悬垂指针的例子。注意,此示例的源代码文件存储路径为ch1/ ch1-cereals/src/main.rs。

清单1.3 试图创建一个悬垂指针

 1 #[derive(Debug)]    ⇽---  允许使用println! 宏来输出枚举体Cereal(谷类)。
 2 enum Cereal {    ⇽---  enum(枚举体,是enumeration的缩写)是一个具有固定数量的合法变体的类型。
 3     Barley, Millet, Rice,
 4     Rye, Spelt, Wheat,
 5 }
 6
 7 fn main() {
 8     let mut grains: Vec <Cereal> = vec![];    ⇽---  初始化一个空的动态数组,其元素类型为Cereal。
 9     grains.push(Cereal::Rye);    ⇽---  向动态数组grains(粮食)中添加一个元素。
10     drop(grains);    ⇽---  删除grains和其中的数据。
11     println!("{:?}", grains);    ⇽---  试图访问已删除的值。
12 }

如清单1.3所示,在第8行中创建的grains,其内部包含一个指针。Vec<Cereal>实际上是使用一个指向其底层数组的内部指针来实现的。但是此清单无法通过编译。尝试去编译会触发一个错误信息,信息的大意是“试图去‘借用’一个已经‘被移动’了的值”。学习如何理解该错误信息并修复潜在的错误,是本书后面几页内容的主题。编译清单1.3的代码,输出信息如下所示:

$ cargo run
   Compiling ch1-cereals v0.1.0 (/rust-in-action/code/ch1/ch1-cereals)
error[E0382 borrow of moved value: 'grains'
  --> src/main.rs:12:22
   |
 8 |     let mut grains: Vec <Cereal> = vec![];
   |         ---------- move occurs because 'grains' has type 
                       'std::vec::Vec <Cereal>', which does not implement 
                        the 'Copy' trait
 9 |     grains.push(Cereal::Rye);
10 |     drop(grains);
   |          ------ value moved here
11 |
12 |     println!("{:?}", grains);
   |                      ^^^^^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try 'rustc --explain E0382'.
error: could not compile 'ch1-cereals'.

清单1.4(参见ch1/ch1-race/src/main.rs文件)展示了一个Rust防止数据竞态条件的示例。之所以会出现这种情况,是因为外部因素的变化而无法确定程序在每次运行中的行为。

清单1.4 Rust防止数据竞态条件的示例

 1 use std::thread;    ⇽---  把多线程的能力导入当前的局部作用域。
 2 fn main() {
 3     let mut data = 100;
 4
 5     thread::spawn(|| { data = 500; });    ⇽---  thread::spawn() 接收一个闭包作为参数。
 6     thread::spawn(|| { data = 1000; });
 7     println!("{}", data);
 8 }

如果你还不熟悉线程这个术语,那么请记住,上述这段代码的要点就是“它的运行结果是不确定的。也就是说,无法知道在main()退出时,data的值是什么样的”。在清单1.4的第5行和第6行中,调用thread :: spawn()会创建两个线程。每次调用都接收一个闭包作为参数——闭包是由竖线和花括号来表示的(例如||{...})。第5行创建的这个线程试图把data变量的值设为500,而第6行创建的这个线程试图把data变量的值设为1000。由于线程的调度是由操作系统决定的,而不是由应用程序决定的,因此根本无法知道先定义的那个线程会不会率先执行。

如果尝试编译清单1.4,就会出现许多错误信息。Rust不允许应用程序中存在多个位置,这些位置都能够对同一数据进行写操作。在此代码中,有3个位置都试图进行这样的访问:一个位置出现在main()中运行的主线程里,另两个位置则出现在由thread :: spawn()创建出的子线程中。编译器的输出信息如下:

$ cargo run
   Compiling ch1-race v0.1.0 (rust-in-action/code/ch1/ch1-race)
error[E0373]: closure may outlive the current function, but it
              borrows 'data', which is owned by the current function
--> src/main.rs:6:19
  |
6 |    thread::spawn(|| { data = 500; });
  |                  ^^ ---- 'data' is borrowed here
  |                  |
  |                  may outlive borrowed value 'data'
  |
note: function requires argument type to outlive ''static'
 --> src/main.rs:6:5
  |
6 |    thread::spawn(|| { data = 500; });
  |    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of 'data'
      (and any other referenced variables), use the 'move' keyword
  |
6 |    thread::spawn(move || { data = 500; });
  |                  ^^^^^^^
...    ⇽---  此处忽略了其他的3个错误。
error: aborting due to 4 previous errors
Some errors have detailed explanations: E0373, E0499, E0502.
For more information about an error, try 'rustc --explain E0373'.
error: could not compile 'ch1-race'.

清单1.5给出了一个由缓冲区溢出而引发恐慌的示例。缓冲区溢出描述的是“试图访问内存中不存在的或者非法的元素”这样一种情况。在这个例子中,如果尝试访问fruit[4],将导致程序崩溃,因为fruit变量中只有3个fruit(水果)。清单1.5的源代码存放在文件ch1/ch1-fruit/ src/main.rs中。

清单1.5 由缓冲区溢出而引发恐慌的示例

 1 fn main() {
 2     let fruit = vec!['', '', ''];
 3
 4     let buffer_overflow = fruit[4];    ⇽---  Rust会让程序崩溃,而不会把一个无效的内存位置赋值给一个变量。
 5     assert_eq!(buffer_overflow,'')    ⇽---  assert_eq!() 会测试其参数是否相等。
 6 }

如果编译并运行清单1.5,你会看到如下所示的错误信息:

$ cargo run
   Compiling ch1-fruit v0.1.0 (/rust-in-action/code/ch1/ch1-fruit)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running 'target/debug/ch1-fruit'
thread 'main' panicked at 'index out of bounds: 
    the len is 3 but the index is 4', src/main.rs:3:25
note: run with 'RUST_BACKTRACE=1' environment variable 
    to display a backtrace

清单1.6所示的是一个迭代器失效的例子。也就是说,在迭代过程中,因迭代器中的值被更改而导致出现问题。清单1.6的源代码存放在文件ch1/ch1-letters/src/main.rs中。

清单1.6 在迭代过程中试图去修改该迭代器

 1 fn main() {
 2     let mut letters = vec![    ⇽---  创建一个可变的动态数组letters。
 3         "a", "b", "c"
 4     ]; 
 5
 6     for letter in letters {
 7         println!("{}", letter);
 8         letters.push(letter.clone());    ⇽---  复制每个letter,并将其追加到letters的末尾。
 9     }
10 }

如果编译清单1.6的代码,就会出现编译失败的情况,因为Rust不允许在该迭代块中修改letters。具体的错误信息如下:

$ cargo run
   Compiling ch1-letters v0.1.0 (/rust-in-action/code/ch1/ch1-letters)
error[E0382]: borrow of moved value: 'letters'
 --> src/main.rs:8:7
  |
2 |   let mut letters = vec![
  |       ----------- move occurs because 'letters' has type
  |                   'std::vec::Vec <&str>', which does not
  |                   implement the 'Copy' trait
...
6 |   for letter in letters {
  |                 -------
  |                 |
  |                 'letters' moved due to this implicit call
  |                 to '.into_iter()'
  |                 help: consider borrowing to avoid moving
  |                 into the for loop: '&letters'
7 |       println!("{}", letter);
8 |       letters.push(letter.clone());
  |       ^^^^^^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try 'rustc --explain E0382'.
error: could not compile 'ch1-letters'.

To learn more, run the command again with --verbose.

虽然错误信息字里行间满是专业术语(borrowmovetrait等),但Rust保护了程序员,使其不至于踏入许多其他语言中会掉入的陷阱。而且不用担心——当你学完本书的前几章后,这些专业术语将会变得更容易理解。

知道一门语言是安全的,能给程序员带来一定程度的自由。因为他们知道自己的程序不会发生“内爆”,所以会更愿意去做各种尝试。在Rust社区中,这种自由催生出了无畏并发的说法。

如果可以,Rust会选择对开发人员来说最容易的选项。Rust有许多可以提高生产力的微妙特性。然而,程序员的生产力很难在书本的示例中得以展示。那就让我们从一些初学者易犯的错误开始吧——在应该使用相等运算符(==)进行测试的表达式中使用了赋值符号(=)。

1 fn main() {
2     let a = 10;
3
4     if a = 10 {
5         println!("a equals ten");
6     }
7 }

在Rust中,这段代码会编译失败。Rust编译器会产生下面的信息:

error[E0308 mismatched types
 --> src/main.rs:4:8
  |
4 |     if a = 10 {
  |        ^^^^^^
  |        |
  |        expected 'bool', found '()'
  |        help: try comparing for equality: 'a == 10'

error: aborting due to previous error

For more information about this error, try 'rustc --explain E0308'.
error: could not compile 'playground'.

To learn more, run the command again with --verbose.

首先,上文中的mismatched types会让人觉得像是一个奇怪的错误信息。我们肯定能够测试变量与整数的相等性。

经过一番思考,你会发现if测试接收到错误的类型的原因。在这里,if接收的不是一个整数,而是赋值表达式的结果。在Rust中,这是一个空的类型(),被称作单元类型[13]

当不存在任何有意义的返回值时,表达式就会返回()。再来看看下面给出的这段代码,在第4行上添加了第二个等号以后,这个程序就可以正常工作了,会输出a equals ten

1 fn main() {
2     let a = 10;
3
4     if a == 10 {    ⇽---  使用一个有效的运算符( == ),让程序通过编译。
5         println!("a equals ten");
6     }
7 }

Rust具有许多工效学特性,如泛型、复杂数据类型、模式匹配和闭包。[14]  用过其他提前编译型语言的人,很可能会喜欢Rust的构建系统,即功能全面的Rust软件包管理器:cargo。

初次接触时,我们看到cargo是编译器rustc的前端,但其实它也为Rust程序员提供了下面这些命令。

cargo new用于在一个新的目录中,创建出一个Rust项目的骨架(cargo init则使用当前目录)。

cargo build用于下载依赖项并编译代码。

cargo run所做的事情和cargo build差不多,但同时会运行生成出来的可执行文件。

cargo doc为当前项目生成HTML文档,其中也包括每个依赖包的文档。

[12] 参见We need a safer systems programming language.

[13] Rust吸收了函数式编程语言的诸多特性,如“单元类型”这个名称就是从函数式编程语言(如Ocaml和F#)家族继承而来的。理论上,单元类型只有一个值,就是它本身。相比之下,布尔类型有两个值(真/假),而字符串可以有无限多个值。

[14] 即使对这些术语不熟悉,也请继续阅读本书。本书其他章节对这些术语进行了解释。

Rust能让程序员精确控制数据结构在内存中的布局及其访问模式。虽然Rust会用合理的默认值来实施其“零成本抽象”的理念,然而这些默认值并不适合所有情况。

有时,管理应用程序的性能是非常有必要的。让数据存储在中而不是中,有可能是很重要的。有时,创建出一个值的共享引用,再给这个引用添加引用计数,有可能很有意义。偶尔为了某种特殊的访问模式,创建自己的指针类型可能就会很有用。设计空间是很大的,Rust提供的各种工具可以让你实现自己的首选解决方案。

注意 如果你对引用计数等术语不熟悉,也请不要放弃!我们将在本书的其他章节中用大量的篇幅来解释这些内容,以及它们是如何一起工作的。

运行清单1.7中的代码,会输出一行信息,即a: 10, b: 20, c: 30, d: Mutex { data: 40 }。其中的每个变量都表示一种存储整数的方式。在接下来的几章中,我们会讲解与每种级别的表示形式相关的权衡和取舍。就现在而言,要记住的重要一点就是,可供选择的各种类型的选项还是很全面的。欢迎你为特定的使用场景选出合适的使用方式。

清单1.7展示了创建整数值的多种方式。其中的每种形式都提供了不同的语义和运行时特征,但是程序员是可以完全控制自己希望做出的权衡和取舍的。

清单1.7 创建整数值的多种方式

1 use std::rc::Rc;
2 use std::sync::{Arc, Mutex};
3
4 fn main() {
5     let a = 10;    ⇽---  在栈中的整数
6     let b = Box::new(20);    ⇽---  在堆中的整数,也叫作装箱的整数。
7     let c = Rc::new(Box::new(30));    ⇽---  包装在一个引用计数器中的装箱的整数。
8     let d = Arc::new(Mutex::new(40));    ⇽---  包装在一个原子引用计数器中的整数,并由一个互斥锁保护。
9     println!("a: {:?}, b: {:?}, c: {:?}, d: {:?}", a, b, c, d);
10 }

要理解Rust为什么会有这么多种不同的方式,请参考以下3条原则。

该语言的第一要务是安全性。

默认情况下,Rust中的数据是不可变的。

编译时检查是强烈推荐使用的。安全性应该是“零成本抽象”的。

我们相信,能够创建出哪些东西来取决于我们使用什么工具。Rust让你能构建出自己想要的软件,同时又不必因为过于担心而不敢去尝试。那么,Rust是什么样的工具呢?从1.6节的3条原则中可以看出,Rust语言有如下3个主要特点。

高性能。

支持并发。

内存使用效率高。

Rust为你提供了计算机可用的全部性能。很有名的一点就是,Rust提供的内存安全性是不依靠垃圾回收器的。

不幸的是,向你承诺实现更快的程序时,存在一个问题:CPU的速度是固定的。因此,要让软件运行得更快,就要让软件做更少的事情。然而,Rust语言的规模很大,为了解决这个矛盾,Rust将重担交给了编译器。

Rust社区倾向于采用规模更大的语言,由编译器去承担更多的工作,而不是采用更简单的语言,由编译器去承担更少的工作。Rust编译器会积极地优化程序的大小和速度。Rust也有一些不太明显的技巧,如下所示。

默认情况下,提供缓存友好的数据结构。在Rust程序中,通常用数组来保存数据,而不是由指针创建的深层嵌套的树结构。这也叫作面向数据编程

有现代化的包管理器可用(cargo),这使得要从数以万计的开源软件包中获益这件事变得很轻松。C和C++在这方面的平滑度要低得多,构建具有众多依赖关系的大型项目往往非常困难。

除非显式地请求动态分发,否则总是采用静态分发的。这使得编译器可以极大地优化代码,有时甚至可以完全消除函数调用的开销。

对软件工程师来说,要让一台计算机同时做多件事情,无疑是非常困难的。从操作系统角度来看,如果程序员犯了一个严重错误,两个独立运行的线程就可能随意地相互破坏。然而,Rust催生了无畏并发的说法。它对安全性的强调跨越了独立线程的界限,而且也没有全局解释器锁(GIL)来限制线程的速度。在本书的第二部分中,我们将探讨其中的一些含义。

Rust让你创建出的程序具有最小的内存使用量。在需要的时候,你可以使用固定大小的数据结构,并且能够明确地知道你的程序是如何管理每个字节的。在使用诸如迭代和泛型类型等高级的语言构造时,Rust也会最小化它们的运行时开销。

人们在谈论这门语言时,很容易会给人一种“这是所有软件工程的灵丹妙药”的感觉。举例来说:

“高级语言的语法,低级语言的性能!”

“并发而不会崩溃!”

“具有完美安全性的C!”

这些宣传口号(有些夸大其词)真的很棒。但是,即便兼具所有的这些优点,Rust也确实有一些缺点。

在Rust中,我们很难对循环数据(比如任意一个图结构)进行建模。实现双向链表是一个大学本科生就能解决的计算机科学问题,然而,Rust的安全性检查确实妨碍了类似结构的代码编写。如果你正在学习Rust语言,在对它有足够的了解之前,你应该尽量避免去实现这类数据结构。

Rust的代码编译速度比同等语言慢。Rust的编译器工具链很复杂,其中有多种中间表示形式,并且会发送大量的代码给低级虚拟机(Low Level Virtual Machine,LLVM)编译器。Rust程序的“编译单元”不是一个单独的文件,而是一个完整的程序包(昵称为crate)。Rust程序包有可能包含众多模块,因此可能会是非常大的编译单元。虽然这样可以针对整个程序包进行优化,但同样地,也必须针对程序包的整体进行编译。

使用Rust编程时,是不可能——好吧,是很难偷懒的。在所有代码都正确之前,程序不能通过编译。编译器很严格,但是也很有帮助。

随着时间的推移,你可能会逐渐喜欢上这个特点。如果你使用过动态语言编程,那么肯定遇到过因为变量名错误而使程序崩溃的情况,并由此体会到挫败感。Rust把出现这种挫败感的时间给提前了,至少使用你的程序的用户不必经历有某些东西崩溃了的挫败感。

Rust语言的规模很大!它有一个丰富的类型系统、几十个关键字,并包含一些其他语言所没有的特性。这些因素叠加起来,就形成了一个陡峭的学习曲线。为了让学习的过程易于管理,我建议逐步地学习Rust。从语言的最小子集开始,当你需要进一步学习某些细节时,再给自己留出时间来学习。这就是本书所采取的方法。高级的概念将会被推迟到比较靠后的、合适的章节中再来讲解。

对于发展得过快和被夸张的宣传过度消费的情况,Rust社区秉持非常谨慎的态度。然而,有不少软件项目都遇到过一个问题,在项目相关的邮件中会出现类似这样的建议:“你考虑过用Rust来重写这个程序吗?”不幸的是,用Rust编写的软件仍然是软件。Rust语言并不能避免所有的安全性(security)问题,而且它也没有能够解决软件工程中所有弊病的灵丹妙药。

为了表明Rust并不能解决所有错误,我们来看一下曾威胁到几乎所有面向互联网的设备的两种严重漏洞,并考虑一下使用Rust能否避免出现这样的情况。

2015年,随着Rust的兴起,SSL/TLS的实现(OpenSSL以及苹果公司的派生版),出现了两种严重的安全漏洞。这两种漏洞被非正式地称为“心脏出血”(Heartbleed)和跳转到失败(goto fail;),这为Rust提出的内存安全性主张提供了检验的机会。针对这两种漏洞,Rust都可能会有所帮助,但是仍然有可能编写出会导致类似问题的Rust代码。

“心脏出血”,这种漏洞正式的命名为CVE-2014-0160[15],是由错误重用缓冲区引起的。缓冲区就是在内存中预留的一片区域,用以接收输入的数据。如果在两次写入操作之间未清空缓冲区中的内容,数据就有可能会从一次读取泄露到下一次读取。

为什么会发生这种状况呢?因为程序员要追求性能。重用缓冲区可以最大限度地减少应用程序向操作系统请求内存的次数。

假设我们希望处理来自多个用户的一些私密信息。出于某种原因,我们决定在程序中重用单个缓冲区。如果我们在每次使用完该缓冲区后没有重置它,那么前一次调用中的信息就会泄露到后一次的调用中。下面给出了一段会发生此错误的代码:

let buffer = &mut[0u8; 1024];    ⇽---  将一个可变(mut)数组([...])的引用(&)绑定到变量buffer,该数组包含1024个无符号8位整数(u8),并初始化为0。
read_secrets(&user1, buffer);    ⇽---  使用user1对象的字节数据去填充buffer。
store_secrets(buffer);
read_secrets(&user2, buffer);    ⇽---  buffer包含的user1数据,有可能被user2全覆盖,也有可能还没有被全部覆盖。
store_secrets(buffer);

Rust不能帮你避免逻辑错误,却能保证数据永远不会在两个地方被同时写入,但并不能确保程序能够避免所有安全性问题。

[15] 参见NIST官方网站CVE-2014-0160相关的内容.

跳转到失败,这种漏洞正式的命名为CVE-2014-1266,是由程序员导致的错误以及C的设计问题(还有潜在的编译器未指出代码中的缺陷)耦合在一起而导致的。一个用于验证加密密钥对的函数最终跳过了所有的检查。以下代码是从原本的问题函数SSLVerifySignedServerKeyExchange中摘录的,其中保留了不少令人费解的语法:

 1 static OSStatus
 2 SSLVerifySignedServerKeyExchange(SSLContext *ctx,
 3                                  bool isRsa,
 4                                  SSLBuffer signedParams,
 5                                 uint8_t *signature,
 6                                 UInt16 signatureLen)
 7{
 8    OSStatus        err;    ⇽---  使用一个表示通过(pass)的值来初始化OSStatus,例如0。
 9    ...
10
11     if ((err = SSLHashSHA1.update(
12         &hashCtx, &serverRandom)) != 0)    ⇽---  一系列防御性的编程检查。
13         goto fail;
14
15     if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
16         goto fail;
17         goto fail;    ⇽---  无条件跳转,跳过了对SSLHashSHA1.final()和非常重要的sslRawVerify() 的调用。
18     if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
19         goto fail;
20
21     err = sslRawVerify(ctx,
22                        ctx->peerPubKey,
23                        dataToSign,              /* plaintext \*/
24                        dataToSignLen,           /* plaintext length \*/
25                        signature,
26                        signatureLen);
27     if(err) {
28         sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
29                     "returned %d\n", (int)err);
30         goto fail;
31     }
32
33 fail:
34     SSLFreeBuffer(&signedHashes);
35     SSLFreeBuffer(&hashCtx);
36     return err;    ⇽---  即使对输入数据的验证测试是失败的,仍然会返回表示通过测试的值0。
37 }

在这个样例代码中,问题就出在第15~17行。在C语言中,逻辑测试中的花括号不是必需的。C编译器会像下面这样,来解释这3行代码:

if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) {
   goto fail;
}
goto fail;

那么用Rust会有帮助吗?有可能。在这种情况下,Rust的语法会捕捉到该错误。它不允许没有花括号的逻辑测试。当某部分代码不可能被执行到(unreachable)时,Rust也会发出警告。但这并不意味着在Rust中就不可能出错。在紧张的截止日期(deadline)压力之下,程序员会犯一些错误。而通常情况下,这样的代码也有可能被编译通过并运行。

提示 请谨慎编码。

虽然Rust是作为系统编程语言而设计的,但实际上它是一门通用语言,现已顺利应用于许多的领域。

在程序员创建命令行实用程序方面,Rust有3个主要优势:最小化的程序启动时间、内存使用量低和易于部署。程序很快就开始“干活”了,这是因为Rust程序没有需要初始化的解释器(Python、Ruby等)或者虚拟机(Java、C#等)。

作为一种裸机语言,Rust生成的程序内存使用效率高。[16] 在本书中,你会见到很多零大小的类型。也就是说,它们仅仅是作为对编译器的提示而存在的,在程序运行时完全不占用内存。

用Rust编写的实用程序默认被编译为静态二进制文件。这种编译方法避免了在程序运行前必须先安装依赖共享库,可创建无须安装即可运行的程序,让它们易于分发。

[16] 有人开玩笑说“Rust尽可能地接近裸机”。

Rust擅长文本处理和其他形式的数据整理。程序员受益于对内存使用的控制和快速的启动时间。截至2017年年中,它号称拥有世界上最快的正则表达式引擎。2019年,Apache Arrow数据处理项目(Python和R数据科学生态系统中的基础级项目),接受了基于Rust的DataFusion项目。

Rust还被用作实现多个搜索引擎、数据处理引擎和日志解析系统的基础。它的类型系统和内存控制,让你能够创建出具有低而稳定的内存占用量、同时又具有高吞吐量的数据管道。小型的过滤器程序可以借助于Apache Storm、Apache Kafka或者Apache Hadoop Streaming,很容易地被嵌入更大的架构。

Rust非常适合用来扩展由动态语言编写的程序。这让我们能够使用Rust来编写JNI(Java Native Interface)扩展、C扩展以及Erlang/Elixir NIF(native implemented functions)扩展。编写C扩展往往是一个吓人的任务。这类扩展倾向于与运行时紧密集成。如果出了错,你可能会看到内存泄漏或完全崩溃而导致的内存消耗失控。Rust消除了程序员们在这方面的许多担心。

Sentry是一家负责处理应用程序错误的公司。他们发现,Rust是重写其Python系统中的CPU密集型组件的理想选择。[17]

Dropbox使用Rust重写了其客户端应用程序的文件同步引擎:“除了性能,(Rust的)工效学特性和对正确性的关注帮助我们‘驯服’了同步功能的复杂性。”[18]

[17] 参见Fixing Python Performance with Rust.

[18] 参见Rewriting the heart of our sync engine.

数十年来,C语言一直“占据着”微控制器领域。然而,物联网时代即将到来。这可能意味着数以十亿计的不安全设备暴露在网络中。任何输入数据解析的代码都会被例行地探测是否存在弱点。考虑到这些设备很少进行固件更新,因此从一开始就确保它们的安全性是至关重要的。Rust在这里能够发挥重要的作用,因为它增加了一层安全性保证,并且不会引入运行时开销。

大多数使用Rust编写的应用程序是在服务器端运行的。这些应用程序可能是服务于Web流量传输,又或者是为企业运行其业务提供支持。它们介于操作系统和你的应用程序之间。在这个领域中,Rust被用来编写数据库、监控系统、搜索类应用和消息系统,部分示例如下。

用于JavaScript和node.js社区的npm程序包仓库是用Rust编写的。[19]

sled:一个嵌入式数据库,在16核的计算机上,可以在不到一分钟的时间里,处理10亿次操作,其中包括5%的写操作。

Tantivy:一个全文搜索引擎,在一台4核的台式计算机上,可以在大约100s内完成8 GB的英文维基百科的索引。[20]

[19] 参见Community makes Rust an easy choice for npm: The npm Registry uses Rust for its CPU-bound bottlenecks.

[20] 参见Of tantivy’s indexing.

在Rust的设计中,没有任何内在因素会妨碍其开发面向用户的软件。Servo,一个Web浏览器引擎,作为Rust的早期开发孵化器,就是一个面向用户的应用程序。当然,开发游戏软件也是没问题的。

编写运行在最终用户计算机上的应用程序,依然有很大的需求量。桌面级应用通常很复杂,难以设计且难以提供支持。凭借Rust符合工效学的部署方法以及它的严格特性,它可能会成为许多应用程序的“秘密武器”。一开始,它们将会由小型团队或独立开发者来构建。随着Rust的成熟,其生态系统也将逐步成熟。

Android、iOS和其他的智能手机操作系统,通常为开发人员提供了一条推荐的开发路径。对于Android系统,开发人员通常使用Java编程;对于iOS系统,开发人员通常使用Swift编程。然而,还有另外一种方式存在。

这两个平台都提供了在其系统上运行“原生应用程序”的能力。这通常是为了让用C++编写的应用程序,能被部署到手机上,比如游戏。Rust可以使用相同的接口与手机交互,且没有额外的运行时开销。

你可能知道,JavaScript是Web编程语言。但随着时间的推移,这种情况会有所改变。浏览器厂商正在开发一种名为WebAssembly(简称Wasm)的标准,该标准有望成为多种语言的编译目标。Rust是首批被选中的语言。只需在命令行上执行两个额外的命令,就可以将一个Rust项目移植到浏览器端。有多家公司正在利用Wasm技术来探索Rust在浏览器中的应用,较著名的公司有CloudFlare和Fastly。

从某种意义上说,系统编程正是Rust存在的理由。目前,已经有许多大型程序使用Rust来实现,包括编译器(Rust本身)、视频游戏引擎和操作系统。也有许多软件的作者来自Rust社区,其中包括解析器、生成器、数据库和文件格式的作者等。

那些与Rust社区有着共同目标的程序员,他们已经证明了Rust是一个高效的开发环境。这个领域有如下3个杰出的项目。

谷歌发起了Fuchsia OS的开发,这是一个提供给多种设备使用的操作系统。[21]

微软正在积极探索使用Rust来编写Windows的底层组件。[22]

AWS正在构建Bottlerocket,这是一个用于云上管理容器的定制化操作系统。[23]

[21] 参见Welcome to Fuchsia!.

[22] 参见Using Rust in Windows.

[23] 参见Bottlerocket: Linux-based operating system purpose-built to run containers.

一门编程语言的成长,需要的不仅是软件。Rust团队做得非常出色的一件事,就是培育了一个积极而热情的Rust社区。

与Rust社区的成员交流时,你很快就会遇到一些有特殊含义的术语。了解了下面的这些术语,你可以更容易地理解为什么Rust会有现在的发展以及它试图要解决哪些问题。

给所有人赋能(empowering everyone)——所有程序员,不论能力和背景,都欢迎参与。编程,特别是系统编程,不应该局限于少数幸运者。

快如闪电(blazingly fast)——Rust是一门快速的编程语言。你将能够编写出在运行速度上匹配甚至超过同级语言的程序,与此同时你还将得到更多的安全保证。

无畏并发(fearless concurrency)——并发和并行编程一直被认为是比较困难的。Rust使你摆脱了困扰同级语言的所有种类的错误。

没有Rust 2.0(no Rust 2.0)——今天编写的代码始终能够在将来的Rust编译器版本上编译。Rust旨在成为一种可靠的编程语言,在未来的几十年内都可以依赖。依照语义化版本,Rust永远不会出现向后不兼容的情况,因此永远不会发布新的主版本。

零开销抽象(zero-cost abstractions)——你从Rust获得的各种特性不会引入运行时开销。使用Rust编程,在保证安全的同时不会牺牲速度。

许多公司使用Rust成功构建了大型的软件项目。

使用Rust编写的软件可以被编译到多种设备或位置上,其中包括PC端、浏览器端、服务器端,以及移动端和物联网。

Rust语言深受软件开发人员的喜爱,多次获得Stack Overflow的“最喜爱的编程语言”称号。

Rust使你无须担心即可开始试验,它提供了其他工具难以提供的正确性保证,同时又不会引入运行时开销。

要学习Rust,需要学习如下3个主要的命令行工具。

cargo:用于管理整个包。

rustup:用于管理Rust的安装。

rustc:用于管理Rust源代码的编译。

Rust项目无法避免所有错误。

Rust代码是稳定的、快速的,而且对资源的占用很轻量。

微信扫码关注【异步社区】微信公众号,回复“e59139”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


相关图书

JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战
玩转Scratch少儿趣味编程
玩转Scratch少儿趣味编程
罗布乐思开发官方指南:Lua 语言编程
罗布乐思开发官方指南:Lua 语言编程

相关文章

相关课程