let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
클로저가 겉으로 봤을때 익명 함수와 비슷하다. 하지만 클로저는 함수와 다르게 호출되는 스코프로부터 변수들을 캡처할 수 있다.
클로저
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
함수
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y)); // 에러 발생
}
error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
--> src/main.rs
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
|
다음은 스코프에 변수가 복사되어 문제가 없다.
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = |z| z == x;
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
move 키워드를 사용하면 스코프 캡처시 소유권 이동이 발생하여 컴파일 에러 발생한다.
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value borrowed here after move
클로저를 사용하면 컴파일러가 타입을 유추하여 클로저를 동작 시킨다.
example_closure는 첫번째 클로저 사용으로 x 가 String으로 해석되었다. 이 상태에서 5 인자를 전달하면 컴파일 에러가 발생한다.
클로저에 타입 어노테이션을 추가하여 선언하는 것이 가능하다.
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
simulated_expensive_calculation 함수가 필요 이상으로 중복 호출되고 있다.
fn generate_workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"Today, do {} pushups!",
simulated_expensive_calculation(intensity) <--
);
println!(
"Next, do {} situps!",
simulated_expensive_calculation(intensity) <-- 중복 호출
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
simulated_expensive_calculation(intensity)
);
}
}
}
필요 이상의 호출을 제거하였지만 호출하지 않아도 되는 상황에서 함수를 호출한다.
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_result =
simulated_expensive_calculation(intensity);
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_result
);
println!(
"Next, do {} situps!",
expensive_result
);
} else {
if random_number == 3 {
--> 호출하지 않아도 되는 상황 <--
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result
);
}
}
}\
expensive_closure 클로저를 정의하여 사용 하였지만 아직이다. 중복 호출 문제가 다시 발생 하고 있다.
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_closure(intensity)
);
println!(
"Next, do {} situps!",
expensive_closure(intensity) <-- 중복 호출
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
Cacher와 클로저를 사용하여 이미 계산한 값을 재활용 하여 불필요한 연산을 줄였다. 또한 호출할 필요 없는 상황에서 호출하는 문제도 해결 하였다.
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
values: HashMap<u32, u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32, // T의 타입이 u32 인자 하나를 받는 함수/클로저
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
values: Default::default(),
}
}
fn value(&mut self, arg: u32) -> u32 {
if let Some(&v) = self.values.get(&arg) {
v
} else {
let v = (self.calculation)(arg);
self.values.insert(arg, v);
v
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_result.value(intensity)
);
println!(
"Next, do {} situps!",
expensive_result.value(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}
이 패턴을 메모이제이션(memoization) 혹은 지연 평가(lazy evaluation)로 불린다.
클로저는 3가지 방식으로 값을 받아올 수 있다.
이 3 방식은 트레잇으로 표현된다.
Rust에서도 함수를 인자로 받는 함수 포인터가 사용 가능 하다.
fn add_onoe(x: i32) -> i32 {
x + 1
}
fn do_twich(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let ans = do_twich(add_onoe, 5);
println!("The answer is: {}", ans);
}
이를 클로저 타입으로 받을 수 도 있다.
fn add_onoe(x: i32) -> i32 {
x + 1
}
fn do_twich<T>(f: T, arg: i32) -> i32
where T: Fn(i32) -> i32 { <--i32 인자 하나를 받고 i32를 반환하는 클로저 타입
f(arg) + f(arg)
}
fn main() {
let ans = do_twich(add_onoe, 5);
println!("The answer is: {}", ans);
}
클로저를 사용하면 인자로 로직을 전달할 수 있다(C++의 함수자와 유사). 대표적인 사용 예가 map이다. map은 인자로 클로저를 받는다.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string()) <-- map 인자에 클로저를 넘기고 있다.
.collect();
}
클로저 타입은 함수도 받을 수 있기에 클로저 대신 함수도 넘길 수 있다.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string) <-- 함수 사용
.collect();
}
enum Status {
Value(u32),// tuple struct
Stop,
}
fn main() {
let list_of_statuses: Vec<Status> = (0_u32..20).map(Status::Value).collect();
println!("{:?}", list_of_statuses);
}
클로저는 인자뿐만 아니라 반환 타입에도 사용 가능 하다.
fn returns_closure() ->
impl Fn(i32) -> i32 { // impl Fn(i32) -> i32 와 같은 클로저를 반환
|x| x + 1
}
fn main() {
let closure = returns_closure();
assert_eq!(2, closure(1));
}
여기서 인자를 받아서 사용하는 클로저라면 문제가 발생한다.
fn returns_closure(base: i32) ->
impl Fn(i32) -> i32 {
|x| x + 1 + base
}
fn main() {
let closure = returns_closure(10);
assert_eq!(2, closure(1));
}
--> src\main.rs:3:5
|
3 | |x| x + 1 + base
| ^^^ ---- `base` is borrowed here
| |
| may outlive borrowed value `base`
base는 클로저에서 참조(borrowed)되었다. Rust의 참조 규칙은 참조된 데이터는 원본 데이터보다 오래 살 수 없다. 이 클로저는 원본이 없어 졌음에도 사용될 수 있기에 메모리 위험이 있다. 이럴 경우 클로저에 소유권을 이전하는 방법이 있다.
fn returns_closure(base: i32) ->
impl Fn(i32) -> i32 {
move |x| x + 1 + base
}
fn main() {
let closure = returns_closure(10);
assert_eq!(12, closure(1));
}
만약 인자 값에 따라 다른 클로저를 반환 한다면 또다른 문제가 발생한다.
fn returns_closure(base: i32) ->
impl Fn(i32) -> i32 {
if base > 10 {
move |x| x + 10 + base
} else {
move |x| x + 0 + base
}
}
fn main() {
let closure = returns_closure(5);
assert_eq!(6, closure(1));
}
--> src\main.rs:6:9
|
3 | / if base > 10 {
4 | | move |x| x + 10 + base
| | ----------------------
| | |
| | the expected closure
| | expected because of this
5 | | } else {
6 | | move |x| x + 0 + base
| | ^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
7 | | }
| |_____- `if` and `else` have incompatible types
클로저는 타입 추론을 하여 컴파일 타임에 정의한다. 컴파일 타임에 인자값이 무엇인지 알 수 없기에 문제가 발생한다.
이 문제를 해결하기 위해 반환되는 트레잇 오브젝트가 동적으로 결정됨을 의미하는 dyn 키워드와 힙 메모리에 할당 가능한 Box 스마트 포인터를 조합하여 사용한다. Box를 사용하는 이유는 컴파일 타임에 반환되는 트레잇 오브젝트 크기를 계산할 수 없기에 사용한다.
fn returns_closure(base: i32) ->
Box<dyn Fn(i32) -> i32> {
if base > 10 {
Box::new(move |x| x + 10 + base)
} else {
Box::new(move |x| x + 0 + base)
}
}
fn main() {
let closure = returns_closure(5);
assert_eq!(6, closure(1));
}