콘텐츠로 이동

Rust 에러 E0631: type mismatch in closure arguments (클로저 인자 타입 불일치)

E0631은 클로저의 매개변수 개수는 맞지만, 적어도 하나의 타입이 틀렸다는 뜻이에요. 열에 아홉은 & 하나 차이예요. |x: i32|라고 썼는데, 반복자는 &i32를 빌려주는 거죠. 가장 빠른 해결책은 대개 타입 명시를 지우고 Rust가 알아서 정하게 두는 거예요. 이 페이지에서는 그 방법을 먼저 보여 주고, 그다음에 &가 어디서 오는지 설명할게요. 그러면 다음번에는 미리 알아챌 수 있을 거예요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0631]: type mismatch in closure arguments
--> src/main.rs:3:43
|
3 | let doubled: Vec<i32> = prices.iter().map(|p: i32| p * 2).collect(); // ❌ 아이템은 &i32예요
| ^^^ -------- found signature defined here
| |
| expected due to this
|
= note: expected closure signature `fn(&{integer}) -> _`
found closure signature `fn(i32) -> _`
note: required by a bound in `map`
help: consider adjusting the signature so it borrows its argument
|
3 | let doubled: Vec<i32> = prices.iter().map(|p: &i32| p * 2).collect(); // ❌ 아이템은 &i32예요
| +

“type mismatch in closure arguments”는 “클로저 인자의 타입이 맞지 않는다”는 뜻이에요. note는 기대한 시그니처와 실제로 찾은 시그니처를 나란히 보여 주고, help는 “인자를 빌려 받도록 시그니처를 고쳐 보라”고 제안해요.

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

fn main() {
let prices = vec![120, 45, 300];
let doubled: Vec<i32> = prices.iter().map(|p: i32| p * 2).collect(); // ❌ 아이템은 &i32예요
println!("{:?}", doubled);
}

rustc는 바로 뒤에 더 긴 에러를 하나 더 출력해요. the method `collect` exists for struct `Map<...>`, but its trait bounds were not satisfied(collect 메서드는 있지만 트레이트 바운드를 만족하지 않음, E0599)예요. 이건 부수 효과일 뿐이에요. E0631을 고치면 사라져요.

두 note 줄이 메시지의 핵심이에요. “메서드는 이걸 원했는데, 나는 저걸 썼다”로 읽으면 돼요.

  • expected fn(&{integer}) -> _: map은 정수의 참조자로 클로저를 호출할 거예요. {integer}는 “Rust가 아직 정하지 않은 어떤 정수 타입”이고, _는 “아무 반환 타입”이라는 뜻이에요.
  • found fn(i32) -> _: 내 클로저는 i32를 값으로 받겠다고 했어요.

나란히 놓고 보면 다른 건 & 하나뿐이에요. help: 줄이 그 한 글자짜리 해결책을 보여 줘요.

옷 보관소를 떠올려 보세요. 코트를 맡기면 번호표를 받죠. .iter()도 똑같아요. 아이템 자체를 건네주는 게 아니라, 아이템의 번호표(참조자)를 건네줘요. 코트 자체만 받겠다고 고집하는 클로저는 번호표를 받아 주지 않아요.

반복자 메서드는 모두 클로저에 정확히 어떤 타입을 넘길지 정해 두고 있고, 그 타입은 반복자를 어떻게 만들었는지에 따라 달라져요.

  • v.iter()는 아이템을 하나씩 빌려줘요: &T.
  • v.into_iter()(내가 소유한 Vec에서)는 아이템을 하나씩 넘겨줘요: T.
  • filter와 find는 아이템을 한 번 더 빌려줘요. 그래서 .iter() 위에서는 &&T가 돼요.
  • sort_by는 아이템 두 개를 빌려줘요: &T와 &T.

매개변수 타입을 생략하면 Rust가 메서드를 보고 채워 넣으니까, 언제나 딱 맞아요. 타입을 직접 쓰면 Rust는 내 말을 그대로 믿고 검사해요. 몰래 값을 복사하거나 참조자를 소유한 값으로 바꿔 주지는 않아요. 그러면 코드가 하는 일이 달라지기 때문이에요(String이라면 복사에 메모리와 시간이 들어요). 반복자를 얻는 세 가지 방법은 반복자 강좌에서, 참조자는 대여에서 설명해요.

먼저 이 방법을 고르세요. 클로저 매개변수에는 타입이 거의 필요 없어요. map, filter, sort_by가 무엇을 넘기는지 Rust가 정확히 알거든요. : i32를 지우면 컴파일돼요.

