我对 Rust 的第二印象以及为什么我认为它是一种很棒的通用语言!
最初发表于deepu.tech。
我在 2019 年 11 月写下了我对 Rust 的初印象。之后,我尝试了几次,但大多是一些非常简单的代码。虽然我喜欢使用 Rust,但并没有感到特别惊讶。表面上看,它感觉或多或少与我接触过的许多其他语言相似。
上个月,当我最终决定用 Rust 构建一个实际用例时,一切都改变了。由于我在云计算和容器方面投入了大量精力,我决定用 Rust 构建一个终端 UI 来监控 Kubernetes 集群。我知道,对于一个 Rust 新手来说,这有点雄心勃勃。但回想起来,这绝对是值得的,而且我的KDash也证明了这一点。
所以,它在终端上有一个漂亮的用户界面,可以显示 Kubernetes 集群的不同资源数据和利用率指标。这有点受K9s的启发,K9s 是用 Go 语言构建的。KDash 的重点是速度和用户体验。有了 Rust,我知道我不必担心速度问题。
该架构相当复杂。它完全由事件驱动、并发且异步,使用不同的线程来处理 UI 绘制、网络请求、流式传输日志和运行命令。我使用通道跨线程传递事件,并使用ARC智能指针和互斥锁共享应用程序状态。我将在另一篇博客中更详细地介绍架构选择和灵感。
虽然我已经用 Go、Java 和 JavaScript 实现了类似的架构,但在 Rust 中实现却颇具挑战性。最初几天,我费力地构建了一个基本结构,并且由于我使用其他语言的习惯,一直在与编译器作斗争。但随着 Rust 的思维方式逐渐成熟,不到一周,我的编译器错误和 Clippy 警告就越来越少了。几周后,我很少在编写新代码时遇到编译器错误(这也要感谢rust-analyzer),并且我开始完善现有代码,使其更加完善或编写更清晰的抽象。
既然我已经设定了背景,我觉得是时候重新审视我最初印象中的观点,看看它们是否仍然成立。在此过程中,我将分享我的观点,解释为什么我认为 Rust 是通用语言的未来,以及它如何席卷软件工程领域。我会尽量避免偏见,并以多语言人士的身份进行比较。如果您还没有读过我之前关于这个主题的文章,我建议您阅读一下,以获得更深入的了解。
我喜欢 Rust 的地方
好吧,我们先把这个说清楚。我爱上了 Rust 的一切,我最初喜欢它的地方。在全面使用 Rust 并积累了更多经验之后,我对它的喜爱之情更加强烈。所以在这里,我将谈谈上一篇文章中没有提到的一些重要的高级内容。
Rust 在其文档中使用了一些流行词,但它们不仅仅是营销噱头,它们实际上是真诚的,而且确实很重要,确实是 Rust 的最大卖点。
安全
对于一门语言来说,“安全”意味着什么?或者更确切地说,“不安全”意味着什么?让我们先了解一下背景,这样才能理解 Rust 的魅力。安全可以分为三类(如果算上空安全,则分为四类)。
微软大约 70% 的 CVE 漏洞都与内存安全问题有关。
三分之二的 Linux 内核漏洞都与内存安全问题有关。
内存安全
这意味着,当你访问变量或数组中的元素时,你可以确保你访问的确实是你想要访问的或被允许访问的内容。换句话说,无论你在程序中做什么,都不会错误地读取/写入另一个变量或指针的内存。
这有什么大不了的?所有主流编程语言不是都保证这一点吗?
是的,程度不同。但有些语言默认是不安全的。在 C 或 C++ 中,你可能会错误地访问另一个变量的内存,或者你可能会两次释放一个指针(双重释放错误)。这类行为被归类为未定义行为,因为它们不可预测,因此可能被黑客滥用来控制程序或泄露特权信息。在内存安全的语言中,如果你尝试访问超出其范围的数组元素,你只会让程序崩溃并引发 panic/error,这是可预测的行为。
这就是为什么 C/C++ 系统中与内存相关的错误经常导致CVE和紧急补丁的原因。C 或 C++ 中还有其他内存不安全的行为:访问已弹出的堆栈框架指针、已释放的内存、迭代器失效等等。
- 空值安全:我将其单独列在内存安全下,因为我有 Java/JS 背景,并且我们已经非常熟悉 null 的概念(它因是编程中最糟糕的发明而臭名昭著)。垃圾回收语言需要“空值”的概念,以便释放指针。但这也会导致问题和痛苦。有人会遇到 NPE 吗?从技术上讲,这属于内存安全范畴,但大多数内存安全的语言仍然允许使用 null 作为值,从而导致空指针错误。
类型安全
这意味着,当你访问变量时,你访问的是它存储的正确数据类型。这让我们有信心处理数据,而无需在运行时手动检查数据类型。内存安全是语言实现类型安全的必要条件。
线程安全
这意味着您可以同时从多个线程访问/修改同一块内存,而无需担心数据争用。这通常通过使用互斥锁 (Mutex) 或线程同步来实现。线程安全是实现最佳内存和类型安全的必要条件,因此通常内存和类型安全的语言也往往是线程安全的。
现在让我们看看 Rust 如何在这些方面提供安全性。
内存安全
Rust 使用其创新的所有权机制和编译器内置的借用检查器,在编译时确保内存安全。除非明确标记为unsafe
不安全块或函数,否则编译器不会允许内存不安全代码。这种静态编译时分析消除了多种类型的内存错误,并通过一些运行时检查,Rust 确保了内存安全。
- 空安全:Rust 语言层面没有空的概念。相反,Rust 提供了 Option 枚举,可以用来标记值的存在与否,这使得生成的代码更加安全,处理起来也更加容易,在 Rust 中你永远不会遇到空指针异常。
类型安全
Rust 是静态类型的语言,它通过严格的编译时类型检查和内存安全来保证类型安全。这并不特殊,因为大多数现代语言都是静态类型的。Rust 也允许在需要时使用dyn
关键字和Any
类型进行一定程度的动态类型转换。但即使在这种情况下,强大的类型推断和编译器也能确保类型安全。
线程安全
Rust 使用与内存安全类似的概念来保证线程安全,同时提供诸如通道、互斥锁和 ARC 等标准库功能。编译器确保共享状态不会引发意外的数据竞争。这让我们可以专注于代码,而将线程间共享数据的问题交给编译器处理。
现在,别误会,您会在 Rust 应用程序中看到崩溃和错误,甚至数组索引越界错误等等。Rust 并没有声称能够安全地避免错误,也不会捕获错误的逻辑,因为没有编译器可以保证不会出现人为错误。它只是使错误可预测,因此当错误发生时,您可以确保它不会像 C/C++ 应用程序那样存在安全问题。
Rust 还允许您在需要时通过显式声明块来编写不安全的代码unsafe
。这在需要时提供了灵活性,因为某些低级系统用例可能需要不安全的内存访问。Rust 无法保证unsafe
代码块中的任何上述安全性,因为它适用于那些对自己的操作有把握并且不希望编译器干扰的用户。
现在,大多数静态类型高级语言(例如 Java、Go 或 C#)也不同程度地提供了上述所有功能,但它们都不提供空安全。不过,它们的实现是以运行时和垃圾收集器为代价的。
这就是为什么 Rust 如此特别,因为它比任何高级语言都更安全,而且没有运行时或内存管理(垃圾收集、引用计数等)的开销。同时,它的速度和性能有时甚至优于C/C++ 等低级语言。
零成本抽象
零成本抽象意味着你编写程序的方式不会影响其性能。例如,你可以选择创建或使用任意数量的抽象来构建程序,可以使用循环或迭代器,可以进行函数式或命令式编程,而结果保持不变。无论你选择哪种代码风格,编译器都会根据用例生成最佳的机器码实现。
你不用的东西,就不用花钱。而且,你用的东西,你手写的代码再好不过了。
只有极少数编程语言提供此功能。C++ 因提供零开销抽象而广受欢迎,但如果考虑编译时成本,它们并不总是零成本。然而,Rust 编译器似乎更智能,在大多数情况下都能提供零开销抽象,并且能够提供更好的开发者体验,这是一个重要因素。此外,诸如数组边界检查之类的开销仍然可以忽略不计。
与所有抽象一样,零成本抽象实际上必须提供比其他选择更好的体验。
——西尔莎
让我们看一个小例子来理解并体会这一点的重要性。我们将比较像 Java 这样的高级语言和 Rust。但从技术上讲,你也可以用任何其他高级语言进行比较,结果可能都差不多。
让我们以下面的 Java 程序为例。运行JMH基准测试,可以得到每个函数的性能数据,并以注释的形式添加到内联函数中。
// Average 10.059 ns/op
public long factorialForLoop(long number) {
long result = 1;
for (; number > 0; number--) {
result *= number;
}
return result;
}
// Average 20.689 ns/op
public long factorialRecursive(long number) {
return number == 1 ? 1 : number * factorialRecursive(number - 1);
}
// Average 23.457 ns/op
public long factorialStream(long number) {
return LongStream.rangeClosed(1, number)
.reduce(1, (n1, n2) -> n1 * n2);
}
/*
# Run complete. Total time: 00:02:30 (JDK 11)
Benchmark Mode Cnt Score Error Units
MyBenchMark.forLoop avgt 3 10.059 ± 1.229 ns/op
MyBenchMark.recursive avgt 3 20.689 ± 4.465 ns/op
MyBenchMark.stream avgt 3 23.457 ± 32.424 ns/op
*/
如你所见,尽管这三个函数的功能相同,但它们的性能却大相径庭。抽象程度最高的那个函数(流迭代)性能下降最严重。现在,让我们在 Rust 中尝试这三个函数,并使用Criterion运行基准测试。
// Average 8.5858 ns/op
fn factorial_loop(mut num: usize) -> usize {
let mut result = 1;
while num > 0 {
result *= num;
num = num - 1;
}
return result;
}
// Average 8.6150 ns/op
fn factorial_recursion(num: usize) -> usize {
return match num {
0 => 1,
_ => num * factorial_recursion(num - 1),
};
}
// Average 6.6387 ns/op
fn factorial_iterator(num: usize) -> usize {
(1..num).fold(1, |n1, n2| n1 * n2)
}
/*
Benchmark time: [min avg max ]
factorial_loop time: [8.4579 ns 8.5732 ns 8.7105 ns]
factorial_recursion time: [8.4394 ns 8.5074 ns 8.5829 ns]
factorial_iterator time: [6.4240 ns 6.4742 ns 6.5338 ns]
*/
正如您所看到的,迭代和递归方法的性能是相同的,并且内置抽象的性能甚至更好(由于内部迭代器优化等)。
因此,就 Rust 而言,甚至可以公平地说,抽象在很多情况下比手动优化的代码提供了更好的性能,甚至在最坏的情况下,它们也能以零成本提供相同的性能。如果你查看不同版本生成的汇编代码,你会发现很多情况下,编译器生成的汇编代码是相同的。
这让我们能够专注于编写最易读、可复用性最高的代码,而无需考虑如何编写最优代码。这并不意味着 Rust 中的所有内容都是零成本抽象。你最终可能会编写一些执行不必要计算之类的代码,这会增加成本,但至少这样做是显而易见的。
Rust 中最值得注意的一些零成本抽象是
- 所有权和借款
- 迭代器和闭包 API
- Async/await 和 Futures
- 不安全和模块边界
无畏的并发
我们已经看到 Rust 是线程安全的,因此从技术上讲,你可以用 Rust 轻松实现各种并发功能。Rust 支持多线程、绿色线程、并行计算和异步编程,它们既可以作为一等公民,也可以通过 Tokio 或 Futures 等 crate 实现。
我用 Rust 构建的第一个实际应用极其支持并发和异步,目前为止我还没有遇到任何与并发相关的问题。Rust 承诺,即使我在执行异步操作的多个线程之间共享状态,也不会遇到数据竞争问题。说实话,用我习惯的任何其他语言做同样的事情,我都不会感到自在,或许 Go 在这方面会更好一些,因为它在并发方面也相当出色。
社区、工具和生态系统
在我看来,Rust 毫无疑问是最好的社区之一。它没有 Java 那样的政治斗争,也没有 JavaScript 那样的臃肿(至少目前是这样😉)。Rust 毫不避讳地借鉴了其他语言的优点,例如 JavaScript 的包管理功能,以及 Haskell、OCaml、Ruby、JavaScript 等语言的特性。
这种多样性在社区中也显而易见。你可以看到来自不同背景的人,以及一个极其热情友好的社区。奇怪的是,Rust 社区论坛比 Stack Overflow 社区更活跃,这说明了很多问题。你会发现有人在帮助你、指导你,而不是像守门人一样。
Rust 还保证了向后兼容性,同时仍在不断改进语言,并拥有与之同步的工具和库生态系统。Rust 库生态系统与 JavaScript 类似,并带有 NPM 的氛围。
Rust 的工具非常出色,Rustc、Rustup 和 Cargo 都是标配,并且有大量插件,例如 Clippy、Rustfmt 等等。Cargo 充当构建运行器、包管理器、插件管理器等等。所有这些功能都集成良好,带来了极佳的开发者体验。虽然偶尔会有一些不足之处,但仍然远远领先于许多其他历史更悠久的语言。
Rust 还提供了最优秀的文档之一。它甚至可以通过rustup docs
命令包含在标准工具链中。
Rust 中的许多其他小东西都很棒,我只是跳过它们而选择高级的东西。
我仍然不喜欢 Rust 的地方
这些是我最初不喜欢 Rust 的地方,虽然我对其中很多方面的看法至今没有改变,但其中一些对我来说现在更有意义了。但这并不意味着我必须完全喜欢它们。如果你想了解更多背景信息,请阅读我的原帖。
复杂
一方面,随着新语言特性的出现,这门语言的复杂性似乎不断增加;另一方面,有些东西却在简化,说实话,我对此有点纠结。
我确实喜欢它提供的很多特性,而且自从我理解了 Rust 中零成本抽象的概念后,我之前关于同一件事情有多种实现方式的问题似乎也不再是什么大问题了。但是,一旦你接触到高级泛型、特质、生命周期等等,它很快就会变得令人难以置信,我能够理解新手的感受,因为学习曲线陡峭。
虽然我理解这种复杂性是必要的,而且大多数情况下是值得的,但考虑到拥有所有权的好处,我很难不渴望更简单一些。希望未来的版本能够不断改进,简化其中的许多方面🤞
同一上下文中的变量的阴影
现在这对我来说更有意义了,而且我最终经常使用它。使用所有权机制,您经常需要重新绑定、创建临时中间体或从选项中获取值或转换值,所以这很有用,但我关于它被滥用的观点也是非常正确的。
在少数情况下,我最终在相同的上下文中对具有相同类型的不同内容使用相同的名称,从而产生了意想不到的结果。它没有导致任何重大问题,但有时确实会影响可读性和逻辑。所以可能没有这个功能也会没问题。但是,使用Clippy,可以添加 lint 规则来禁止这种情况,所以我想这样就足够了。
函数不是一等公民
好吧,事实证明它们是一等公民,只是 Rust 中的函数非常复杂。我理解其中的原因和方法,它们也合情合理。但我还是希望至少能用一些语法糖来简化它。不过这没什么大不了的,现在更像是一个吹毛求疵的问题。
特征的隐式实现
用了 Rust 之后,这变得有意义了,而且我比 Go 等语言更喜欢 Rust 的实现方式。所以这对我来说不再是问题了😸
此外,我在原帖中提出的那些挑剔之处也不再困扰我了😄
Rust 代表着未来
Rust,而不是 Firefox,是 Mozilla 对行业的最大贡献
科技共和国
首先,我要说的是,我已经爱上这门语言了。所以,我的观点可能有点偏颇。我已经好多年没有编程这么享受过乐趣了。用 Rust 写代码时,你会有一种奇特的满足感。现在我明白了为什么 Rust 在Stack Overflow 开发者调查中连续五年荣登最受欢迎语言榜首。
别误会我的意思。Rust 并非灵丹妙药,因为它存在学习曲线陡峭、复杂性高等问题。但在我看来,它是最接近灵丹妙药的语言。但这并不意味着我会开始用 Rust 做所有事情。我仍然喜欢成为一名多语言开发者,并且仍然在 Java、JS/TS 和 Go 等语言上投入精力。但如果用例需要速度和/或并发性,或者需要构建系统工具或命令行界面 (CLI),那么我会优先考虑 Rust,Go 可能会退居次要地位,因为在类似的用例中,Go 并没有任何比 Rust 更具优势。
通常,一门语言会在安全性、速度和高级抽象之间提供选择。最好的情况下,你只能选择其中两个。例如,使用 Java/C#/Go,你可以以运行时开销为代价获得安全性和高级抽象,而 C++ 则以牺牲安全性为代价获得速度和抽象。但 Rust 兼具这三者,并且还额外提供了良好的开发体验。
随着我们的 IT 环境日益复杂且资源匮乏,这种组合至关重要。Rust 的外观和感觉都像是一种通用的高级语言,但却提供了低级系统语言的性能和内存效率。因此,它可以成为一种通用语言,它不会做出任何妥协,不需要运行时,并且是跨平台的,而且不像 C/C++ 那样难用。还有什么理由不爱它呢?
由于这些独特的特性,Rust 不仅在系统编程领域站稳了脚跟,还在目前由高级语言主导的领域(例如 Web 应用程序、微服务和 CLI 工具)中站稳了脚跟。由于其占用空间小且对WASM 的支持出色,它作为 Web 汇编语言也越来越受欢迎。此外,它还涉足了嵌入式/物联网领域。此外,Rust 还有许多其他用例,例如无服务器、JS 运行时、游戏引擎、游戏开发、操作系统,甚至恶意软件🤦
Rust 正在迅速普及。考虑到它才诞生五年,这真是令人印象深刻。微软、谷歌、苹果、亚马逊和 Facebook 等巨头都已投资Rust,并计划用 Rust 取代 C/C++ 代码。这不会一蹴而就,但 Rust 会慢慢取代很多 C/C++ 代码。就连 C 语言的典范 Linux,最近也批准在内核的某些部分(例如驱动程序代码)中使用 Rust 。
与许多通用语言不同,由于它们所做的权衡,它们不适合某些用例,Rust 具有独特的优势,可以在整个范围内工作而没有任何重大缺点,并且是任何用例的通用语言,从客户端到系统编程。
在我看来,C/C++ 和 Go 可能是短期内被 Rust 取代最多的语言。我认为 Java/JS/TS/Python 等语言在很长一段时间内是安全的,因为它们在大型应用中占有大量份额,生态系统成熟,而且迁移成本也较低。
在我看来,唯一阻碍 Rust 发展的是图书馆生态系统的成熟度,而它的进步只是时间问题。
最后我想说:除非你花几周时间用 Rust 构建一些东西,否则你不会真正喜欢上它。最初陡峭的学习曲线可能会令人沮丧,也可能充满挑战,这取决于你怎么看待它,但一旦过了这个阶段,你就很难不爱上它了。毕竟,它就像一个拥有超能力的幼儿💗
参考
- engineering.fb.com
- www.techrepublic.com
- threatpost.com
- blogs.gartner.com
- benchmarksgame-team.pages.debian.net
- 深普科技
- www.zdnet.com
- www.zdnet.com
- boats.gitlab.io
- medium.com/ingeniouslysimple
如果您喜欢这篇文章,请点赞或留言。
文章来源:https://dev.to/deepu105/my-second-impression-of-rust-and-why-i-think-it-s-the-best-general- Purpose-language-31jh