首页
/ Rust 编译器错误 E0183 详解:为什么手动实现 `Fn`/`FnMut`/`FnOnce` 需要开启夜鹰特性

Rust 编译器错误 E0183 详解:为什么手动实现 `Fn`/`FnMut`/`FnOnce` 需要开启夜鹰特性

2026-09-06 17:27:45作者:温艾琴Wonderful

E0183 是 rustc 针对"手动实现 Fn* 系列 trait"发出的错误码,完整解释文档位于 E0183.md。本文基于该文档,并结合 rustc_hir_analysis 中的诊断实现,讲清楚这条错误的触发条件、报错信息、正确的 feature 开启方式,以及 Fn 系列 trait 类型参数为什么必须用圆括号记法。读完后你将能够复现并修复该错误,并理解 rustc 在类型参数降低(type lowering)阶段拦截此写法的源码路径。

一、错误概览:手动实现 Fn* trait 是实验性的

FnFnMutFnOnce 是 Rust 的"非箱型闭包"(unboxed closures)体系中的核心 trait,普通闭包表达式 || {} 由编译器自动为其生成实现。E0183 针对的场景是:用户在稳定版代码中直接为某个类型写出 impl Fn / impl FnMut / impl FnOnce

根据错误文档,这类手动实现是实验性(unstable)的,必须同时开启两个夜鹰特性:

#![feature(fn_traits, unboxed_closures)]

缺少 feature 门控时,rustc 报错:

error[E0183]: manual implementations of `FnOnce` are experimental
 --> src/main.rs:4:1
  |
4 | impl FnOnce<()> for MyClosure {  // error
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

help: add `#![feature(unboxed_closures)]` to the crate attributes to enable
  |
1 | #![feature(unboxed_closures)]

其中 help 提示由诊断结构体直接给出(后文源码分析会看到),主信息为 manual implementations of FnOnce are experimental,即"对 FnOnce 的手动实现是实验性的"。

二、完整复现:文档中的错误示例

原始文档给出的错误代码示例(在 rustc 错误码测试体系中标记为 compile_fail,E0183,即预期编译失败)如下:

struct MyClosure {
    foo: i32
}

impl FnOnce<()> for MyClosure {  // error
    type Output = ();
    extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
        println!("{}", self.foo);
    }
}

该示例包含三个关键要素,理解它们有助于理解错误成因:

  1. impl FnOnce<()> for MyClosure:用圆括号记法 FnOnce<()> 声明"无参、无返回"的调用签名,这是手动实现时必须使用的形式(见第四节)。
  2. type Output = ();FnOnce 的关联类型 Output,表示调用结果类型;空元组 () 表示调用不产生有意义返回值。
  3. extern "rust-call" fn call_once(...)FnOnce::call_once 必须使用 "rust-call" 这个特殊的 ABI。它不是平台 C ABI,而是专供闭包调用约定的内部 ABI——编译器通过它知道该函数是"可被当作闭包调用"的入口。Fn/FnMut 分别对应 call(接收 &self)与 call_mut(接收 &mut self),而 FnOnce 接收 self(按值消费)。

在没有 #![feature(fn_traits, unboxed_closures)] 的代码中编译上述示例,即触发 E0183。

三、修复方式:开启夜鹰特性后同一代码可通过

文档给出的"正确"示例与错误示例的唯一区别,就是头部加上了 feature 声明:

#![feature(fn_traits, unboxed_closures)]

struct MyClosure {
    foo: i32
}

impl FnOnce<()> for MyClosure {  // ok!
    type Output = ();
    extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
        println!("{}", self.foo);
    }
}

适用前提需要明确说明:

  • 只能在 nightly 工具链下编译fn_traitsunboxed_closures 均为不稳定 feature,stable 版 rustc 会直接拒绝(即使加了声明,也会报 "feature is only available in nightly" 类错误)。
  • 这两个 feature 跟踪的是同一事项,文档引用了跟踪 issue rust#29625unstable: manual implementations of Fn* traits)。这意味着手动实现闭包 trait 的语法与语义仍在演化,随时可能破坏性变更,因此不建议在生产代码中使用;它主要服务于编译器基础设施、元编程研究以及需要自定义"可调用对象"语义的高级场景。

四、类型参数格式约束:必须用圆括号记法

文档特别强调了一条格式规则:

The arguments must be a tuple representing the argument list. (Fn* trait 的类型参数必须是一个元组,用于表示参数列表。)