fn main() {
let prices = vec![120, 45, 300];
let ages = vec![12, 34, 17, 50];
let mut scores = vec![42, 7, 99, 15];
// 타입 명시 없음: Rust가 반복자가 실제로 주는 타입을 써요
let doubled: Vec<i32> = prices.iter().map(|p| p * 2).collect();
let adults: Vec<&i32> = ages.iter().filter(|age| **age >= 18).collect();
scores.sort_by(|a, b| b.cmp(a));
println!("{:?}", doubled);
println!("{:?}", adults);
println!("{:?}", scores);
}

실행 결과:

[240, 90, 600]
[34, 50]
[99, 42, 15, 7]
▶ 직접 실행해 보기

p * 2가 &i32에서도 동작하는 건, Rust의 숫자 연산자가 참조자도 받기 때문이에요. filter 안에서 age는 &&i32라서, **age로 참조자 두 개를 따라가야 숫자에 닿아요. b.cmp(a) 같은 비교는 참조자에서 바로 동작해요.

방법 2: 메서드가 실제로 넘기는 타입 쓰기

섹션 제목: “방법 2: 메서드가 실제로 넘기는 타입 쓰기”

긴 체인을 읽기 쉽게 하려는 등의 이유로 타입을 눈에 보이게 남기고 싶을 때 이 방법을 고르세요. rustc의 help:를 따라 &를 붙이면 돼요. &를 패턴에 넣을 수도 있어요. |&p|는 참조자를 풀어서 숫자의 평범한 복사본을 줘요(정수 같은 Copy 타입에서 통해요).

fn main() {
let prices = vec![120, 45, 300];
let ages = vec![12, 34, 17, 50];
let mut scores = vec![42, 7, 99, 15];
// 진짜 타입을 써요: .iter()는 &, filter 안에서는 &&
let doubled: Vec<i32> = prices.iter().map(|p: &i32| p * 2).collect();
let adults: Vec<&i32> = ages.iter().filter(|age: &&i32| **age >= 18).collect();
scores.sort_by(|a: &i32, b: &i32| b.cmp(a));
// 또는 & 패턴으로 숫자를 복사해서 꺼내요
let tax: Vec<i32> = prices.iter().map(|&p| p / 10).collect();
println!("{:?}", doubled);
println!("{:?}", adults);
println!("{:?}", scores);
println!("{:?}", tax);
}

실행 결과:

[240, 90, 600]
[34, 50]
[99, 42, 15, 7]
[12, 4, 30]
▶ 직접 실행해 보기

차이를 눈여겨보세요. |p: &i32|는 “p는 참조자예요“라는 뜻이고, |&p|는 “참조자를 받을 테니, 그 뒤에 있는 값을 주세요”라는 뜻이에요.

방법 3: 반복자가 넘겨주는 것 바꾸기

섹션 제목: “방법 3: 반복자가 넘겨주는 것 바꾸기”

참조자가 아니라 정말로 값이 필요할 때 이 방법을 고르세요. 값을 얻는 방법은 두 가지예요.

  • .iter().copied()는 &i32를 하나하나 i32로 바꿔요. 숫자, char, bool 같은 작은 Copy 타입에 쓰세요. (.cloned()는 String 같은 타입에서 복제를 해서 같은 일을 해요.)
  • Vec에서 .into_iter()를 쓰면 아이템 자체를 넘겨줘요. 그 뒤로 Vec은 다 써 버린 상태가 되니, 더 필요 없을 때 고르세요.
fn main() {
let prices = vec![120, 45, 300];
// .copied()는 &i32 아이템을 i32 아이템으로 바꿔요
let doubled: Vec<i32> = prices.iter().copied().map(|p: i32| p * 2).collect();
println!("{:?}", doubled);
// Vec의 .into_iter()는 아이템 자체를 넘겨줘요 (그리고 Vec을 다 써 버려요)
let names = vec![String::from("ferris"), String::from("corro")];
let loud: Vec<String> = names.into_iter().map(|name: String| name.to_uppercase()).collect();
println!("{:?}", loud);
}

실행 결과:

[240, 90, 600]
["FERRIS", "CORRO"]
▶ 직접 실행해 보기

반복이 끝난 뒤에도 names가 필요하다면 .iter()를 그대로 두고 &String을 받으세요. 빌리기만 한 Vec에서 값을 꺼내 가려고 하면 다른 에러인 E0507이 나와요.

방법 4: 이름 있는 함수가 반복자가 주는 타입을 받게 하기

섹션 제목: “방법 4: 이름 있는 함수가 반복자가 주는 타입을 받게 하기”

