클로저와 소유권
앞에서 클로저를 단순히 익명 함수라고만 설명하고 넘어갔습니다. 하지만 이제 스코프와 소유권을 배웠기 때문에, 클로저에 대해 좀더 자세한 얘기를 해보려고 합니다. 클로저의 가장 큰 특징은 익명 함수를 만들고 이를 변수에 저장하거나 다른 함수의 인자로 전달할 수 있다는 것입니다.
클로저의 환경 캡처
클로저는 클로저가 선언된 스코프에 있는 지역 변수를 자신의 함수 내부에서 사용할 수 있는데, 이를 환경 캡처(Environment capture)라고 부릅니다. 클로저가 변수를 자신의 스코프 내부로 가져가는 방법은 총 3가지가 존재합니다.
- 불변 소유권 대여
- 가변 소유권 대여
- 소유권 가져가기
먼저 아래 예제를 보면, 클로저 func
는 같은 스코프에 선언된 변수 multiplier
를 자신의 함수 내부에서 사용할 수 있습니다. 이때 multiplier
의 값은 클로저에서 사용된 이후에도 스코프 내부에서 사용이 가능합니다. 따라서 클로저는 multiplier
를 불변 소유권 대여 방법으로 자신의 내부에서 사용한 것입니다.
fn main() { let multiplier = 5; let func = |x: i32| -> i32 { x * multiplier }; for i in 1..=5 { println!("{}", func(i)); } println!("{}", multiplier); // 👍 }
실행 결과
5
10
15
20
25
5
아래 예제는 multiplier
를 가변 변수로 선언하고, 클로저 내부에서 multiplier
의 값을 변경시키고 있습니다. 방금 살펴본 예제와 마찬가지로 클로저 호출이 끝난 다음에도 여전히 multiplier
에 접근이 가능합니다.
fn main() { let mut multiplier = 5; let mut func = |x: i32| -> i32 { multiplier += 1; x * multiplier }; for i in 1..=5 { println!("{}", func(i)); } println!("{}", multiplier); // 👍 }
실행 결과
6
14
24
36
50
10
move
를 사용한 소유권 이동
클로저가 환경으로부터 사용하는 값의 소유권을 가져갈 수도 있습니다. 클로저가 같은 스코프에 선언된 지역 변수의 소유권을 가져가도록 하려면 클로저의 파라미터를 선언하는 코드 앞에 move
키워드를 사용하면 됩니다.
move | param, ... | body;
다음 예제에서는 클로저를 리턴하는 함수 factory
를 만들었습니다. 여기서 리턴되는 클로저는 factory
함수의 파라미터인 factor
를 캡처해 사용합니다. 그 다음 factory
를 main
함수에서 사용해 만든 클로저를 호출하면 multiplier
변수를 모든 클로저에서 공유할 수 있게 됩니다.
fn factory(factor: i32) -> impl Fn(i32) -> i32 {
|x| x * factor
}
fn main() {
let multiplier = 5;
let mult = factory(multiplier);
for i in 1..=3 {
println!("{}", mult(i));
}
}
하지만 위 코드를 컴파일하면, 아래와 같은 에러가 발생합니다.
error[E0597]: `factor` does not live long enough
--> src/main.rs:2:13
|
2 | |x| x * factor
| --- ^^^^^^ borrowed value does not live long enough
| |
| value captured here
3 | }
| -
| |
| `factor` dropped here while still borrowed
| borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `notebook` due to previous error
factor
변수가 클로저 안에 캡처될 때, 소유권이 factory
로부터 클로저로 대여됩니다. 하지만 factory
함수가 종료되면 factor
변수의 값이 삭제되기 때문에 리턴된 클로저에서 더 이상 factor
를 사용할 수 없는 문제가 발생합니다. 이를 방지하기 위해서는 클로저 안으로 factor
의 소유권을 이동시키면 됩니다. 이때 사용되는 키워드가 move
입니다. move
는 캡처된 변수의 소유권을 클로저 안으로 이동시킵니다.
fn factory(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor } fn main() { let multiplier = 5; let mult = factory(multiplier); for i in 1..=3 { println!("{}", mult(i)); } }
실행 결과
5
10
15
클로저에서
move
를 가장 많이 사용하는 경우는 멀티스레드 혹은 비동기 프로그래밍을 작성할 때입니다.