一位 JavaScript 铁杆粉眼中的 Rust!
以下为译文:
我使用 Rust 编写了一些小工具,而且觉得很有乐趣。我的日常工作需要大量使用 JavaScript,而 Rust 给我一种非常熟悉的感觉,因此我决定尝试一下Rust。但与此同时,使用 Rust 完成真正有意义的工作需要重新思考代码的结构和合理性。编译器是最公正无私的,然而反复修改代码,直到最终通过编译也是一种乐趣。
在这篇文章中,我将分享我在 Rust 之旅中的一些想法,以及作为 JavaScript 铁杆粉,我对 Rust 的看法。
好消息
现代 Rust“看起来”与现代 JavaScript 非常相似。你可以使用 let 声明变量,而且函数看上去也很相似,由于 TypeScript 的流行,我对 Rust 的类型也不陌生,还有 async/await,总的来说,我对 Rust 有一种莫名的熟悉感。
坏消息
问题的核心不是语法,而是 Rust 对程序内部结构的推理方式。高级语言中包含大量抽象,因此你不必担心计算机的工作方式。这非常合情合理,如果你的目标是开车赶到办公室,那么只需要知道如何驾驶汽车,而不必搞懂内燃机的内部结构。相比之下,在低级语言中,你会得到螺栓和螺丝钉,为了开车去超市,你必须成为一名汽车修理工。
每种方法都有各自的优缺点,因此,它们的主要问题领域也不同。而 Rust 的目标是中间地带。你既可以使用 Rust 访问基础设施,也可以使用清晰易懂的高级抽象。但是,开发人员势必会为此付出代价:你必须学习一种新的程序推理方式。
内存管理
计算机程序依赖内存中的读写。内存读写是不可避免的,就像巧妇难为无米之炊。做饭的时候,我们必须将食材都买回来,然后洗干净切好,再按照正确的顺序将它们放入锅中,吃完饭后还需要收拾厨房里的烂摊子。
而高级语言提供了垃圾收集器,就像我们的父母,他们会耐心地帮你打扫卫生,而你则无需弄脏双手。然而,Rust 的内存管理是:“嗯……真正的厨师会清理自己的垃圾”。这也并非全无道理,因为垃圾收集器本身就有很深奥的问题,而且会带来意料之外的结果。但与此同时,Rust 借鉴了其他语言的过往经验,并强制程序员管理好内存。
作用域
在 Rust 中,变量只能在某个作用域内使用。如果这个作用域不再有效,则这块内存就会返回给系统。编译器会向在代码中注入一段代码来确保这一点。这在 Rust 中是铁一样的定律。
下面,我们来看一个示例。
这段代码有两个作用域。一个外层作用域来自 main,还有一个内层作用域。Rust 的所有权如下:
main 拥有 a 和 b;
a 想要使用内层作用域,所以 main 将 a 的所有权转移到内层;
内层作用域处理a,然后完成;
Rust的隐藏代码丢弃 a 的作用域;
main 处理 b,然后完成;
Rust 丢弃 b 的作用域。
请注意,所有权都会还给系统,而不是作用域的起源。a 的所有权不会返回给main 作用域。
等一等,这种做法听起来很危险。如果遇到如下代码,该怎么办?
在这段代码中,main 作用域想再次使用 a,但是我们说当内层作用域结束时,Rust 已经删除了 a。
程序执行到这里的时候,不会崩溃吗?
没错,程序会崩溃。
编译器
Rust 编译器会彻查一切,并评估程序是否可以安全运行。只有通过所有的检查,它才会生成可执行文件。
在上面这个例子中,编译器拒绝提供二进制文件。
程序员的职责是了解 Rust 的法则,并遵守这些法则。包括这门语言所有的细节,所有的怪癖,所有的假设。否则,Rust 编译器就会冲我们大吼大叫。另一方面,Rust 团队一直在努力通过创建大量语法糖和清晰的错误消息,帮助我们理解错误。而且,Rust 还有非常完善的文档和一个伟大的社区。
Rust的角色扮演游戏
在 Rust 大陆中,变量是玩家。玩家必须属于某个职业:法师、牧师、结构体。此外,每个玩家可能拥有不同的装备。当然,你可以拥有两个牧师,一个拿着权杖,一个拿着魔杖。
还记得上述代码中的dbg!()吗?这是一个宏,相当于 JavaScript 的 console.log。下面,我们来创建一个有类型的变量,并输出日志。
我们创建了一个 struct,本质上是一个类型。然后我们又创建了一个该类型的对象。最后,我们输出该对象。
以上,Noob 类型的 player 连调试信息都没有……
关键在于,我们手动创建的变量都是从 1 级开始的,没有装备。这里需要装备(用 Rust的术语说,就是 traits)。
我们来修改一下。
这一次可以了。唯一的不同就在于开头的第一行。我们为 Noob 配备了 Debug 特性。现在,我们的player就有资格输出日志了。巨大的进步!
Rust 拥有大量的装备,比其他语言更普遍。而且,你还可以自己设计并打造新装备。
有些 trait 可以由编译器为我们自动生成。而有些则需要自己实现。你想给你的法师打造一个盔甲?没问题,当然可以,但是你必须提供实际的代码。
trait 在 Rust 的结构中根深蒂固。我们再来看一看上述那个报错的例子。仔细阅读错误消息,我们会注意到,编译器向我们解释,必须“移动”变量的所有权,因为字符串没有实现 trait:Copy。
Copy trait 意味着你可以获取一段内存,然后 memcpy 到其他地方,直接对字节进行操作。
那么,既然字符串没有 Copy trait,我们可不可以要求编译器提供一个?抱歉,不行。Copy 的级别太低,字符串无法安全地使用这个trait。编译器知道这一点,所以不提供。当然,如果故事就此结束,Rust 就不会成为一门非常实用的语言了。实际上,字符串有一个更明确的 trait,可以完成相同的工作:Clone。而字符串也具有 Clone trait,因此我们只能用 Clone 来代替 Copy。
我们来稍微调整一下代码,像下面这样:
在这段代码中,编译器看到我们想在内层作用域中使用a,而且它看到我们可以使用 clone 来完成操作。所以,
a 的所有权归 main;
a.clone 在创建后,被借用到内层作用域;
内层作用域执行操作,然后完成;
Rust 丢弃 a.clone 的作用域;
main 可以使用 a,因为 a 的所有权始终归它所有。
当然,这不是唯一解决这个问题的方法,但我们可以通过这个例子初步探索一下所有权和trait。
总结
文本介绍的内容对于 Rust 学习来说,不过是冰山一角。根据我的个人经历,Rust 的学习曲线很陡峭,但整个学习过程很有趣,而且物有所值!我会继续努力学习下去!
原文链接:https://blogs.harvard.edu/kapolos/rust-from-a-javascript-perspective/
声明:本文由CSDN翻译,转载请注明来源。
☞“搏一搏,单车变摩托!”华为天才少年耗时四个月,将自行车强势升级为自动驾驶