跳转到内容

Rust 错误 E0592:duplicate definitions with name x(方法重复定义)

E0592 的意思是:同一个类型在 impl 块里有两个同名的方法、关联函数或常量。一个类型可以有任意多个 impl 块,但它们共用同一份名字列表,所以每个名字只能用一次。这个错误通常来自复制粘贴、合并代码,或者另一个文件里的第二个 impl 块。修法是只留一个,或者给另一个起个新名字。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0592]: duplicate definitions with name `reset`
--> src/main.rs:10:5
|
10 | fn reset(&mut self) {
| ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `reset`
...
16 | fn reset(&mut self) { // ❌ `reset` is already defined above
| ------------------- other definition for `reset`

第一行的意思是“名为 reset 的定义重复了”。

产生这个错误的程序是这样的:

struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn reset(&mut self) {
self.count = 0;
}
}
impl Counter {
fn reset(&mut self) { // ❌ `reset` is already defined above
self.count = 0;
}
}
fn main() {
let mut c = Counter::new();
c.reset();
println!("{}", c.count);
}

rustc 还会在调用 c.reset() 的地方报 E0034:“multiple applicable items in scope”(作用域里有多个适用的项),因为它分不清你指的是两个 reset 中的哪一个。这是连带出现的错误:修好 E0592,E0034 也会跟着消失。

第一行给出了被定义两次的名字(reset)。^^^ 标出一处定义,--- 那一行标出“other definition”(另一处定义)。rustc 会显示两处的行号,你可以直接跳过去。两处离得很远时,中间的 ... 只是表示“省略了一些行”。

两个 reset 分别写在不同的 impl Counter 块里,甚至在 crate 的不同文件里,都没有关系。对 Rust 来说,它们都是同一个类型的方法,所以 Counter::reset 必须只指向一个函数。关联常量也遵循同样的规则:给同一个类型定义两个 const MAX: u32,同样是 E0592。

就像手机通讯录里有两个都叫“妈妈”的联系人。你说“打给妈妈”,手机该拨哪个号码呢?

把方法分到好几个 impl 块里是允许的,而且往往很有用:一个块放构造函数,一个块放 getter,一个块放在 #[cfg(test)] 后面。参见用方法添加行为。但当你调用 c.reset() 或 Counter::reset(&mut c) 时,Rust 必须找到一个函数。如果有两个,到底运行哪一个,就要取决于一些光看调用看不出来的细节。

所以 Rust 在定义的地方就拒绝,而不只是在调用的地方。这能抓住一个真实的 bug:复制粘贴出来的方法,你可能只在其中一份里修了问题,另一份却还在运行。Rust 确保需要修的版本永远只有一个。

修复 1:删掉重复的那个,只保留一个定义

Section titled “修复 1:删掉重复的那个,只保留一个定义”

大多数时候都选这个方法。如果两份做的是同一件事(复制粘贴、没合并干净、方法挪走了却没删掉旧的),就删掉一个。删之前比较一下两个函数体:如果其中一个有另一个没有的修复,就留下那一个。把几个 impl 块合并成一个,以后也更容易发现重复。

struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn increment(&mut self) {
self.count += 1;
}
fn reset(&mut self) { // ✅ `Counter` 只有一个 `reset`
self.count = 0;
}
}
fn main() {
let mut c = Counter::new();
c.increment();
c.increment();
println!("重置前:{}", c.count);
c.reset();
println!("重置后:{}", c.count);
}

输出:

重置前:2
重置后:0
▶ 动手试一试

如果找不到第二处定义,就在整个项目里搜索:grep -rn "fn reset" src/。impl Counter 块可以放在 crate 的任何模块里。

当两个定义做的是不同的事时,选这个方法。Rust 没有重载:你不能有两个同名但参数不同的方法,比如 reset() 和 reset(start: u32)。那同样是 E0592。给每个方法起一个能说明用途的名字:

struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn reset(&mut self) {
self.count = 0;
}
}
impl Counter {
fn reset_to(&mut self, start: u32) { // ✅ 不同的行为,用不同的名字
self.count = start;
}
}
fn main() {
let mut c = Counter::new();
c.reset_to(10);
println!("调用 reset_to(10) 之后:{}", c.count);
c.reset();
println!("调用 reset() 之后:{}", c.count);
}

