对Rust的批判性审视:优势与权衡


Last updated on

※ 免责声明: 本文包含作者个人对特定编程语言设计哲学及其现实权衡的批判性分析。对技术的评估可以从多种角度进行,本文仅代表其中一种观点。

Rust通过其强大的社区和市场营销,形成了一种特定的认知,这有时会影响对其优缺点的客观评价。本文旨在从批判性的角度重新审视Rust的核心特性。

零成本抽象(Zero-Cost Abstractions):更像一个营销术语?

C++语言也有效地支持“零成本抽象”。事实上,很少有语言能提供像C一样具有成本效益的抽象。然而,Rust倾向于将这个概念作为其宣传的重点。虽然这是解释Rust优势的核心逻辑之一,但也有观点认为这只是一个为了市场营销而强调的口号

Rust所说的“零成本抽象”是指通过其所有权系统和借用检查器来减少运行时开销的概念。然而,这是以开发者必须学习并遵守编译器严格规则为代价的。在C++语言中,开发者可以自行控制并最大化效率的领域,Rust则在语言层面进行强制,为初学者带来了陡峭的学习曲线。

总而言之,Rust的“零成本抽象”可以被看作是对C++语言已提供功能的一种不同方式的重新诠释,并且带有更多的限制。将其强调为Rust独有的特殊优点,可能被视为一种通过赋予现有概念新含义来强化其“最新技术”认知的策略。真正的成本效益源于开发者的能力和灵活的控制,而不仅仅是语言的严格强制。

Rust没有段错误(Segmentation Fault)?

众所周知,用Rust编写的应用程序不会发生段错误(Segmentation Fault)。这是因为Rust强有力地保证了内存安全。然而,虽然不会发生段错误,但程序可能会因为panic而终止。

当然,与C语言中程序在不发生段错误的情况下发生故障,从而导致严重安全漏洞的最坏情况相比,Rust的panic无疑是一种安全性的提升。因为panic会以一种可控的方式引导程序终止,而不是导致不可预测的内存错误。

但是,“Rust没有段错误”这个口号很容易让人误解为程序不会因任何错误而终止。从最终用户的角度来看,无论是程序因段错误突然关闭,还是因panic而终止,结果都是一样的:应用程序中断。尽管Rust能阻止特定类型的内存错误,但我们必须警惕,不要给人一种程序完全“不会死机”的印象。

Rust Panic:是错误处理的漏洞,还是哲学?

我编译了官方的示例源代码。

https://doc.rust-lang.org/book/ch12-01-accepting-command-line-arguments.html

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");
}

在没有参数的情况下运行时,它会像这样终止:

$ ./main

thread 'main' panicked at main.rs:6:22:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

在给定的代码示例中,Rust程序因参数不足而引发panic并终止,这是Rust的预期行为。Rust社区将其解释为一种安全地中止程序的机制,以防止其导致未定义行为(undefined behavior)。这与Rust防止C/C++中常见的不可预测内存错误或安全漏洞的核心哲学相符。

然而,尽管Rust能在编译时捕获某些类型的错误,但在实际的应用程序运行环境中,由于意外的输入或外部环境的变化,仍然可能发生panic

即使在Rust中,如果程序员疏于panic处理或Result类型的妥善使用,应用程序仍然可能突然终止。Rust的“安全性”在特定内存错误领域表现出明显优势,但这并不完美保证程序的整体健壮性(robustness),即在任何情况下都能稳定运行并从错误中恢复的能力。

最终,Rust的panic在明确暴露危险错误和帮助调试方面具有积极意义。但对最终用户而言,它带来的结果与段错误相同,即程序突然终止。虽然Rust在语言层面上“强制”了安全性,但必须清楚地认识到,这并不能免除程序员的所有责任,也不能消除所有类型的运行时错误。

对Rust Result的批判性评价:明确性即是不便吗?

Rust的Result类型在以下官方文档中有详细说明。

