콘텐츠로 이동

Rust 에러 E0283: type annotations needed (sum, collect, into의 타입 명시)

E0283은 Rust가 내 값이 무엇을 할 수 있어야 하는지는 아는데, 그걸 할 수 있는 타입이 여러 개라서 대신 골라 주지 않겠다는 뜻이에요. 보통 .sum(), .collect(), .into() 바로 뒤에서 만나요. 고치기 가장 쉬운 에러 중 하나예요. 원하는 타입을 적어 주기만 하면 끝이거든요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0283]: type annotations needed
--> src/main.rs:3:9
|
3 | let total = prices.iter().sum();
| ^^^^^ --- type must be known at this point
|
= note: the type must implement `Sum<&i32>`
= help: the following types implement trait `Sum<A>`:
`Duration` implements `Sum<&'a Duration>`
`Duration` implements `Sum`
`Option<T>` implements `Sum<Option<U>>`
`Result<T, E>` implements `Sum<Result<U, E>>`
`Saturating<u128>` implements `Sum<&'a Saturating<u128>>`
`Saturating<u128>` implements `Sum`
`Saturating<u16>` implements `Sum<&'a Saturating<u16>>`
`Saturating<u16>` implements `Sum`
and 88 others
note: required by a bound in `std::iter::Iterator::sum`
help: consider giving `total` an explicit type
|
3 | let total: /* Type */ = prices.iter().sum();
| ++++++++++++

“type annotations needed”는 “타입 명시가 필요하다”는 뜻이에요. 첫 번째 help는 “Sum<A> 트레이트를 구현한 타입은 다음과 같다”며 목록을 보여 주고, 마지막 help는 “total에 명시적인 타입을 붙여 보라”고 제안해요. /* Type */는 자리 표시용 주석이니, 그 자리에 진짜 타입을 쓰면 돼요.

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

fn main() {
let prices = vec![250, 120, 99]; // 가격 (센트 단위)
let total = prices.iter().sum();
println!("Total: {total} cents");
}

.collect()와 .into()도 note만 다르고 같은 에러를 내요. let words = text.split(' ').collect();라면 note는 the type must implement `FromIterator<&str>`(그 타입은 FromIterator<&str>을 구현해야 함)예요. let name = "Ferris".into();라면 the type must implement `From<&str>`이에요.

메시지를 위에서 아래로 읽어 보세요.

  1. total 아래의 ^^^^^: Rust가 타입을 정하지 못한 변수예요.
  2. sum 아래의 ---: “type must be known at this point”, 즉 “이 시점에는 타입을 알아야 한다”예요. sum은 여러 다른 타입을 반환할 수 있어서, Rust는 여기서 답이 필요해요.
  3. note: the type must implement `Sum<&i32>`. Rust가 찾아낸 규칙이에요. “total은 &i32 값들을 더해서 만들 수 있는 타입이어야 한다”는 거죠.
  4. help 목록: “무언가의 합”이 될 줄 아는 타입들이에요. 아주 많아요. 모든 숫자 타입, Duration, 심지어 Option과 Result도 있어요. Rust는 여덟 개를 보여 주고 “and 88 others”(그 밖에 88개)라고 해요. 흔히 multiple impls satisfy this bound(이 바운드를 만족하는 impl이 여러 개)라고 말하는 게 바로 이거예요. 규칙을 따르는 impl을 가진 타입이 하나가 아닌 거예요.

카페에서 “마실 것 아무거나 주세요”라고 주문했다고 생각해 보세요. 규칙은 분명해요(마실 수 있어야 해요). 하지만 차도, 주스도, 물도 다 해당돼요. 내가 고르기 전까지 직원은 아무것도 가져올 수 없어요. E0283은 Rust가 쟁반을 들고 내 대답을 기다리는 상황이에요.

sum, collect, into는 반환 타입이 제네릭이에요. 함수가 무엇을 할지는 내가 요청한 타입이 정해요. 트레이트(Sum, FromIterator, From)는 규칙일 뿐이에요. 그래서 타입을 알려 주는 단서가 없으면, Rust에게는 규칙은 있지만 하나뿐인 답은 없어요.

그리고 그 답은 중요해요. 같은 단어들을 Vec<&str>로 모으면 중복이 남지만, HashSet<&str>로 모으면 중복이 사라져요. u8로 더하면 255에서 오버플로가 나지만, u64는 그렇지 않아요. Rust가 대신 타입을 골랐다면, 다른 곳의 작은 수정 하나로 프로그램의 동작이 아무 말 없이 바뀔 수도 있어요. 그래서 Rust는 한 번 물어보는 쪽을 택해요.

