Closure란?
Closure는 함수처럼 동작하지만 주변의 변수들을 캡처할 수 있는 특징이 있습니다.
Rust에서 클로저는 주로 짧고 간단한 작업을 수행할 때 유용하게 사용됩니다.
즉, inline 함수처럼 짧게 사용하되, 현재 environment에서 변수들을
불러올 수 있다는 특징이 있습니다.
문법
두 가지로 나뉩니다. 먼저 가장 명시적으로 closure를 정의하는 방법입니다.
let closure_annotated = |i: i32| -> i32 { i + outer_var };
closure에 사용될 인자들은 | | 사이에 전달합니다. 그리고 어떤 값을 return 할지,
어떤 동작을 할지 함수처럼 정의합니다.
이때 outer_var은 현재 범위에 정의된 변수여야 합니다.
또는, closure를 추론적으로 정의하는 방법입니다.
let closure_inferred = |i | i + outer_var ;
이때 i와 return 값은 현재 범위에서 추론되어 return 됩니다.
즉, 인자로 사용될 변수와, return 값의 타입형을
명시하지 않아도 된다는 겁니다.
사용법은 일반 함수와 같습니다.
println!("closure_annotated: {}", closure_annotated(1));
println!("closure_inferred: {}", closure_inferred(1));
인자가 없거나 많은 경우 아래와 같이 정의됩니다.
let no_arg_closure = || println!("Hello, world!");
no_arg_closure(); // 출력: Hello, world!
let multi_arg_closure = |a, b, c| a + b + c;
let result = multi_arg_closure(1, 2, 3);
println!("Result: {}", result); // 출력: Result: 6
Capturing
Closure는 | | 내 인자로 받는 함수 외, 연산에 필요한 값을
현재 범위에서 "Capture" 할 수 있습니다.
let color = String::from("green");
let print = || println!("`color`: {}", color);
예를 들어, print 라는 Closure 를 정의하고 이때 사용할 변수를
인자로 받는 게 아니라, 현재 범위에서 color라는 식별자를
받아오는 겁니다.
그러면 이런 질문을 할 수 있습니다.
"함수의 인자로 사용된다면 소유권이 넘어가는 것 아니냐?"
[Rust] Ownership과 Reference
Rust의 Ownership Rust의 소유권 개념입니다. https://doc.rust-lang.org/rust-by-example/scope/move.html?highlight=ownership#ownership-and-moves Ownership and moves - Rust By ExampleBecause variables are in charge of freeing their own resources, r
swc0317.tistory.com
Closure은 3가지 방법으로 변수를 Capture할 수 있습니다.
먼저 가장 기본입니다.
- by reference: &T
let x = 5;
let borrow_closure = || println!("{}", x); // x를 불변 참조
borrow_closure();
println!("{}", x); // 여전히 x를 사용할 수 있습니다.
기본적인 이용방법입니다. 어떤 환경 변수가, Closure 내부에서 사용되더라도,
여전히 Closure 외부에서도 접근할 수 있습니다. 즉
Closure의 환경 변수 접근 방법은 기본적으로 참조 (&) 인거죠.
- by mutable reference: &mut T
let mut y = 10;
let mut mutable_borrow_closure = || y += 5; // y를 가변 참조
mutable_borrow_closure();
println!("{}", y); // 출력: 15
불변참조와 문법적으로 Closure가 다른 건 없습니다.
그냥 환경 변수가 정의될 때 mutable 하게 정의되면 충분합니다.
- by move
Closure 내 환경 변수들을 오직 Closure 내부에서만 접근할 수 있게
소유권을 변경할 때 사용됩니다.
fn main() {
let haystack = vec![1, 2, 3];
let contains = move |needle| haystack.contains(needle);
println!("{}", contains(&1));
println!("{}", contains(&4));
}
위의 예시는 "haystack" 이라는 벡터를 정의합니다.
그리고 contains closure를 정의할 때 | | 전 move 라는 예약어를 통해,
Closure 내부 환경 변수는 Closure 내부에서만 접근할 수 있다고 명시합니다.
그래서, haystack이라는 벡터는 이제 외부에서 접근할 수 없습니다.
Closure가 함수 인자로 사용될 때
앞서 본 3가지 방법의 Closure를 함수의 인자로 전달할 때 주의해야 할 점이 있습니다.
fn apply<F>(f: F)
where
F: Fn() {
f();
}
fn main() {
let x = 5;
let closure = || println!("{}", x);
apply(closure);
}
일단 closure를 함수 인자로 전달해 보겠습니다. 이때 타입은 F입니다.
이후, 해당 closure가 특히 어떤 방식으로 환경 변수들을 capture하는 지 명시해야 합니다.
위의 경우엔 where F: Fn()을 통해
해당 closure가 불변 참조로 환경 변수를 참조한다고 명시합니다.
아래는 각 Capture 방법에 따른 예약어입니다.
- Fn: 클로저가 값을 불변 참조로 사용합니다 (&T).
- FnMut: 클로저가 값을 가변 참조로 사용합니다 (&mut T).
- FnOnce: 클로저가 값을 소유권 이동으로 사용합니다 (T).
예제로 보겠습니다.
불변참조는 위의 예시를 봤으니, 넘어가겠습니다.
fn apply<F>(mut f: F)
where
F: FnMut() {
f();
}
fn main() {
let mut x = 5;
let mut closure = || x += 1;
apply(closure);
println!("{}", x); // 출력: 6
}
fn apply<F>(f: F)
where
F: FnOnce() {
f();
}
fn main() {
let s = String::from("Hello");
let closure = || println!("{}", s);
apply(closure);
// println!("{}", s); // 이 줄은 컴파일 에러를 발생시킵니다. s의 소유권이 이동되었기 때문입니다.
}
또는 아래와 같은 방법으로 F라는
closure 제네릭을 사용하지 않을 수 있습니다.
fn call_me<F: Fn()>(f: F) {
f();
}
fn main() {
let closure = || println!("Hello, world!");
call_me(closure); // 클로저 전달, 출력: Hello, world!
}
함수가 함수 인자로 사용될 때
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let result = apply(add_one, 5);
println!("Result: {}", result); // 출력: Result: 6
}
이런 식으로 함수의 스키마를 타입처럼 사용할 수 있습니다.
fn(i32) -> i32 처럼요.
신기한 건, closure 를 인자로 받는 위치에, 함수도 전달할 수 있다는 겁니다.
당연히 closure의 특징과 유사해야 합니다.
예를 들어 Fn()의 경우 해당 위치 함수가 불변 참조를 해야 합니다.
fn call_me<F: Fn()>(f: F) {
f();
}
fn function() {
println!("I'm a function!");
}
fn main() {
let closure = || println!("I'm a closure!");
call_me(closure);
call_me(function);
}
근데, 함수라는 것은, 애초에 이전 스택 프레임에 접근할 수 없으니,
환경 변수에 접근할 수가 없습니다. 그래서 Fn, FnMut, FnOnce 모두에
적용 가능합니다.
함수 return 값으로의 Closure
fn create_fn() -> impl Fn() {
let text = "Fn".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnmut() -> impl FnMut() {
let text = "FnMut".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnonce() -> impl FnOnce() {
let text = "FnOnce".to_owned();
move || println!("This is a: {}", text)
}
fn main() {
let fn_plain = create_fn();
let mut fn_mut = create_fnmut();
let fn_once = create_fnonce();
fn_plain();
fn_mut();
fn_once();
}
마찬가지로, 해당 closure가 capture by reference인지,
value 인지, mutable 인지를 명시하며 return 할 수 있습니다.
왜 모든 return Closure 들이 move를 포함해야 함에도 Fn, FnMut trait로 명시되는가?
move를 통해 환경 변수들의 소유권을 Closure로 종속시키는
Closure에 해당하는 Trait은 FnOnce라고 설명드렸습니다. 그런데 왜
모든 Closure에 move를 포함하여 return 하는가?
일단 return 타입은 정말 해당 Closure가 사용될 trait에 맞게 정의하시면 됩니다.
왜 move가 포함되어야 하느냐?
함수에서 정의된 지역 변수들은 해당 범위를 벗어날 때 자연스럽게 drop 됩니다.
이는 소유권, 수명 등의 개념으로 컴파일 타임에 정적으로
메모리 안전을 보장하는 Rust의 Memory Safety Principle에 기인합니다.
문제는, 함수 내부에서 정의된 지역 변수들을
Closure의 환경 변수로 이용할 때,
함수가 끝나며 해당 지역 변수들이 drop 되었음에도,
여전히 Closure 함수를 사용하려 하면, 해당 지역 변수들에
접근 가능해야 합니다.
그러니 move를 통해 소유권을 전달하지 않으면
해당 지역 변수들이 Dangling Reference가 되어
이후 Closure 함수를 통해 해당 환경 변수들을
접근하려 하면 이미 Drop되어
use-after-free error가 발생합니다.
그래서 move를 통해 환경 변수들의 소유권을 Closure로
전달하여 수명을 늘려야 합니다.
'프로그래밍 언어 > [Rust]' 카테고리의 다른 글
[Rust] ToolChain Nightly (1) | 2025.02.15 |
---|---|
[Rust] Rust Functions (0) | 2025.02.09 |
[Rust] Expression (0) | 2025.01.07 |
[Rust] Type Conversion (0) | 2025.01.07 |
[Rust] Types (1) | 2025.01.03 |