클로저
클로저 (Closures)
클로저는 변수에 저장하거나 다른 함수에 인자로 넘길 수 있는 익명 함수예요. 어느 한 곳에서 클로저를 만들어 두고, 나중에 다른 맥락에서 호출해서 그 값을 평가할 수도 있고요. 무엇보다 함수와 달리 정의된 스코프의 값을 캡처할 수 있다는 점이 핵심이에요. 이번 장에서는 이러한 클로저의 특징이 코드 재사용과 동작 커스터마이징을 어떻게 가능하게 하는지 살펴볼게요.
출처: The Rust Book
환경 캡처하기 (Capturing the Environment)
먼저 클로저가 정의된 환경에서 값을 캡처해 나중에 쓰는 방법을 볼게요. 이렇게 상상해 봐요. 티셔츠 회사에서 가끔 메일링 리스트에 있는 사람에게 한정판 티셔츠를 경품으로 주는 프로모션을 해요. 리스트에 있는 사람은 프로필에 좋아하는 색을 선택적으로 등록할 수 있어요. 무료 티셔츠 당첨자의 프로필에 좋아하는 색이 등록되어 있으면 그 색을 주고, 등록되어 있지 않으면 회사가 현재 가장 많이 보유한 색을 줍니다.
이걸 구현하는 방법은 여러 가지가 있어요. 이 예시에서는 색을 단순화해서 Red, Blue 두 변형만 가진 ShirtColor라는 enum을 쓸게요. 회사의 재고는 shirts 필드에 Vec<ShirtColor>(현재 보유 중인 티셔츠 색 목록)을 담는 Inventory 구조체로 표현해요. Inventory에 정의된 giveaway 메서드는 무료 티셔츠 당첨자의 선호 색(Option<ShirtColor>)을 받아서 그 사람이 받게 될 티셔츠 색을 반환해요. 이 설정이 Listing 13-1에 나와 있어요.
Filename: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
Listing 13-1: 티셔츠 회사 경품 상황
main에서 만든 store에는 이 한정판 프로모션용으로 파란 티셔츠 두 벌과 빨간 티셔츠 한 벌이 남아 있어요. 빨간 티셔츠를 선호하는 사용자와 아무 선호도 없는 사용자에게 각각 giveaway 메서드를 호출해요.
이 코드도 여러 방식으로 구현할 수 있는데, 클로저에 집중하기 위해 giveaway 메서드의 본문에서 클로저를 쓰는 부분을 빼면 지금까지 배운 개념만으로 이루어져 있어요. giveaway 메서드는 사용자 선호를 Option<ShirtColor> 타입의 매개변수로 받아서 user_preference에 unwrap_or_else 메서드를 호출해요. Option<T>의 unwrap_or_else 메서드는 표준 라이브러리에 정의되어 있어요.
이 메서드는 인자를 하나 받는데, 바로 인자가 없는 클로저로 T 타입 값(이 경우 Option<T>의 Some 변형에 저장된 것과 같은 타입인 ShirtColor)을 반환해요. Option<T>가 Some 변형이면 unwrap_or_else는 Some 안의 값을 그대로 반환하고, None 변형이면 클로저를 호출해 그 클로저가 반환한 값을 돌려줘요.
unwrap_or_else의 인자로 클로저 표현식 || self.most_stocked()을 지정했어요. 이 클로저는 매개변수를 하나도 받지 않아요(매개변수가 있었다면 두 세로 파이프 사이에 들어갔을 거예요). 클로저 본문은 self.most_stocked()을 호출해요. 여기서는 클로저를 정의만 하고 있고, 결과가 실제로 필요해질 때 unwrap_or_else의 구현이 이 클로저를 호출해요.
이 코드를 실행하면 다음과 같이 출력돼요:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
여기서 흥미로운 점은 현재 Inventory 인스턴스의 self.most_stocked()을 호출하는 클로저를 넘겼다는 거예요. 표준 라이브러리는 우리가 정의한 Inventory나 ShirtColor 타입, 이 시나리오에서 쓰려는 로직을 전혀 몰라도 됐어요. 클로저가 self Inventory 인스턴스에 대한 불변 참조를 캡처해서 우리가 지정한 코드와 함께 unwrap_or_else 메서드로 넘겨줬거든요. 반면 함수는 이렇게 자신의 환경을 캡처할 수 없어요.
클로저 타입 추론과 어노테이션 (Inferring and Annotating Closure Types)
함수와 클로저 사이에는 차이가 더 있어요. 클로저는 보통 fn 함수처럼 매개변수나 반환 값의 타입을 명시할 필요가 없어요. 함수에 타입 어노테이션이 필요한 이유는 그 타입이 사용자에게 노출되는 명시적 인터페이스의 일부이기 때문이에요. 함수가 어떤 타입의 값을 받고 반환하는지 모두가 동의하도록 이 인터페이스를 엄격히 정의하는 게 중요해요. 반면 클로저는 이렇게 노출되는 인터페이스에서 쓰이지 않아요. 변수에 저장되고, 이름을 붙여 라이브러리 사용자에게 노출하지 않은 채 그냥 사용되거든요.
클로저는 보통 짧고, 임의의 시나리오보다는 좁은 맥락 안에서만 의미가 있어요. 이렇게 제한된 맥락 안에서는 컴파일러가 대부분의 변수 타입을 추론하듯, 매개변수와 반환 타입을 추론할 수 있어요(드물게 컴파일러도 클로저 타입 어노테이션을 요구하는 경우가 있긴 해요).
변수와 마찬가지로, 명시성과 명확성을 높이고 싶다면 타입 어노테이션을 추가할 수 있어요. 대신 꼭 필요한 것보다 장황해지는 비용이 들죠. 클로저에 타입을 어노테이션하면 Listing 13-2의 정의처럼 됩니다. 이 예시에서는 Listing 13-1처럼 인자로 넘기는 자리에서 정의하는 대신, 클로저를 정의해 변수에 저장하고 있어요.
Filename: src/main.rs
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
Listing 13-2: 클로저의 매개변수와 반환 값 타입에 선택적 타입 어노테이션 추가하기
타입 어노테이션을 추가하면 클로저 구문이 함수 구문과 더 비슷해 보여요. 여기서는 매개변수에 1을 더하는 함수와 같은 동작을 하는 클로저를 비교용으로 정의했어요. 관련 부분을 정렬하기 위해 공백을 조금 넣었죠. 이걸 보면 클로저 구문이 파이프를 쓰는 점과 선택 사항인 구문의 양을 빼면 함수 구문과 얼마나 비슷한지 알 수 있어요:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
첫 줄은 함수 정의이고, 두 번째 줄은 완전히 어노테이션된 클로저 정의예요. 세 번째 줄에서는 타입 어노테이션을 뺐고요, 네 번째 줄에서는 중괄호까지 제거했어요. 클로저 본문이 표현식 하나뿐이라 중괄호는 선택 사항이거든요. 이것들은 모두 유효한 정의이며 호출하면 같은 동작을 보여요. add_one_v3와 add_one_v4 줄은 사용처에서 타입이 추론되기 때문에 컴파일하려면 클로저를 평가해야 해요. let v = Vec::new();도 러스트가 타입을 추론할 수 있도록 타입 어노테이션이나 어떤 타입의 값이 Vec에 들어가야 하는 것과 비슷하죠.
클로저 정의에서 컴파일러는 각 매개변수와 반환 값에 대해 구체적인 타입 하나를 추론해요. 예를 들어 Listing 13-3은 받은 값을 그대로 반환하는 짧은 클로저의 정의인데, 이 예시 목적 외에는 별로 쓸모없어요. 정의에 타입 어노테이션을 하나도 추가하지 않았다는 점에 주목하세요. 타입 어노테이션이 없기 때문에 어떤 타입으로든 클로저를 호출할 수 있고, 여기서는 처음에 String으로 호출했어요. 그런 다음 example_closure를 정수로 호출하면 오류가 나요.
Filename: src/main.rs
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
Listing 13-3: 타입이 추론된 클로저를 두 가지 다른 타입으로 호출 시도하기
컴파일러는 이렇게 오류를 알려줘요:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
처음 example_closure를 String 값으로 호출하면 컴파일러가 x의 타입과 클로저의 반환 타입을 String으로 추론해요. 그 타입들은 example_closure 안의 클로저에 고정되고, 같은 클로저에 다른 타입을 쓰려 하면 타입 오류가 나는 거죠.
참조 캡처하기 또는 소유권 이동하기 (Capturing References or Moving Ownership)
클로저는 환경에서 값을 캡처하는 방법이 세 가지인데, 함수가 매개변수를 받는 세 가지 방법과 정확히 대응돼요. 즉 불변으로 빌리기, 가변으로 빌리기, 소유권 가져가기예요. 클로저는 본문이 캡처한 값으로 무엇을 하느냐에 따라 이 중 어느 것을 쓸지 결정해요.
Listing 13-4에서는 list라는 벡터의 불변 참조를 캡처하는 클로저를 정의해요. 값을 출력하기 위해 불변 참조만 있으면 되기 때문이죠.
Filename: src/main.rs
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
Listing 13-4: 불변 참조를 캡처하는 클로저 정의와 호출
이 예시는 또 변수가 클로저 정의에 바인딩될 수 있고, 이후에 변수 이름을 함수 이름처럼 쓰고 괄호를 붙여 클로저를 호출할 수 있다는 것도 보여줘요.
list에 대한 불변 참조는 여러 개 동시에 가질 수 있으므로, list는 클로저 정의 전의 코드에서도, 클로저 정의 후 호출 전에도, 클로저 호출 후에도 계속 접근할 수 있어요. 이 코드는 컴파일되고 실행되며 다음과 같이 출력해요:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
다음으로 Listing 13-5에서는 클로저 본문을 바꿔 list 벡터에 요소를 추가하게 했어요. 이제 클로저는 가변 참조를 캡처해요.
Filename: src/main.rs
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
Listing 13-5: 가변 참조를 캡처하는 클로저 정의와 호출
이 코드는 컴파일되고 실행되며 이렇게 출력돼요:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
borrows_mutably 클로저의 정의와 호출 사이에 더 이상 println!이 없다는 점에 주목하세요. borrows_mutably가 정의될 때 list에 대한 가변 참조를 캡처하는데, 클로저 호출 후에는 다시 쓰지 않으므로 가변 빌림이 끝나요. 클로저 정의와 호출 사이에서 출력용 불변 빌림은 허용되지 않아요. 가변 빌림이 있는 동안에는 다른 빌림이 허용되지 않거든요. 그 자리에 println!을 넣어 보면 어떤 오류가 나는지 확인해 보세요!
클로저 본문이 꼭 필요하지 않더라도 환경에서 사용하는 값의 소유권을 가져가도록 강제하고 싶다면, 매개변수 목록 앞에 move 키워드를 붙이면 돼요.
이 기법은 대부분 클로저를 새 스레드에 넘길 때 데이터를 이동시켜 새 스레드가 소유하게 하는 데 유용해요. 스레드와 왜 쓰는지는 16장 동시성에서 자세히 다루겠지만, 지금은 move 키워드가 필요해서 클로저로 새 스레드를 생성하는 걸 간단히 살펴볼게요. Listing 13-6은 Listing 13-4를 수정해 메인 스레드 대신 새 스레드에서 벡터를 출력하도록 한 거예요.
Filename: src/main.rs
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
Listing 13-6: move를 사용해 스레드용 클로저가 list의 소유권을 가져가도록 강제하기
새 스레드를 생성하면서 해당 스레드가 실행할 클로저를 인자로 넘겼어요. 클로저 본문은 list를 출력해요. Listing 13-4에서 클로저가 list를 불변 참조로만 캡처했던 건 출력에 필요한 list 접근이 그게 전부였기 때문이에요. 그런데 이 예시에서는 클로저 본문이 여전히 불변 참조만 필요함에도, 클로저 정의 앞에 move 키워드를 붙여 list가 클로저 안으로 이동해야 한다고 명시했어요. 메인 스레드가 새 스레드에서 join을 호출하기 전에 다른 작업을 더 수행한다면, 새 스레드가 메인 스레드보다 먼저 끝나거나 메인 스레드가 먼저 끝날 수 있어요. 메인 스레드가 list의 소유권을 유지한 채 새 스레드보다 먼저 끝나 list를 drop해 버리면, 스레드 안의 불변 참조가 무효가 되거든요. 그래서 컴파일러는 참조가 유효하도록 list가 새 스레드에 주어지는 클로저 안으로 이동하도록 요구해요. move 키워드를 빼거나 클로저 정의 후 메인 스레드에서 list를 사용해 보면 어떤 컴파일러 오류가 나는지 확인해 보세요!
캡처한 값 클로저 밖으로 이동하기 (Moving Captured Values Out of Closures)
클로저가 정의된 환경에서 값을 참조로 캡처하거나 소유권을 캡처하면(즉 무엇이 클로저 안으로 이동하는지에 영향), 나중에 클로저가 평가될 때 본문의 코드가 참조나 값에 무슨 일을 하는지 정의해요(즉 무엇이 클로저 밖으로 이동하는지에 영향을 주죠).
클로저 본문이 할 수 있는 일은 다음과 같아요. 캡처한 값을 클로저 밖으로 이동시키기, 캡처한 값을 변형하기, 이동도 변형도 하지 않기, 아니면 처음부터 환경에서 아무것도 캡처하지 않기.
클로저가 환경의 값을 어떻게 캡처하고 처리하는지는 클로저가 구현하는 트레이트에 영향을 줘요. 트레이트는 함수와 구조체가 어떤 종류의 클로저를 쓸 수 있는지 명시하는 방법이에요. 클로저는 본문이 값을 어떻게 처리하느냐에 따라 이 Fn 트레이트 중 하나, 둘, 또는 셋을 **더해가는 방식(additive)**으로 자동 구현해요:
-
FnOnce는 한 번만 호출할 수 있는 클로저에 적용돼요. 모든 클로저는 호출될 수 있으므로 최소한 이 트레이트는 구현해요. 캡처한 값을 본문 밖으로 이동시키는 클로저는 한 번만 호출될 수 있으므로FnOnce만 구현하고 다른Fn트레이트는 구현하지 않아요. -
FnMut는 캡처한 값을 본문 밖으로 이동시키지 않지만 변형할 수 있는 클로저에 적용돼요. 이런 클로저는 여러 번 호출될 수 있어요. -
Fn는 캡처한 값을 본문 밖으로 이동시키지도 않고 변형하지도 않는 클로저, 그리고 환경에서 아무것도 캡처하지 않는 클로저에 적용돼요. 이런 클로저는 환경을 변형하지 않고 여러 번 호출될 수 있는데, 이는 클로저를 동시에 여러 번 호출하는 경우처럼 중요해요.
Listing 13-1에서 쓴 Option<T>의 unwrap_or_else 메서드 정의를 살펴볼게요:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
T는 Option의 Some 변형에 저장된 값의 타입을 나타내는 제네릭 타입이라는 걸 기억하세요. 그 T 타입은 unwrap_or_else 함수의 반환 타입이기도 해요. 예를 들어 Option<String>에 unwrap_or_else를 호출하는 코드는 String을 얻게 되죠.
다음으로 unwrap_or_else 함수에 추가 제네릭 타입 매개변수 F가 있다는 점을 보세요. F 타입은 f라는 매개변수의 타입인데, unwrap_or_else를 호출할 때 우리가 제공하는 클로저예요.
제네릭 타입 F에 지정된 트레이트 바운드는 FnOnce() -> T예요. 즉 F는 한 번 호출될 수 있고, 인자를 받지 않고, T를 반환할 수 있어야 한다는 뜻이에요. 트레이트 바운드에 FnOnce를 쓴 것은 unwrap_or_else가 f를 두 번 이상 호출하지 않겠다는 제약을 표현한 거예요. unwrap_or_else 본문을 보면 Option이 Some이면 f를 호출하지 않고, None이면 f를 한 번 호출해요. 모든 클로저가 FnOnce를 구현하므로 unwrap_or_else는 세 종류의 클로저를 모두 받아들일 수 있어 최대한 유연해요.
참고: 환경에서 값을 캡처할 필요가 없는 경우라면
Fn트레이트 중 하나를 구현하는 무언가가 필요한 자리에 클로저 대신 함수의 이름을 쓸 수 있어요. 예를 들어Option<Vec<T>>값에서 값이None일 때 비어 있는 새 벡터를 얻기 위해unwrap_or_else(Vec::new)처럼 호출할 수 있죠. 컴파일러는 함수 정의에 대해 적용 가능한Fn트레이트 중 어느 것이든 자동으로 구현해 줘요.
이제 슬라이스에 정의된 표준 라이브러리 메서드 sort_by_key를 보면서, 이게 unwrap_or_else와 어떻게 다르고 왜 트레이트 바운드로 FnOnce 대신 FnMut를 쓰는지 알아볼게요. 이 클로저는 현재 고려 중인 슬라이스 항목에 대한 참조 형태의 인자를 하나 받아서, 정렬 가능한 K 타입 값을 반환해요. 이 함수는 슬라이스를 각 항목의 특정 속성으로 정렬하고 싶을 때 유용해요. Listing 13-7에서는 Rectangle 인스턴스 목록을 width 속성 기준으로 낮은 것부터 높은 것까지 정렬하기 위해 sort_by_key를 사용해요.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
Listing 13-7: sort_by_key를 사용해 사각형을 width로 정렬하기
이 코드는 다음과 같이 출력해요:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
sort_by_key가 FnMut 클로저를 받도록 정의된 이유는 클로저를 여러 번 호출하기 때문이에요. 슬라이스의 각 항목마다 한 번씩이죠. 클로저 |r| r.width는 환경에서 아무것도 캡처하지도, 변형하지도, 밖으로 이동시키지도 않으므로 트레이트 바운드 요구 사항을 만족해요.
반대로 Listing 13-8은 환경에서 값을 밖으로 이동시키기 때문에 FnOnce 트레이트만 구현하는 클로저의 예시예요. 컴파일러는 이 클로저를 sort_by_key에 사용하지 못하게 해요.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
Listing 13-8: sort_by_key와 함께 FnOnce 클로저 사용 시도하기
이건 list를 정렬할 때 sort_by_key가 클로저를 몇 번 호출하는지 세어 보려는 억지스럽고 복잡한(그리고 동작하지 않는) 방법이에요. 이 코드는 클로저 환경에서 온 String인 value를 sort_operations 벡터에 push해서 호출 횟수를 세려고 해요. 클로저는 value를 캡처한 다음 value의 소유권을 sort_operations 벡터로 이전하면서 value를 클로저 밖으로 이동시켜요. 이 클로저는 한 번만 호출될 수 있어요. 두 번째 호출하려 하면, value가 더 이상 환경에 없어서 sort_operations에 다시 push할 수 없거든요! 그래서 이 클로저는 FnOnce만 구현해요. 이 코드를 컴파일하면 해당 클로저가 FnMut를 구현해야 하므로 value를 클로저 밖으로 이동할 수 없다는 오류가 나요:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
오류가 클로저 본문에서 value를 환경 밖으로 이동시키는 줄을 가리켜요. 고치려면 클로저 본문이 환경 밖으로 값을 이동시키지 않도록 바꿔야 해요. 환경에 카운터를 두고 클로저 본문에서 그 값을 증가시키는 게 클로저 호출 횟수를 세는 더 간단한 방법이에요. Listing 13-9의 클로저는 num_sort_operations 카운터에 대한 가변 참조만 캡처하므로 여러 번 호출될 수 있어서 sort_by_key와 함께 동작해요.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listing 13-9: sort_by_key와 함께 FnMut 클로저 사용은 허용된다.
Fn 트레이트는 클로저를 활용하는 함수나 타입을 정의하거나 사용할 때 중요해요. 다음 절에서는 반복자(iterator)를 다룰게요. 많은 반복자 메서드가 클로저 인자를 받으니, 이 클로저 내용을 기억하며 넘어가죠!