.map(shout)처럼 함수를 이름으로 넘길 때 이 방법을 고르세요. 그러면 메시지에 “closure” 대신 “function”이라고 나와요.

error[E0631]: type mismatch in function arguments
--> src/main.rs:7:51
|
1 | fn shout(word: &String) -> String {
| --------------------------------- found signature defined here
...
7 | let loud: Vec<String> = words.into_iter().map(shout).collect();
| --- ^^^^^ expected due to this
| |
| required by a bound introduced by this call
|
= note: expected function signature `fn(&str) -> _`
found function signature `fn(&String) -> _`

“함수 인자의 타입이 맞지 않는다”는 뜻이에요. note를 보면 map은 fn(&str)을 기대했는데, 찾은 건 fn(&String)이에요.

아이템은 &str인데, shout는 &String을 원해요. 좋은 습관이 하나 있어요. 텍스트를 읽기만 하는 함수는 &str을 받게 하세요. 그러면 문자열 리터럴, &String 등을 모두 받을 수 있어요. filter처럼 반복자가 &를 하나 더 붙일 때는, 호출을 짧은 클로저로 감싸세요. 평범한 함수 호출 안에서는 Rust가 남는 &를 알아서 벗겨 주지만(이걸 역참조 강제 변환, deref coercion이라고 해요), 함수를 이름으로 넘길 때는 그렇게 해 줄 수 없거든요.

// &str을 받아요: 문자열 리터럴, &String 등에서 모두 통해요
fn shout(word: &str) -> String {
word.to_uppercase()
}
fn is_long(word: &str) -> bool {
word.len() > 4
}
fn main() {
let words = vec!["crab", "rustacean", "ferris"];
// 아이템이 &str이라서 shout를 이름으로 넘길 수 있어요
let loud: Vec<String> = words.iter().copied().map(shout).collect();
println!("{:?}", loud);
// filter는 &&str을 줘요: 짧은 클로저로 감싸면 Rust가 대신 변환해 줘요
let long: Vec<&str> = words.iter().copied().filter(|w| is_long(w)).collect();
println!("{:?}", long);
}

실행 결과:

["CRAB", "RUSTACEAN", "FERRIS"]
["rustacean", "ferris"]
▶ 직접 실행해 보기

이 예제의 단어는 영어로 남겨 뒀어요. len()은 바이트 수를 세는데, 한글은 한 글자가 3바이트라서 결과가 달라지기 때문이에요. &str과 String의 차이는 문자열에서 더 볼 수 있어요.

타입을 붙인 정렬 매개변수. scores.sort_by(|a: i32, b: i32| b.cmp(&a))는 expected closure signature `for<'a, 'b> fn(&'a {integer}, &'b {integer}) -> _`와 함께 E0631을 내요. for<'a, 'b> 부분은 “어떤 대여든”이라는 뜻일 뿐이에요. 해결: |a, b| b.cmp(a). 정렬을 참고하세요.

소유 타입으로 쓴 맵 항목. HashMap<String, i32>에서 stock.iter().map(|(name, count): (String, i32)| ...)라고 쓰면 expected closure signature `fn((&String, &_)) -> _`가 나와요. 맵의 .iter()는 키와 값을 둘 다 빌려줘요. 해결: 타입 명시를 지우거나, (&String, &i32)라고 쓰세요.

내가 만든 제네릭 함수. fn apply<F: Fn(i32) -> i32>(f: F, x: i32)에 클로저 |x: f64| x / 2.0을 넘기면, rustc는 바운드를 가리키며 expected closure signature `fn(i32) -> _`와 found closure signature `fn(f64) -> _`를 알려 줘요. 해결: 클로저가 i32를 받게 하고 안에서 변환하거나(x as f64 / 2.0), 정말 f64를 원했다면 바운드를 바꾸세요. 클로저를 받는 함수 작성하기를 참고하세요.

String을 쓴 for_each. Vec<&str>에서 words.iter().for_each(|w: String| ...)라고 쓰면 fn(&&str)을 기대한다고 나와요. &str은 String이 아니에요. 해결: |w| 또는 |w: &&str|.

이해도 점검

1. let v = vec![1, 2, 3];일 때 v.iter().map(|x| ...)의 클로저는 어떤 타입을 받을까요?

2. note에 expected closure signature fn(&{integer}) -> _, found closure signature fn(i32) -> _라고 나와요. 가장 빠른 해결책은 무엇일까요?

3. prices.iter().map(...) 안의 |&p| p * 2는 무슨 일을 할까요?

점수: 0 / 3