https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html
https://doc.rust-lang.org/std/result/

正如官方文档所解释的,Rust的Result类型是一个强大的工具,它明确地强制进行错误处理。但这种强制性是一把双刃剑,是以牺牲开发者的便利性和生产力为代价的。Result试图解决现有语言错误处理方式的问题,但在这一过程中,它创造了一种新形式的“成本”,即开发流程的不便,这一点无法回避批评。

例如,JavaScript或Python中的try...catch语法具有将主逻辑与错误处理逻辑分离的明显优点,提高了代码的可读性,并帮助开发者专注于核心逻辑。但这种结构往往也内含着未检查异常(unchecked exception)可能在运行时随时爆发的不稳定性。

C语言中常用的errno全局变量方式,在简洁性方面无出其右。无需复杂的错误传播逻辑,只需几行代码即可检查和处理错误。

const char *err_msg = strerror (errno);
printf ("failed: %s\n", err_msg);

正是在这一点上,Rust的Result应运而生。Result试图在语言和编译器层面从根本上解决errnotry...catch的根本问题,即“可被遗忘的错误”和“隐藏的控制流”。它通过在类型系统中明确所有可能的错误,并由编译器强制处理,从而将程序的可靠性提升到极致。

然而,这个解决方案要求巨大的代价。强制明确处理所有可能的错误,必然会产生更多的样板代码和复杂的匹配模式。这在需要快速原型设计或业务逻辑快速实现的环境中,可能成为显著阻碍开发生产力的因素

从这个角度看,Rust的选择似乎是一种极端的权衡,为了“安全”这一价值,甘愿支付开发生产力的“成本”。Result提供的明确性和稳定性无疑是强大的优点,但这是以放弃其他语言所重视的简洁性和开发速度的哲学优势为代价的。这感觉像是一种过度矫正(over-correction),就像为了防止所有微小的风险,在每个路口都安装了不必要且复杂的安全装置,最终使走路本身变得痛苦不堪。

Rust二进制文件的“臃肿”:原因、证据及反驳的真相

在使用Rust开发应用程序时,关于生成二进制文件大小的疑问,往往超出了“增加”的程度。特别是与C等传统编译型语言相比,Rust的二进制文件给人留下的印象是“大得惊人”。这个问题源于Rust特定的设计哲学和分发方式,是一个难以避免的现实。

1. 根本原因:不稳定的ABI和实际上强制的静态链接

用Rust创造任何东西,都免不了使用标准库(libstd)。问题的核心在于,这个libstd目前完全不保证ABI(应用程序二进制接口)的稳定性

libstd以包含版本识别哈希值的文件名形式分发,如下所示:

$ pkg list rust | grep libstd
...
/usr/local/lib/rustlib/x86_64-unknown-freebsd/lib/libstd-441959e578fbfa8d.so
...

通过一个动态链接“Hello World!”应用程序的实验,可以清楚地看到为什么这是个问题。

fn main() {
    println!("Hello World!");
}

如果使用动态链接选项编译此代码,并用strip命令移除不必要的信息,二进制文件大小可以减小到5960字节。

rustc -C opt-level=s -C prefer-dynamic -C target-feature=-crt-static hello.rs

但使用ldd命令检查依赖项时,会暴露出一个致命问题。

$ LD_LIBRARY_PATH=/usr/local/lib/rustlib/x86_64-unknown-freebsd/lib ldd ./hello
./hello:
  libstd-441959e578fbfa8d.so => /usr/local/lib/rustlib/x86_64-unknown-freebsd/lib/libstd-441959e578fbfa8d.so (0x25f114c46000)
  libc.so.7 => /lib/libc.so.7 (0x25f115e0f000)
  libthr.so.3 => /lib/libthr.so.3 (0x25f114e6b000)
  libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x25f11756d000)
  [vdso] (0x25f1130d4000)

ldd命令确认了libstd-441959e578fbfa8d.so被链接。

