本文是“发现 Rust”系列第二篇,第一篇参见:《发现 Rust:从项目化角度谈效率之变(一)》
1、前言
Rust 作为一种安全的系统语言,将语言层面的语义约束与编译器自动化推导深度结合,实现了更加严谨的编程风格和更加安全的编程方式。从 Linux 6.1 内核正式合入 Rust 支持开始,它与 Linux 内核的深度融合就再也不是悬念,未来的发展充满想象。
对于开发者,从传统语言切换到 Rust 语言,需要一定的心理准备和思维方式转换。但这并不是多么高的门槛,关键在于对于编程技术发展趋势的理解和对 Rust 所倡导的编程理念的接纳。一旦接受和开始使用 Rust,你会发现一切门槛都不存在。我自己的感受是,一旦开始使用 Rust 进行编程,完成第一个项目后,就会无法逆转地喜欢上它,再也回不到传统的编程语言。
2、体验
我使用过很多种编程语言,其中最熟悉的应该是 C 语言。我从传统语言切换到 Rust 语言的过程,应该算是成功的。因为通过使用 Rust 我超预期地达成了项目目标,更重要的是在这个过程中我喜欢上了使用 Rust 进行开发,甚至希望参与到 Rust 本身的演进发展中。
2.1.独一无二的 Rust
如果对于 Rust 带来的未来缺乏了解,那么很难对使用 Rust 产生多少兴趣。安全编程所需要解决的问题,Rust 还在不断完善中,但是更加严谨的编程的理念已经不可阻挡。Rust 本身能够发展到什么程度?它是否可能被更好的语言和编译器取代?我不知道。但是,Rust 目前是独一无二的。
Rust 更深地剖析了编程安全性的本质,并在语言层面提供了工具。内存安全和线程安全问题的本质,是数据对象的生命周期和所有权问题。内存回收和锁是在运行时解决这两个问题的方法。内存回收和锁的长期使用,给这两个问题蒙上一层迷雾,似乎他们就是运行时问题。但 Rust 挥开了这层迷雾,开创性的把这两个问题从运行时放到了语言层面解决。从而使编程语言迈进了一大步。
在语言设计进步的基础上,Rust 的编译器设计有很大的不同,它可以完成更多的推导工作,使得很多编程错误可以在编译阶段被发现。这也是一个很大的进步,编译器有了进行推导的基础,使编译器的能力得到了释放,可以承担更多的逻辑分析、安全检查工作。这使 Rust 成为一种安全的编程语言。
另外,由于大部分的软件问题和所有的编码问题在编译阶段就已经被发现,这大大降低了调试的难度。本项目目前不涉及多线程处理,仅仅是单线程的前端解释器。因为不涉及更复杂的多线程处理,所以本项目开发过程中的所有问题,基本都是在编译阶段被发现和解决,除了极个别的在调用 C 语言库的时候,由于不熟悉 FFI,在处理字符串的 unsafe 代码时,由于对规则细节了解不全导致的问题之外。整体的感受是,Rust 编写的程序只要编译通过,就是可以正确运行,名副其实的安全语言。对于一个长期使用 C 语言开发,习惯于化大量时间 debug 的开发者来说,这是一个非常特别的体验。使我对于“零”调试的开发方式,充满了期待,我觉得这个可能就是未来。
2.2.改变思维习惯
传统开发中,我们大量的时间花费在调试各种 Bug 中,这带来低效、糟糕的体验,以及研发和测试的矛盾。但是 Rust 全然不同,Rust 语言的表达能力很完善,同时 Rust 的编译器足够强大。大多数原本在调试阶段才能发现和处理的问题,在使用 Rust 的开过程发中,都提前到了编译阶段,这无疑是一大进步。
诚然 Rust 带来了进步,但 Rust 也全然不同于以往的编程语言,需要开发人员也做出相应的改变。
编程是一个交互行为,是人与机器之间的交互,是使机器的表现和人的期望达成一致的过程。人与机器的行为的伦理差异在于,机器的行为都是固定的、被设计的,效率也是固定的、受限的。而人的行为则更复杂,有波峰有波谷,有偏差有创新。人的行为整体取决于期望,但行为过程和效率取决于更多复杂的因素。对于编程行为来说,编程体验对于效率和结果的影响很大。
心理学家认为,成就感是主体心中的愿望和眼前的现实达到平衡时,产生的一种心理感受。编程行为中,成就感来源于机器反馈的结果,如果机器反馈符合预期,则很有成就感。编程过程中,是否能够高效地达成预期,决定了编程体验的好坏。
但高效这个词是模糊的,因人而异,跟习惯强相关。在传统的 C 语言编程中,由于 C 语言的自由、低约束和命令式,开发者更倾向于快速编写完成主要流程,尽快让程序运行起来,然后再解决其他问题。因此 C 语言的开发过程中,编码部分是简约的、不断被压缩的,而调试部分则拉得很长。相信大家体验过,开始的时候疯狂码代码,等程序运行起来就觉得很放松,然后转而进行反反复复修修补补的调试过程。前一阶段看似高效,但是遗留下很多问题,整个开发过程效率、质量其实是不高的。从开发体验来说,前一段像打了鸡血,后一段被 BUG 按在地上摩擦,整体感受也是很差的。
这样的开发过程比较符合“习惯”,因为每个人都希望尽快的看到结果,但这种模式是不科学的,因为前一阶段遗留的问题在后面的阶段是很难弥补,调试 BUG 的过程更谈不上享受,很容易遗漏 BUG。
在 Rust 中这一切则恰恰相反,更加科学、严谨,但是不太符合“习惯”。所以需要开发者打破固有习惯,才能发现 Rust 之美。在 Rust 中,不仅需要开发者在逻辑层面清晰、完整、无漏地表述,还需要在语义层面明确借用和生命周期等约束。也就是说,在编码阶段不仅要大致表述清楚需要机器做什么,还需要表述清楚细节。
Rust 更加适合希望严谨、准确表达想法的人,在编译阶段之前解决逻辑和编程问题,从而避免低效重复的调试过程。
打个极端点的比喻,使用 Rust 编写软件就像在写一部逻辑严密、环环相扣的推理小说,整个过程连贯一致、恰到好处;而用 C 语言则像在编写 Tweet,快速直接,不断补充、修正。
从我的体验来说,Rust 程序只要编译通过就一定是正确的,唯一的例外情况出现在与 C 语言的 unsafe 边界上,而这些边界以后随着 Rust 原生库的丰富,会逐渐消除。因此,Rust 编程的重心在设计和编码上,调试的重要性大大降低。因此我们需要改变急于使程序运行起来,通过调试来修补程序的习惯,而应该在编码阶段更加严谨、细致,把大量花在与调试器交互的时间,花在与提示器和编译器上的交互上。
改变思维习惯,在编程发展历程中,并不是第一次发生。从机器语言、到汇编、到 C、再到百花齐放的 C++、Java、C#等,再到 Rust。开发者的习惯一直在改变,开发者应该更多地提供信息而不是指令,编写严谨的程序而不是解决调试器发现的 BUG。
2.3.只需花费一周时间来阅读
我的印象中,故往的每一种流行编程语言都有几本很厚的被冠以“圣典”的书。
但我学些 Rust 的体验是,学习 Rust 语言不需要看很多、很厚的书。Rust 的编程帮助很丰富,可以一边做项目,一边学习。它是一种安全的语言,所以你的生疏并不会导致软件可靠性问题。这可能是学习 Rust 的过程中最奇妙的一点,一个懵懵懂懂的初学者编写出来的代码和一个经验丰富的早鸟编写出来的代码,一样的安全和可靠。
因人而异,取决于不同的编程基础和背景,但最短一周的熟悉时间,应该就可以使用 Rust 开始由浅入深的开发过程,在开发中不断扩展和加深学习,可以快速实现到 Rust 的过渡。
在这个逐渐上手的过程中,很大部分时间是根据自己的具体需要,查阅 Rust 语言手册的相关部分,加深理解。查阅 Rust 标准库手册,了解基于标准库,如何实现自己的功能需求。还有就是在 crates.io 上查找相关的例子程序,学习各种使用 Rust 解决具体问题的方法和技巧。
比较奇妙的是,在这个上手过程中,最初的时候,你对 Rust 可能只有最基础的概念,犹如一个婴儿,但是即使如此,你也不会写出包含隐晦的、难以调试的错误的程序。整个上手过程,是一个稳健的,逐渐由模糊到清晰的,充满正面感受的过程。绝不会出现其他语言那种,让你百思不得其解、需要推倒重来,或者绝望到放弃的晦涩的问题。
原因就在于,Rust 的严密的语法逻辑以及强大编译推导能力,将你的一切编程错误都在编译阶段陈列在眼前,并给出了清晰的提示,甚至是准确的修改建议。这几乎是等于把菜整齐地排列在桌上,把筷子放在你的手里,耐心地等你五指捏紧,然后扶着你的手去夹菜。作为一个 C 程序员,从来没有被这样爱护过。
2.4.函数式元编程
元编程可以通过更高的抽象,使程序表达更加精炼,也可以使程序表达更加的严谨,消除内部自洽的问题,提高外部可复用性。元编程也可以克服不同语言之间的隔阂,更好地实现多语言的融合。
元编程是编程行为的必要部分,除了 Hello world 这样的简单程序,达到一定复杂度的程序,比如逻辑层次超过三层,可能就需要一定程度的元编程。元编程使代码更加简洁优美和更易维护。
C 语言中能够与元编程相关的就是宏了,但是 C 语言的宏很简陋,不支持循环,实际上并不完整地具备元编程能力。因此使用 C 语言开发,越到后来越感觉力有不逮,总有些繁琐丑陋的部分无法清除。
Rust 的元编程能力比 C 语言强太多,Rust 的宏很丰富,支持安全的迭代。Rust 的元编程能力初始设计很不错,起点很高。Rust 虽然还在发展过程中,但其核心的语言要素已经基本固定,唯有元编程这部分,感觉还有很大的潜力可挖,对此我充满期待。
在解释器项目中,需要使用 eBPF 指令来实现 Unliang 的指令和内置函数,还需要封装 C 的数据定义,因此也使用了很多宏。比如:对 FUSE 的一些定义的封装。
上面一段,是 Rust 的声明式宏。下面一段,是一段原生的 C 语句段,通过 Rust 宏进行转换为 Rust 的定义。
通过观察 C 语言语句片段的特征,利用宏提取语法规律,实现直接从 C 语言片段生成 Rust 的定义,这就是 Rust 元编程能力的一种体现。
2.5.审美与表现力
编程语言,去掉编程两个字,也是一种语言。人通过语言实现人与人、人与机器的交互。语言的美感,来源于人不仅通过语言交换信息,还通过语言传达思想。
不同的人对事物的审美存在着天地之别般的差异,对于编程语言也是一样。你是希望简介表达,还是深层次表达,决定了你对 Rust 的审美。如果你只关注当下的结果,Rust 无疑是晦涩的,同样的事物在 Rust 中的表达,远比 C 语言中复杂。但是如果你关注的是软件本身的正确性和语义的完整表达,Rust 的就非常犀利,它能够把你最严谨和微妙的想法准确表达出来。从这一点说,Rust 有无与伦比的美。
要想充分体验编程的乐趣,我们需要理解不同编程语言的美与其表现力。
C 语言的美集中在指针。指针是机器本质之美,因为指针是机器处理的灵魂,指针的发明使高级语言深入机器的灵魂,获得了魔力。指针也是自由、灵活之美,通过它可以打破类型、范围的约束,更直接高效的实现功能。但它是不安全的。
Rust 是圆满如一、无缺无漏之美。Rust 的美集中在始终。始终是数据的本质之美,万事万物皆信息,始终是更本质的美。理解 Rust 的美,是一种编程思想的涅槃。
2.6.体验后的遗憾
使用 Rust 开发项目是一个令人愉悦的体验,整个过程出乎我预料得成功,但也有“遗憾”。
在体验 Rust 编程的的安全可靠后,再次使用 C 编程的时候,我会有一种焦虑情绪。习惯了 Rust 那种严谨、完整的表达后,我对我写出来的 C 代码的信心大大降低,总觉得言之不尽,缺漏百出。因为对比 Rust 我们可以很轻易的发现用 C 编写的代码有很多深层语义约束是没有表达出来的,缺乏一种手段来保证代码的安全,这种感觉就像在裸奔编程。只能寄希望与更加详细的单元测试和集成测试,但事实早已证明这是不够。同时,这样做带来的是更多的测试开销,更加冗长反复的研发周期。
3、结语
Rust 是实至名归的安全语言,它的安全性有坚强的内核,来源于语言本身的创新和编译器技术的发展。这是编程语言发展历程中的一次飞跃,对于编程这种交互行为中人与工具的边界进行了重新定义,不仅产生了全新的编程方式,也使工具的作用发生了突变。这种进步不仅蕴育出编程的安全性,还衍生出了在开发效率、学习门槛上的奇异变化。
在我们的长期认知中,调试是与编程伴生的行为,有多少编程输出,就有多少调试。但遗憾的是,一个让人沮丧的统计结论是,有多少调试就有多少 BUG,有解决的 BUG 就一定有遗漏的 BUG。本质上说,调试不是编程的伴生物,是开发安全性的提现,是不安全编程的标志。
随着 Rust 这样的安全语言的出现,调试的比重大大降低。甚至一定条件下可以完全消除,比如:在纯函数式编程的范围内。测试的复杂度也大大降低,单元测试、集成测试的定义可能会被改写,他们的工作量会降低、结论会更加准确、有效。
整个开发过程的重心将会发生偏移,开发过程变得紧凑,甚至缩短。软件的设计和编码会更加重要,不仅体现在时间占比上,也体现在技术与工具的支持,比如:提示器、编译器,甚至未来的 AI 辅助编程。开发者应该改变固有的习惯,尝试、接受和喜爱这种新的开发方式。
对于有想法使用 Rust 开发项目的经理等管理角色,需要明确的一个概念是,Rust 是安全的语言,开发人员的编码水平和对语言的熟悉程度,并不会影响项目的质量,这一点和 C 语言项目完全不同,经验丰富的 C 开发人员对于软件质量有至关重要的影响。
使用 Rust 编程或者启动基于 Rust 的产品项目,其门槛并不像想像的那么高,一旦你决定尝试 Rust,历史的车轮就会隆隆地开始转动。
作者介绍
钟俊,统信软件研发技术专家,专注于内核与编译器技术,长期在通信、云计算、安全、信创等多个行业从事底层软件技术研发及相关工作。
评论