跳转到内容

Rust 错误 E0428:the name is defined multiple times(名称重复定义)

E0428 的意思是:你在同一个地方定义了两个同名的东西:两个叫 area 的函数、两个叫 User 的结构体,或者一个枚举和一个结构体都叫 Status。这通常发生在你想像 Java、C# 或 C++ 那样重载(overload)函数的时候。Rust 没有函数重载,但它有很好的替代办法,本页会一一介绍。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0428]: the name `area` is defined multiple times
--> src/main.rs:5:1
|
1 | fn area(width: f64, height: f64) -> f64 {
| --------------------------------------- previous definition of the value `area` here
...
5 | fn area(radius: f64) -> f64 { // ❌ a second `area`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `area` redefined here
|
= note: `area` must be defined only once in the value namespace of this module

第一行的意思是“名字 area 被定义了多次”。

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

fn area(width: f64, height: f64) -> f64 {
width * height
}
fn area(radius: f64) -> f64 { // ❌ a second `area`
3.14159 * radius * radius
}
fn main() {
println!("{}", area(2.0, 3.0));
println!("{}", area(1.0));
}

对这个程序,rustc 还会在 area(1.0) 上再报一个错误:E0061,“this function takes 2 arguments but 1 argument was supplied”(这个函数需要 2 个参数,却只给了 1 个)。这只是连带效应。Rust 保留了第一个 area,忽略了第二个,所以只传一个参数的调用就对不上了。修好 E0428,E0061 也会跟着消失。

rustc 指出了两个位置:

  1. --- 那一行(第 1 行):“previous definition of the value area here”(值 area 之前在这里定义过)。这是第一个 area,也就是 Rust 保留的那个。
  2. ^^^ 那一行(第 5 行):“area redefined here”(area 在这里被重新定义)。这是第二个,被拒绝的就是它。
  3. note: 说 area 在“这个模块的值命名空间(value namespace)”里只能定义一次。每个模块就像一个抽屉,同一个抽屉里每个名字都必须是唯一的。

如果是两个同名结构体,note 里写的会是“type namespace”(类型命名空间)。Rust 把类型(结构体、枚举、trait、模块)和值(函数、常量、静态变量)放在两份不同的名单里。所以 mod shapes 和 fn shapes 可以并存,但两个 fn area 或两个 struct User 不行。

想象一个文件夹里有两个都叫 report.txt 的文件。电脑会拒绝第二个,不是因为它有什么问题,而是因为再说 report.txt,电脑就分不清你指的是哪个了。

在 Rust 里,模块里的一个名字只指向一个项。写 area(...) 时,Rust 只靠名字找到这个函数,而不看参数的个数或类型。这样代码读起来很简单:只有一个 area 要找,而且新加一个函数,永远不会悄悄改变旧的调用用的是哪个版本。

支持重载的语言会根据参数来挑选版本,一旦涉及数字之间的类型转换或者默认参数,规则就变得很绕。Rust 选择不这样做。当你想要“同一个名字处理不同的输入”时,就用 trait 或泛型,选择哪个版本都清楚地体现在类型上。函数的更多内容见函数。

修复 1:给每个函数起自己的名字

Section titled “修复 1:给每个函数起自己的名字”

大多数时候都选这个方法。接收不同输入的两个函数,做的通常是不同的事,名字可以把这一点说清楚。如果其中一个是你忘了删掉的旧副本,那就直接删掉它:

fn rectangle_area(width: f64, height: f64) -> f64 {
width * height
}
fn circle_area(radius: f64) -> f64 {
3.14159 * radius * radius
}
fn main() {
println!("{}", rectangle_area(2.0, 3.0));
println!("{}", circle_area(1.0));
}

输出:

6
3.14159
▶ 动手试一试

标准库也是这么做的:String::new()、String::with_capacity(10) 和 String::from("hi") 是创建 String 的三种方式,用了三个不同的名字。

当这个短名字在各自的场景里确实就是最好的名字时,选这个方法。比如每种图形都叫 area,或者数据库代码里有一个 User,API 代码里又有另一个 User。每个模块都有自己的一套名字,冲突就没了。调用时在前面写上模块名:

mod rectangle {
pub fn area(width: f64, height: f64) -> f64 {
width * height
}
}
mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
fn main() {
println!("{}", rectangle::area(2.0, 3.0));
println!("{}", circle::area(1.0));
}

输出:

6
3.14159
▶ 动手试一试

别忘了 pub,否则在 main 里调用会得到 E0603:“function is private”(函数是私有的)。参见模块。

当你想要一个调用能用于多种类型时,选这个方法,这通常也正是人们想用重载的原因。为每种输入定义一个类型,再让它们实现同一个 trait。这样每种图形都有自己的 area,Rust 会根据值的类型挑出正确的那个:

trait Shape {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Circle {
radius: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.14159 * self.radius * self.radius
}
}
fn print_area<T: Shape>(shape: &T) {
println!("面积:{}", shape.area());
}
fn main() {
print_area(&Rectangle { width: 2.0, height: 3.0 });
print_area(&Circle { radius: 1.0 });
}

输出:

面积:6
面积:3.14159
▶ 动手试一试

这比修复 1 的代码多,所以当你有好几种图形,或者好几个遵循同一模式的函数(area、perimeter、draw)时才划算。泛型函数 print_area<T: Shape> 对你以后新加的任何图形都适用。参见把 trait 用作函数参数和泛型。

如果你想要的是“同一个函数,多传或少传一个参数”,还有一个更轻巧的办法:接收一个 Option,比如 fn greet(name: &str, title: Option<&str>),调用时传 None 或 Some("Dr.")。

修复 4:用 use ... as ... 给导入改名

Section titled “修复 4:用 use ... as ... 给导入改名”

当冲突发生在你自己的项和一行 use 之间时,选这个方法。严格来说,这是一个兄弟错误 E0255,第一行的说法是一样的:

error[E0255]: the name `Error` is defined multiple times
--> src/main.rs:3:1
|
1 | use std::io::Error;
| -------------- previous import of the type `Error` here
2 |
3 | enum Error {
| ^^^^^^^^^^ `Error` redefined here
|
= note: `Error` must be defined only once in the type namespace of this module
help: you can use `as` to change the binding name of the import
|
1 | use std::io::Error as OtherError;
| +++++++++++++

help: 建议用 as 给导入换一个绑定名,也就是起一个本地的别名。比起 rustc 给的占位名 OtherError,最好选一个能说明来历的名字,比如 IoError:

use std::io::Error as IoError;
use std::io::ErrorKind;
#[derive(Debug)]
enum Error {
NotFound(String),
Invalid(u32),
}
fn main() {
let mine = Error::NotFound(String::from("config.toml"));
let other = Error::Invalid(42);
let disk = IoError::new(ErrorKind::Other, "磁盘已满");
println!("{:?}", mine);
println!("{:?}", other);
println!("{}", disk);
}

输出:

NotFound("config.toml")
Invalid(42)
磁盘已满
▶ 动手试一试

两行 use 互相冲突,比如 use std::fmt::Result; 和 use std::io::Result;,会得到第三个兄弟错误 E0252(`Result` reimported here,Result 在这里被重复导入)。修法一样:写成 use std::io::Result as IoResult;,或者导入模块(use std::io;)再写 io::Result。把你自己的枚举改名(AppError)也可以。

在 impl 块里重载方法。 在 impl Timer 里写两个 fn new,会得到另一个错误代码 E0592:duplicate definitions with name `new` (名为 new 的定义重复了)。原因一样,修法也一样:按它们的用途命名,比如 Timer::new() 和 Timer::with_seconds(30)。参见结构体。

重构时复制粘贴。 你复制了一个函数打算改一改,或者从别的文件粘贴了一个 struct User,却忘了删掉旧的那个。测试里也很常见:同一个 mod tests 里有两个 #[test] fn it_works(),就会得到 E0428。修复:删掉旧副本,或者给测试改个名。

重复的枚举变体。 enum Color { Red, Red } 也会报 E0428,指向第二个 Red,note 里写的是“type namespace of this enum”(这个枚举的类型命名空间)。修复:删掉或改名重复的变体。参见枚举。

  • E0061:参数个数不对。你想重载函数时,它常常紧跟在 E0428 后面出现。
  • E0425:正好相反的问题:你用到的名字一次都没有定义。
  • E0433:像 circle::area 这样的路径,第一部分(模块)找不到。
  • E0034:两个 trait 给同一个类型提供了同名方法,你需要说明用的是哪一个。
  • E0255(use 和本地项冲突)、E0252(两行 use 冲突)、E0592(impl 里方法重复):上面介绍过的兄弟错误。
  • 所有编译错误:错误页面的完整列表。

小测验

1. 你在一个文件里写了 fn total(a: u32) 和 fn total(a: u32, b: u32)。会发生什么?

2. mod report 和 fn report() 能放在同一个模块里吗?

3. 你的文件里有 use std::fmt::Result;,你又定义了 enum Result。干净的修法是什么?

得分: 0 / 3