콘텐츠로 이동

Rust 에러 E0790: cannot call associated function on trait (타입 지정 필요)

E0790은 타입만 줄 수 있는 것을 트레이트에게 달라고 했다는 뜻이에요. Default::default()만 쓰는 건 “기본값 주세요”라고 하면서 무엇의 기본값인지 말하지 않는 것과 같아요. 흔한 에러이고, 해결책은 보통 앞에 타입 이름을 쓰는 거예요. Default::default() 대신 Settings::default()처럼요. 이 페이지에서는 고치는 방법을 먼저 보여 주고, 그다음에 트레이트와 타입의 차이를 설명할게요. 그러면 이 에러가 더는 당황스럽지 않을 거예요. 이 페이지는 Rust 컴파일러 에러 해설의 일부예요.

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:8:20
|
8 | let settings = Default::default(); // ❌ 어떤 타입의 기본값일까요?
| ^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use a fully-qualified path to a specific available implementation
|
8 | let settings = </* self type */ as Default>::default(); // ❌ 어떤 타입의 기본값일까요?
| +++++++++++++++++++ +

“해당하는 impl 타입을 지정하지 않고는 트레이트의 연관 함수를 호출할 수 없다”는 뜻이에요. help는 “사용할 수 있는 특정 구현을 완전한 경로로 써 보라”고 제안해요.

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

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
fn main() {
let settings = Default::default(); // ❌ 어떤 타입의 기본값일까요?
println!("{:?}", settings);
}

내가 만든 트레이트의 함수를 Shape::new()처럼 트레이트 이름으로 호출해도 같은 에러가 나요. 이번에는 rustc가 Shape를 구현한 타입을 두 개 찾았기 때문에, 둘 다 선택지로 보여 줘요.

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:33:17
|
2 | fn new() -> Self;
| ----------------- `Shape::new` defined here
...
33 | let shape = Shape::new();
| ^^^^^^^^^^^^ cannot call associated function of trait
|
help: use a fully-qualified path to one of the available implementations
|
33 | let shape = <Circle as Shape>::new();
| ++++++++++ +
33 | let shape = <Square as Shape>::new();
| ++++++++++ +

여기서 help는 “사용할 수 있는 구현 중 하나를 완전한 경로로 써 보라”는 뜻이에요.

Plugin::ID 같은 트레이트 상수라면 첫 줄이 조금 달라요. cannot refer to the associated constant on trait without specifying the corresponding `impl` type(해당하는 impl 타입을 지정하지 않고는 트레이트의 연관 상수를 참조할 수 없음)이에요. 문제도 같고, 해결 방법도 같아요.

rustc가 표시한 부분을 보세요.

  1. Default::default() 아래의 ^^^^: “cannot call associated function of trait”(트레이트의 연관 함수를 호출할 수 없음). default는 Default 트레이트에 속한 함수인데, 그걸 트레이트 이름으로 호출했어요.
  2. help는 </* self type */ as Default>::default()를 제안해요. /* self type */ 부분은 내가 채워야 할 빈칸이에요. rustc는 “어떤 타입의 default를 말하는 거예요?“라고 묻고 있어요.
  3. Shape 예제에서는 rustc가 찾은 타입(Circle, Square)으로 빈칸을 직접 채워 줘요. 트레이트를 구현한 타입이 하나뿐이면 그 타입만 제안해요.

핵심은 이거예요. 트레이트는 약속이에요. “Default를 구현한 타입이라면 자기 자신의 시작 값을 만들 수 있다”는 약속이요. 타입은 그 약속을 지키는 쪽이에요. Settings, String, u32, Vec<T>는 각자 자기 방식으로 약속을 지켜요. Default::default()는 약속 자체에게 값을 달라고 하는 건데, 약속은 아무것도 만들 수 없어요. 만들 수 있는 건 타입뿐이에요.

“포장 가능” 스티커를 떠올려 보세요. 많은 식당이 창문에 붙여 둬요. 하지만 스티커에게 저녁을 주문할 수는 없어요. 식당을 골라야 하죠. Settings::default()가 식당을 고르는 거예요.

대부분의 트레이트 메서드는 circle.area()처럼 self를 받아요. 이런 메서드는 E0790을 절대 내지 않아요. 값이 Rust에게 타입을 알려 주거든요. circle은 Circle이니까 Circle의 area를 호출해요. 이 내용은 트레이트에서 더 볼 수 있어요.

