“别把好好的Rust,写得跟Java一样”

【CSDN 编者按】在当今多语言并存的世界里,开发者们常常会在学习一门新语言时,不自觉地带入他们对现有语言的理解和习惯。当转向 Rust 这样一门强调内存安全、高性能且拥有独特所有权模型的语言时,这种倾向尤其明显。本文作者以自身经历出发,提出:要想真正发挥 Rust 的潜力,必须学会放下过往的习惯,拥抱 Rust 的独特魅力。

原文链接:
https://jgayfer.com/dont-write-rust-like-java

作者 | James Gayfer 翻译 | 郑丽媛
出品 | CSDN(ID:CSDNnews)

我对 Rust 这门语言好奇好几年——具备类型安全、内存安全,并强调代码正确性,这样的语言怎能不让人喜爱呢?

在我开发 Apollo(一个用 Python 编写的应用)时,遇到的错误中有很大一部分是可以通过 Rust 编译器捕捉到的(虽然不能说是 100%,但也非常接近)。通常来说,编译器能捕捉到很多使用动态语言(如 Python 或 Ruby)时可能遗漏的问题,可并非所有编译器都一样出色。类型安全固然很好,但 Rust 对于代码正确性的重视才是最吸引我的地方。

最近我在工作中写了不少 Java 代码,虽然它不是我最喜欢的编程语言,但它的编译时检查功能确实让人感到安心。在 Java 中进行大规模重构时,不会像在 Python 或 Ruby 中那样令人胆战心惊,因为还有编译器会帮你。一个错误或缺失的导入语句,并不会导致程序在运行时突然崩溃,我们也通常会用测试来捕捉这类问题,但我还是要说:能把这些检查直接内置到语言中,真的非常好!

不过,Java 编译器也并非完美无缺。它无法防止某些类别的错误,其中最烦人的就是空引用错误。在 Java 中,几乎所有东西都可能为 ,而你只有在运行时才能发现这一点。相比之下,Rust 设计了一套机制来引导你处理未知的值。当然,你也可以选择忽略这些引导,但编译器还是会迫使你去那样做。

那么,Rust 会是一种更好的 Java 吗?总体来说,Rust 所承诺的一切都让我感到非常兴奋,但我的 Rust 学习之旅并非一路顺风顺水。尽管 Rust 和 Java 有些许相似之处,但它始终不是 Java——直到我不再试图让 Rust 变成 Java 之后,我才真正体验到了编写 Rust 代码的乐趣。


“一切都必须是一个接口”

虽然这种说法并不完全准确,但它确实反映了一些事实:Java 开发者倾向于将所有东西都设计成接口(我就是其中之一)。在 Java 中,使用接口很有趣,它可以让应用由一个个小的工作单元组成,而每个工作单元都不必直接了解其他单元的内部运作细节。虽然在构建依赖关系树时需要做一些前期工作,但一旦完成,你就等于拥有了一支随时听候调遣的独立服务大军。

在 Rust 中,我们没有接口,但有 Traits(特征)。它在很多方面与 Java 中的接口相似,但试图在 Rust 中将所有东西都变成 Trait 并不好玩。还记得 Rust 的一个重要特点——内存安全吗?这背后是有代价的,那就是不能轻易地“注入”实现了某个 Trait 的对象。

trait Named { fn name(&self) -> String;}
struct Service { named: Named}

上述代码无法编译,因为编译器无法确定 Box 的大小。为了解决这个问题,我们可以使用“装箱”(boxing)的方式来指向堆上动态分配的内存(称为 Trait 对象)。这样,Box 的大小就变成了已知,从而允许我们的程序通过编译。

trait Named { fn name(&self) -> String;}
struct Service { named: Box}

“装箱”并不是我喜欢的模式,因为它用起来有些别扭,所以一般我会尽量避免使用这种方式。相反,我们可以使用泛型来指定 Trait 类型。

trait Named { fn name(&self) -> String;}
struct Service { named: T}

