Rust 클로저
클로저(closure)는 변수에 담아 두고 여기저기 넘겨줄 수 있는, 이름 없는 작은 함수예요. 일반 함수와 다른 점은 자신이 만들어진 스코프의 변수를 “캡처”할 수 있다는 거예요.
기본 클로저 문법
섹션 제목: “기본 클로저 문법”fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}실행 결과:
6세로 막대 |x: i32|가 일반 함수의 괄호 역할을 해요. 두 가지를 나란히 비교해 볼게요.
fn add_one_fn(x: i32) -> i32 { x + 1 } // 일반 함수let add_one_closure = |x: i32| x + 1; // 같은 동작을 하는 클로저Rust는 매개변수 타입과 반환 타입도 추론할 수 있는 경우가 많아서, 클로저를 이보다 더 짧게 쓴 코드도 자주 볼 수 있어요.
let add_one = |x| x + 1;매개변수가 없는 클로저는 빈 세로 막대를 써요. let say_hi = || println!("hi");처럼 만들고, say_hi()로 호출해요.
여러 줄짜리 클로저
섹션 제목: “여러 줄짜리 클로저”표현식이 둘 이상이라면 함수처럼 본문을 중괄호로 감싸요.
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "짝수" } else { "홀수" }; format!("{n}은(는) {kind}예요") };
println!("{}", describe(7));}실행 결과:
7은(는) 홀수예요format!은 println!처럼 동작하지만, 텍스트를 출력하는 대신 String으로 돌려줘요. 여기서는 그 String이 중괄호 안의 마지막 표현식이라서 클로저의 반환값이 돼요.
주변 스코프의 변수 캡처하기
섹션 제목: “주변 스코프의 변수 캡처하기”일반 함수에는 없는 기능이 바로 이거예요. 클로저는 따로 넘겨주지 않아도, 자신이 정의된 곳의 변수를 쓸 수 있어요.
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}실행 결과:
9040discount는 클로저의 매개변수가 아니에요. 그냥 main의 스코프에 있을 뿐인데, 클로저가 손을 뻗어서 직접 써요. 일반 fn은 이렇게 할 수 없어요. discount를 인자로 넘겨받아야 해요.
캡처한 변수를 바꾸는 클로저
섹션 제목: “캡처한 변수를 바꾸는 클로저”클로저는 캡처한 변수를 바꿀 수도 있어요. 단, 그 변수가 mut여야 해요. 규칙이 하나 더 있어요. 클로저 자체도 mut 변수에 담아야 해요. 클로저를 호출하면 무언가가 바뀌니까, Rust는 그 사실이 코드에 드러나길 원하거든요.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("현재 count: {count}"); };
increment(); increment(); increment();
println!("최종 count: {count}");}실행 결과:
현재 count: 1현재 count: 2현재 count: 3최종 count: 3increment에 mut를 빼먹으면 컴파일러가 무엇이 잘못됐는지 정확히 알려 줘요: calling increment requires mutable binding due to mutable borrow of count. “count를 가변으로 대여하고 있으니, increment를 호출하려면 가변 바인딩이 필요하다”는 뜻이에요.
알아 두면 좋은 함정이 하나 있어요. 클로저를 아직 호출할 수 있는 동안에는 클로저가 count를 바꿀 권리를 갖고 있어서, 다른 누구도 count를 건드릴 수 없어요. 두 번의 increment() 호출 사이에 println!("{count}");를 넣으면 error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable 에러가 나요. “이미 가변으로 대여 중이라서 불변으로 대여할 수 없다”는 뜻이에요. 마지막 increment() 호출이 끝나면 클로저의 할 일도 끝나고, count는 다시 온전히 여러분의 것이 돼요. 그래서 최종 count 줄은 잘 동작해요. 이 규칙의 전체 이야기는 대여 페이지에서 배워요.
때로는 클로저가 캡처한 값의 소유권을 통째로 가져와야 할 때가 있어요. 예를 들어 클로저를 다른 스레드에 넘길 때가 그래요. 그럴 때는 세로 막대 앞에 move를 써요: move || { ... }. 실제로 쓰는 모습은 동시성 페이지에서 볼 수 있어요.
실제로 클로저가 쓰이는 곳
섹션 제목: “실제로 클로저가 쓰이는 곳”클로저를 단독으로 쓸 일은 많지 않아요. 클로저가 정말 유용한 곳은 다른 함수에 원하는 동작을 넘겨줄 때예요. 특히 반복자 페이지에서 배울 반복자 메서드와 함께 많이 써요.
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}실행 결과:
[2, 4, 6, 8, 10]여기서 |n| n * 2는 .map()에 바로 넘겨준 클로저예요. 이런 패턴(작은 클로저가 .map()이나 .filter() 같은 메서드를 움직이는 것)이 매일 실제로 클로저를 쓰게 되는 곳이에요.
연습 문제
섹션 제목: “연습 문제”1. 세금 포함 가격
섹션 제목: “1. 세금 포함 가격”tax_percent 변수를 20으로 만드세요. 그다음 가격을 받아서, 캡처한 tax_percent를 이용해 세금을 더한 가격을 돌려주는 클로저 with_tax를 작성하세요. 가격 50과 120에 대한 결과를 출력해 보세요. 힌트: 세금은 price * tax_percent / 100이에요.
정답 보기
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50원에 세금을 더하면 {}원", with_tax(50)); println!("120원에 세금을 더하면 {}원", with_tax(120));}실행 결과:
50원에 세금을 더하면 60원120원에 세금을 더하면 144원tax_percent는 한 번도 넘겨주지 않았어요. 클로저가 main에서 캡처한 거예요. 바로 일반 fn이 할 수 없는 일이죠.
2. ID 생성기
섹션 제목: “2. ID 생성기”처음 호출하면 1, 두 번째에는 2, 세 번째에는 3을 돌려주는 식으로 동작하는 클로저 next_id를 작성하세요. 세 번 호출해서 각 ID를 출력해 보세요. 힌트: 클로저 바깥에 mut 카운터를 두고, 안에서 1을 더한 다음, 카운터를 클로저의 마지막 표현식으로 두면 그 값이 반환돼요.
정답 보기
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("첫 번째 사용자의 ID: {}", next_id()); println!("두 번째 사용자의 ID: {}", next_id()); println!("세 번째 사용자의 ID: {}", next_id());}실행 결과:
첫 번째 사용자의 ID: 1두 번째 사용자의 ID: 2세 번째 사용자의 ID: 3last_id와 next_id 둘 다 mut가 필요해요. last_id는 값이 바뀌기 때문이고, next_id는 클로저를 호출하면 캡처한 값이 바뀌기 때문이에요.
- 클로저는 이름 없는 작은 함수예요:
|매개변수| 표현식. - 타입은 추론되는 경우가 많아서, 클로저는 보통 같은 동작을 하는
fn보다 짧아요. - 일반 함수와 달리, 클로저는 자신이 정의된 스코프의 변수를 캡처할 수 있어요.
- 캡처한 변수를 바꾸는 클로저는
mut변수에 담아야 하고, 클로저를 쓰는 동안에는 그 변수를 대여하고 있어요. - 클로저는 다른 함수에 넘겨줄 때 가장 유용해요. 특히
.map()같은 반복자 메서드와 함께요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 클로저는 매개변수를 쓸 때 함수의 괄호 대신 어떤 기호를 쓸까요?
2. 일반 fn은 못 하는데 클로저는 할 수 있는 일은 무엇일까요?
3. 이 페이지에 따르면 실제로 클로저는 주로 어디에 쓰일까요?
4. 클로저가 캡처한 변수에 count += 1을 해요. 그 밖에 무엇이 필요할까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 클로저 — The Rust Programming Language 한국어판: 클로저, 타입 추론,
Fn트레이트를 다루는 공식 장이에요. - 참조자를 캡처하거나 소유권 이동하기 — The Rust Programming Language 한국어판: 클로저가 변수를 캡처하는 세 가지 방식과
move를 언제 쓰는지 설명해요. - Closures — Rust by Example (영어): 클로저와 함수를 비교하는 짧은 예제를 실행해 볼 수 있어요.
- Capturing — Rust by Example (영어): 변수를 읽고, 바꾸고, 소유권을 가져가는 클로저 예제가 더 있어요.
- Closure expressions — The Rust Reference (영어): 클로저 문법과 캡처에 대한 정확한 규칙이에요.