Rust编程的详细指南

394 阅读13分钟

根据StackOverflow的调查,Rust在过去五年中连续成为最受喜爱的编程语言

大多数尝试过Rust的人都希望继续使用它。但如果你没有使用过它,你可能会想--什么是Rust,为什么它这么特别,是什么让它在开发者中如此受欢迎?

在本指南中,我将尝试快速介绍并回答你可能有的关于Rust的所有问题。

什么是Rust?

Rust是一种低级静态类型的多范式编程语言,专注于安全和性能。

Rust解决了C/C++长期以来一直在努力解决的问题,比如内存错误和构建并发程序。

它有三个主要优点:

  • 由于编译器的存在,内存安全性更高。
  • 由于防止数据竞赛的数据所有权模型,并发性更容易。
  • 零成本的抽象。

让我们依次来看看这些。

没有分离故障

如果你想做系统编程,你需要内存管理提供的低级控制。不幸的是,在C语言中,手动管理会带来很多问题。尽管有Valgrind这样的工具存在,但捕捉内存管理问题还是很棘手。

Rust可以防止这些问题。Rust的所有权系统在编译时分析了程序的内存管理,确保由于内存管理不善而产生的bug不会发生,而且垃圾收集也是不必要的。

此外,如果你想以类似C语言的方式做超级优化的实现,你可以这样做,同时用不安全关键字将它们与代码的其他部分明确分开。

更容易并发

由于有了借贷检查器,Rust可以在编译时防止数据竞赛。

当两个线程同时访问相同的内存时,就会发生数据竞赛,它们会导致一些讨厌的、不可预知的行为。值得庆幸的是,防止未定义的行为正是Rust的宗旨所在。

零成本抽象

零成本抽象确保你所使用的抽象几乎没有运行时开销。更简单地说:低级别的代码和用抽象编写的代码在速度上没有区别。

这些东西重要吗?是的。例如,在过去的12年里,微软解决的问题中,大约70%是内存错误。谷歌浏览器也是如此

Rust是用来做什么的?

Rust是一种相当低级的语言,当你需要从你所拥有的资源中挤出更多的东西时,它就很有用。由于它是静态类型的,类型系统可以帮助你在编译过程中阻止某些类别的错误。因此,当你的资源有限,并且你的软件不会失败是很重要的时候,你会倾向于使用它。相比之下,像Python和JavaScript这样的高级动态类型语言则更适合于快速原型这样的事情。

以下是Rust的一些使用情况:

  • 强大的、跨平台的命令行工具。
  • 分布式在线服务。
  • 嵌入式设备。
  • 其他任何需要系统编程的地方,比如浏览器引擎,也许还有Linux内核

Rust是面向对象的吗?

现在谁知道面向对象是什么意思?

答案是*不太清楚。*Rust有一些面向对象的特性:你可以创建结构体,它们既可以包含数据,也可以包含这些数据的相关方法,这有点类似于减去继承的类。但与Java等语言相比,Rust没有继承性,而是使用特征来实现多态性。

Rust是一种函数式编程语言吗?