Rust가 스스로 타입을 알아내는 방식(타입 추론, type inference)은 E0282 페이지에서 설명하니 여기서는 되풀이하지 않을게요. 이 메서드들 뒤에 있는 트레이트는 트레이트와 제네릭을 참고하세요.

E0282, E0283, E0284: 문제는 하나, 코드는 셋

섹션 제목: “E0282, E0283, E0284: 문제는 하나, 코드는 셋”

세 에러 모두 “type annotations needed”(타입 명시 필요)라고 말하고, 나에게는 같은 뜻이에요. 타입을 적어 주세요. 코드 번호는 Rust가 무엇을 하다가 막혔는지만 알려 줘요.

  • E0282: Rust에게 타입에 대한 단서가 전혀 없거나, 단서에 닿기 전에 타입이 필요했어요(예를 들어 .len() 같은 메서드를 찾아야 할 때).
  • E0283(이 페이지): 트레이트 규칙이라는 단서는 있지만(must implement `Sum<&i32>`), 그 규칙을 따르는 타입이 많아요.
  • E0284: 규칙이 모르는 타입에 딸린 타입에 관한 거예요. parse의 에러 타입 같은 거죠(cannot satisfy `<_ as FromStr>::Err == _`). 바깥 타입을 알기 전에는 Rust가 확인할 수 없어요.

코드를 쓰는 도중에 번호가 바뀌기도 해요. let words = text.split(' ').collect(); 다음 줄이 println!("{:?}", words)라면 E0283이에요. 다음 줄을 words.len()으로 바꾸면 E0282가 돼요. 이제 메서드를 찾으려면 타입이 필요하니까요. 아래의 해결책은 세 가지 모두에 통해요.

대부분의 경우에는 이 방법을 고르세요. 변수 이름 뒤에 : 타입을 붙이면 돼요. 짧고, 무엇을 만드는지 다음에 코드를 읽을 사람에게 정확히 알려 줘요.

use std::collections::HashSet;
fn main() {
let prices = vec![250, 120, 99];
let total: u32 = prices.iter().sum();
let tags = "rust,web,rust,cli";
let list: Vec<&str> = tags.split(',').collect();
let unique: HashSet<&str> = tags.split(',').collect();
println!("합계: {total}센트");
println!("{:?}", list);
println!("중복 없는 태그: {}개", unique.len());
}

실행 결과:

합계: 469센트
["rust", "web", "rust", "cli"]
중복 없는 태그: 3개
▶ 직접 실행해 보기

똑같은 collect()인데 결과가 두 가지예요. 내가 적은 타입이 정말로 지시 사항인 거예요. 안쪽 타입은 let list: Vec<_> = ...처럼 _로 써서 Rust에게 맡겨도 돼요.

방법 2: sum과 collect에 터보피시 쓰기

섹션 제목: “방법 2: sum과 collect에 터보피시 쓰기”

println! 안이나 반복자 체인 중간, 함수 인자처럼 타입을 붙일 변수가 없을 때 이 방법을 고르세요. ::<타입>(“터보피시”)으로 호출에 직접 타입을 붙이면 돼요.

fn main() {
let prices = vec![250, 120, 99];
println!("합계: {}센트", prices.iter().sum::<u32>());
let line = "tea,cake,soup";
let items = line.split(',').collect::<Vec<_>>();
let initials = line.split(',').map(|s| &s[..1]).collect::<String>();
println!("{:?}", items);
println!("첫 글자: {initials}");
}

실행 결과:

합계: 469센트
["tea", "cake", "soup"]
첫 글자: tcs
▶ 직접 실행해 보기

&s[..1]은 첫 1바이트를 잘라 내는데, 한글은 한 글자가 3바이트라서 이 예제의 단어는 영어로 남겨 뒀어요. .product::<u64>()도 똑같이 쓰면 돼요. 방법 1과 방법 2는 똑같이 올바르니, 더 읽기 좋은 쪽을 고르세요.

방법 3: .into()에는 대상 타입을 직접 쓰기 (터보피시는 안 돼요)

섹션 제목: “방법 3: .into()에는 대상 타입을 직접 쓰기 (터보피시는 안 돼요)”

.into()와 .try_into()에는 이 방법을 고르세요. "Ferris".into::<String>()이라고 쓰고 싶어지지만, 그러면 다른 에러가 나요. 타입 매개변수가 메서드가 아니라 Into 트레이트에 붙어 있기 때문이에요.

error[E0107]: method takes 0 generic arguments but 1 generic argument was supplied
--> src/main.rs:2:25
|
2 | let name = "Ferris".into::<String>();
| ^^^^ expected 0 generic arguments