从文件名libstd-441959e578fbfa8d.so可以看出,每次Rust编译器版本更新,libstd的内部结构都可能改变。这从根本上杜绝了向后兼容性——传统动态链接的最大优点。如果一个应用程序动态链接到libstd,那么每当系统的Rust版本更新时,该应用极有可能无法运行。此外,如果从系统中删除rust包,libstd也会被一并删除,导致所有相关应用失效。

由于Rust这种根本性的ABI不稳定性,在实际部署环境中,为了稳定运行,将libstd直接包含在二进制文件中的静态链接,实际上是唯一且被强制的选择。而这正是二进制文件大小“臃肿”的最大原因。

2. 现实的证据:实际应用比较

这种“臃肿”并非仅限于理论,通过实际应用的比较可以清晰地看出。

案例1:rg (Rust) vs grep (C)

让我们比较一下用Rust制作的grep替代工具rg (ripgrep)与grep的二进制文件大小。

debian:~$ ls -l /bin/grep /bin/rg
-rwxr-xr-x 1 root root  203072 Nov 10  2020 /bin/grep
-rwxr-xr-x 1 root root 4345184 Jan 19  2021 /bin/rg

rg的二进制文件大小比grep大了约21倍。当然,可能会有反驳说rg提供了并行处理等更多功能。这个主张是合理的。但即使考虑到功能差异,超过20倍的大小差距也难以轻易接受。那么,功能少得多的情况又如何呢?

案例2:kime (Rust) vs nimf (C) 输入法比较

$ ls -al kime_ubuntu-22.04_v3.1.1_amd64.deb nimf_2023.01.26-bookworm_amd64.deb
-rw-r--r--  1 user user 3197276 Jun 27 07:38 kime_ubuntu-22.04_v3.1.1_amd64.deb
-rw-r--r--  1 user user  275728 Jun 27 07:37 nimf_2023.01.26-bookworm_amd64.deb

基于Rust的kime软件包大小约3.2MB,而基于C的nimf软件包仅约0.28MB,相差超过11倍。功能上,kime专注于韩语输入,而nimf是一个支持包括韩语、中文、日语在内的数十种语言的框架。

功能远比专注于特定语言的kime广泛的nimf,其软件包大小不到十分之一,这一事实表明,Rust二进制文件的“臃肿”是一个根本性问题,不能仅用功能增加来解释。

3. 社区的解释及其背后:min-sized-rust的不便真相(2025年版)

针对此类批评,Rust社区通过min-sized-rust等文档声称可以减小二进制文件大小。然而,该文档中提出的方法大多是不切实际的技巧,或者需要放弃Rust的优点。

设定比较基准:C语言 Hello World

首先,我们需要明确一个比较的基准。以下是基本的C语言“Hello World”代码。

#include <stdio.h>
int main ()
{
  puts ("hello world");
  return 0;
};

cc -O2 -o hello hello.c编译此代码,再用strip hello命令去除不必要信息后,最终的二进制文件大小为4,960字节(约5KB)。这不是什么特殊的“技巧”,而是C语言中优化二进制文件的常规流程。我们将以此5KB的大小为基准来评估Rust的成果。

规范方法及其局限性

min-sized-rust指南首先提出了在Cargo.toml中添加几项设置的“规范方法”。

[profile.release]
strip = true      # 自动去除符号
opt-level = "z"   # 优化大小而非速度
lto = true        # 链接时优化
codegen-units = 1 # 最大化优化机会

即使应用了所有这些标准优化,“Hello World”二进制文件的大小仍然在277KB左右。与C的5KB相比,仍然大了50多倍,结果令人失望。

改变行为的优化:panic = "abort"

指南下一步建议使用panic = "abort"选项。这通过在发生panic时立即中止程序而不是展开堆栈来减小二进制文件大小。但指南自己也警告说这“会影响程序的行为”。这是为了安全性而放弃便利性的第一步。

“技巧”的领域:Nightly和不稳定功能