输出:

调用 reset_to(10) 之后:10
调用 reset() 之后:0
▶ 动手试一试

Rust 里常见的命名方式能帮上忙:new 和 with_capacity,from_str 和 from_bytes,get 和 get_mut。如果你真的想让一个名字接受多种参数类型,就用泛型参数或 trait,比如 fn reset_to(&mut self, start: impl Into<u32>)。参见泛型。

修复 3:避免泛型 impl 块互相重叠

Section titled “修复 3:避免泛型 impl 块互相重叠”

当两个定义中有一个在 impl<T> Wrapper<T> 这样的泛型块里时,选这个方法。这个块也覆盖了 Wrapper<i32>,所以再写一个带同名方法的 impl Wrapper<i32>,就重复了:

error[E0592]: duplicate definitions with name `describe`
--> src/main.rs:4:5
|
4 | fn describe(&self) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `describe`
...
10 | fn describe(&self) -> String {
| ---------------------------- other definition for `describe`

背后的程序:

struct Wrapper<T>(T);
impl<T> Wrapper<T> {
fn describe(&self) -> String {
String::from("something")
}
}
impl Wrapper<i32> {
fn describe(&self) -> String {
format!("the number {}", self.0)
}
}
fn main() {
println!("{}", Wrapper(5).describe());
}

Rust 不允许具体类型的 impl 覆盖泛型的 impl(这个功能叫特化(specialization),还没有稳定)。有两个好办法:只为具体类型写 impl 块,这样它们就不会重叠;或者把行为放进一个 trait,每个类型实现一次:

struct Wrapper<T>(T);
// 方案 A:为具体类型写的 impl 块永远不会重叠
impl Wrapper<i32> {
fn describe(&self) -> String {
format!("数字 {}", self.0)
}
}
impl Wrapper<&str> {
fn describe(&self) -> String {
format!("文本“{}”", self.0)
}
}
// 方案 B:一个 trait,每个类型实现一次
trait Label {
fn label(&self) -> String;
}
impl Label for i32 {
fn label(&self) -> String {
format!("整数 {}", self)
}
}
impl Label for bool {
fn label(&self) -> String {
format!("开关 {}", self)
}
}
impl<T: Label> Wrapper<T> {
fn label(&self) -> String {
self.0.label() // 一个泛型方法,每个类型各自的部分放在 trait 里
}
}
fn main() {
println!("{}", Wrapper(5).describe());
println!("{}", Wrapper("hi").describe());
println!("{}", Wrapper(7).label());
println!("{}", Wrapper(true).label());
}

输出:

数字 5
文本“hi”
整数 7
开关 true
▶ 动手试一试

从长远来看,trait 版本通常是更好的设计:要支持一个新类型,只需再写一个 impl Label for ...。参见 trait。

第二个 impl 块里的测试辅助函数。 你在 #[cfg(test)] mod tests 里加了 impl Counter { fn new() -> Self { ... } } 来构造测试数据,可 Counter::new 已经存在了。测试里的这个块仍然是 impl Counter。修复:给它起个名字,比如 fn for_test(),或者直接用真正的 new。参见测试。

合并或重构之后。 两个分支都加了一个 fn len(&self),或者一个方法挪到了新文件,旧的副本却还留着。rustc 会显示两处的行号,保留更好的那个就行。

同名的 trait 方法没问题。 impl Counter { fn reset(&mut self) } 和 impl Resettable for Counter { fn reset(&mut self) } 同时存在不会报 E0592:一个是类型自己的方法,另一个属于 trait,c.reset() 调用的是类型自己的那个。两个trait 有同名方法也能编译,但用 c.reset() 调用时会得到 E0034;改成 Resettable::reset(&mut c) 就能解决。

小测验

1. 一个类型有两个 impl 块。两个块里能都有一个叫 reset 的方法吗?

2. 你想要 area() 和 area(scale: f64)。在 Rust 里该怎么做?

3. impl<T> Wrapper<T> 和 impl Wrapper<i32> 都定义了 describe,为什么会冲突?

得分: 0 / 3