跳转至

Xhy

介绍

Rust 是一种静态和强类型语言,目标是 C 和 C++占主导地位的系统编程领域。其强类型属性使 Rust 可以安全地重构代码,并在编译时捕获大多数错误。 Rust 的很多设计决策中强调的首要理念是编译期内存安全、零成本抽象和支持高并发。通过对程序员做出更多安全方面的限制,反过来为程序员赋能,以提高开发效率与质量。 此外,它拥有高级函数式语言的大部分特性,例如闭包、高阶函数和惰性迭代器,使得可以安全高效地开发程序。

Rust 相较于其他语言的优势

便于协作开发:

Rust 是高效的协作工具,许多在其他语言的协作开发场景中容易出现而不易察觉的 bug ,在 Rust 中将以不被允许通过编译的方式,在编译期消除。在 Rust 的特性与其编译器的协助下,可以轻松地排查 bug ,也可以轻松重构代码且无需担心会引入新的 bug 。

开发环境:

Rust 有丰富的生态,包含:内置的依赖管理器和构建工具 Cargo 、格式化工具 Rustfmt 、为 IDE 提供强大的代码补全和内联错误信息功能的Rust Language Server 等。

编译期内存安全:

Rust 可以通过所有权和生命周期的概念,从而在没有垃圾回收机制的条件下,在编译期跟踪变量资源,即保证了内存安全,又不影响程序运行时的效率。

所有权 所有程序都必须管理其运行时使用计算机内存的方式。或通过垃圾回收机制,或要求程序员必须亲自分配和释放内存;而 Rust 则通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何规则,程序都不能编译。这让 Rust 无需垃圾回收( garbage collector )即可保障内存安全,且不会减慢程序的运行速度。

所有权规则 1. Rust 中的每一个值,在任一时刻都有且只有一个所有者。 2. 当所有者(变量)离开作用域,这个值将被丢弃。 3. 当堆上的数据被赋值时(包括函数的传参与返回),传递所有权。

堆上变量的移动 Rust 在对 String 等存储在堆上的变量进行复制时,会只复制其在栈上的变量,并使得赋值的变量不再有效,从而被释放。这被称作移动。这避免了二次释放错误的产生。 这也意味着 Rust 永远也不会自动创建数据的 “深拷贝”,但如果确实需要,可以使用 clone() 函数。栈上的数据的深浅复制则相同,被称作拷贝。

变量与引用默认的不可变性 Rust 的变量默认为不可变的,可以对其创建引用,从而获取其值而不使得其因为赋值而无效。(多用于参数传递) 引用的作用域从声明的地方开始一直持续到最后一次使用为止,没有对数据的所有权。 引用也默认是不可变的,但是允许创建可变引用,但同时只能至多存在一个可变引用,且有未失效的不变引用的范围内,不能同时存在可变引用。这是为了避免“数据竞争”,后者会导致未定义行为,难以在运行时追踪,并且难以诊断和修复。 在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针。而 Rust 相应地,也不会产生垂悬引用,即不会在引用失效前将其指向的内存分配给其它持有者。

生命周期 Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。 大部分时候,生命周期是隐含并可以通过生命周期省略规则推断的: 生命周期省略规则 1. 编译器为每一个引用参数都分配一个生命周期参数。 2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。 3. 如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self ,说明是个对象的方法,那么所有输出生命周期参数被赋予 self 的生命周期。

在不能通过这三条规则推断出引用的生命周期时,需要通过生命周期注解来标明输入引用与输出引用间的关系。 生命周期保证了不会产生垂悬引用,而其检查也发生在编译期,从而不会对程序的运行时效率产生影响。

运行时安全:

Rust 将错误分为两大类:可恢复的和不可恢复的,大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理他们。而 Rust 用 Result 类型处理可恢复的错误,用 panic! 宏,在程序遇到不可恢复的错误时停止执行。这防止了各种未定义行为,保证了程序的运行时安全。(如访问越界的索引时,会产生 panic!)

函数式语言功能:

函数式编程是 Rust 设计的重要灵感来源之一。 Rust 实现了闭包、迭代器这两种特性,用以编写函数式风格的高性能 Rust 代码。 闭包 闭包是可以保存在一个变量中或作为参数传递给其他函数的匿名函数,允许捕获被定义时所在的环境,即可通过不可变借用,可变借用和获取所有权三种方式中的任意一种获取参数。 迭代器 迭代器负责遍历序列中的每一项和决定序列何时结束的逻辑,抽象掉了循环时所用的高度重复的代码,而将编程的重心放在了代码所特有的概念上,比如迭代器中每个元素必须面对的过滤条件。 此外,迭代器也可以极大提升程序的性能。 对于性能的优化 闭包和迭代器是 Rust 零成本抽象原则的典型产物。藉由 Rust 对他们的编译实现,使用它们表达高级抽象的同时,并不影响程序的运行时性能,甚至能得到不亚于底层手写代码的性能,可以极大地提高安全性与运行效率。

零成本抽象:

抽象是指程序员对底层代码或逻辑的逐层封装与抽象的过程,以增加代码的可读性与可管理性。如:循环、函数或类的封装等。 在一般的语言中,抽象同时也意味着性能的下降与额外的开销,而在 c++ 和 Rust 中,其零成本抽象原则使得我们在进行高层抽象时,不必担心会增加运行时成本,事实上,他们会被编译成难以继续优化的机器码,并且能够获得与复杂的手写优化相近的性能。 具体地, Rust 将许多运行时的开销放在编译期,比如: Rust 通过静态内存管理,规避了 gc 带来的性能开销; c++ 的虚函数表所带来的运行时多态,产生了性能损耗,而 Rust 则通过编译时单态化的原则进行了规避。

支持高并发:

并发代表程序的不同部分相互独立的执行。如今随着多核处理器与分布式系统的流行,这一概念显得愈发重要,而其编程又一直困难且容易出错。 Rust 则通过所有权和类型系统,将许多并发错误转化为了编译时错误,从而避免在部署到生产环境后修复代码或出现竞争、死锁或其他难以复现和修复的 bug ,实现了高效而安全的并发。 具体的方法主要有两种: 消息传递并发 线程或 actor 通过发送包含数据的消息来相互沟通,而不是通过共享内存。为此, Rust 提供了一个信道的实现,并借此传递所有权。 共享状态并发 让多个线程拥有相同的共享数据。这通常意味着多所有权, Rust 通过类型系统和所有权规则,极大的协助了正确地管理智能指针,从而管理这些所有权。

通用性:

Rust 提供了与其他语言的相互调用的接口,这使得可以与 C/C++ , Python 等语言混合编程。这是通过 extern 关键字实现的。但其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任。(事实上,由 extern 定义的外部接口必须在 unsafe 块中调用)

与 scala 的对比:

spark 选择 scala 的最大原因即其对函数式编程的优秀支持,函数式编程在如今多核 CPU 的硬件条件下,在并发方面的优势越发显现。而 Rust 也融合了函数式编程的特征,也能与 spark 较好地契合。 scala 所有的对象都是在堆中的,有 Head 的,生命周期由 GC 管控的。虽然有不用关心分配、释放的自由。却也导致了 STW 和更大的内存占用。 而 Rust 通过编译器带来的约束,利用其精心设计的内存机制,高速且低消耗地实现了内存安全。

总结:

Rust 通过零成本抽象等方式,拥有了相当的运行时性能和运行时安全,尤其擅长高效安全地处理高并发场景。在对安全或性能要求很高的场景、以及高实时、高并发等一些场景中,可以有相当出色的表现。我们的改写可以集中在这些方面。