동작하는 코드
Rust에서는 이름 없는 함수를 만들 수 있어요. 이걸 클로저(closure) 라고 불러요.
fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5)); // 6
// 여러 줄도 가능해요
let add_and_print = |a: i32, b: i32| -> i32 {
let sum = a + b;
println!("{a} + {b} = {sum}");
sum
};
add_and_print(3, 4); // 3 + 4 = 7
}
|매개변수| 본문 — 이게 클로저의 기본 형태예요. 일반 함수와 비교해볼까요?
// 일반 함수
fn add_one_fn(x: i32) -> i32 {
x + 1
}
// 클로저
let add_one = |x: i32| x + 1;
클로저는 타입을 생략할 수도 있어요. 컴파일러가 추론해줘요.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
직접 해보기
|x| x * x로 제곱을 계산하는 클로저를 만들어보세요.|a, b| if a > b { a } else { b }로 두 수 중 큰 값을 반환하는 클로저를 만들어보세요.
"왜?" — 클로저는 주변을 기억해요
클로저가 일반 함수와 다른 핵심 포인트는 환경 캡처예요. 클로저는 자신이 정의된 곳의 변수를 "잡아서" 사용할 수 있어요.
fn main() {
let threshold = 10;
// 클로저가 바깥의 threshold를 캡처!
let is_big = |x: i32| x > threshold;
println!("{}", is_big(5)); // false
println!("{}", is_big(15)); // true
}
일반 함수에서는 이렇게 할 수 없어요. threshold를 매개변수로 명시적으로 넘겨야 해요.
캡처 방식 3가지
클로저가 변수를 캡처하는 방식에 따라 트레이트가 달라져요. 소유권 규칙과 연결되는 개념이에요!
| 캡처 방식 | 트레이트 | 비유 | 예시 |
| ---------------- | -------- | ---------------- | ----- | --- | ------------------ |
| 불변 참조로 빌림 | Fn | 책을 읽기만 함 | | x | x + threshold |
| 가변 참조로 빌림 | FnMut | 책에 메모를 적음 | | x | { count += 1; x } |
| 소유권을 가져감 | FnOnce | 책을 가져가 버림 | move | x | { drop(name); x } |
fn main() {
// Fn — 불변 참조로 캡처
let name = String::from("Rust");
let greet = || println!("안녕, {name}!");
greet();
greet(); // 여러 번 호출 가능
println!("{name}"); // name 아직 사용 가능
// FnMut — 가변 참조로 캡처
let mut count = 0;
let mut increment = || {
count += 1;
println!("count: {count}");
};
increment(); // count: 1
increment(); // count: 2
// FnOnce — 소유권을 가져감
let message = String::from("bye");
let consume = move || {
println!("{message}");
// message의 소유권이 클로저 안으로 이동
};
consume();
// consume(); // 두 번째 호출도 가능할 수 있지만...
// println!("{message}"); // ❌ message는 이미 이동됨
}
팁: 대부분의 경우 Rust가 캡처 방식을 자동으로 결정해요.
Fn→FnMut→FnOnce순서로 가장 제한적인 것부터 시도해요. 직접 지정할 필요는 거의 없어요!
move 키워드
move를 붙이면 클로저가 캡처하는 모든 변수의 소유권을 강제로 가져가요.
fn main() {
let name = String::from("민수");
// move 없이: name을 빌려서 사용
let greet = || println!("안녕, {name}!");
greet();
println!("아직 사용 가능: {name}");
// move 있으면: name의 소유권이 클로저로 이동
let greet_move = move || println!("안녕, {name}!");
greet_move();
// println!("{name}"); // ❌ 소유권이 이동됨
}
move는 스레드에 데이터를 넘길 때 특히 중요해요. 나중에 배울 내용이에요!
클로저를 인자로 받는 함수
클로저를 다른 함수에 전달할 수 있어요. 이때 트레이트 바운드를 사용해요.
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn main() {
let double = |x| x * 2;
let result = apply_twice(double, 3);
println!("{result}"); // 12 (3 → 6 → 12)
// 일반 함수도 넘길 수 있어요
fn add_ten(x: i32) -> i32 { x + 10 }
let result2 = apply_twice(add_ten, 5);
println!("{result2}"); // 25 (5 → 15 → 25)
}
모듈 21에서 배운 제네릭과 트레이트 바운드가 여기서 쓰여요! F: Fn(i32) -> i32는 "i32를 받아서 i32를 반환하는 함수(또는 클로저)"라는 뜻이에요.
실전 활용: 캐시 클로저
클로저와 구조체를 결합하면 캐시(memoization) 패턴을 만들 수 있어요.
struct Cacher<F: Fn(i32) -> i32> {
calculation: F,
value: Option<i32>,
}
impl<F: Fn(i32) -> i32> Cacher<F> {
fn new(calculation: F) -> Cacher<F> {
Cacher {
calculation,
value: None,
}
}
fn get(&mut self, arg: i32) -> i32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
let mut expensive = Cacher::new(|num| {
println!("계산 중...");
num * 2
});
println!("결과: {}", expensive.get(5)); // "계산 중..." 출력
println!("결과: {}", expensive.get(5)); // 캐시된 값 사용, 출력 없음
}
비용이 큰 계산을 한 번만 실행하고 결과를 저장해둘 수 있어요!
연습 1. 아래 함수를 완성하세요. 벡터의 각 요소에 클로저를 적용한 새 벡터를 반환해요.
fn transform<F: Fn(i32) -> i32>(numbers: &[i32], f: F) -> Vec<i32> {
// 여기를 완성하세요
// 힌트: 빈 Vec을 만들고 for 루프로 f(n)을 push
}
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let squared = transform(&nums, |x| x * x);
let tripled = transform(&nums, |x| x * 3);
println!("제곱: {:?}", squared); // [1, 4, 9, 16, 25]
println!("3배: {:?}", tripled); // [3, 6, 9, 12, 15]
}
연습 2. 아래 코드에서 filter_by 함수를 완성하세요. 조건을 만족하는 요소만 모아서 반환해요.
fn filter_by<F: Fn(&i32) -> bool>(numbers: &[i32], predicate: F) -> Vec<i32> {
// 여기를 완성하세요
}
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evens = filter_by(&nums, |x| x % 2 == 0);
let big = filter_by(&nums, |x| *x > 5);
println!("짝수: {:?}", evens); // [2, 4, 6, 8, 10]
println!("5 초과: {:?}", big); // [6, 7, 8, 9, 10]
}
Q1. 클로저와 일반 함수의 가장 큰 차이는?
- A) 클로저는 반환값이 없다
- B) 클로저는 주변 환경의 변수를 캡처할 수 있다
- C) 클로저는 매개변수를 받을 수 없다
- D) 일반 함수가 더 빠르다
Q2. 아래 코드에서 클로저의 캡처 방식은?
let mut total = 0;
let mut add = |x: i32| { total += x; };
add(5);
- A)
Fn— 불변 참조로 캡처 - B)
FnMut— 가변 참조로 캡처 - C)
FnOnce— 소유권을 가져감 - D) 캡처하지 않음
Q3. move 키워드를 클로저 앞에 붙이면?
- A) 클로저의 실행 속도가 빨라진다
- B) 클로저가 캡처하는 변수의 소유권이 클로저로 이동한다
- C) 클로저를 여러 번 호출할 수 없게 된다
- D) 클로저가 값을 반환하지 않게 된다