어떤 트레이트 함수에는 self가 없어요. new()나 default() 같은 생성자, From::from 같은 변환, ID 같은 트레이트 상수가 그래요. 들여다볼 값이 없으니, Rust가 타입을 알아낼 수 있는 곳은 두 군데뿐이에요. 앞에 쓴 이름(Settings::)이나, 결과가 가져야 할 타입(let s: Settings = ...)이에요. 둘 다 없으면 Rust는 추측할 수 없어요. Default를 구현한 타입은 아주 많고, 아무거나 하나 고르면 프로그램이 하는 일이 몰래 바뀌어 버릴 거예요.

Java나 C#의 인터페이스와 똑같아요. 인터페이스에 new를 호출할 수는 없고, 그걸 구현한 클래스가 필요하죠. Rust는 그걸 컴파일할 때, 고칠 곳까지 정확히 짚어서 알려 줄 뿐이에요.

거의 항상 이 방법을 고르세요. 트레이트 이름을 타입 이름으로 바꾸면 돼요. 가장 짧은 해결책이고, 읽기도 좋아요. “기본 Settings”, “새 Circle“처럼요.

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
trait Shape {
fn new() -> Self;
fn name() -> String;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn new() -> Self {
Circle { radius: 1.0 }
}
fn name() -> String {
String::from("원")
}
}
fn main() {
let settings = Settings::default(); // Default::default() 대신
let shape = Circle::new(); // Shape::new() 대신
println!("{:?}", settings);
println!("{}의 반지름: {}", Circle::name(), shape.radius);
}

실행 결과:

Settings { volume: 0, dark_mode: false }
원의 반지름: 1
▶ 직접 실행해 보기

new가 impl Circle 블록이 아니라 트레이트에 정의되어 있어도 Circle::new()는 잘 동작해요. Rust는 Circle에서 new를 찾다가 impl Shape for Circle을 통해 찾아내고, 그걸 호출해요. 단, 트레이트가 스코프 안에 있어야 해요(같은 파일에 정의되어 있거나, use로 가져왔거나요).

Default::default() 스타일이 마음에 들거나, 타입 이름이 길 때 이 방법을 고르세요. 변수에 타입을 쓰면, Rust가 그걸 보고 어떤 default를 호출할지 알아내요.

#[derive(Debug, Default)]
struct Settings {
volume: u8,
dark_mode: bool,
}
fn main() {
let settings: Settings = Default::default();
let small: u8 = 42;
let big: u32 = From::from(small);
println!("{:?}", settings);
println!("{}", big);
}

실행 결과:

Settings { volume: 0, dark_mode: false }
42
▶ 직접 실행해 보기

이 방법은 default(), new(), from()처럼 함수가 타입 자기 자신(Self)을 반환할 때만 통해요. fn name() -> String에는 안 통해요. 답이 String이라는 걸 알아도, 어떤 도형을 말하는지는 알 수 없으니까요. 그런 경우에는 방법 1이나 방법 3을 쓰세요. 타입 명시에 대해서는 데이터 타입에서 더 볼 수 있어요.

방법 3: 완전 정규화 문법 <Type as Trait>::function()

섹션 제목: “방법 3: 완전 정규화 문법 <Type as Trait>::function()”

rustc가 제안하는 “전부 다 말하기” 형태예요. <Circle as Shape>::new()는 “Circle이 Shape를 구현해서 갖게 된 new“라고 읽으면 돼요. 언제나 통하지만 Circle::new()가 더 읽기 쉬우니, 짧은 형태가 모호할 때만 꼭 필요해요.

  • 타입이 같은 이름의 함수를 가진 트레이트 두 개를 구현했을 때, 또는
  • 타입에 같은 이름의 자기 함수가 있는데, 트레이트 쪽 버전을 쓰고 싶을 때예요.

여기서 Sale은 CSV 행으로도, 출력용 보고서로도 쓸 수 있고, 두 트레이트 모두 header()가 있어요. Sale::header()라고 쓰면 E0034(영어) “multiple applicable items in scope”(스코프에 적용 가능한 항목이 여러 개)가 나기 때문에, 어느 쪽인지 말해 줘야 해요.

trait Csv {
fn header() -> String;
}
trait Report {
fn header() -> String;
}
struct Sale;
impl Csv for Sale {
fn header() -> String { String::from("date,amount") }
}
impl Report for Sale {
fn header() -> String { String::from("월간 매출") }
}
fn main() {
println!("{}", <Sale as Csv>::header());
println!("{}", <Sale as Report>::header());
}

실행 결과:

date,amount
월간 매출
▶ 직접 실행해 보기

고급 트레이트 강좌에 &self를 받는 메서드까지 포함한 예제가 하나 더 있어요.

방법 4: 제네릭 코드에서는 T::와 터보피시 쓰기

