Rust编程之道by张汉东

前言

社区中有人误仿阿西莫夫的机器人三大定律,总结了程序的三大定律:

  • 程序必须正确
  • 程序必须可维护,但不能违反笫一条定律。
  • 程序必须高效,但不能违反前两条定律.

相应地,

  • Rust借鉴了Haskell的类型系统,保证了程序的正确性。在类型系统的基础上,Rust借鉴了现代C++的内存管理机制,建立了所有权系统.不仅保证了类型安全.还保证了内存安全,同时,也解决了多线程并发编程中的数据竞争问题,默认线程安全,
  • 可维护性: Rust代码的可读性和抽象能力都是一流的.
  • Rust不仅拥有较高的开发效率,还拥有和C,C++媲美的性能

Rust 编程语言虽然融合了很多其他语言的特性和范式,但它不是进行简单的内容堆叠,而是有机地融合了它们。也就是说,Rust 遵循着高度的一致性内核来融合这些特性。我们只需要从 Rust 的设计哲学出发, 牢牢地把握它的设计一致性, 就可以把它的所有特性都串起来,从而达到掌握它的目的。这正是本书遵循的写作逻辑.

从设计哲学出发,探索 Rust 语言的内在一致性。 设计哲学是一门优秀编程语言保持语言一致性的关键所在。设计哲学是语言特性和语法要素设计的诱因和淮则。 理解 Rust 语言的设计哲学,有助于把握 Rust 语言的内核与一致性,把 Rust 看似纷繁复杂的特性都系统地串起来。

从源码分析入手,探索 Rust 地道的编程风格。Rust 是一门自举的语言,也就是说,Rust语言由 Rust 自身实现.通过阅读 Rust 标准库和一些第三方库的源码, 不仅可以深入理解 Rust提供的数据类型和数据结构,更能体验和学习地道的 Rust 编程风格。

从工程角度着手,探索 Rust 对健壮性的支持。Rust 通过类型系统、断言、错误处理等机制保证内存安全的同时,还保证了系统的健壮性。从工程角度去看 Rust,才能看到 Rust对系统健壮性的支持是多么优雅。

从底层原理开始,探索 Rust 内存安全的本质。只有深入底层,才能理解 Rust 所有权机制对于内存安全的意义。而且可以进一步理解 Rust 的类型系统,以及 Unsafe Rust 存在的必要性。

Rust 学习曲线陡的根本原因在于 Rust 语言融合了多种语言特性和多种编程范式。这就意味着,Rust 涉及的知识范围非常广泛,涵盖了面向对象、函数式、泛型、底层内存管理、类型系统、设计模式等知识。从底层到上层抽象,从模式到工程化健壮性,无所不包。可以说,Rust 是编程语言发展至邻的集大成者。对于大多数 Rust 语言的初学者来说,他掌握的知识体系范围是小于 Rust 所包含的知识量的,所以在学习 Rust 的过程中会遇到无法理解的内容。

随书源码地址: https://github.com/ZhangHanDong/tao-of-rust-codes

第1章 新时代的语谨

不谋全局者,不足谋一域

设计哲学

Rust 语言遵循了三条设计斩学:

  • 内存安全
  • 零成本抽象
  • 实用性

也就是说,Rust 语言中所有语法特性都围绕这三条哲学而设计,这也是 Rust 语言一致性的基础。

1.内存安全

现代编程语言早已发展到了“程序即类型证明”的阶段,类型系统基本已经成为了各大编程语言的标配,尤其是近几年新出现的编程语言。类型系统提供了以下好处:

  • 允许编译器侦测无意义甚至无效的代码,暴露程序中隐含的错误。
  • 可以为编译器提供有意义的类型信息,帮助优化代码。
  • 可以增强代码的可读性,更直白地阐述开发者的意图。
  • 提供了一定程度的高级抽象,提升开发效率。

一般来说,一门语言只要保证类型安全,就可以说它是一门安全的语言。简单来说,类型安全是指类型系统可以保证程序的行为是意义明确、不出错的。像 CC语言的类型系统就不是类型安全的,因为它们并没有对无意义的行为进行约束。一个最简单的例子就是数组越界,在 C/C语言中并不对其做任何检查,导致发生了语言规范规定之外的行为,也就是未定义行为 (Undefined Behavior)。而这些未定义行为恰恰是漏洞的温床。所以,像 C/C++这种语言就是类型不安全的语言。