为了实现有意义的大小缩减,最终必须依赖稳定版(stable)中不可用的Nightly专用功能。指南中展示最显著大小缩减的build-std功能就是典型代表。该方法需要用rustup安装nightly工具链和rust-src组件,并使用如下非常复杂且冗长的命令。

# macOS 示例
$ RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none" cargo +nightly build \
  -Z build-std=std,panic_abort \
  -Z build-std-features="optimize_for_size,panic_immediate_abort" \
  --target x86_64-apple-darwin --release

指南声称,经过所有这些步骤,最终的二进制文件可以减小到30KB。30KB这个数字令人印象深刻,但它掩盖了以下事实:

  • 不切实际: 在稳定版中无法实现,需要经过大量不稳定(-Z)标志和复杂的构建过程才能获得。这不是标准的开发方式。
  • 仍然很大: 即便费尽“技巧”得到的30KB,与C的5KB相比,仍然大了6倍。

放弃Rust优点的极端选择

指南甚至更进一步,提出了no_mainno_std等极端方法。

  • no_main 指南自己警告说:“预计代码会变得取巧且不可移植(Expect the code to be hacky and unportable)”。
  • no_std 承认你将“失去对大多数Rust crate的访问权限”,意味着必须放弃Rust生态系统的优点。

到了这个阶段,开发者几乎放弃了Rust安全性和生态系统的所有优点,实际上以与C无异的方式编码。这是为了减小二进制文件大小而使用Rust的自相矛盾之举。

2025年也未改变的不便真相

通过以上验证可知,即使到了2025年,min-sized-rust指南的本质也未改变。该指南展示了将Rust二进制文件变小的“可能性”,但其方法大多不实用、不稳定,或者需要放弃Rust的核心价值。

因此,在以min-sized-rust指南为依据主张“Rust二进制文件也可以变小”之前,有必要清楚地认识到这些方法所具有的现实制约和权衡。现实情况是,采用标准开发方式,二进制文件大小仍然很大,需要就此点进行更均衡的讨论。

“内存安全”就等于“应用程序稳定”吗?:librsvg案例分析

期望使用保证内存安全(memory-safe)的Rust会使应用程序整体上更安全,这是合理的。然而,“内存安全”是否真的等同于“应用程序的整体稳定性”,让我们通过从C成功移植到Rust的代表性案例librsvg来进行批判性审视。

librsvg是一个用于渲染SVG(可缩放矢量图形)文件的库,大约从2016年10月开始从C向Rust转换。

截至2025年6月,可以确认librsvg的代码库大部分由Rust构成。

Rust 87.0%
C 7.7%
Python 2.1%
Meson 1.8%
Shell 1.1%
Batchfile 0.3%

那么,大部分用Rust重写的librsvg是否真的变得完全“安全”了呢?查看项目的议题跟踪器(issue tracker)就会面对现实。

议题跟踪器的现实表明,尽管获得了“内存安全”,由panicfault引起的异常终止仍在发生。这意味着,尽管Rust可能从源头上阻止了特定类型的内存错误,但它并不能防止应用程序所有潜在的崩溃。“内存安全”并非保证“无缺陷”或“不中断”的万能药

此外,这种语言转换还引发了另一个现实问题:二进制文件大小的激增。让我们比较一下过去用C编写的版本和现在用Rust重写的版本的软件包大小。

$ ls -l librsvg*~*
-rw-r--r--  1 root wheel  201832 Jun 25 05:13 librsvg2-2.40.21_4~f605eba4b2.pkg
-rw-r--r--  1 root wheel 3202515 Jun 24 10:28 librsvg2-rust-2.60.0~c96f7b1992.pkg

基于C的librsvg 2.40版本软件包大小约0.2MB,而移植到Rust的2.60版本软件包大小约3.2MB,大小激增了近16倍。当然,两个软件包之间存在相当大的版本差异,不能否认功能增加对大小增长有所贡献。但考虑到前面的kime案例中,即使功能更少,文件大小也相差10倍以上,很难将大小增长完全归因于功能改进。

