Rust RFC 1685 深度解析:废弃 trait 方法中的匿名参数(anonymous parameters)
Rust RFC 1685 深度解析:废弃 trait 方法中的匿名参数(anonymous parameters)
导读
本文围绕本仓库(Rust 官方 RFC 提案仓库)中的 RFC 1685:废弃匿名参数 展开,系统讲解 Rust 早期允许在 trait 方法中省略参数名的历史语法、它带来的六大技术问题与学习成本、向后兼容性分析,以及"软弃用 / 硬弃用 / 自动迁移"三种弃用策略的取舍。读完本文,你将理解为什么 fn foo(i32); 这类写法在 Rust 中被逐步淘汰,掌握用 _: T 或显式命名进行无痛迁移的方法,并能从后续 RFC 的演进脉络中看清这一语法在 Rust 2018 版(edition)中的最终归宿。
RFC 背景与定位
RFC 1685(Feature Name:deprecate_anonymous_parameters,启动日期 2016-07-19)是 Rust 语言层面的一则"弃用类"提案。它的核心主张非常克制:只提议废弃(deprecate)匿名参数语法,明确不提议立即删除该语法。
在 Rust 的 RFC 流程中(参见 RFC 2:RFC 流程),"删除语言特性"属于典型的"实质性变更",必须先经过 RFC 评审、社区共识与 FCP(最终评论期)流程。RFC 1685 正是这一机制的产物:它不追求一步到位,而是为未来可能发生的彻底移除铺平道路。所有提案都要从 0000-template.md 模板起步,包含 Summary、Motivation、Detailed design、Drawbacks、Alternatives、Unresolved questions 等标准章节,RFC 1685 完整遵循了这一结构。
匿名参数是什么:历史语法回顾
在 Rust 2015 时代,trait 方法声明允许省略参数名,只写类型:
trait T {
fn foo(i32);
fn bar_with_default_impl(String, String) {
}
}
这里的 fn foo(i32); 表示"接收一个类型为 i32 的参数"——没有名字,只有类型。对于有 C 语言背景的开发者,这很好理解;但对于从 Python、JavaScript 等动态语言转来的开发者,fn foo(T) 里的 T 看起来更像是一个参数名而不是类型,语义存在明显的误读空间。
RFC 将这种语法的由来定性为"历史意外"(historic accident),而非经过深思熟虑的设计。它在早期 Rust 中遗留了下来,并持续带来一系列技术上的麻烦。
为什么需要废弃:六大技术问题
RFC 1685 的 Motivation 部分列举了匿名参数造成的六类问题,逐一拆解如下。
1. trait 中令人困惑的模式语法
匿名参数在 trait 中实际上是被当作"模式"来解析的,这导致模式复杂度的限制非常诡异:
trait T {
fn foo(x: i32); // Ok
fn bar(&x: &i32); // Ok
fn baz(&&x: &&i32); // Ok
fn quux(&&&x: &&&i32); // Syntax error
}
也就是说,只有 _、foo、&foo、&&foo、mut foo 这一档次的简单模式被允许,一旦嵌套引用层数加深(&&&x)就变成语法错误。这种"越界即报错"的边界毫无规律可言,用户必须靠试错才能摸清规则。
2. trait 默认实现与 impl 块实现之间的不一致
同样写一个元组解构模式,在 trait 的默认实现里是语法错误,在 impl 块里却合法:
trait T {
fn foo((x, y): (usize, usize)) { // Syntax error
}
}
impl T for S {
fn foo((x, y): (usize, usize)) { // Ok
}
}
同一套写法在两处语法规则不同,直接破坏了语言的一致性。
3. trait 方法声明与 extern 块之间的不一致
trait T {
fn foo(i32); // Ok
}
extern "C" {
fn foo(i32); // Syntax error
}
trait 里允许省略参数名的声明,到了 extern 块中同样的写法却直接报错。FFI 声明与 trait 声明遵循两套规则。
4. 增加 LL 风格解析器的语法分析复杂度
解析器在读到参数位置时,必须"猜测"当前解析的是模式还是类型。这种不确定性让 LL 风格的语法分析器需要额外的试探与回溯逻辑,属于纯粹的解析器负担。
5. 给源码分析工具带来额外负担
源代码分析器(例如 IntelliJ Rust)以及潜在的语言替代实现,都必须为这种"无参数名"的形态单独处理。任何需要遍历函数签名的工具,都要多写一份兼容逻辑。
6. 与未来命名参数/默认参数语法的潜在歧义
一旦 Rust 未来引入命名参数(named parameters)或默认参数(default parameters)语法,fn foo(i32) 这类形态可能产生新的解析歧义,给语法演进埋下隐患。
RFC 作者自己也承认:这些问题单独看都不算严重("None of these issues is significant"),但它们真实存在,且叠加起来足以构成改变语法的理由。
学习曲线与一致性层面的额外理由
除上述技术问题外,RFC 还从语言教学角度提出了两个论点。
其一,学习成本。 需要具备 C 语言背景才能读懂 fn foo(T); 表示"接收一个 T 类型的参数"。对动态语言背景的开发者,这个 T 更接近参数名而非类型,理解门槛明显更高。
其二,trait 与 impl 的复制粘贴陷阱。 一种常见的实现方式是直接从 trait 复制方法原型到 impl 块再补全逻辑。匿名参数存在时,复制出来的原型会导致语法错误:
trait T {
fn foo(i32); // 复制到 impl 块中就会变成语法错误
}
这迫使用户在复制后额外手动补名字,体验上是一种"无声的惩罚"。从更宏观的角度看,允许省略参数名并不能让 Rust 更具表现力,也没有带来可感知的 ergonomic 提升——补一个参数名是举手之劳,而真正省略名字的声明只占很小比例,语言却为此承担了全部的复杂度与不一致。
向后兼容性:破坏面有多大
形式上的破坏性变更与 _: 修复法
从语言规范角度看,移除匿名参数是正式的破坏性变更(breaking change)。但修复极其简单且可机械化——把省略的名字替换为下划线占位符即可(由 @nagisa 建议):
trait T {
fn foo(_: i32);
fn bar_with_default_impl(_: String, _: String) {
}
}
_: T 与匿名参数在语义上完全等价:参数被接收但不绑定到任何名字,调用方行为不变。
实际破坏面的量化证据
RFC 给出了当时的实测数据:对 crates.io 上 5560 个包进行扫描,其中 416 个包含至少一次匿名参数使用(该统计完整报告见 RFC 1685 的 PR 讨论)。虽然参数名被省略的情形"很少发生",但确实存在,且波及面足以让"直接删除"在实践层面显得过于激进。
RFC 还举了一个标准库中的真实例子——std::fmt::Display 当时的定义:
trait Display {
fn fmt(&self, &mut Formatter) -> Result;
}
连核心格式化 trait 都使用了匿名参数,可见该语法在标准库生态中也并非罕见,进一步印证了不能贸然硬删。
弃用策略设计:三种路径的取舍
RFC 的核心设计部分提出两种弃用策略,并附带一种自动化迁移手段。
硬弃用(Hard deprecation):默认产生编译器警告
直接让编译器对匿名参数发出警告(warning)。优点是向后兼容、信号明确,能强力推动生态清理;缺点是在实践上会迫使 crate 作者为了消灭警告而主动改代码,造成大规模的代码变动(code churn)。
软弃用(Soft deprecation):文档标注 + 默认关闭的 lint
将语法在文档中明确标注为"已废弃",同时配套三层工具支持:
- 一个 allow-by-default 的 rustc lint(默认不告警);
- 一个 clippy lint;
- 一个 IntelliJ Rust inspection(IDE 静态检查项)。
默认情况下编译器不产生任何警告,让迁移过程更加平缓渐进;代价是废弃信息的传播速度变慢,语法的淘汰效率不如硬弃用。
自动迁移(Automatic transition):工具一键改写
Rustfmt 和 IntelliJ Rust 可以自动把匿名参数改写为 _: T。但 RFC 特别提醒:手动补上真实参数名通常更好——因为真实名字能表达 impl 侧预期接收的参数语义,_ 只解决"编译通过"的问题,不解决"可读性"的问题。
权衡与备选方案
Drawbacks:弃用的代价
RFC 坦承了四点不利因素:
- 硬弃用会造成代码变动(code churn);
- 软弃用对"从使用中消除该语法"的效率可能不足;
- 在所有技术问题中,只有把废弃升级为硬错误才能真正解决,在此之前问题依旧存在;
- 该语法能否被彻底移除,本身就不确定。
Alternatives:被否定的其他路线
RFC 列举了三条备选路线:
- 维持现状(Status quo)——继续容忍匿名参数及其所有问题;
- 先制定精确的移除计划,再执行弃用——把"何时、如何移除"写清楚后再开始废弃,比先弃用再观望更主动;
- 用解析器层面的手段绕开问题——例如在解析器中采用无界前瞻(unbounded look ahead),从而同时支持匿名参数与完整模式语法,从根源上消解"模式 vs 类型"的歧义。
未决问题
RFC 收尾时留下的核心未决问题只有一个:究竟应该选择哪种弃用策略? 这个问题的答案后来由实际的语言演进(版本与 edition 机制)给出——软硬结合、以 edition 边界彻底切割,详见下文。
后续演进:从仓库其他 RFC 看匿名参数的归宿
在本仓库中,RFC 1685 并非孤立提案,其后续走向可以在其他 RFC 中得到印证。
RFC 2565:参数属性提案确认了语法的 edition 边界
RFC 2565:正式函数参数属性(2018-10-14)允许在正式函数参数位置使用属性。在该 RFC 的语法部分,作者两次明确写道:
- "In Rust 2015 edition, since anonymous parameters are allowed, you may also write:
trait Beta { fn bar(#[lemon] self, #[orange] u8); }"; - "In Rust 2015 edition, as
fns may have anonymous parameters, e.g.trait Foo { fn bar(u8); }"。
从这段表述可以推断:到 RFC 2565 撰写时(2018 年),匿名参数已经变成 Rust 2015 edition 专属的语法——即 Rust 2018 edition 中该语法已不可用。这与 RFC 1685 的弃用方向一致:弃用最终借助 edition 机制完成切割,新 edition 直接拒绝该语法,旧 edition 保持兼容。
RFC 2565 的互补性
RFC 2565 允许在匿名参数上附加属性(如 trait Foo { fn bar(#[attr] u8); }),这一设计恰好发生在弃用过渡期内,说明语言团队在"语法尚存"的阶段仍然为其补齐了工具链能力,属于过渡期的兼容性考量,而非对语法的重新肯定。
给开发者的实战建议
基于 RFC 1685 与后续演进,当前编写 Rust 代码时应遵循以下准则:
- 永远显式写出参数名,即使参数未被使用也写作
_: T或_name: T,杜绝fn foo(T);形态; - trait 声明与 impl 实现保持同一套写法,避免因匿名参数引发的复制粘贴语法错误;
- 若维护的代码库仍存在匿名参数(例如需要兼容 Rust 2015 的存量 crate),使用
_: T即可实现与删除语法完全等价的无痛迁移; - 关注仓库中的 RFC 1685 原文 及其在 RFC 2565 中反映出的 edition 切割结果,理解历史语法在 Rust 中的生命周期管理方式。
总结
RFC 1685 是一则"教科书式"的语言弃用提案:它用六个技术问题和一个学习曲线论点证明了匿名参数是历史的包袱,用量化的 crates.io 数据说明了不能立即删除,再用"硬弃用 / 软弃用 / 自动迁移"的组合拳为生态提供了渐进退出路径。最终,这一语法通过 edition 机制在 Rust 2018 中被移除,而 RFC 1685 所倡导的"先弃用、再移除"节奏,也为 Rust 之后处理其他历史语法提供了可复用的方法论范本。