为什么你需要近距离接触Rust 1.0

经过几年的迭代改进,Rust编程语言日前发布1.0版本。作为一种现代系统语言,Rust从大量语言(如C/C++)中取其精髓,去其糟粕,同时具备底层控制、高性能和强大的并发性。为了做到这一点,Rust打破了许多传统的取舍,它提供:

  • 内存安全却没有垃圾收集
  • 具有并发性却没有数据竞争
  • 零开销抽象
  • 稳定且没有停滞

没有垃圾收集

软件工程中,垃圾回收是一个强大的工具,让你从手动跟踪内存的烦恼中解脱,而专注于写出更好的代码。虽然有垃圾回收器很不错的,但是它在一些领域却不那么合适,诸如操作系统、嵌入式库和(软)实时应用,它们通常需要更大程度的控制和可预测性,而垃圾回收不能提供这些。

Rust允许开发人员完全放弃垃圾收集器,且不会面临忘记释放资源、悬挂指针、段错误的风险。所有权(ownership)和借用(borrowing)是Rust实现这些的关键概念,这个想法在编程中无处不在,也是现代C++的一个重要组成部分。但与其他语言不同的是,Rust把他们放在了核心地带,静态地检查和利用它们,来保证没有垃圾回收器情况下内存的安全,这是之前不能想象的东西。

关于所有权(Ownership),Rust的理念是每一个值只能有一个所有者(parent)对其完全控制。随着这些值被重新分配、放在数据结构中或传递给函数,它们的所有权会转移,且不能再通过原来的路径访问。如果超出它的作用域(scope),所有权还未被转移,它就会被自动销毁。为了使所有权在一定范围内运作,Rust提供了一种在值的作用域范围内临时借用(指针指向值)的方法。

所有权不仅替代了垃圾回收,对保证并发性也至关重要,甚至避免诸如迭代器失效这类型的bug。Rust还适用于内存以外的其他资源,例如当你关闭套接字或者文件时,可以将你从管理中解放出来。

并发性

如前所述,所有权也保证了你的并发程序不会陷入数据竞争(data races)的隐患之中。所有权会使之保持不牢固的内存模型,通过硬件来接近它。

用Rust开始一个并发程序很简单,通过标准库将闭包传递给函数:

use std::thread;
 
fn main() {
 
    let some_string = "from the parent";
 
    thread::spawn(move || {
 
        // run on a new thread
 
        println!("printing a string {}", some_string);
 
    });
 
}

许多并发程序设计语言的原则之一就是共享状态应该最小化或者甚至完全禁止,取而代之的是通过消息传递,所有权意味着Rust语言中的值默认只有一个所有者,所以通过通道将一个值发送给新的线程时保证原始线程不能访问它——静态禁止共享。

然而,消息传递仅仅是工具箱中的一个工具,共享内存可能也非常有用。类型系统确保只有线程安全的数据才可以在线程之间进行共享。例如,标准库提供了两种类型的引用计数:Arc提供线程安全的A共享内存(默认不可变),而Rc类型则可以放弃那些需要保证线程安全的同步,在Arc上提供一个性能提升。这种类型的系统静态地确保不会不小心从一个线程向另一个线程发送Rc值。

当你想确实想改变内存时,所有权提供了进一步的帮助。标准库Mutex类型为数据提供了一个类型参量,可以受到锁的保护。之后所有权确保这个数据只有在持有锁的时候才能访问,但你不能刻意提前开锁。这种访问控制贯穿了Rust类型系统,并被广泛的用于它的标准库。

零成本抽象性能和可预测性是Rust的目标之一,达到这个目标很重要的一步就是不但要保证安全,还要提供比C++更强大的零成本抽象。Rust允许你构建高层次抽象,编译为特定代码的范型库,避免为各种情况编写代码。

为了做到这点,Rust精确控制内存布局,数据可以直接放置于堆栈或内联在其他数据结构之间,而堆分配要比大多数托管语言少,有助于获得更好的缓存局部性,而这是现代硬件中提升性能到重要因素。

这个简单的、直接的数据布局意味着优化器可以可靠地消除函数调用和类型的层,将高层代码编译为高效和可预测的机器码。迭代器是其中一个主要的例子,下面的代码是对一个32位整数序列平方求和的惯用方法:

fn sum_squares(nums: &[i32]) -> i32 {
 
    nums.iter()
 
        .map(|&x| x * x)
 
        .fold(0, |a, b| a + b)
 
}

在整数切片上一次运算完成,当优化开启时,甚至编译为SIMD向量指令。

强大的类型

传统上,函数式编程语言提供类似代数数据类型、模式匹配、闭包和灵活的类型推断的功能。Rust是许多新开发的语言之一,它并不是直接适用具有这些特性的函数模型,而是用一种方式合并它们——允许在不损耗性能的情况下适用的一个灵活的应用程序接口(APIs)。

上面的迭代器示例得益于许多这样的想法:这完全是静态类型,但类型推断意味着不必要写明那些类型。闭包也是至关重要的,它允许简洁地编写操作。

在多数主流编程语言中,代数数据类型是枚举(enums)的一个扩展,它允许一个数据类型由一组离散的选择组成,且每个选择都会附加信息:

struct Point {
 
    x: f64,
 
    y: f64
 
}
 
enum Shape {
 
    Circle {
 
        center: Point,
 
        radius: f64
 
    },
 
    Rectangle {
 
        top_left: Point,
 
        bottom_right: Point
 
    },
 
    Triangle {
 
        a: Point,
 
        b: Point,
 
        c: Point
 
    }
 
}

模式匹配是使操纵这些类型变得简单的关键,如果shape是一个Shape类型的值,你可以处理各种可能性:

match shape {
 
    Shape::Circle { radius, .. } => println!("found a circle with radius {}", 
 
radius),
 
    Shape::Rectangle { top_left: tl, bottom_right: br } => {
 
        println!("found a rectangle from ({}, {}) to ({}, {})",
 
                 tl.x, tl.y,
 
                 br.x, br.y)
 
    }
 
    Shape::Triangle { .. } => println!("found a triangle"),

这些编译器可以确保你解决所有情况(选择性加入一个catch-all分句),并极大程度地帮助重构。

枚举(enums)也使得Rust防治所谓的“价值数十亿美元”的错误:空引用(null references)。在Rust中引用永远不会是空的,选择类型允许你以一种类型安全、本地化的方式选择nullability。

结论

Rust是一种由Mozilla赞助的语言,它在保留C++性能的同时提供零开销抽象,用于web浏览器开发,还能同时保证内存安全和缓解并发编程的压力。

Rust填充了一个也许被认为是不可能的利基:在不背离安全和抽象的情况下,提供底层控制和性能。当然,世界上没有免费的午餐,编译器作为一个要求助理不能容忍一点点风险,而且所有权模式还有点陌生,需要一些时间学习。

1.0版本的核心语言和库已经被测试和精炼过,而且重要的是关于稳定的保证:现在编译的代码应该也可以在未来的新版本中得到编译。并且,这个版本并不意味语言的完成,Rust正在采用火车模型,新版本每六周发布一次,新的和不稳定的特性可以通过常规的预发布版本betas和nightlies发现。目前,一个标准的包管理器Cargo已经被用来建立一个不断增长的库系统。

正如语言一样,Rust生态还不完善,所以没有像许多老一辈的语言提供的那样宽广的工具和包(简单的FFI虽然对后者有所帮助)。尽管如此,语言本身是强大的,也是一个没有传统危险的,去做底层开发很好的方式。

本文文字及图片出自 jaxenter.com

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注