콘텐츠로 이동

Rust 에러 E0282: type annotations needed (타입 명시 필요)

E0282는 Rust가 타입을 스스로 알아내려고 했는데 단서가 부족했다는 뜻이에요. 내 로직이 틀린 건 아니에요. 컴파일러가 타입 하나만 알려 달라는 거고, 보통 몇 글자만 더 쓰면 돼요. 이 페이지에서는 고치는 방법을 먼저 보여 주고, 그다음 Rust가 타입을 추측하는 방식(타입 추론, type inference)을 설명할게요. 그러면 언제 도와줘야 하는지 알 수 있을 거예요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0282]: type annotations needed for `Vec<_>`
--> src/main.rs:2:9
|
2 | let mut scores = Vec::new(); // 무엇의 Vec일까요...?
| ^^^^^^^^^^ ---------- type must be known at this point
|
help: consider giving `scores` an explicit type, where the type for type parameter `T` is specified
|
2 | let mut scores: Vec<T> = Vec::new(); // 무엇의 Vec일까요...?
| ++++++++

“Vec<_>에 타입 명시가 필요하다”는 뜻이에요. help는 “타입 매개변수 T의 타입을 정해서 scores에 명시적인 타입을 붙여 보라”고 제안해요.

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

fn main() {
let mut scores = Vec::new(); // 무엇의 Vec일까요...?
println!("Scores recorded: {}", scores.len());
}

메시지는 같은데 코드가 다를 때

섹션 제목: “메시지는 같은데 코드가 다를 때”

“type annotations needed”(타입 명시 필요)라는 말은 다른 코드 세 개에서도 나와요. 뜻은 같고, 이 페이지의 해결책이 모두 통해요.

  • E0283: 대상 타입 없이 .collect()나 .sum()을 쓴 경우예요. 결과가 될 수 있는 타입이 여러 개라서, rustc가 the type must implement `FromIterator<&str>`나 `Sum<&i32>` 같은 note를 붙여요.
  • E0284: 대상 타입 없이 "42".parse().unwrap()을 쓴 경우예요. note에 cannot satisfy `<_ as FromStr>::Err == _`라고 나오는데, “무엇으로 파싱할까요?“를 무섭게 말한 거예요.
  • E0790: let settings = Default::default();인 경우예요. 첫 줄에 cannot call associated function on trait without specifying the corresponding `impl` type(해당 impl 타입을 지정하지 않고는 트레이트의 연관 함수를 호출할 수 없다)라고 나와요. Rust는 누구의 기본값을 원하는지 모르는 거예요.

어떤 코드가 나올지는 정확히 어떤 줄인지에 따라 달라지고, 코드를 더 쓰면 바뀌기도 해요. 예를 들어 let age = "42".parse().unwrap();는 E0284를 내지만, 다음 줄에서 age.pow(2)를 호출하면 E0282가 돼요. 번호는 신경 쓰지 마세요. rustc가 가리키는 변수를 보면 돼요.

2번째 줄 아래의 표시를 보세요.

  1. mut scores 아래의 ^^^^^^^^^^: Rust가 타입을 알아내지 못한 변수예요.
  2. Vec::new() 아래의 ----------: “type must be known at this point”, 즉 “이 시점에는 타입을 알아야 한다”예요. Vec::new()는 빈 목록을 만드는데, 무엇의 빈 목록일까요? 숫자? 이름? 알맞은 종류의 저장 공간을 마련하려면 Rust가 알아야 해요.
  3. help는 let mut scores: Vec<T>를 제안해요. 여기서 T는 그냥 자리 표시자예요. Vec<u32>처럼 진짜 타입으로 바꿔 쓰면 돼요.

우체국에 맡긴 상자의 “내용물” 칸이 비어 있다고 생각해 보세요. 누군가 거기에 무언가를 적기 전까지 직원은 상자를 보낼 수 없어요. 내가 직접 적어도 되고, 상자에 뭔가를 넣어서 직원이 볼 수 있게 해도 돼요.

Rust의 모든 값에는 컴파일할 때 정해지는 정확한 타입 하나가 있어요. 대부분은 내가 쓰지 않아도 돼요. Rust가 코드를 읽고 알아내기 때문이에요. let count = 5;는 i32이고, let name = String::from("Ferris");는 String이에요. 이게 타입 추론이고, 변수 강좌에서 더 많이 볼 수 있어요.

