深入浅出Rust(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-21 00:58:46

点击下载

作者:范长春

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入浅出Rust

深入浅出Rust试读:

前言

A language that doesn’t affect the way you think about programming is not worth knowing.——Alan PerlisRust简介

Rust是一门新的编程语言。

我想,大部分读者看到本书,估计都会不约而同地想到同样的问题:现存的编程语言已经多得数不清了,再发明一种新的编程语言有何意义?难道现存的那么多编程语言还不够用吗,发明一种新的编程语言能解决什么新问题?

俗话说,工欲善其事,必先利其器。在程序员平时最常用的工具排行榜中,编程语言当仁不让的是最重要的“器”。编程语言不仅是给程序设计者使用的工具,反过来,它也深刻地影响了设计者本身的思维方式和开发习惯。

卓越的编程语言,可以将优秀的设计、先进的思想、成功的经验,自然而然地融入其中,使更多的使用者开阔眼界、拓展思路,受益无穷。

A programming language is a tool that has profound influence on our thinking habits.——Edsger Dijkstra

所以说关于这个问题,我认为,如果与现有的各种语言相比,新设计的语言有所进步、有所发展、有所创新,那么它的出现就很有意义。

最近这些年,的确涌现出了一大批编程语言,可以说是百花争艳、繁华似锦。但是在表面的繁荣之下,我们是否可以自满地说,编程语言的设计和发展已经基本成熟、趋于完美了呢?恐怕不尽然吧!

那些优秀的编程语言中,不少都有自己的“绝活”。有的性能非常高,有的表达力非常强,有的擅长组织大型程序,有的适合小巧的脚本,有的专注于并发,有的偏重于科学计算,等等,不一而足。即便如此,新兴的Rust语言面市后依旧展现出了它的独特魅力,矫矫不群,非常值得大家关注。

作为多年来鲜有的新一代系统编程语言,它的设计准则是“安全,并发,实用”。Rust的设计者是这样定位这门语言的:

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

安全

是的,安全性很重要。Rust最重要的特点就是可以提供内存安全保证,而且没有额外的性能损失。

在传统的系统级编程语言(C/C++)的开发过程中,经常出现因各种内存错误引起的崩溃或bug。比如空指针、野指针、内存泄漏、内存越界、段错误、数据竞争、迭代器失效等,血泪斑斑,数不胜数。这些问题不仅在教科书中被无数次提起,而且在实践中也极其常见。因此,各种高手辛苦地总结了大量的编程经验,许多代码检查和调试工具被开发出来,各种代码开发流程和规范被制定出来,无数人呕心沥血就是为了系统性地防止各类bug的出现。尽管如此,我们依然无法彻底解决这些问题。

教科书解决不了问题,因为教育不是强制性的;静态代码检查工具解决不了问题,因为传统的C/C++对静态代码检查不友好,永远只能查出一部分问题;软件工程解决不了问题,因为规范依赖于执行者的素质,任何人都会犯错误。事后debug也不是办法,解决bug的代价更高。

鉴于手动内存管理非常容易出问题,因此先辈们发明了一种自动垃圾回收的机制(Garbage Collection),故而程序员在绝大多数情况下不用再操心内存释放的问题。新发明的绝大多数编程语言都使用了基于各种高级算法的自动垃圾回收机制,因为它确实方便,解放了程序员的大脑,使大家能更专注于业务逻辑的部分。但是到目前为止,不管使用哪种算法的GC系统,在性能上都要付出比较大的代价。要么需要较大的运行时占用较大内存,要么需要暂停整个程序,要么具备不确定性的时延。当然,在现实的许多业务场景中,这点开销是微不足道的,因此问题不大。可是如果在性能敏感的领域,这是完全不可接受的。

很遗憾,到目前为止,在系统级编程语言中,我们依然被各种内存安全问题所困扰。这些年来,许多新的语言特性被发明出来,许多优秀的编程范式被总结出来,许多高质量的代码库被开发出来。但是内存安全问题依然像一个幽灵一样,一直徘徊在众多程序员的头顶,无法摆脱。再多的努力,也只能减少它出现的机会,很难保证完整地解决掉这一类错误。

Rust对自己的定位是接近芯片硬件的系统级编程语言,因此,它不可能选择使用自动垃圾回收的机制来解决问题。事实证明,要想解决内存安全问题,小修小补是不够的,必须搞清楚导致内存错误的根本原因,从源头上解决。Rust就是为此而生的。Rust语言是可以保证内存安全的系统级编程语言。这是它的独特的优势。本书将用大量的篇幅详细介绍“内存安全”。

并发

在计算机单核性能越来越接近瓶颈的今天,多核并行成了提高软件执行效率的发展趋势。一些编程语言已经开始从语言层面支持并发编程,把“并发”的概念植入到了编程语言的血液中。然而,在传统的系统级编程语言中,并行代码很容易出错,而且有些问题很难复现,难以发现和解决问题,debug的成本非常高。线程安全问题一直以来都是非常令人头痛的问题。

Rust当然也不会在这一重要领域落伍,它也非常好地支持了并发编程。更重要的是,在强大的内存安全特性的支持下,Rust一举解决了并发条件下的数据竞争(Data Race)问题。它从编译阶段就将数据竞争解决在了萌芽状态,保障了线程安全。

Rust在并发方面还具有相当不错的可扩展性。所有跟线程安全相关的特性,都不是在编译器中写死的。用户可以用库的形式实现各种高效且安全的并发编程模型,进而充分利用多核时代的硬件性能。

实用

Rust并不只是实验室中的研究型产品,它的目标是解决目前软件行业中实实在在的各种问题。它的实用性体现在方方面面。

Rust编译器的后端是基于著名的LLVM完成机器码生成和优化的,它只需要一个非常小巧的运行时即可工作,执行效率上可与C语言相媲美,具备很好的跨平台特性。

Rust摈弃了手动内存管理带来的各种不安全的弊端,同时也避免了自动垃圾回收带来的效率损失和不可控性。在绝大部分情况下,保持了“无额外性能损失”的抽象能力。

Rust具备比较强大的类型系统,借鉴了许多现代编程语言的历史经验,包含了众多方便的语法特性。其中包括代数类型系统、模式匹配、闭包、生成器、类型推断、泛型、与C库ABI兼容、宏、模块管理机制、内置开源库发布和管理机制、支持多种编程范式等。它吸收了许多其他语言中优秀的抽象能力,海纳百川,兼容并蓄。在不影响安全和效率的情况下,拥有不俗的抽象表达力。

有意思的地方是,在程序语言设计领域,按照传统思路,有些设计目标是互相冲突的。而Rust的优异之处在于,它能游刃有余地游走在各种设计目标之间,扬长避短,保持良好的妥协和平衡。本书结构

本书将详细描述Rust语言的基本语法,穿插讲解一部分高级使用技巧,并尽量以更容易理解的方式向读者解释其背后的设计思想。语法只是基础,并非本书的重点,笔者更希望读者能理解到这些语法设计背后的理念,读完本书之后,可以从中受到一点启发,对程序语言有更多的认识,从而对编程本身有更深的理解。

Learning a language that is significantly different than you are used to is certainly tough at first,but it’s a great way to expand your horizons a bit.

在本书中,笔者尽量避免要求读者有很多的基础知识。当然,如果读者对其他的一种或多种编程语言有所了解更佳,其中包括C/C++的基础知识、内存错误、手动内存管理、自动垃圾回收、多线程并发和同步、操作系统相关的基础概念等。

本书共分为五个部分。

第一部分介绍Rust基本语法。因为对任何程序设计语言来说,语法都是基础,学习这部分是理解其他部分的前提。

第二部分介绍属于Rust独一无二的内存管理方式。它设计了一组全新的机制,既保证了安全性,又保持了强大的内存布局控制力,而且没有额外性能损失。这部分是本书的重点和核心所在,是Rust语言的思想内核精髓之处。

第三部分介绍Rust的抽象表达能力。它支持多种编程范式,以及较为强大的抽象表达能力。

第四部分介绍并发模型。在目前这个阶段,对并行编程的支持是新一代编程语言无法绕过的重要话题。Rust也吸收了业界最新的发展成果,对并发有良好支持。

第五部分介绍一些实用设施。Rust语言有许多创新,但它绝不是高高在上、孤芳自赏的类型。设计者们在设计过程中充分考虑了语言的工程实用性。众多在其他语言中被证明过的优秀实践被吸收了进来,有利于提升实际工作效率。

为了内容的完整性,本书并没有严格按照知识点顺序组织内容,少数地方会直接使用后续章节中的知识点。笔者相信对读者来说,这不是一个很大的障碍,各位读者在碰到这种情况的时候,可以自行前后参照来理解。总结和勘误

在计算机程序设计语言的领域中,一代又一代的语言潮起潮落,其兴起和衰落的节奏往往并非取决于技术本身的发展。对于Rust这门新出现的语言来说,以后究竟会有多大的影响,是否会成为取代某种语言的“新时代的宠儿”,实在难以预测,而且毫无必要预测。

笔者认为,Rust语言是最近若干年内系统级编程语言领域的集大成者之一。不论其最终发展如何,它的许多设计思想和令人惊叹的特性都值得大家学习。在本人的学习过程中,也时常为某些精彩的设计发出由衷的赞叹。

Rust语言是一门优秀的语言,同时也是门槛较高的一门语言,要完全掌握它不是一件很容易的事。因此,笔者并不希望将本书写成语言特性的逐一简单罗列,而更希望向读者解释清楚这些语言特性背后的设计思想。

所幸的是,Rust语言是完全开源的,不仅代码是开源的,而且整个设计过程、思辩讨论都是对社区完全开放的。它的许多非常有价值的学习资料,如同星星点点,散落在各个地方,包括官方文档、邮件列表、讨论组、GitHub、个人博客等。在学习和写作的过程中,能有幸一窥新语言创造者们的心路历程,也是难得的机缘。

要想把Rust语言的方方面面讲好、讲透,实在是一个无比繁重的任务。动笔之际,方知“看人挑担不吃力,自家挑担压断脊”,诚惶诚恐,战战兢兢,生怕有误人子弟之嫌。笔者水平有限,如有错漏,在所难免,欢迎读者批评指正。笔者将会在GitHub上发布最新的勘误列表,网址为https://github.com/F001/rust_book_feedback。读者可以在这个项目中新建bug提交问题,也可以通过邮件(rust-lang@qq.com)与笔者联系。同时也欢迎读者关注微信公众号:Rust编程,后面还会发布更多关于Rust的文章。致谢

感谢Rust设计组,为软件开发行业创造了一份宝贵的财富。

感谢我所在的公司synopsys给予的大力支持。

感谢梁自泽导师对我的培养。

感谢林春晓博士拨冗为本书做了最后一轮审校。

感谢妻子的包容和呵护,否则本书不可能面世。

感谢杨绣国编辑细致的工作。

感谢各位老师、同学和同事对我的支持,正是因为你们的帮助,才使我技术水平更上一层楼。范长春(F001)中国,武汉,2018年3月第一部分 基础知识

在这一部分中,我们将对Rust语言的主要语法特性做一个循序渐进的介绍。Rust语言的基本语法特性并不复杂,它也并没有贪多求全地堆砌大量华而不实的语法特性。相反,它在吸收各种优秀语法规则的同时也做了裁剪,去芜存菁,张弛有度。第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-staticexport 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// 安装RLSrustup component add rls --toolchain nightlyrustup component add rust-analysis --toolchain nightlyrustup component add rust-src --toolchain nightly

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

有了上面这些准备工作,我们就可以正式开始Rust编程之旅了。首先,打开命令行工具,看看rustc编译器能否正常运行,使用-V命令查看rustc的版本:$ rustc -Vrustc 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.rsfn 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模块内提供的一些函数来完成的。如果用户需要更精细地控制标准输出操作,也可以直接调用标准库来完成。第2章变量和类型2.1 变量声明

Rust的变量必须先声明后使用。对于局部变量,最常见的声明语法为:let variable : i32 = 100;

与传统的C/C++语言相比,Rust的变量声明语法不同。这样设计主要有以下几个方面的考虑。

1.语法分析更容易

从语法分析的角度来说,Rust的变量声明语法比C/C++语言的简单,局部变量声明一定是以关键字let开头,类型一定是跟在冒号:的后面。语法歧义更少,语法分析器更容易编写。

2.方便引入类型推导功能

Rust的变量声明的一个重要特点是:要声明的变量前置,对它的类型描述后置。这也是吸取了其他语言的教训后的结果。因为在变量声明语句中,最重要的是变量本身,而类型其实是个附属的额外描述,并非必不可少的部分。如果我们可以通过上下文环境由编译器自动分析出这个变量的类型,那么这个类型描述完全可以省略不写。Rust一开始的设计就考虑了类型自动推导功能,因此类型后置的语法更合适。

3.模式解构

let语句不光是局部变量声明语句,而且具有pattern destructure(模式解构)的功能。关于“模式解构”的内容在后面的章节会详细描述。

实际上,包括C++/C#/Java等传统编程语言都开始逐步引入这种声明语法,目的是相似的。

Rust中声明变量缺省是“只读”的,比如如下程序:fn main() { let x = 5; x = 10;}

会得到“re-assignment of immutable variable`x`”这样的编译错误。

如果我们需要让变量是可写的,那么需要使用mut关键字:let mut x = 5; // mut x: i32x = 10;

此时,变量x才是可读写的。

实际上,let语句在此处引入了一个模式解构,我们不能把let mut视为一个组合,而应该将mut x视为一个组合。

mut x是一个“模式”,我们还可以用这种方式同时声明多个变量:let (mut a, mut b) = (1, 2);let Point { x: ref a, y: ref b} = p;

其中,赋值号左边的部分是一个“模式”,第一行代码是对tuple的模式解构,第二行代码是对结构体的模式解构。所以,在Rust中,一般把声明的局部变量并初始化的语句称为“变量绑定”,强调的是“绑定”的含义,与C/C++中的“赋值初始化”语句有所区别。

Rust中,每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误,在Rust中是不可能出现的(利用unsafe做hack除外)。如下这个简单的程序,也不能编译通过:fn main() { let x: i32; println!("{}", x);}

错误信息为:error: use of possibly uninitialized variable: `x`

编译器会帮我们做一个执行路径的静态分析,确保变量在使用前一定被初始化:fn test(condition: bool) { let x: i32; // 声明 x,不必使用 mut 修饰 if condition { x = 1; // 初始化 x,不需要 x 是 mut 的,因为这是初始化,不是修改 println!("{}", x); } // 如果条件不满足,x 没有被初始化 // 但是没关系,只要这里不使用 x 就没事}

类型没有“默认构造函数”,变量没有“默认值”。对于let x:i32;如果没有显式赋值,它就没有被初始化,不要想当然地以为它的值是0。

Rust里的合法标识符(包括变量名、函数名、trait名等)必须由数字、字母、下划线组成,且不能以数字开头。这个规定和许多现有的编程语言是一样的。Rust将来会允许其他Unicode字符做标识符,只是目前这个功能的优先级不高,还没有最终定下来。另外还有一个raw identifier功能,可以提供一个特殊语法,如r#self,让用户可以以关键字作为普通标识符。这只是为了应付某些特殊情况时迫不得已的做法。

Rust里面的下划线是一个特殊的标识符,在编译器内部它是被特殊处理的。它跟其他标识符有许多重要区别。比如,以下代码就编译不过:fn main() { let _ = "hello"; println!("{}", _);}

我们不能在表达式中使用下划线来作为普通变量使用。下划线表达的含义是“忽略这个变量绑定,后面不会再用到了”。在后面讲析构的时候,还会提到这一点。2.1.1 变量遮蔽

Rust允许在同一个代码块中声明同样名字的变量。如果这样做,后面声明的变量会将前面声明的变量“遮蔽”(Shadowing)起来。fn main() { let x = "hello"; println!("x is {}", x); let x = 5; println!("x is {}", x);}

上面这个程序是可以编译通过的。请注意第5行的代码,它不是x=5;,它前面有一个let关键字。如果没有这个let关键字,这条语句就是对x的重新绑定(重新赋值)。而有了这个let关键字,就是又声明了一个新的变量,只是它的名字恰巧与前面一个变量相同而已。

但是这两个x代表的内存空间完全不同,类型也完全不同,它们实际上是两个不同的变量。从第5行开始,一直到这个代码块结束,我们没有任何办法再去访问前一个x变量,因为它的名字已经被遮蔽了。

变量遮蔽在某些情况下非常有用,比如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给它们起不同的名字。再比如,在同一个函数内部,需要修改一个变量绑定的可变性。例如,我们对一个可变数组执行初始化,希望此时它是可读写的,但是初始化完成后,我们希望它是只读的。可以这样做:// 注意:这段代码只是演示变量遮蔽功能,并不是Vec类型的最佳初始化方法fn main() { let mut v = Vec::new(); // v 必须是mut修饰,因为我们需要对它写入数据 v.push(1); v.push(2); v.push(3); let v = v; // 从这里往下,v成了只读变量,可读写变量v已经被遮蔽,无法再访问 for i in &v { println!("{}", i); }}

反过来,如果一个变量是不可变的,我们也可以通过变量遮蔽创建一个新的、可变的同名变量。fn main() { let v = Vec::new(); let mut v = v; v.push(1); println!("{:?}", v);}

请注意,这个过程是符合“内存安全”的。“内存安全”的概念一直是Rust关注的重点,我们将在第二部分详细讲述。在上面这个示例中,我们需要理解的是,一个“不可变绑定”依然是一个“变量”。虽然我们没办法通过这个“变量绑定”修改变量的值,但是我们重新使用“可变绑定”之后,还是有机会修改的。这样做并不会产生内存安全问题,因为我们对这块内存拥有完整的所有权,且此时没有任何其他引用指向这个变量,对这个变量的修改是完全合法的。Rust的可变性控制规则与其他语言不一样。更多内容请参阅本书第二部分内存安全。

实际上,传统编程语言C/C++中也存在类似的功能,只不过它们只允许嵌套的区域内部的变量出现遮蔽。而Rust在这方面放得稍微宽一点,同一个语句块内部声明的变量也可以发生遮蔽。2.1.2 类型推导

Rust的类型推导功能是比较强大的。它不仅可以从变量声明的当前语句中获取信息进行推导,而且还能通过上下文信息进行推导。fn main() { // 没有明确标出变量的类型,但是通过字面量的后缀, // 编译器知道elem的类型为u8 let elem = 5u8; // 创建一个动态数组,数组内包含的是什么元素类型可以不写 let mut vec = Vec::new(); vec.push(elem); // 到后面调用了push函数,通过elem变量的类型, // 编译器可以推导出vec的实际类型是 Vec println!("{:?}", vec);}

我们甚至还可以只写一部分类型,剩下的部分让编译器去推导,比如下面的这个程序,我们只知道players变量是Vec动态数组类型,但是里面包含什么元素类型并不清楚,可以在尖括号中用下划线来代替:fn main() { let player_scores = [ ("Jack", 20), ("Jane", 23), ("Jill", 18), ("John", 19), ]; // players 是动态数组,内部成员的类型没有指定,交给编译器自动推导 let players : Vec<_> = player_scores .iter() .map(|&(player, _score)| { player }) .collect(); println!("{:?}", players);}

自动类型推导和“动态类型系统”是两码事。Rust依然是静态类型的。一个变量的类型必须在编译阶段确定,且无法更改,只是某些时候不需要在源码中显式写出来而已。这只是编译器给我们提供的一个辅助工具。

Rust只允许“局部变量/全局变量”实现类型推导,而函数签名等场景下是不允许的,这是故意这样设计的。这是因为局部变量只有局部的影响,全局变量必须当场初始化而函数签名具有全局性影响。函数签名如果使用自动类型推导,可能导致某个调用的地方使用方式发生变化,它的参数、返回值类型就发生了变化,进而导致远处另一个地方的编译错误,这是设计者不希望看到的情况。2.1.3 类型别名

我们可以用type关键字给同一个类型起个别名(type alias)。示例如下:type Age = u32;fn grow(age: Age, year: u32) -> Age { age + year}fn main() { let x : Age = 20; println!("20 years later: {}", grow(x, 20));}

类型别名还可以用在泛型场景,比如:type Double = (T, Vec); // 小括号包围的是一个 tuple,请参见后文中的复合数据类型

那么以后使用Double的时候,就等同于(i32,Vec),可以简化代码。2.1.4 静态变量

Rust中可以用static关键字声明静态变量。如下所示:static GLOBAL: i32 = 0;

与let语句一样,static语句同样也是一个模式匹配。与let语句不同的是,用static声明的变量的生命周期是整个程序,从启动到退出。static变量的生命周期永远是'static,它占用的内存空间也不会在执行过程中回收。这也是Rust中唯一的声明全局变量的方法。

由于Rust非常注重内存安全,因此全局变量的使用有许多限制。这些限制都是为了防止程序员写出不安全的代码:

·全局变量必须在声明的时候马上初始化;

·全局变量的初始化必须是编译期可确定的常量,不能包括执行期才能确定的表达式、语句和函数调用;

·带有mut修饰的全局变量,在使用的时候必须使用unsafe关键字。

示例如下:fn main() {//局部变量声明,可以留待后面初始化,只要保证使用前已经初始化即可 let x; let y = 1_i32; x = 2_i32; println!("{} {}", x, y);//全局变量必须声明的时候初始化,因为全局变量可以写到函数外面,被任意一个函数使用 static G1 : i32 = 3; println!("{}", G1);//可变全局变量无论读写都必须用 unsafe修饰 static mut G2 : i32 = 4; unsafe { G2 = 5; println!("{}", G2); }//全局变量的内存不是分配在当前函数栈上,函数退出的时候,并不会销毁全局变量占用的内存空间,程序退出才会回收}

Rust禁止在声明static变量的时候调用普通函数,或者利用语句块调用其他非const代码:// 这样是允许的static array : [i32; 3] = [1,2,3];// 这样是不允许的static vec : Vec = { let mut v = Vec::new(); v.push(1); v };

调用const fn是允许的:#![feature(const_fn)]fn main() { use std::sync::atomic::AtomicBool; static FLAG: AtomicBool = AtomicBool::new(true);}

因为const fn是编译期执行的。这个功能在编写本书的时候目前还没有stable,因此需要使用nightly版本并打开feature gate才能使用。

Rust不允许用户在main函数之前或者之后执行自己的代码。所以,比较复杂的static变量的初始化一般需要使用lazy方式,在第一次使用的时候初始化。在Rust中,如果用户需要使用比较复杂的全局变量初始化,推荐使用lazy_static库。2.1.5 常量

在Rust中还可以用const关键字做声明。如下所示:const GLOBAL: i32 = 0;

使用const声明的是常量,而不是变量。因此一定不允许使用mut关键字修饰这个变量绑定,这是语法错误。常量的初始化表达式也一定要是一个编译期常量,不能是运行期的值。它与static变量的最大区别在于:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。因此,用户千万不要用hack的方式,通过unsafe代码去修改常量的值,这么做是没有意义的。以const声明一个常量,也不具备类似let语句的模式匹配功能。2.2 基本数据类型2.2.1 bool

布尔类型(bool)代表的是“是”和“否”的二值逻辑。它有两个值:true和false。一般用在逻辑表达式中,可以执行“与”“或”“非”等运算。fn main() { let x = true; let y: bool = !x; // 取反运算 let z = x && y; // 逻辑与,带短路功能 println!("{}", z); let z = x || y; // 逻辑或,带短路功能 println!("{}", z); let z = x & y; // 按位与,不带短路功能 println!("{}", z); let z = x | y; // 按位或,不带短路功能 println!("{}", z); let z = x ^ y; // 按位异或,不带短路功能 println!("{}", z);}

一些比较运算表达式的类型就是bool类型:fn logical_op(x: i32, y: i32) { let z : bool = x < y; println!("{}", z);}

bool类型表达式可以用在if/while等表达式中,作为条件表达式。比如:if a >= b { ...} else { ...}2.2.2 char

字符类型由char表示。它可以描述任何一个符合unicode标准的字符值。在代码中,单个的字符字面量用单引号包围。let love = '❤'; // 可以直接嵌入任何 unicode 字符

字符类型字面量也可以使用转义符:let c1 = '\n'; // 换行符let c2 = '\x7f'; // 8 bit 字符变量let c3 = '\u{7FFF}'; // unicode字符

因为char类型的设计目的是描述任意一个unicode字符,因此它占据的内存空间不是1个字节,而是4个字节。

对于ASCII字符其实只需占用一个字节的空间,因此Rust提供了单字节字符字面量来表示ASCII字符。我们可以使用一个字母b在字符或者字符串前面,代表这个字面量存储在u8类型数组中,这样占用空间比char型数组要小一些。示例如下:let x :u8 = 1;let y :u8 = b'A';let s :&[u8;5] = b"hello";let r :&[u8;14] = br#"hello \n world"#;2.2.3 整数类型

Rust有许多的数字类型,主要分为整数类型和浮点数类型。本节讲解整数类型。各种整数类型之间的主要区分特征是:有符号/无符号,占据空间大小。具体见表2-1。表 2-1

所谓有符号/无符号,指的是如何理解内存空间中的bit表达的含义。如果一个变量是有符号类型,那么它的最高位的那一个bit就是“符号位”,表示该数为正值还是负值。如果一个变量是无符号类型,那么它的最高位和其他位一样,表示该数的大小。比如对于一个byte大小(8 bits)的数据来说,如果存的是无符号数,那么它的表达范围是0~255,如果存的是有符号数,那么它的表达范围是-128~127。

关于各个整数类型所占据的空间大小,在名字中就已经表现得很明确了,Rust原生支持了从8位到128位的整数。需要特别关注的是isize和usize类型。它们占据的空间是不定的,与指针占据的空间一致,与所在的平台相关。如果是32位系统上,则是32位大小;如果是64位系统上,则是64位大小。在C++中与它们相对应的类似类型是int_ptr和uint_ptr。Rust的这一策略与C语言不同,C语言标准中对许多类型的大小并没有做强制规定,比如int、long、double等类型,在不同平台上都可能是不同的大小,这给许多程序员带来了不必要的麻烦。相反,在语言标准中规定好各个类型的大小,让编译器针对不同平台做适配,生成不同的代码,是更合理的选择。

数字类型的字面量表示可以有许多方式:let var1 : i32 = 32; // 十进制表示let var2 : i32 = 0xFF; // 以0x开头代表十六进制表示let var3 : i32 = 0o55; // 以0o开头代表八进制表示let var4 : i32 = 0b1001; // 以0b开头代表二进制表示

注意!在C/C++/JavaScript语言中以0开头的数字代表八进制坑过不少人,Rust中设计不一样。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载