如此,librsvg的案例明确显示,向Rust的转换在获得“内存安全”的同时,在“应用程序稳定性”和“部署效率”方面产生了新的权衡。在实际环境中采用技术时,必须采取批判性的态度,亲自查看各种项目的议题跟踪器,多方面审视可能发生的问题,而不是依赖于网络上的评价或市场口号。

Rust的竞争对手是C++?

Rust将“内存安全”作为其主要卖点,但事实上,许多主流语言,如Java、C#、Python、JavaScript、Go、Swift、Ada、Kotlin、Ruby、PHP等,在TIOBE指数前20名内,也都默认支持内存安全。唯独Rust如此特别地强调这一点,可能被视为一种过于强调“安全”价值而忽视技术复杂权衡的倾向。

综合所有这些点,Rust很难在所有领域取代C/C++。存在着前面指出的不稳定ABI、相对较大的二进制文件、因panic导致的程序中断可能性、复杂的C ABI交互、陡峭的学习曲线、编码生产力等现实的权衡。

当然,Rust的合适用武之地是明确存在的。那就是同时极度要求性能和安全的部分系统编程领域。例如,处理客户信息的服务器或云基础设施,在这些地方,C/C++中因错误内存引用导致的信息泄露可能造成严重损失。在这样的环境中,Rust比Java快,同时又能期待Java级别的稳定性,即使牺牲开发成本,也完全值得考虑。

但是,将此推广并主张“C/C++不安全,所以一切都应该用Rust替代”,可能是无视现实的片面逻辑。这就像主张将道路上所有的汽车都换成最高安全等级的装甲车一样。每种工具都有其适合的权衡和用途。

内存泄漏、程序的突然中断(panic)仍然是程序员的责任,仅凭“内存安全”一点就说所有问题都解决了,很难说是准确的表达。

Rust的用武之地,别把“利基市场”的增长误认为“大势所趋”

近年来,Rust在开发者调查中持续被评为“最受喜爱的语言”,并被主要的大型科技公司采用,取得了显著的增长,这是事实。但有必要冷静地审视这种增长的背后。

Rust的增长主要集中在最能发挥其优势的利基市场:系统编程,包括操作系统、云基础设施、区块链等。在占据软件行业绝大多数工作岗位的广泛的商业应用开发领域,Rust的采用率与其声誉不符,这是现实。

因此,必须正视“最受喜爱的语言”这一头衔与“拥有最多工作岗位的语言”的含义不同这一现实。在被对Rust的热情氛围所席卷,期待“玫瑰色的未来”之前,必须区分这究竟是少数专家的利基市场的增长,还是能够为我的职业生涯提供实际机会的主流市场的变化。

对于大多数开发求职者而言,除非目标是Rust的特定应用领域,否则打好C/C++、数据结构、网络等计算机科学的基础,仍然能提供更稳定和广泛的机会。相比于追随特定语言的潮流,积累可以应用于任何语言的、普遍且重要的知识,是更明智的长期投资。

结论: 尽管如此,Rust仍具有明确的价值

尽管有所有这些批评,但我们也必须明确承认Rust在特定领域为何受到如此强烈支持的原因。

在C/C++中,错误的内存引用有时可能在未被察觉的情况下引发程序故障,或者在最坏的情况下,导致泄露敏感信息的严重安全漏洞。

正是在这一点上,Rust提供了不妥协的价值。所有权系统和借用检查器在编译时从源头上阻止了无数潜在的内存错误,即使在运行时发生未预料到的内存访问错误,也会以立即的panic代替未定义行为(Undefined Behavior)来中止程序,从而防止更大的灾难。

最终,这两个强大的机制相结合,完成了Rust的核心价值——“提升可靠性与安全性”。本文中指出的无数权衡,正是为获得这一核心价值而付出的代价。