콘텐츠로 이동

Rust 에러 E0428: the name is defined multiple times (이름 중복 정의)

E0428은 같은 곳에 이름이 같은 것을 두 개 정의했다는 뜻이에요. area라는 함수 두 개, User라는 구조체 두 개, 또는 둘 다 Status라는 이름의 열거형과 구조체처럼요. Java, C#, C++에서처럼 함수를 오버로딩(overloading)하려고 할 때 자주 생겨요. 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 { // ❌ 두 번째 `area`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `area` redefined here
|
= note: `area` must be defined only once in the value namespace of this module

“the name area is defined multiple times”는 “area라는 이름이 여러 번 정의됐다”는 뜻이에요. 각 라벨과 note는 아래에서 풀어 볼게요.

이 에러를 내는 프로그램은 다음과 같아요.

fn area(width: f64, height: f64) -> f64 {
width * height
}
fn area(radius: f64) -> f64 { // ❌ 두 번째 `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가 “in the value namespace of this module”, 즉 이 모듈의 값 네임스페이스에서 한 번만 정의돼야 한다고 말해요. 모듈은 서랍 하나와 같아서, 서랍 안의 이름은 모두 달라야 해요.

구조체 두 개라면 note에 “type namespace”(타입 네임스페이스)라고 나와요. Rust는 타입(구조체, 열거형, 트레이트, 모듈)과 값(함수, 상수, static)을 서로 다른 두 목록에 보관해요. 그래서 mod shapes와 fn shapes는 나란히 있을 수 있지만, fn area 두 개나 struct User 두 개는 안 돼요.

한 폴더에 report.txt라는 파일이 두 개 있는 것을 떠올려 보세요. 컴퓨터는 두 번째 파일을 거부해요. 그 파일에 문제가 있어서가 아니라, report.txt라는 이름으로는 어느 파일을 말하는지 더는 알 수 없기 때문이에요.

Rust에서 모듈 안의 이름 하나는 정확히 하나의 항목을 가리켜요. area(...)라고 쓰면 Rust는 인자의 개수나 타입이 아니라 이름만으로 함수를 찾아요. 그래서 코드를 읽기가 쉬워요. 찾아볼 area는 하나뿐이고, 새 함수를 추가해도 예전 호출이 쓰는 함수가 몰래 바뀌는 일이 절대 없어요.

오버로딩이 있는 언어는 인자를 보고 버전을 고르는데, 숫자가 서로 변환되거나 기본값이 끼어들면 규칙이 까다로워져요. Rust는 그렇게 하지 않기로 했어요. “입력은 달라도 이름은 같게” 하고 싶을 때는 트레이트나 제네릭을 써요. 그러면 무엇이 선택되는지 타입에 드러나요. 함수에 대해서는 함수에서 더 볼 수 있어요.

대부분은 이 방법을 고르세요. 입력이 다른 두 함수는 보통 하는 일도 달라서, 이름으로 그 차이를 말할 수 있어요. 둘 중 하나가 지우는 걸 깜빡한 예전 복사본이라면 그냥 지우세요.

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”(함수가 비공개다)이 나요. 모듈 정의하기를 참고하세요.

방법 3: 오버로딩 대신 트레이트 쓰기

섹션 제목: “방법 3: 오버로딩 대신 트레이트 쓰기”

여러 타입에 동작하는 호출 하나를 원할 때 이 방법을 고르세요. 오버로딩이 보통 노리는 게 바로 이거예요. 입력의 종류마다 타입을 만들고, 공통 트레이트를 구현하세요. 그러면 도형마다 자기 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>는 나중에 추가하는 어떤 도형에도 동작해요. 트레이트를 함수 매개변수로 쓰기와 제네릭을 참고하세요.

인자가 하나 더 있거나 없는 같은 함수를 원한다면 더 가벼운 방법이 있어요. fn greet(name: &str, title: Option<&str>)처럼 Option을 받고, None이나 Some("Dr.")로 호출하세요.

방법 4: use ... as ...로 가져온 이름 바꾸기

섹션 제목: “방법 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;
| +++++++++++++

라벨은 “타입 Error를 여기서 이미 가져왔다”와 “Error가 여기서 다시 정의됐다”는 뜻이고, help는 “as로 가져온 이름을 바꿀 수 있다”는 뜻이에요.

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 std::fmt::Result;와 use std::io::Result;처럼 use 줄 두 개가 충돌하면 세 번째 형제인 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: 두 트레이트가 한 타입에 같은 이름의 메서드를 줘서, 어느 쪽인지 말해 줘야 해요.
  • 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