Rust 在语言的实现上, 保证了类型安全.

Rust 语言如果想保证内存安全,首先要做的就是保证类型安全。

在诸多编程语言中,OCaml 和 Haskell 是公认的类型安全的典范,它们的类型系统不仅仅有强大的类型论理论“背书” 而且在实践生产环境中也久经考验。所以,Rust 语言借鉴了它们的类型系统来保证类型安全,尤其是 Haskell,你能在 Rust 语言中看到更多 Haskell 类型系统的影子。

然而,直接使用 Haskell 的类型系统也无法解决内存安全问题。类型系统的作用是定义编程语言中值和表达式的类型, 将它们归类, 赋予它们不同的行为,指导它们如何相互作用.Haskell 是一门纯函数式编程语言,它的类型系统主要用于承载其“纯函数式”的思想,是范畴论的体现。而对于 Rust 来说,它的类型系统要承载其“内存安全”的思想。所以,还需要有一个安全内存管理模型,并通过类型系统表达出来,才能保证内存安全。

Rust需要同时保证类型安全和内存安全.

那么,什么是内存安全呢? 简单来说,就是不会出现内存访问错误.只有当程序访问未定义内存的时候才会产生内存错误。一般来说,发生以下几种情况就会产生内存错误:

  • 引用空指针。
  • 使用未初始化内存。
  • 释放后使用,也就是使用悬垂指针。
  • 缓冲区溢出,比如数组越界。
  • 非法释放已经释放过的指针或未分配的指针,也就是重复释放。

这些情况之所以会产生内存错误, 是因为它们都访问了未定义内存。为了保证内存安全, Rust 语言建立了严格的安全内存管理模型:

  • 所有权系统。每个被分配的内存都有一个独占其所有权的指针。只有当该指针被销毁时,其对应的内存才能随之被释放。
  • 借用和生命周期。每个变量都有其生命周期,一旦超出生命周期,变量就会被自动释放。如果是借用,则可以通过标记生命周期参数供编译器检查的方式,防止出现悬垂 指针,也就是释放后使用的情况。

其中所有权系统还包括了从现代 C++那里借鉴的 RAII 机制,这是Rust无GC但是可以安全管理内存的基石。

建立了安全内存管理模型之后,再用类型系统表达出来即可。Rust 从 Haskell 的类型系统那里借鉴了以下特性:

  • 没有空指针
  • 默认不可变
  • 表达式
  • 高阶函数
  • 代数数据类型
  • 模式匹配
  • 泛型
  • trait 和关联类型
  • 本地类型推导

为了实现内存安全,Rust 还具备以下独有的特性:

  • 仿射类型 〈Affine Type),该类型用来表达 Rust 所有权中的 Move 语义。
  • 借用、生命周期。

借助类型系统的强大,Rnust 编译器可以在编译期对类型进行检查,看其是否满足安全内存模型,在编译期就能发现内存不安全问题,有效地阻止未定义行为的发生。

内存安全的 Bug 和并发安全的 Bug 产生的内在原因是相同的, 都是因为内存的不正当访问而造成的。同样, 利用装载了所有权的强大类型系统, Rust 还解决了并发安全的问题。Rust编译器会通过静态检查分析,在编译期就检查出多线程并发代码中所有的数据竞争问题。

2.零成本抽象

除了安全性,Rust 还追求高效开发和性能。

编程语言如果想做到高效开发,就必须拥有一定的抽象表达能力。关于抽象表达能力,最具代表性的语言就是 Ruby。Ruby 代码和 Rust 代码的对比示意如代码清单 所示。

代码清单 Ruby 代码和 Rust 代码对比示意
1. # Ruby 代码
2. 5.times{ puts "Hello Rupy"}
3. 2.days.from now
4. // Rust 代码
5. 5.times(||printlnl("Hello Rust") );
6. 2.days().from_now();