尽管Rust在表面上与C语言很相似,但它在很大程度上受到ML语言家族的影响。(例如,Rust的traits基本上是Haskell的typeclasses,而且Rust有非常强大的模式匹配能力。

Rust的特点是比函数式程序员通常所习惯的更多的可变性。我们可以这样想:Rust和FP都试图避免共享易变的状态。当FP专注于避免易变态时,Rust则试图避免危险的共享部分。Rust还缺少很多能让函数式编程在其中实现的东西,比如尾部调用优化和对函数式数据结构的良好支持。

总而言之,Rust中对函数式编程的支持足以让人写一本书来介绍它。

Rust适合于游戏开发吗?

理论上,是的。由于Rust专注于性能,并且不使用垃圾收集器,所以用它编写的游戏应该是高性能和可预测的快速。

不幸的是,这个生态系统还很年轻,而且没有任何用Rust编写的东西可以与虚幻引擎相比,例如。不过,这些作品都在那里,而且Rust有一个活跃的社区。如果你想看看用Rust编写的游戏的例子,你可以去Rust游戏开发子论坛看看。

更多关于Rust游戏开发的信息:我们的游戏还没有开始吗?

Rust适合于网络开发吗?

Rust有多个用于网络开发的框架,如Actix WebRocket,这些框架非常实用,而且结构良好。特别是,如果你在寻找纯粹的速度,Actix Web在框架基准中名列前茅。

不过,Rust没有任何东西可以与Django和Rails等框架的生态系统竞争。由于Rust是一种相当年轻的语言,所以缺少很多方便的实用库,这意味着开发过程并不那么简单和容易。

更多关于Rust的网络开发。我们的网络了吗?


TL;DRRust是编写内存安全和线程安全的应用程序的强大工具,同时保持快速。虽然它有很大的潜力,但在此刻需要相当多的库支持的领域,是否应该选择Rust,目前还不清楚。

数据所有权模型

让我们深入了解一下Rust的一个特点--它的借贷检查器。

为了开始解释Rust的数据所有权,我需要向你介绍低级编程中的两种内存:堆栈和堆。

堆栈用于静态内存分配,而堆则用于动态内存分配。简单地说:堆栈用于我们知道其内存大小的东西(比如整数或str,在Rust中是内存中的字符串),而堆用于其大小可能发生重大变化的东西(一个普通的字符串)。为了对这些易变的东西进行操作,我们在堆上为它们分配空间,并在堆上放置一个指向该空间的指针。

Data ownership in Rust

移动

但是有一个问题:如果两个变量在堆上被分配了一个指向相同数据的指针,该怎么做?

Example: two variables are assigned a pointer to the same data

如果我们试图通过改变下面的数据来改变其中一个变量,另一个变量也会改变,这往往不是我们想要的。

如果有两个线程在操作相同的数据,同样(甚至更糟)的情况也会发生。

Illustration: two threads operating with the same data

想象一下,如果其中一个线程在另一个线程读取数据时突变了堆上的数据。哦,那将是多么可怕的事情啊!我们把这种情况称为数据竞赛。我们把这称为数据竞赛。

因此,在Rust中,只有一个变量可以拥有某块数据。一旦你把这个数据分配给另一个变量,它就会被移动或复制。

举个例子。

let mut s1 = String::from("All men who repeat a line from Shakespeare are William Shakespeare.");
let mut s2 = s1;
s1.push_str("― Jorge Luis Borges");

这不会被编译,因为数据的所有权被转移到了s2,而s1 ,在转移之后就不能再被访问。

借用

现在,手动移动所有权是相当麻烦的,因为你总是需要确保将其归还。

为了解决这个问题,我们可以通过创建变量的引用来借用它们。使用这些引用并不能转移所有权,但可以让我们读取变量(不可变的引用或& ),甚至可以变异它(可变的引用或mut & )。

但是引用是有限制的,因为拥有多个可变引用就相当于拥有多个所有者。

这就是为什么编译器对事物的引用执行了一个规则。

你可以做以下两种事情:

  • 多个不可变的引用(只读)。
  • 一个可变的引用(可读可写)。

这里有一个直观的比喻,我厚着脸皮从Rust借来的,用简单的英语解释

把数据引用想象成一个Powerpoint演示文稿。你可以编辑这个演示文稿(可变引用),也可以把它展示给任何数量的人(不可变引用),但如果它在被编辑的时候被展示出来,相关部门的人可能会大跌眼镜。

Rust vs. C++

现在我们知道了Rust的特别之处,我们可以将它与另一种主要的系统编程语言--C++进行比较。

Rust and C++ comparison

为什么选择Rust而不是C++

在C++中,开发人员在试图避免未定义行为时有更多问题。在Rust中,借贷检查器使你能够通过设计避免不安全行为。这就根除了一整类bug,这相当重要。

此外,Rust是一种更现代的语言,而且在某些方面,设计得更好。特别是,强大的类型系统会帮助你,即使它的主要目标不是捕捉内存错误,而且作为新的语言,它可以在创建工具时考虑到最佳实践,而不用担心遗留代码库的问题。

如果你不想放弃你的老C代码,Rust有一个解决方案。你可以通过FFI(外国函数接口)轻松地调用你的函数。当然,编译器不能保证这段代码的安全性,但这是一个很好的最后手段。

为什么选择C++而不是Rust

C和C++已经存在了几十年。无论你想解决什么问题,很可能有一大堆人的库都有同样的问题。

有时,这意味着不可能使用Rust,因为它实际上不可能复制生态系统的支持。特别是C++的游戏引擎和框架,我们在相当长的一段时间内都不会在Rust上看到。

Rust所解决的同样的问题,现代C++已经用(有些迂回)的方式解决了,所以如果你不想在Rust中冒险,相信有经验的C++开发者是一个合理的安全选择。

当然,为了编写Rust,你有时需要与编译器搏斗。这并不适合所有人。


归根结底,Rust的口号是 "一种赋予每个人建立可靠和高效软件的语言"。

虽然Rust最初是作为C++的替代品,但很明显,他们的目标更远,试图让越来越多的人能够使用低级别的编程,而这些人也许无法处理C++。

这使得这种比较变得有点无意义。Rust不是一种替代品,而是一种开辟新的可能性空间的语言,我们将在下一节讨论其中的一个。

Rust和WebAssembly

如果你还没有听说过它,WebAssembly就像是......网络的汇编

历史上,浏览器可以运行HTML、CSS和JavaScript,HTML负责结构,CSS负责外观,而JavaScript负责交互。如果你不喜欢写普通的JavaScript,你可以从其他各种语言中转译出来,这些语言增加了类型、类似Haskell或OCaml的代码,以及其他东西。

但是,JavaScript并不具备运行游戏等计算密集型应用程序所需的可预测的快速性能。(这是由于垃圾收集器和动态类型的原因)。

WebAssembly有助于解决这个问题。它是一种用于浏览器的语言,可以作为任何语言的编译目标,如Rust、Python、C++。这意味着,你可以把基本上任何现代编程语言的代码,放在浏览器中。

与其他语言相比,Rust非常适合编写代码来编译成WebAssembly:

  • 最小的运行时间:WebAssembly没有自己的运行时间,所以它需要和代码一起运送。运行时间越小,用户需要下载的东西就越少。
  • 静态类型化:由于Rust是静态类型的,它可以编译成一个更有效的WebAssembly,因为编译器可以使用类型来优化代码。
  • 我们有了一个先机:最重要的是,Rust已经全心全意地拥抱了WebAssembly。Rust已经有一个很好的社区和工具来编译WebAssembly,说实话,这是这三个中最重要的优势。

开始使用Rust

要开始使用Rust代码,你可以在这里下载rustup ,或者使用Rust Playground,这是一个在线工具,可以让你运行一些Rust代码并见证其结果。😅

一旦你准备好了你的Rust环境,让我们来做一些代码。在这里,我们将做一个Rust版本的fizzbuzz,让大家简单了解一下Rust的能力。

要创建一个新的项目,进入你想要的项目目录,然后执行cargo new fizzbuzz 。这将指示Rust的构建管理器创建一个新项目。一旦你这样做了,去 /src 文件夹并打开main.rs

首先,让我们写点东西,接受一个数字并返回:

  • "fizz "表示除以3的数字。
  • "buzz "表示除以5的数字。
  • "fizzbuzz "表示同时被3和5整除的数字。
  • 如果没有被除数,则为字符串。

Rust在匹配语句中有一个非常强大的工具可以做到这一点:

fn fizzbuzz (number: u32) -> String {
    match (number % 3, number % 5) {
        (0, 0) => "fizzbuzz".to_string(),
        (0, _) => "fizz".to_string(),
        (_, 0) => "buzz".to_string(),
        (_, _) => number.to_string()
    }
}

由于引号中的文本在内存中是一个字符串,或者在Rust中是str ,我们需要将其转换为一个字符串。

现在,我们需要一种从1开始数到某个数字的方法。我们要写一个新的函数,把数字作为参数,创建一个从1到数字的范围,应用 fizzbuzz 函数,并打印出结果。在Rust中,我们可以通过一个简单的for循环来实现这个目标:

fn count_up_to (number: u32) -> () {
    for i in 1..=number {
        println!("{}", fizzbuzz(i))
    }
}

为了在终端实现任何结果,我们需要有一个主函数。让我们用这个来代替hello_world:

fn main () {
    count_up_to(100);
}

现在,我们可以使用cargo run main.rs 命令,而且很可能,应该在我们的终端上看到一串咝咝声和嗡嗡声。

但是,嘿!也许fizzbuzz不是我们唯一玩的游戏?也许新的热点是wubbalubba?让我们迅速修改我们的计数代码,以确保我们能够对付城里的任何计数游戏。

要做到这一点,我们需要我们的Rust函数接受另一个函数,这个函数接受一个无符号的32位整数并返回一个字符串。在类型签名中加入所谓的函数指针后,最坏的情况已经过去了:

fn count_up_to_with (number: u32, function: fn(u32) -> String) -> () {
}

在里面,我们只需要用函数变量代替fizzbuzz:

fn count_up_to_with (number: u32, function: fn(u32) -> String) -> () {
    for i in 1..=number {
        println!("{}", function(i))
    }
}

如果我们添加一个新的游戏,以某种方式将整数变成字符串,我们的函数将能够处理它。

为了方便起见,这里有wubbalubba,几乎没有什么创造性的发明:

fn wubbalubba (number: u32) -> String {
    match (number * 2 % 3, number % 4) {
        (0, 0) => "dub dub".to_string(),
        (0, _) => "wubba".to_string(),
        (_, 0) => "lubba".to_string(),
        (_, _) => number.to_string()
    }
}

以及调用它所需的函数:

fn main() {
    count_up_to_with(100, wubbalubba);
}