即写作 Fn<(i32, &str) -> i64>,而不是 Fn<i32, &str>。这一约束在 rustc 源码中有明确的强制点。从 hir_ty_lowering/errors.rs 可以看到:当类型参数中的 Fn 系列 trait 段没有使用 ParenSugar(圆括号记法)时,rustc 会发出 unboxed_closures 特性错误,措辞是:

"the precise format of Fn-family traits' type parameters is subject to change" (Fn 系列 trait 类型参数的精确格式仍可能变化。)

并且在不处于 trait 实现(impl)上下文时,编译器还会附带一条 MaybeIncorrect 适用性的建议:use parenthetical notation instead(改用圆括号记法),并给出转换后的类型文本;但在 impl 上下文中,源码注释明确说明不给出该建议,因为那样的脱糖(desugaring)会引入关联类型约束,不适合直接作为修复方案。这也解释了为什么 E0183 场景中你不能简单把 FnOnce<()> 换成尖括号形式来"绕开"——那是另一条特性错误,且格式本身仍在演化中。

五、源码级追踪:E0183 在 rustc 中的报错路径

结合本仓库源码,可以完整还原 E0183 的触发链路:

1. 错误码注册

compiler/rustc_error_codes/src/lib.rs 中的 error_codes! 宏集中登记了所有在用错误码,0183 出现在其中(第 114 行附近)。该文件头部注释说明:错误码解释文档统一存放在 error_codes/EXXXX.md,且格式遵循 RFC 1567(错误码长解释的规范化),并由 tidy 工具检查。这解释了 E0183.md 的文档结构为何固定为"错误描述 + 失败示例 + 解释 + 正确示例"。同一文件的注释区还保留了错误码演化痕迹:E0173 // manual implementations of unboxed closure traits are experimental 已被合并/废弃,其语义由 E0183 承接——即历史上"手动实现非箱型闭包 trait 是实验性的"这一报错,现在统一以 E0183 呈现。

2. 诊断定义

诊断结构体定义在 compiler/rustc_hir_analysis/src/diagnostics.rs

#[derive(Diagnostic)]
#[diag("manual implementations of `{$trait_name}` are experimental", code = E0183)]
#[help("add `#![feature(unboxed_closures)]` to the crate attributes to enable")]
pub(crate) struct ManualImplementation {
    #[primary_span]
    #[label("manual implementations of `{$trait_name}` are experimental")]
    pub span: Span,
    pub trait_name: String,
}

要点:

  • code = E0183 把该诊断绑定到错误码 E0183,与错误码文档一一对应;
  • {$trait_name} 是动态填充的,因此报错信息会随你手动实现的具体 trait 显示为 FnFnMutFnOnce
  • #[help(...)] 固定给出开启 #![feature(unboxed_closures)] 的建议。注意编译器只提示 unboxed_closures,而文档要求同时写 #![feature(fn_traits, unboxed_closures)],以错误文档的完整声明为准。

3. 触发点

实际发射位置在 compiler/rustc_hir_analysis/src/hir_ty_lowering/errors.rs:当 rustc 在类型参数降低阶段判定当前 Fn 系列 trait 段处于 impl 上下文(变量 is_impl 为真)时,即执行 self.dcx().emit_err(ManualImplementation { span, trait_name })。从源码结构看,判定逻辑位于同一函数的"括号/尖括号记法检查"之后——也就是说,rustc 先处理记法格式问题(第四节),再对 impl 上下文单独报 E0183。这也意味着该检查发生在 HIR 类型降低阶段,早于完整的类型检查与借用检查,属于"语法-类型边界"层面的 feature 门控拦截。

六、实战要点小结

  • 触发条件:为任意类型写出 impl Fn/FnMut/FnOnce 的手动实现,且未开启对应 nighty feature。
  • 修复方式:在 crate 根加 #![feature(fn_traits, unboxed_closures)],并仅在 nightly 下编译;这是唯一让 E0183 消失的合法途径,尖括号记法等变通方式不成立。
  • 书写规范:类型参数用元组圆括号记法(Fn<(A, B) -> C>),实现方法使用 extern "rust-call" ABI 与 call/call_mut/call_once 约定。
  • 风险提示:由于跟踪 issue rust#29625 尚未稳定,相关语法(尤其是 Fn 系列 trait 类型参数的精确格式)存在变更可能,生产代码应优先使用闭包表达式或 dyn trait 对象,而非手动实现闭包 trait。

相关延伸阅读:错误码总表见 rustc_error_codes 的 lib.rsFn* 相关的诊断定义见 rustc_hir_analysis/src/diagnostics.rs

登录后查看全文
热门项目推荐
相关项目推荐