섹션 제목: “방법 4: 제네릭 코드에서는 T::와 터보피시 쓰기”

제네릭 함수를 작성할 때 이 방법을 고르세요. fn register<T: Plugin>() 안에서 타입은 T이니, Plugin::name()이 아니라 T::name()과 T::ID를 호출하세요. 함수를 호출할 때는 터보피시(turbofish) ::<Type>로 타입을 넘겨 주세요. Rust가 타입을 알아낼 인자가 없거든요.

trait Plugin {
const ID: u32;
fn name() -> String;
}
struct Logger;
struct Backup;
impl Plugin for Logger {
const ID: u32 = 7;
fn name() -> String { String::from("logger") }
}
impl Plugin for Backup {
const ID: u32 = 12;
fn name() -> String { String::from("backup") }
}
fn register<T: Plugin>() -> String {
format!("플러그인 #{}: {}", T::ID, T::name())
}
fn main() {
println!("{}", register::<Logger>());
println!("{}", register::<Backup>());
}

실행 결과:

플러그인 #7: logger
플러그인 #12: backup
▶ 직접 실행해 보기

터보피시를 빼고 register()라고 쓰면 E0283이 나요. “cannot infer type of the type parameter T”(타입 매개변수 T의 타입을 추론할 수 없음)라는 에러예요. 여기서는 T도 타입이니까, T::는 방법 1의 Circle::과 똑같이 동작해요. 타입 매개변수에 대해서는 제네릭에서 더 볼 수 있어요.

단서가 하나도 없는 Default::default(). let config = Default::default();는 실패하지만, 같은 호출이라도 타입이 이미 알려진 곳이라면 잘 동작해요. 구조체 필드, 함수 인자, “나머지 채우기” 문법 같은 곳이요.

let settings = Settings { volume: 8, ..Default::default() }; // OK: Rust는 이게 Settings라는 걸 알아요

실패하는 줄은 Settings::default()나 let config: Settings = Default::default();로 고치세요. 자주 쓰는 트레이트를 참고하세요.

제네릭 함수 안에서. T 대신 트레이트 이름을 쓴 경우예요.

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:9:29
|
2 | fn name() -> String;
| -------------------- `Shape::name` defined here
...
9 | format!("This is a {}", Shape::name())
| ^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
9 | format!("This is a {}", <Circle as Shape>::name())
| ++++++++++ +

help는 “사용할 수 있는 유일한 구현을 완전한 경로로 써 보라”는 뜻이에요. 조심하세요. rustc가 <Circle as Shape>를 제안하는 건 지금까지 도형이 Circle 하나뿐이기 때문이에요. 컴파일은 되지만, 그러면 함수는 T가 무엇이든 항상 “circle”이라고 말하게 돼요. 올바른 해결책은 방법 4처럼 T::name()이에요.

트레이트 상수. println!("{}", Plugin::ID);는 약속에게 숫자를 달라고 하는 건데, 숫자는 플러그인마다 하나씩 갖고 있어요. 해결: Logger::ID, 제네릭 코드에서는 T::ID를 쓰세요.

변환할 대상이 없는 From::from(x). let total = From::from(42u8);은 E0790을 내요. u8로부터 만들 수 있는 타입이 많기 때문이에요. 해결: u32::from(42u8)이나 let total: u32 = 42u8.into();를 쓰세요. 타입 변환을 참고하세요.

  • E0282: 타입 명시가 필요해요. let c = Shape::new(); 다음에 c.area()를 쓰는 것처럼 값을 나중에 쓰면, rustc는 대신 E0282를 알려 줘요. 고치는 방법은 같아요.
  • E0283: 맞는 타입이 여러 개라서 타입 명시가 필요해요. 대상 없이 Into::into(n)을 쓰거나, 제네릭 함수를 터보피시 없이 호출할 때 나요.
  • E0599: 함수를 찾을 수 없어요. Settings가 Default를 구현하지 않았다면 Settings::default()가 이 에러를 내요. #[derive(Default)]를 붙이세요. 같은 실수를 let s: Settings = Default::default();로 쓰면 E0277로 나타나요.
  • E0424: self 매개변수가 없는 함수에서 self를 썼어요. 이것도 값 하나가 아니라 타입에 속한 함수에 관한 에러예요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. let opts = Default::default();에서 E0790이 나요. Options는 Default를 derive했어요. 어느 줄로 고칠 수 있을까요?

2. let s: Settings = Default::default();는 컴파일되는데, let n: String = Shape::name();은 왜 여전히 E0790이 날까요?

3. fn show<T: Shape>() 안에서 트레이트 함수 name()은 어떻게 호출해야 할까요?

점수: 0 / 3