타입 추론은 한 줄만이 아니라 함수 전체에서 단서를 모아요. 나중에 scores.push(90)을 호출하면 그게 단서예요. 이 Vec에는 숫자가 들어 있다는 거죠. &[u32]를 받는 함수에 scores를 넘기면 더 좋은 단서가 돼요. E0282는 단서가 전혀 없을 때, 또는 Rust가 단서에 닿기 전에 타입이 필요할 때(예를 들어 메서드를 찾아야 할 때) 나와요.

어떤 함수들은 반환 타입이 제네릭이에요. parse, collect, sum, Default::default, Vec::new, None 같은 것들이요. 이들은 여러 다른 타입을 만들어 낼 수 있어서, Rust가 대신 하나를 고를 수 없어요. Rust는 추측하지 않아요. 잘못 추측하면 프로그램의 동작이 아무 말 없이 바뀌어 버리기 때문이에요. 함수 하나가 어떻게 여러 타입을 반환할 수 있는지는 제네릭을 참고하세요.

대부분의 경우에는 이 방법을 고르세요. 변수 이름 뒤에 : 타입을 붙이면 돼요. 짧고, 다음에 코드를 읽을 사람에게 설명도 되어 줘요.

fn main() {
let scores: Vec<u32> = Vec::new();
let age: u32 = "42".parse().unwrap();
let words: Vec<&str> = "빨강 초록 파랑".split(' ').collect();
let winner: Option<String> = None;
println!("기록된 점수: {}개", scores.len());
println!("내년에는 {}살이에요", age + 1);
println!("{:?}", words);
println!("{:?}", winner);
}

실행 결과:

기록된 점수: 0개
내년에는 43살이에요
["빨강", "초록", "파랑"]
None
▶ 직접 실행해 보기

항상 타입 전체를 쓸 필요는 없어요. let words: Vec<_> = ...collect();는 “Vec이긴 한데, 안에 뭐가 들어가는지는 알아서 정해 줘”라는 뜻이에요. _는 그 부분을 Rust에게 채워 달라고 부탁하는 거예요. 타입을 쓰는 법은 데이터 타입에서 더 볼 수 있어요.

반복자 호출이 줄줄이 이어지는 중간처럼, 타입을 붙일 변수가 없을 때 이 방법을 고르세요. 함수 호출에 바로 ::<타입>을 붙여요. ::<>가 물고기처럼 생겨서 사람들은 이걸 “터보피시”(turbofish)라고 불러요.

fn main() {
let input = "4 15 7";
let numbers = input
.split(' ')
.map(|s| s.parse::<u32>().unwrap())
.collect::<Vec<_>>();
let total = numbers.iter().sum::<u32>();
println!("{:?}", numbers);
println!("합계: {total}");
}

실행 결과:

[4, 15, 7]
합계: 26
▶ 직접 실행해 보기

가장 흔한 터보피시 호출 세 가지는 parse::<i32>(), collect::<Vec<_>>(), sum::<i32>()예요. 방법 1과 방법 2는 똑같이 올바른 방법이에요. let total: u32 = numbers.iter().sum();도 같은 일을 해요. 읽기 편한 쪽을 고르세요.

방법 3: 값을 써서 Rust가 타입을 추론하게 하기

섹션 제목: “방법 3: 값을 써서 Rust가 타입을 추론하게 하기”

코드를 아직 다 쓰지 않아서 에러가 날 때 이 방법을 고르세요. 빈 Vec을 먼저 만들고, 채우는 코드는 나중에 쓰는 경우가 많아요. 값을 쓰기 시작하면 Rust에게 단서가 생겨서, 타입 명시 없이도 에러가 사라져요.

fn print_team(names: &[String]) {
for name in names {
println!("- {name}");
}
}
fn main() {
let mut team = Vec::new(); // 타입은 아직... 몰라요
team.push(String::from("Ferris")); // 이제 Rust가 알아요: Vec<String>
team.push(String::from("Corro"));
print_team(&team);
}

실행 결과:

- Ferris
- Corro
▶ 직접 실행해 보기

