클로저: 자신의 환경을 캡처하는 익명 함수

러스트의 클로저는 변수에 저장하거나 다른 함수에 인수로 전달할 수 있는 익명 함수입니다. 한 곳에서 클로저를 만들고 다른 컨텍스트의 다른 곳에서 이를 호출하여 평가할 수 있습니다. 함수와 다르게 클로저는 정의된 스코프에서 값을 캡처할 수 있습니다. 앞으로 클로저의 이러한 기능이 어떻게 코드 재사용과 동작 커스터마이징을 가능하게 하는지 살펴볼 것입니다.

클로저로 환경 캡처하기

먼저 클로저가 정의된 환경으로부터 나중에 사용할 목적으로 값을 캡처하는 방법을 시험해 보겠습니다. 여기 시나리오가 있습니다: 종종 우리 티셔츠 회사는 프로모션으로 메일링 리스트에 있는 사람들에게 독점 공급하는 한정판 티셔츠를 증정합니다. 메일링 리스트에 있는 사람들은 추가로 자신의 프로파일에 제일 좋아하는 색상을 추가할 수 있습니다. 만일 무료 티셔츠에 추첨된 사람이 좋아하는 색상을 설정해 두었다면, 그 색상의 티셔츠를 받게 됩니다. 만일 그 사람이 좋아하는 색상을 특정하지 않았다면 회사가 현재 제일 많이 가지고 있는 색상을 받게 됩니다.

이를 구현하는 방법은 여러 가지가 있습니다. 이번 예제에서는 RedBlue 배리언트가 있는 ShirtColor라는 열거형을 이용해 보겠습니다. (단순한 예제를 위해 가능한 색상을 제한했습니다.) 회사의 재고는 Inventory 구조체로 표현하는데 여기에는 shirts라는 이름의 필드가 있고, 이 필드는 현재 재고에 있는 셔츠 색상을 나타내는 Vec<ShirtColor> 타입입니다. Inventory에 정의된 giveaway 메서드는 무료 티셔츠를 타게 된 사람의 추가 색상 설정값을 얻어와서 그 사람이 받게 될 셔츠 색상을 반환합니다. 이러한 설정이 예제 13-1에 있습니다:

파일명: 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
    );
}

예제 13-1: 셔츠 회사 증정 상황

main에 정의된 store에는 이 한정판 프로모션 배포를 위해 남은 두 개의 파란색 셔츠와 하나의 빨간색 셔츠가 있습니다. 여기서 빨간색 셔츠로 설정한 고객과 색상 설정이 없는 고객에 대하여 giveaway 메서드를 호출하였습니다.

다시 한번 말하지만, 이 코드는 여러 가지 방법으로 구현될 수 있고, 여기서는 클로저에 초점을 맞추기 위해서 클로저가 사용된 giveaway 메서드 본문을 제외하고는 이미 배운 개념만 사용했습니다. giveaway 메서드에서는 고객의 설정을 Option<ShirtColor> 타입의 매개변수 user_preferenceunwrap_or_else 메서드를 호출합니다. Option<T>unwrap_or_else 메서드 Option<T>는 표준 라이브러리에 정의되어 있습니다. 이것은 하나의 인수를 받습니다: 바로 아무런 인수도 없고 T 값을 반환하는 클로저 입니다. (이때 TOption<T>Some 배리언트에 저장되는 타입과 동일하며, 지금의 경우 ShirtColor입니다.) 만일 Option<T>Some 배리언트라면, unwrap_or_else는 그 Some 안에 들어있는 값을 반환합니다. 만일 Option<T>None 배리언트라면, unwrap_or_else는 이 클로저를 호출하여 클로저가 반환한 값을 반환해 줍니다.

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 [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()를 호출하는 클로저를 넘겼다는 것입니다. 표준 라이브러리는 우리가 정의한 InventoryShirtColor 타입이나, 혹은 이 시나리오에서 우리가 사용하고자 하는 로직에 대해 전혀 알 필요가 없습니다. 이 클로저는 self Inventory 인스턴스의 불변 참조자를 캡처하여 우리가 지정한 코드와 함께 이 값을 unwrap_or_else 메서드에 넘겨줍니다. 반면에 함수는 이런 방식으로 자신의 환경을 캡처할 수 없습니다.

클로저 타입 추론과 명시

함수와 클로저 간의 차이점은 더 있습니다. 클로저는 보통 fn 함수에서처럼 매개변수 혹은 반환 값의 타입을 명시하도록 요구하지 않습니다. 함수의 타입 명시는 그 타입이 사용자들에게 노출되는 명시적인 인터페이스의 일부분이기 때문에 요구됩니다. 이러한 인터페이스를 엄격하게 정의하는 것은 함수가 어떤 타입의 값을 사용하고 반환하는지에 대해 모두가 납득하는 것을 보증하는 데에 중요합니다. 반면에 클로저는 함수처럼 노출된 인터페이스로 사용되지 않습니다: 클로저는 이름이 지어지거나 라이브러리의 사용자들에게 노출되지 않은 채로 변수에 저장되고 사용됩니다.

클로저는 통상적으로 짧고, 임의의 시나리오가 아니라 짧은 컨텍스트 내에서만 관련됩니다. 이러한 한정된 컨텍스트 내에서, 컴파일러는 대부분의 변수에 대한 타입을 추론하는 방법과 비슷한 식으로 클로저의 매개변수와 반환 타입을 추론합니다. (컴파일러가 클로저 타입을 명시하도록 요구하는 경우도 드물게는 있습니다.)

변수와 마찬가지로, 꼭 필요한 것보다 더 장황해지더라도 명시성과 명확성을 올리고 싶다면 타입 명시를 추가할 수 있습니다. 클로저에 대한 타입 명시를 추가하면 예제 13-2의 정의와 비슷해집니다. 이 예제에서는 예제 13-1에서처럼 인수로 전달하는 위치에서 클로저를 정의하기보다는, 클로저를 정의하여 변수에 저장하고 있습니다.

파일명: 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);
}