“이 메서드는 제네릭 인자를 0개 받는데 1개를 넘겼다”는 뜻이에요.

대신 변수에 타입을 붙이거나, 대상 타입의 From을 직접 호출하세요. 이쪽이 읽기에도 좋아요.

fn main() {
let name: String = "Ferris".into();
let city = String::from("리스본");
let small: u8 = 200;
let big: u64 = small.into();
let also_big = u64::from(small);
let level: i64 = 42;
let byte: u8 = level.try_into().unwrap();
let too_big = u8::try_from(300_i64);
println!("{name}의 집은 {city}에 있어요");
println!("{big} {also_big} {byte}");
println!("300이 u8에 들어갈까요? {}", too_big.is_ok());
}

실행 결과:

Ferris의 집은 리스본에 있어요
200 200 42
300이 u8에 들어갈까요? false
▶ 직접 실행해 보기

String::from(...)과 u64::from(...)은 줄 맨 앞에서 대상 타입을 밝혀 줘서, 다른 단서가 없을 때는 .into()보다 이쪽을 좋아하는 사람이 많아요. From과 Into: 언제나 성공하는 변환을 참고하세요.

방법 4: 타입이 정해진 곳에 값을 쓰기

섹션 제목: “방법 4: 타입이 정해진 곳에 값을 쓰기”

함수 매개변수나 구조체 필드처럼, 어차피 값이 타입이 정해진 곳으로 갈 때 이 방법을 고르세요. Rust는 함수 전체를 읽으니까, 나중에 쓰는 곳만으로도 충분해요.

fn print_receipt(total: u32, items: Vec<&str>) {
println!("아이템 {}개, {}센트", items.len(), total);
}
fn main() {
let prices = vec![250, 120, 99];
let total = prices.iter().sum(); // print_receipt 덕분에 u32
let items = "tea,cake,soup".split(',').collect(); // 같은 이유로 Vec<&str>
print_receipt(total, items);
}

실행 결과:

아이템 3개, 469센트
▶ 직접 실행해 보기

모든 사용이 단서가 되는 건 아니에요. if prices.iter().sum() > 400은 여전히 E0283이에요. >는 여러 타입 쌍을 비교할 수 있거든요. impl Display 같은 제네릭 매개변수에 넘기는 것도 도움이 안 돼요. Display인 타입이 많으니까요. 값을 쓰는 곳이 정확히 하나의 타입을 정해 줘야 해요.

.collect()를 바로 println!에 넣을 때. println!("{:?}", line.split(',').collect());는 E0283을 내고, rustc가 직접 해결책을 제안해요. .collect::<Vec<_>>()예요.

.try_into().unwrap(). let small = big.try_into().unwrap();는 the type must implement `TryFrom<i64>`와 함께 E0283을 내요. 해결: let small: u8 = big.try_into().unwrap(); 또는 u8::try_from(big).unwrap().

제네릭 함수에 넘긴 Default::default(). fn show(value: impl Debug)가 있을 때 show(Default::default())는 E0283이에요. Rust는 누구의 기본값을 원하는지 알 수 없어요. 해결: show(Settings::default()) 또는 show(u32::default()). 따로 한 줄에 let s = Default::default();라고 쓰면 대신 E0790이 나와요. Default: 적당한 시작 값을 참고하세요.

내가 만든 제네릭 함수. fn make<T: Default>() -> T처럼 타입 매개변수가 반환 타입에만 나오면, let v = make();는 E0283이에요. 해결: make::<Settings>() 또는 let v: Settings = make();.

  • E0282: type annotations needed. Rust에게 타입에 대한 단서가 전혀 없을 때(또는 메서드 호출에 타입이 필요할 때) 나와요.
  • E0284: cannot satisfy `<_ as FromStr>::Err == _`가 붙은 type annotations needed예요. 거의 언제나 .parse()에서 나와요.
  • E0790: 어떤 타입인지 밝히지 않고 Default::default()(또는 다른 트레이트 함수)를 호출했어요.
  • E0277: 타입은 적었지만 그 타입을 그런 식으로 만들 수 없어요. 예를 들어 let n: Vec<i32> = "1 2".split(' ').collect();예요.
  • E0107: 제네릭 인자 개수가 틀렸어요. .into::<String>()이라고 쓰면 나오는 에러예요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. E0283의 note the type must implement Sum<&i32>는 무엇을 알려 줄까요?

2. let name = "Ferris".into();를 고치는 줄은 무엇일까요?

3. 같은 단어들을 Vec<&str>와 HashSet<&str>로 모으면 왜 결과가 다를까요?

점수: 0 / 3