有什么不同?乍一看,结果是一样的,不同之处在于动态分发和静态分发之间的差异。使用 Trait 对象时,具体类型是在运行时确定的;而使用泛型时,具体类型则是在编译时确定的。

在实践中,这意味着只要我们可以在编译时推断出所有类型,就可以使用泛型。如果类型直到运行时才能确定,则必须使用“装箱”(boxing)。


所有权问题如何解决?

所有权问题仍然存在。假设我们的 Trait 是应用程序中其他服务的必要依赖项怎么办?我们是创建一个“主”实例,并将其引用传递给每个依赖项,从而引入生命周期管理吗?

struct Service<'a> { named: &'a dyn Named}

还是使用 Arc,让我们的依赖服务持有一个 Arc,从而允许并发访问已拥有的资源?

struct Service { named: Arc}

这两种方法我都试过,也都奏效,但效果并不太好,特别是当应用中的每个服务都受到影响时。


函数也是可以使用的

强迫 Rust 成为一种纯粹的面向对象语言并不好玩。虽然我仍然会像上面的例子那样编写“服务对象”,但我尽量只在必要时才使用它们,我更倾向于使用函数。

举个例子,想一个用于处理 Stripe 结账会话完成事件的函数,该函数会更新系统中的 Stripe 客户 ID。

async fn handle_session_completed( user_repo: &mut impl UserRepo, session: &CheckoutSession,) -> anyhow::Result<()> {
let user_id = session .client_reference_id .clone() .context("Missing client reference ID")?;
let customer_id = session .customer_id .clone() .context("Missing customer ID")?;
user_repo .update_stripe_customer_id(user_id, &customer_id) .await?;
Ok(())}

虽然我们可以将其写成一个服务,并注入一个 UserRepo 值,但这样做太过复杂。实际上也没有理由将其写成服务,因为我们仍然可以轻松地注入 UserRepo 的不同实现,例如提供一个不访问实时数据库的实现。虽然这样做的缺点是函数签名可能会变得有点复杂,但这种程度的“痛苦”与其它方案相比算不上什么。


接受 Rust 的本质

我曾深陷“Rust 很难”的困境之中,其中一个重要原因是因为我坚持认为 Rust 代码应该像我以前写的代码那样。虽然从过去的经验中汲取教训是件好事,但接受现有的编程范式对掌握一门语言而言至关重要。

学习 Rust 需要一种思维方式的转变:不要因为 Rust 不是你熟悉的样子而抗拒它,慢慢去接受它的本质。

相关文章

如何使用Java构建一个多语言翻译服务平台

如何使用Java构建一个多语言翻译服务平台在当今全球化的世界里,多语言翻译服务变得越来越重要。无论是企业网站、移动应用还是桌面软件,为用户提供多语言支持可以极大地提升用户体验。本文将向您介绍如何使用J...

Java虚拟机和Dalvik虚拟机有哪些区别和不同?

Dalvik是Google公司自己设计的用于Android平的虚拟机,它可以简单地完成进程隔离和线程管理,并且可以提高内存的使用效率。每一个Android应用程序在底层都会对应一个独立的Dalvik虚...

快讯|这个月,火山翻译做了什么?

火山翻译最近又有大动作!快来看看这次有什么更新吧~“新增合作Bob translate、iTab火山翻译与第三方工具Bob translate、iTab开展合作,大家可以更加方便地使用火山翻译啦~三步...

每日一点:java原来是这么运行的

Java 是一种高级编程语言,它被设计成可以跨平台执行的语言,也就是说,Java 代码可以在任何支持 Java 虚拟机(JVM)的操作系统上运行。在 Java 中,源代码需要被编译成 Java 字节码...

C,Java和Python之间的性能比较

这是我用所有三种语言运行矩阵乘法时发生的情况> Photo by paolo candelo on Unsplash 在过去的两年中,我为C语言做了大量的实现工作。 我之所以选择C语言而不是其他...