예제 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_v3add_one_v4 줄을 컴파일하기 위해서는 이 클로저들이 평가되는 곳이 필요한데, 그 이유는 이 클로저들이 사용된 곳에서 타입이 추론될 것이기 때문입니다. 이는 let v = Vec::new();가 러스트에 의해 타입이 추론되기 위해서 타입 명시 혹은 Vec 안에 집어넣을 어떤 타입의 값이 필요한 것과 유사합니다.

클로저 정의에 대하여, 컴파일러는 각각의 매개변수와 반환 값마다 하나의 고정 타입을 추론할 것입니다. 예를 들면 예제 13-3은 자신이 매개변수로 받은 값을 그냥 반환하는 짧은 클로저의 정의를 보여주고 있습니다. 이 클로저는 이 예제 용도 말고는 그다지 유용하진 않습니다. 정의에 아무런 타입 명시를 하지 않았음을 주의하세요. 아무런 타입 명시도 없으므로 아무 타입에 대해서나 이 클로저를 호출할 수 있는데, 여기서는 처음에 String에 대해 호출했습니다. 그런 다음 정수에 대해 example_closure의 호출을 시도한다면, 에러를 얻게 됩니다.

파일명: src/main.rs

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

예제 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);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected struct `String`, found integer
  |             arguments to this function are incorrect
  |
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` due to previous error

처음 String을 가지고 example_closure를 호출하면, 컴파일러는 클로저의 x 타입과 반환 타입이 String이라고 추론합니다. 그러면 이 타입이 example_closure 클로저에 고정되고, 그다음 동일한 클로저를 가지고 다른 타입에 대해 사용 시도했을 때 타입 에러를 얻게 됩니다.

참조자를 캡처하거나 소유권 이동하기

클로저는 세 가지 방식으로 자신의 환경으로부터 값을 캡처할 수 있는데, 이는 함수가 매개변수를 취하는 세 가지 방식과 직접적으로 대응됩니다: 불변으로 빌려오기, 가변으로 빌려오기, 그리고 소유권 이동이죠. 클로저는 캡처된 값이 쓰이는 방식에 기초하여 캡처할 방법을 결정할 것입니다.

예제 13-4에서 정의한 클로저는 list라는 이름의 벡터에 대한 불변 참조자를 캡처하는데, 이는 그저 값을 출력하기 위한 불변 참조자가 필요한 상태이기 때문입니다:

파일명: 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);
}

예제 13-4: 불변 참조자를 캡처하는 클로저의 정의와 호출

또한 이 예제는 어떤 변수가 클로저의 정의에 바인딩될 수 있고, 이 클로저는 나중에 마치 변수 이름이 함수 이름인 것처럼 변수 이름과 괄호를 사용하여 호출될 수 있음을 보여줍니다.

list에 대한 여러 개의 불변 참조자를 동시에 가질 수 있기 때문에, list에는 클로저 정의 전이나 후 뿐만 아니라 클로저의 호출 전과 후에도 여전히 접근이 가능합니다. 이 코드는 컴파일 및 실행이 되고, 다음을 출력합니다:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [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]

다음으로 예제 13-5에서는 클로저의 본문을 바꾸어 list 벡터에 요소를 추가하도록 했습니다. 클로저는 이제 가변 참조자를 캡처합니다:

파일명: 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);
}

예제 13-5: 가변 참조자를 캡처하는 클로저의 정의와 호출

이 코드는 컴파일되고, 실행되고, 다음을 출력합니다:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [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 키워드가 필요한 클로저를 사용하는 새 스레드의 생성을 살짝 보겠습니다. 예제 13-6은 예제 13-4를 수정하여 메인 스레드가 아닌 새 스레드에서 벡터를 출력하는 코드를 보여줍니다:

파일명: 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();
}

예제 13-6: 스레드에 대한 클로저가 list의 소유권을 갖도록 move 사용하기

여기서는 새 스레드를 생성하여 여기에 인수로 실행될 클로저를 제공합니다. 클로저의 본문에서는 리스트를 출력합니다. 예제 13-4에서는 클로저가 불변 참조자만 사용하여 list를 캡처했는데, 이것이 list를 출력하기 위해 필요한 최소한의 접근 수준이기 때문입니다. 이 예제에서는 클로저 본문이 여전히 불변 참조자만 필요할지라도, 클로저 정의의 앞부분에 move 키워드를 집어넣어 list가 이동되어야 함을 명시할 필요가 있습니다. 새로운 스레드가 메인 스레드의 나머지 부분이 끝나기 전에 끝날 수도 있고, 혹은 메인 스레드가 먼저 끝날 수도 있습니다. 만일 메인 스레드가 list의 소유권을 유지하고 있는데 새 스레드가 끝나기 전에 끝나버려서 list를 제거한다면, 새 스레드의 불변 참조자는 유효하지 않게 될 것입니다. 따라서 컴파일러는 list를 새 스레드에 제공될 클로저로 이동시켜 참조자가 유효하도록 요구합니다. move 키워드를 제거하거나 클로저가 정의된 이후 메인 스레드에서 list를 사용하면 어떤 컴파일러 에러를 얻게 되는지 시도해 보세요!

캡처된 값을 클로저 밖으로 이동하기와 Fn 트레이트

어떤 클로저가 자신이 정의된 환경으로부터 값의 참조자 혹은 소유권을 캡처하면 (그래서 클로저의 안으로 이동되는 것에 영향을 준다면), 클로저 본문의 코드는 이 클로저가 나중에 평가될 때 그 참조자나 값에 어떤 일이 발생하는지 정의합니다. (그래서 클로저의 밖으로 무언가 이동되는 것에 영향을 줍니다.) 클로저 본문은 다음의 것들을 할 수 있습니다: 캡처된 값을 클로저 밖으로 이동시키기, 캡처된 값을 변형하기, 이동시키지도 변형시키지도 않기, 혹은 시작 단계에서부터 환경으로부터 아무 값도 캡처하지 않기 세 가지 입니다.

클로저가 환경으로부터 값을 캡처하고 다루는 방식은 이 클로저가 구현하는 트레이트에 영향을 주고, 트레이트는 함수와 구조체가 사용할 수 있는 클로저의 종류를 명시할 수 있는 방법입니다. 클로저는 클로저의 본문이 값을 처리하는 방식에 따라서 이 Fn 트레이트들 중 하나, 둘, 혹은 셋 모두를 추가하는 방식으로 자동으로 구현할 것입니다:

  1. FnOnce는 한 번만 호출될 수 있는 클로저에게 적용됩니다. 모든 클로저들은 호출될 수 있으므로, 최소한 이 트레이트는 구현해 둡니다. 캡처된 값을 본문 밖으로 이동시키는 클로저에 대해서는 FnOnce만 구현되며 나머지 Fn 트레이트는 구현되지 않는데, 이는 이 클로저가 딱 한 번만 호출될 수 있기 때문입니다.
  2. FnMut은 본문 밖으로 캡처된 값을 이동시키지는 않지만 값을 변경할 수는 있는 클로저에 대해 적용됩니다. 이러한 클로저는 한 번 이상 호출될 수 있습니다.
  3. Fn은 캡처된 값을 본문 밖으로 이동시키지 않고 캡처된 값을 변경하지도 않는 클로저는 물론, 환경으로부터 아무런 값도 캡처하지 않는 클로저에 적용됩니다. 이러한 클로저는 자신의 환경을 변경시키지 않으면서 한번 이상 호출될 수 있는데, 이는 클로저가 동시에 여러 번 호출되는 등의 경우에서 중요합니다.

예제 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(),
        }
    }
}

TOptionSome 배리언트 내 값의 타입을 나타내는 제네릭 타입임을 상기합시다. 이 타입 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_elsef를 아무리 많아야 한 번만 호출할 것이라는 제약 사항을 표현해 줍니다. unwrap_or_else의 본문을 보면 OptionSome일 때 f가 호출되지 않을 것임을 알 수 있습니다. 만일 OptionNone라면 f가 한 번만 호출될 것입니다. 모든 클로저가 FnOnce를 구현하므로 unwrap_or_else는 가장 다양한 종류의 클로저를 허용하며 될 수 있는 한 유연하게 동작합니다.

Note: 함수도 이 세 종류의 Fn 트레이트를 모두 구현할 수 있습니다. 만일 하고자 하는 것이 환경으로부터 값을 캡처할 필요가 없다면, Fn 트레이트 중 하나를 구현한 무언가가 필요한 곳에 클로저 대신 함수 이름을 사용할 수 있습니다. 예를 들면 Option<Vec<T>>의 값 상에서 unwrap_or_else(Vec::new)를 호출하여 이 값이 None일 경우 비어있는 새 벡터를 얻을 수 있습니다.

이제 표준 라이브러리에서 슬라이스 상에 정의되어 있는 메서드인 sort_by_key를 살펴보면서 unwrap_or_else와는 어떻게 다르고 sort_by_key의 트레이트 바운드는 왜 FnOnce 대신 FnMut인지를 알아봅시다. 이 클로저는 처리하려는 슬라이스에서 현재 아이템에 대한 참조자를 하나의 인수로 받아서, 순서를 매길 수 있는 K 타입의 값을 반환합니다. 이 함수는 각 아이템의 특정 속성을 이용하여 슬라이스를 정렬하고 싶을 때 유용합니다. 예제 13-7에는 Rectangle 인스턴스의 리스트가 있고 sort_by_key를 사용하여 width 속성을 낮은 것부터 높은 순으로 정렬합니다:

파일명: 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);
}

예제 13-7: sort_by_key를 사용하여 너비로 사각형 정렬하기

이 코드는 다음을 출력합니다:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [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_keyFnMut 클로저를 갖도록 정의된 이유는 이 함수가 클로저를 여러 번 호출하기 때문입니다: 슬라이스 내 각 아이템마다 한 번씩요. 클로저 |r| r.width는 자신의 환경으로부터 어떤 것도 캡처나 변형, 혹은 이동을 시키지 않으므로, 트레이트 바운드 요건을 충족합니다.

반면 예제 13-8은 FnOnce 트레이트만 구현한 클로저의 예를 보여주는데, 이 클로저는 환경으로부터 값을 이동시키고 있습니다. 컴파일러는 이 클로저를 sort_by_key에 사용할 수 없게 할 것입니다:

파일명: 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("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

예제 13-8: FnOnce 클로저를 sort_by_key에 사용 시도하기

이는 list를 정렬할 때 sort_by_key가 클로저를 호출하는 횟수를 세려고 시도하는 부자연스럽고 대단히 난해한 (동작하지 않는) 방식입니다. 이 코드는 클로저 환경의 Stringvaluesort_operations 벡터로 밀어 넣는 형태로 횟수 세기를 시도하고 있습니다. 클로저는 value를 캡처한 다음 value의 소유권을 sort_operations 벡터로 보내서 value를 클로저 밖으로 이동시킵니다. 이 클로저는 한 번만 호출될 수 있습니다; 두 번째 호출 시도에서는 value가 더 이상 이 환경에 남아있지 않은데 sort_operations로 밀어 넣으려고 하므로 동작하지 않을 것입니다! 따라서, 이 클로저는 오직 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("by key called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` due to previous error

이 에러는 환경에서 value 값을 빼내는 클로저 본문의 라인을 지적합니다. 이를 고치기 위해서는 클로저 본문을 수정하여 환경에서 값을 이동시키지 않도록 할 필요가 있습니다. sort_by_key가 호출되는 횟수를 세기 위해서는 환경 쪽에 카운터를 유지하면서 클로저 본문에서 이 값을 증가시키는 것이 더 직관적으로 계산하는 방법이겠습니다. 예제 13-9의 클로저는 sort_by_key에서 동작하는데, 이는 num_sort_operation 카운터에 대한 가변 참조자를 캡처할 뿐이라서 한 번 이상 호출이 가능하기 때문입니다:

파일명: 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!("{:#?}, sorted in {num_sort_operations} operations", list);
}

예제 13-9: FnMut 클로저를 sort_by_key에 사용하는 것은 허용됩니다

Fn 트레이트는 클로저를 사용하는 함수 혹은 타입을 정의하고 사용할 때 중요합니다. 다음 절에서는 반복자를 다루려고 합니다. 많은 반복자들이 클로저 인수를 받으니, 계속 진행하면서 이러한 클로저 세부 내용을 새겨둡시다!