代码第 2 行和第 3 行是 Ruby 代码, 分别表示“输出 5 次"Hello Ruby"”和“从现在开始两天之后” 代码的抽象表达能力已经非常接近自然语言。再看第 5 行和第 6行的 Rust 代码,它和 Ruby 语言的抽象表达能力是不相上下的。

但是 Ruby 的抽象表达能力完全是靠牺牲性能换来的。而 Rust 的抽象是零成本的,Rust的抽象并不会存在运行时性能销,这一切都是在编译期完成的。代码清单中的迭代 5次的抽象代码,在编译期会被展开成和手写汇编代码相近的底层代码,所以不存在运行时因为解释这一层抽象而产生的性能开销。对于一门系统级编程语言而言,运行时零成本是非常重要的。这一点,Rust 做到了。

Rust 中零成本抽象的基石就是泛型和 trait

3 实用性

如何评价一门编程语言的实用性? 事实上并没有统一的说法,但可以从以下三个方面进行评判:

  • 。 实践性,首先必须能够应用于开发工业级产品,其次要易于学习和使用。
  • 。 有益性,是指能够对业界产生积极的效果或影响。
  • 。稳定性,是指语言自身要稳定。在解决同一个问题时,不会因为使用者不同而出现随机的结果。

那么 Rust 语言在这三个方面的表现如何呢?

实践性

Rust 已经为开发工业级产品做足了准备。

  • 为了保证安全性,Rust 引入了强大的类型系统和所有权系统,不仅保证内存安全,还保证了并发安全,同时还不会牺牲性能。
  • 为了保证支持硬实时系统,Rust 从 C那里借鉴了确定性析构、RAII 和智能指针,用于自动化地、确定性地管理内存,从而避免了 GC 的引入,因而就不会有“世界暂停”的问题了。这几项虽然借鉴自 C,但是使用起来比 C++更加简洁。
  • 为了保证程序的健壮性,Rust 重新审视了错误处理机制。日常开发中一般有三类非正常情况: 失败、错误和异常。但是像 C 语言这种面向过程的语言,开发者只能通过返回值、goto等语句进行错误处理,并且没有统一的错误处理机制。而 C++和 Java 这种高级语言虽然引入了异常处理机制,但没有专门提供能够有效区分正常逻辑和错误昂的语法,而只是统一全局进行处理,导致开发者只能将所有的非正常情况都当作异常去处理,这样不利于健壮系统的开发。并且异常处理还会带来比较大的性能开销。Rust 语言针对这三类非正常情况分别提供了专门的处理方式,让开发者可以分情况去先Rust 语言针对这三类非正常情况分别提供了专门的处理方式,让开发者可以分情况去选择。
    • 对于失败的情况,可以使用断言工具。
    • 对于错误,Rust 提供了基于返回值的分层错误处理方式,比如 Option<T>可以用来处理可能存在空值的情况,而Result<T>就专门用来处理可以被合理解决并需要传播的错误。
    • 对于异常, Rust 将其看作无法被合理解决的问题, 提供了线程恐慌机制,在发生异常的时候,线程可以安全地退出。

通过这样精致的设计,开发者就可以从更细的粒度上对非正常情况进行合理处理, 最终编写出更加健壮的系统。

语言架构

为了便于学习,笔者针对Rust 语言概念的层次结构进行了梳理

图 1-2 将 Rust 语言中的概念分成了 4 个层次。

  • 最底层是安全内存管理层,该层主要是涉及内存管理相关的概念。
  • 倒数第二层是类型系统层,该层起到承上启下的作用。类型系统层承载了上层的所有权系统语义和混合编程范式,赋予了 Rust 语言高级的抽象表达能力和安全性。同时,还保留了对底层代码执行、数据表示和内存分配等操作的控制能力.

对于开发者而言,只需要掌握类型系统、所有权系统和混合式编程范式即可,不需要操心底层的内存是否安全,因为有编译器和类型系统帮忙处理。

在这个语言架构之下,人和编译器共用同一套"心智模型"这样可以极大地保证系统的安全和健壮性。

在后续的章节中,会依照该语言架构对Rust 语言自底向上进行分层探索,以帮助读者对Rust 语言的概念融会贯通。

© Licensed under CC BY-NC-SA 4.0

退潮时, 便可知道谁在裸泳。——巴菲特

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!