순서가 조금 중요해요. Rust가 메서드를 호출하려고 타입이 필요해지기 전에 단서가 나와야 해요. let shout = |s| s.to_uppercase(); 같은 클로저는 바로 다음 줄에서 shout("hi")를 호출해도 여전히 실패해요. .to_uppercase()를 찾으려면 Rust가 s가 무엇인지 알아야 하기 때문이에요. 이럴 때는 타입을 쓰세요. |s: &str| s.to_uppercase()처럼요. 클로저를 참고하세요.

방법 4: None과 Default::default()에 타입 붙이기

섹션 제목: “방법 4: None과 Default::default()에 타입 붙이기”

단서가 전혀 없는 값에는 이 방법을 고르세요. None은 무엇을 담을지 말하지 않고, Default::default()는 누구의 기본값을 원하는지 말하지 않아요. 식 안에 타입을 써 넣으세요.

#[derive(Debug, Default)]
struct Settings {
volume: u8,
muted: bool,
}
fn main() {
let fastest_lap = None::<f64>; // None 대신
let settings = Settings::default(); // Default::default() 대신
let backup: Settings = Default::default(); // 타입 명시도 돼요
println!("{:?}", fastest_lap);
println!("{:?}", settings);
println!("백업 볼륨: {}", backup.volume);
}

실행 결과:

None
Settings { volume: 0, muted: false }
백업 볼륨: 0
▶ 직접 실행해 보기

Settings::default()가 가장 분명해요. “기본 Settings“라고 그대로 읽히니까요. None::<f64>도 편하지만, let fastest_lap: Option<f64> = None;(방법 1)이 더 흔해요. Option을 참고하세요.

텍스트를 숫자로 파싱할 때. let guess = line.trim().parse().unwrap();는 E0284를 내요(다음 줄에서 guess의 메서드를 호출하면 E0282). parse는 i32, u8, f64 등 아주 많은 타입을 만들 수 있어요. 해결: let guess: i32 = ...나 .parse::<i32>(). 여기서는 나중에 guess를 i32 변수와 비교하는 것만으로는 부족해요. parse는 호출하는 그 자리에서 타입이 필요하기 때문이에요.

.collect(). Vec, String, HashSet, HashMap… 무엇이든 만들 수 있어서, 항상 대상을 알아야 해요. 해결: let words: Vec<_> = ...collect();나 .collect::<Vec<_>>(). 반복자를 참고하세요.

.sum()과 .product(). nums.iter().sum()은 the type must implement `Sum<&i32>`와 함께 E0283을 내요. i32를 더하면 i32가 될 수도 있지만, 다른 숫자 타입도 합계가 될 수 있어요. 해결: let total: i32 = nums.iter().sum();나 .sum::<i32>().

구조체 생성자 안의 빈 컬렉션. 아직 다 쓰지 않은 new() 함수 안의 let cache = HashMap::new(); 같은 경우예요. 해결: 전체 타입을 한 번 쓰거나(HashMap<String, u32>), 필드 타입이 이미 선언된 구조체에 담아서 반환하세요. 그것도 Rust가 쓸 수 있는 단서예요.

  • E0308: mismatched types (타입 불일치). let age: u32 = "42";처럼 내가 쓴 타입과 값이 서로 다를 때 나와요.
  • E0277: trait bound not satisfied (트레이트 바운드 불충족). 타입은 정했지만 그 방식으로는 만들 수 없을 때예요. 예를 들어 let n: Vec<i32> = "1 2".split(' ').collect();(이건 Vec<&str>이에요. 각 아이템을 먼저 파싱하세요).
  • E0599: no method found (메서드 없음). 예를 들어 let n: u32라고 명시한 뒤 .trim() 같은 String 메서드를 호출할 때요.
  • E0004: non-exhaustive patterns (모든 경우를 다루지 않은 패턴). 이것도 rustc가 해결책을 대신 써 주는 에러예요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. let ids = Vec::new();의 Vec에 u64 값을 담으려고 해요. E0282를 고치는 줄은 무엇일까요?

2. 텍스트가 분명히 숫자인데도 let n = "7".parse().unwrap();에 타입이 필요한 이유는 무엇일까요?

3. .collect::<Vec<_>>()의 _는 무슨 뜻일까요?

점수: 0 / 3