Rust Fn, FnMut, FnOnce 클로저 트레이트
클로저 페이지에서는 클로저를 쓰는 법, 클로저가 변수를 캡처하는 방식, 캡처한 값을 바꾸는 방법을 배웠어요. 이번 페이지는 그다음 질문에 답해요. 클로저를 받는 함수를 직접 만들려면 어떻게 써야 할까요? 그리고 함수에서 클로저를 돌려주려면요?
답은 작은 트레이트 세 개, Fn, FnMut, FnOnce예요. 도서관 이용 규칙을 떠올려 보세요. 책을 읽기만 하는 사람이 있고(Fn), 여백에 메모를 적는 사람이 있고(FnMut), 책을 집에 가져가서 돌려주지 않는 사람도 있어요(FnOnce). Rust는 클로저가 하는 일을 보고 그에 맞는 이름표를 붙여 줘요. 처음에는 트레이트가 무섭게 보여도 걱정하지 마세요. 딱 세 개뿐이고, 어떤 걸 붙일지는 컴파일러가 알아서 골라 줘요.
클로저에 트레이트가 필요한 이유
섹션 제목: “클로저에 트레이트가 필요한 이유”모든 클로저는 컴파일러가 만들어 낸 자기만의 고유한 타입을 가져요. 그 타입의 이름은 코드로 쓸 수 없어요. 생김새가 완전히 똑같은 클로저 두 개도 타입은 서로 달라요.
fn main() { let mut rules = vec![|price: u32| price * 2]; rules.push(|price: u32| price * 2);}error[E0308]: mismatched types --> src/main.rs:3:16 |3 | rules.push(|price: u32| price * 2); | ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure | = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait object컴파일러는 “타입이 맞지 않아요. 클로저를 기대했는데 다른 클로저가 왔어요”라고 말하고, “똑같이 생긴 클로저라도 같은 타입은 아니다”라고 덧붙여요.
그래서 함수는 “X 타입의 클로저를 주세요”라고 말할 수 없어요. 대신 “이렇게 호출할 수 있는 타입이면 뭐든 주세요”라고 말해요. 이게 트레이트 바운드예요. 제네릭과 트레이트 페이지에서 본 것과 똑같아요.
fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 { rule(price)}
fn main() { let coupon = 15; let half_price = |price: u32| price / 2; let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80)); println!("{}", price_after(use_coupon, 80));}실행 결과:
4065F: Fn(u32) -> u32는 “F는 u32를 넣어 호출할 수 있고, u32를 돌려주는 어떤 타입”이라고 읽으면 돼요. 두 클로저는 타입이 서로 다르지만 둘 다 이 조건에 맞아요.
세 가지 트레이트: Fn, FnMut, FnOnce
섹션 제목: “세 가지 트레이트: Fn, FnMut, FnOnce”컴파일러는 클로저 본문이 캡처한 값으로 무엇을 하는지 살펴봐요.
Fn: 캡처한 값을 읽기만 해요. 몇 번이든 호출할 수 있어요.FnMut: 캡처한 값을 바꿔요. 여러 번 호출할 수 있지만mut이 필요해요.FnOnce: 캡처한 값을 내줘요(밖으로 이동해요). 한 번 호출하면 값이 사라지기 때문에 딱 한 번만 호출할 수 있어요.
fn main() { let shop = String::from("모퉁이 빵집");
// Fn: `shop`을 읽기만 해요 let greet = || println!("{shop}에 오신 걸 환영해요!"); greet(); greet();
// FnMut: `orders`를 바꿔요 let mut orders = 0; let mut take_order = || { orders += 1; println!("주문 #{orders} 접수"); }; take_order(); take_order();
// FnOnce: `receipt`를 호출한 쪽에 넘겨줘요 let receipt = String::from("크루아상 2개, 커피 1잔"); let hand_over = || receipt; let customer_copy = hand_over(); println!("손님이 받은 영수증: {customer_copy}");}실행 결과:
모퉁이 빵집에 오신 걸 환영해요!모퉁이 빵집에 오신 걸 환영해요!주문 #1 접수주문 #2 접수손님이 받은 영수증: 크루아상 2개, 커피 1잔hand_over는 receipt의 복사본이 아니라 receipt 자체를 돌려줘요. 영수증을 한 번 넘겨주고 나면 클로저에는 더 줄 것이 남아 있지 않아요. 그래서 FnOnce예요.
머릿속에 이런 그림을 그려 두면 좋아요. 원 세 개가 안쪽으로 겹쳐 있다고 상상해 보세요.
- 가장 큰 바깥 원은
FnOnce예요. 모든 클로저는 적어도 한 번은 호출할 수 있으니까, 모든 클로저가 여기에 들어가요. - 그 안의 조금 작은 원은
FnMut이에요. 여러 번 반복해서 호출할 수 있는 클로저들이에요. - 한가운데에는
Fn이 있어요. 아무것도 바꾸지 않고 여러 번 호출할 수 있는 클로저들이에요.
그러니까 모든 Fn 클로저는 FnMut이기도 하고 FnOnce이기도 해요. 함수를 만들 때 이게 중요해요. FnOnce를 요구하는 매개변수는 어떤 클로저든 받아요. Fn을 요구하는 매개변수가 가장 까다로워요.
많은 사람이 놀라는 점이 하나 있어요. move 키워드는 트레이트를 정하지 않아요. move || println!("{shop}")은 shop의 소유권을 가져가지만 읽기만 하니까 여전히 Fn이에요. 트레이트를 정하는 건 오직 본문이에요.
클로저를 받는 함수 작성하기
섹션 제목: “클로저를 받는 함수 작성하기”같은 바운드를 쓰는 방법이 세 가지 있어요. 뜻은 모두 같으니까 읽기 편한 걸 고르면 돼요.
// 1. 바운드가 붙은 제네릭 매개변수fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x)}
// 2. 인자 위치의 `impl Fn`: 더 짧고, 이름을 붙일 필요가 없어요fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x))}
// 3. `where` 절: 바운드가 길어질 때 깔끔해요fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>where F: Fn(i32) -> i32,{ scores.iter().map(|&s| f(s)).collect()}
fn main() { let bonus = 10; let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50)); println!("{}", apply_twice(add_bonus, 50)); println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));}실행 결과:
6070[50, 75, 100]add_bonus를 어떻게 세 번이나 넘길 수 있는지 궁금할 수도 있어요. 캡처한 값을 읽기만 하는 클로저는 복사하는 비용이 싸서, Rust가 넘길 때마다 복사해 줘요.
이제 FnMut과 FnOnce를 볼게요. FnMut은 클로저를 호출하면 그 안의 값이 바뀌기 때문에 매개변수에 mut이 필요해요. FnOnce는 한 번 호출하면 그걸로 끝이에요.
fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { for task in tasks { on_done(task); }}
fn on_shutdown<F: FnOnce()>(cleanup: F) { println!("종료하는 중..."); cleanup();}
fn main() { let mut finished = Vec::new(); process_tasks(&["이메일 보내기", "데이터베이스 백업"], |task| { finished.push(task.to_string()); }); println!("완료: {:?}", finished);
on_shutdown(|| { // 이 반복문이 `finished`의 소유권을 가져가므로 이 클로저는 FnOnce예요 for task in finished { println!("보관 완료: {task}"); } });}실행 결과:
완료: ["이메일 보내기", "데이터베이스 백업"]종료하는 중...보관 완료: 이메일 보내기보관 완료: 데이터베이스 백업좋은 원칙이 하나 있어요. 함수가 감당할 수 있는 가장 느슨한 트레이트를 요구하세요. 클로저를 한 번만 호출한다면 FnOnce를 요구하세요. 그러면 호출하는 쪽에서 어떤 클로저든 넘길 수 있어요. 표준 라이브러리에서도 이걸 볼 수 있어요. Option::map은 FnOnce를 받고, Iterator::map은 FnMut을 받아요.
바운드를 잘못 골랐을 때
섹션 제목: “바운드를 잘못 골랐을 때”컴파일러는 맞지 않는 경우를 모두 잡아내고, 이때 나오는 메시지도 아주 친절해요. 함수는 Fn을 요구하는데 클로저가 카운터를 바꾼다고 해 볼게요.
fn run_report<F: Fn()>(f: F) { f(); f();}
fn main() { let mut runs = 0; run_report(|| { runs += 1; println!("보고서 {runs}번째 실행"); });}error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure --> src/main.rs:9:9 |1 | fn run_report<F: Fn()>(f: F) { | - change this to accept `FnMut` instead of `Fn`...8 | run_report(|| { | ---------- -- in this closure | | | expects `Fn` instead of `FnMut`9 | runs += 1; | ^^^^^^^^^ cannot assign“Fn 클로저 안에서 캡처한 변수 runs에 값을 대입할 수 없어요”라는 뜻이에요. 게다가 고치는 방법까지 알려 줘요. Fn 대신 FnMut을 받고(change this to accept FnMut), 매개변수에 mut을 붙이면 돼요. 이렇게 바꾸면 프로그램이 보고서 1번째 실행과 보고서 2번째 실행을 출력해요.
fn run_report<F: FnMut()>(mut f: F) { f(); f();}또 하나의 대표적인 실수는 FnOnce 클로저를 두 번 호출하는 거예요.
fn main() { let report = String::from("3분기 매출"); let send = || { let sent = report; println!("{sent} 보내는 중"); }; send(); send();}error[E0382]: use of moved value: `send` --> src/main.rs:8:5 |7 | send(); | ------ `send` moved due to this call8 | send(); | ^^^^ value used here after move |note: closure cannot be invoked more than once because it moves the variable `report` out of its environment --> src/main.rs:4:20 |4 | let sent = report; | ^^^^^^note에 이유가 그대로 적혀 있어요. 클로저가 변수 report를 환경 밖으로 옮기기 때문에 두 번 이상 호출할 수 없다는 거예요. 첫 번째 호출에서 report를 내줬으니 두 번째 호출 때는 남은 게 없어요. 정말 두 번 호출해야 한다면 대신 빌려 쓰세요(let sent = &report;). 그러면 클로저가 Fn이 돼요. 이런 클로저를 Fn을 요구하는 함수에 넘기면 비슷한 메시지가 나와요. error[E0507]: cannot move out of report, a captured variable in an Fn closure예요.
impl Fn으로 클로저 반환하기
섹션 제목: “impl Fn으로 클로저 반환하기”함수가 클로저를 만들어서 돌려줄 수도 있어요. 반환 타입은 impl Fn(i32) -> i32로 써요. “어떤 클로저 타입인데, 이름은 몰라도 돼요”라는 뜻이에요.
fn make_adder(amount: i32) -> impl Fn(i32) -> i32 { move |x| x + amount}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor}
fn main() { let add_tip = make_adder(5); let triple = make_multiplier(3);
println!("{}", add_tip(20)); println!("{}", triple(7)); println!("{}", add_tip(triple(10)));}실행 결과:
252135왜 move가 필요할까요? move가 없으면 클로저는 amount를 빌리기만 해요. 그런데 amount는 make_adder의 것이라서 make_adder가 반환되는 순간 사라져요. 클로저는 아무것도 없는 곳을 가리키는 참조를 들고 있게 되는 거죠. 그래서 Rust가 거부해요.
error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function --> src/main.rs:2:5 |2 | |x| x + amount | ^^^ ------ `amount` is borrowed here | | | may outlive borrowed value `amount` |help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword |2 | move |x| x + amount | ++++“클로저가 현재 함수보다 오래 살 수 있는데, 현재 함수가 소유한 amount를 빌리고 있어요”라는 뜻이고, help에서 move 키워드를 쓰라고 알려 줘요. move를 붙이면 클로저가 amount의 복사본을 직접 챙겨서 들고 다녀요. 도시락을 남의 냉장고에 두고 오는 대신 직접 들고 가는 것과 같아요.
박스에 담은 클로저: 여러 클로저를 하나의 타입으로
섹션 제목: “박스에 담은 클로저: 여러 클로저를 하나의 타입으로”impl Fn은 숨겨진 타입 하나를 뜻해요. 그래서 if 분기에서는 이 클로저를, else 분기에서는 다른 클로저를 돌려줄 수 없어요. 서로 다른 클로저들을 같은 Vec에 넣을 수도 없어요. 두 경우 모두 각 클로저를 박스에 담으면 돼요. Box<dyn Fn(f64) -> f64>는 “이런 모양을 한 아무 클로저나, 힙에 저장한 것”이라는 뜻이에요. 트레이트 객체 페이지에서 본 것과 같은 아이디어예요.
다음은 이름 붙은 가격 규칙 목록을 순서대로 적용하는 계산대예요.
fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { if express { Box::new(move |total| total + fee * 2.0) } else { Box::new(move |total| total + fee) }}
fn main() { let member_percent = 10.0; let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![ ("회원 할인", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))), ("쿠폰", Box::new(move |total: f64| total - coupon)), ("일반 배송비", shipping_rule(false, 3.5)), ];
let mut total = 80.0; println!("시작: {total:.2}"); for (name, rule) in &rules { total = rule(total); println!("{name} 적용 후: {total:.2}"); }}실행 결과:
시작: 80.00회원 할인 적용 후: 72.00쿠폰 적용 후: 67.00일반 배송비 적용 후: 70.50박스에 담으면 힙 할당이 조금 생기고, 호출할 때마다 포인터를 한 번 거쳐요. 그래도 문제가 되는 경우는 거의 없어요. 클로저 타입이 하나라면 impl Fn을, 여러 개라면 Box<dyn Fn>을 쓰세요.
구조체에 클로저 저장하기
섹션 제목: “구조체에 클로저 저장하기”구조체도 클로저를 담을 수 있어요. 클로저의 타입 이름은 쓸 수 없으니, 구조체를 그 타입에 대해 제네릭으로 만들어요. 다음은 클릭하면 코드를 실행하는 작은 버튼이에요.
struct Button<F: Fn()> { label: String, on_click: F,}
impl<F: Fn()> Button<F> { fn click(&self) { println!("[{}] 클릭됨", self.label); (self.on_click)(); }}
fn main() { let user = String::from("지민"); let save = Button { label: String::from("저장"), on_click: move || println!("{user}님의 임시 글을 저장하는 중"), };
save.click(); save.click();}실행 결과:
[저장] 클릭됨지민님의 임시 글을 저장하는 중[저장] 클릭됨지민님의 임시 글을 저장하는 중(self.on_click)()의 괄호를 눈여겨보세요. “필드를 꺼낸 다음, 그걸 호출해”라고 Rust에게 알려 주는 괄호예요. 클릭할 때마다 버튼이 캡처한 값을 바꿔야 한다면 F: FnMut()을 쓰고 메서드가 &mut self를 받게 하세요.
함수 포인터: fn(i32) -> i32
섹션 제목: “함수 포인터: fn(i32) -> i32”일반 함수도 이리저리 넘길 수 있어요. 일반 함수의 타입은 fn(i32) -> i32처럼 소문자 fn으로 써요. 이것을 함수 포인터(function pointer)라고 하는데, 코드가 있는 위치를 가리키는 단순한 주소예요. 아무것도 캡처하지 않는 클로저도 함수 포인터로 바뀔 수 있어요.
fn double(x: i32) -> i32 { x * 2}
fn square(x: i32) -> i32 { x * x}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 { op(x)}
fn main() { println!("{}", apply_op(double, 4)); println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1]; let mut value = 3; for step in steps { value = step(value); } println!("{value}");}실행 결과:
8337함수 포인터는 모두 같은 타입이라서 박스에 담지 않아도 배열에 넣을 수 있어요. 단, 무언가를 캡처한 클로저는 함수 포인터가 될 수 없어요. 그러면 컴파일러가 closures can only be coerced to fn types if they do not capture any variables(변수를 캡처하지 않은 클로저만 fn 타입으로 바꿀 수 있다)라고 알려 줘요. 반대 방향은 좋은 소식이에요. 모든 일반 함수는 Fn, FnMut, FnOnce도 구현하니까, 앞에서 만든 apply에 double을 넘길 수 있어요. 그래서 대부분의 코드는 클로저 트레이트를 써요. 둘 다 받을 수 있으니까요.
자주 하는 실수
섹션 제목: “자주 하는 실수”FnMut 매개변수에 mut을 빠뜨리기. FnMut을 호출하면 그 값이 바뀌기 때문에 바인딩이 mut이어야 해요.
error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable --> src/main.rs:3:9 |3 | on_done(task); | ^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { | +++“on_done이 가변으로 선언되지 않아서 가변으로 빌릴 수 없어요”라는 뜻이고, help가 mut을 붙일 자리를 보여 줘요.
-> impl Fn에서 서로 다른 클로저 반환하기. 생김새가 비슷해도 클로저 두 개는 타입도 두 개예요.
fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 { if express { move |total| total + fee * 2.0 } else { move |total| total + fee }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if express {3 | | move |total| total + fee * 2.0 | | ------------------------------ | | | | | the expected closure | | expected because of this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `if` and `else` have incompatible types | = note: expected closure `{closure@src/main.rs:3:9: 3:21}` found closure `{closure@src/main.rs:5:9: 5:21}` = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objecthelp: you could change the return type to be a boxed trait object |1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new(move |total| total + fee * 2.0)4 | } else {5 ~ Box::new(move |total| total + fee) |컴파일러가 제안하는 대로 하면 돼요. Box<dyn Fn(f64) -> f64>를 반환하고, 각 클로저를 Box::new(...)로 감싸세요.
클로저 필드를 메서드처럼 호출하기. self.on_click()이라고 쓰면 Rust는 on_click이라는 이름의 메서드를 찾아요.
error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope --> src/main.rs:9:14 |9 | self.on_click(); | ^^^^^^^^ field, not a method |help: to call the type parameter stored in `on_click`, surround the field access with parentheses |9 | (self.on_click)(); | + +“메서드가 아니라 필드예요”라고 알려 주고, 필드에 저장된 값을 호출하려면 괄호로 감싸라고 제안해요.
연습 문제
섹션 제목: “연습 문제”1. 번호표 발급기
섹션 제목: “1. 번호표 발급기”클로저를 반환하는 함수 ticket_dispenser(start: u32)를 작성하세요. 클로저를 호출할 때마다 다음 번호표가 나와요. 처음에는 start, 그다음은 start + 1, 이런 식이에요. 발급기를 두 개(100번부터 시작하는 빵집, 1번부터 시작하는 약국) 만들어서 서로 따로 센다는 걸 보여 주세요. 힌트: 클로저가 자기 카운터를 바꾸니까 반환 타입은 impl FnMut() -> u32이고, move가 필요해요.
정답 보기
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 { let mut next = start; move || { let ticket = next; next += 1; ticket }}
fn main() { let mut bakery = ticket_dispenser(100); let mut pharmacy = ticket_dispenser(1);
println!("빵집: {}", bakery()); println!("빵집: {}", bakery()); println!("약국: {}", pharmacy()); println!("빵집: {}", bakery());}실행 결과:
빵집: 100빵집: 101약국: 1빵집: 102발급기마다 자기만의 next를 안으로 옮겨 가져갔기 때문에, 두 발급기가 카운터를 공유하는 일은 없어요. bakery와 pharmacy는 FnMut이라서 변수에 mut이 필요해요.
2. 재시도 도우미
섹션 제목: “2. 재시도 도우미”retry(max_attempts, task)를 작성하세요. task가 Ok를 돌려줄 때까지, 또는 max_attempts번 시도할 때까지 task를 호출해요. 실패한 시도마다 한 줄씩 출력하세요. 두 번은 "서버가 바빠요"로 실패하고 그다음에 성공하는 클로저로 테스트해 보세요. 힌트: 테스트용 클로저가 자기가 호출된 횟수를 세니까 바운드는 FnMut() -> Result<String, String>이고, 매개변수에 mut이 필요해요.
정답 보기
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>where F: FnMut() -> Result<String, String>,{ for attempt in 1..=max_attempts { match task() { Ok(value) => return Ok(value), Err(e) => println!("{attempt}번째 시도 실패: {e}"), } } Err(String::from("포기했어요"))}
fn main() { let mut calls = 0; let result = retry(5, || { calls += 1; if calls < 3 { Err(String::from("서버가 바빠요")) } else { Ok(String::from("데이터 다운로드 완료")) } }); println!("{calls}번 호출한 결과: {:?}", result);}실행 결과:
1번째 시도 실패: 서버가 바빠요2번째 시도 실패: 서버가 바빠요3번 호출한 결과: Ok("데이터 다운로드 완료")retry는 task를 여러 번 호출하니까 FnOnce로는 안 되고, task가 calls를 바꾸니까 Fn으로도 안 돼요. FnMut이 딱 맞아요. retry가 반환되면 클로저의 일은 끝나니까, main에서 다시 calls를 읽을 수 있어요.
- 모든 클로저는 자기만의 고유한 타입을 가져요. 그래서 함수는
F: Fn(i32) -> i32같은 트레이트 바운드로 클로저를 받아요. Fn은 캡처한 값을 읽기만 하고,FnMut은 바꾸고,FnOnce는 밖으로 옮기며 한 번만 호출할 수 있어요. 트레이트를 정하는 건move가 아니라 본문이에요.- 모든
Fn은FnMut이면서FnOnce이기도 해요. 함수에 필요한 가장 느슨한 트레이트를 요구하세요. - 바운드는
<F: Fn(..)>,impl Fn(..),where절 중 하나로 써요.FnMut매개변수에는mut이 필요해요. - 클로저 하나를 반환할 때는
-> impl Fn(..)을 쓰고(보통move와 함께), 분기마다 다른 클로저를 반환하거나Vec에 담을 때는Box<dyn Fn(..)>을 써요. fn(i32) -> i32는 함수 포인터예요. 일반 함수와 아무것도 캡처하지 않는 클로저가 여기에 맞아요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 어떤 클로저가 캡처한 변수에 total += price를 해요. 이 클로저가 구현하는 가장 엄격한 트레이트는 무엇일까요?
2. 함수가 클로저 매개변수를 정확히 한 번만 호출해요. 가장 많은 클로저를 받을 수 있는 바운드는 무엇일까요?
3. make_adder(amount: i32) -> impl Fn(i32) -> i32에 move |x| x + amount가 필요한 이유는 무엇일까요?
4. 서로 다른 할인 클로저 여러 개를 담는 Vec을 만들고 싶어요. 요소의 타입은 무엇이어야 할까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 캡처된 값을 클로저 밖으로 이동하기와 Fn 트레이트 — The Rust Programming Language 한국어판: 컴파일러가
Fn,FnMut,FnOnce중 무엇을 고르는지 설명하는 공식 문서예요. Fn,FnMut,FnOnce— 표준 라이브러리 문서 (영어): 각 트레이트의 정의와, 언제 쓰는지 보여 주는 짧은 예제가 있어요.- Input parameters — Rust by Example (영어): 세 가지 바운드로 클로저를 받는 함수 예제를 직접 실행해 볼 수 있어요.
- Output parameters — Rust by Example (영어):
impl Fn으로 클로저를 반환하는 방법과move가 필요한 이유를 다뤄요. - Closure types — The Rust Reference (영어): 클로저가 값을 캡처하는 방식과 어떤 트레이트를 구현하는지에 대한 정확한 규칙이에요.