[Rust] 클로저(closure)

김민재·2023년 4월 11일

Rust Basic

목록 보기
3/3
post-thumbnail

클로저의 정의

일급 객체 함수의 개념을 이용하여 스코프에 묶인 변수를 바인딩 하기 위한 기술이다. 클로저는 함수를 저장한 레코드이며, 스코프의 인수 들은 클로저가 생성될 때 정의 된다. 스코프 내부영역이 소멸 되었어도 그에 대한 접근은 복사본인 클로저를 통해 이루어 진다.

Rust에서 클로저를 사용하는 이유

  1. 함수를 파라미터로 받는 함수를 호출할경우 필요한 함수가 한곳에서만 호출되는 경우 굳이 이름이 필요하지 않기 때문에
  2. 함수 내부에서 익명함수를 바로 볼수 있기 때문에 경우에 따라서 가독성이 더 높다.
  3. 메모리 및 CPU 사용량이 똑같은 함수의 비용보다 크지 않음
  4. Rust의 경우 일부러 Box, Vec등의 컨테이너에 집어 넣지 않을경우 힙이 아닌 스택에 할당되며 경우에 따라서 이점이 됨

Rust에서 클로저

기본 문법

struct City{
	name: String,
    population : i64,
    country: String,
    ...
}

// Error Case
fn sort_cities(cities: &mut Vec<City>){
	cities.sort();  // Error 무엇을 기준으로 정렬 해야할지 모름...
}

fn city_population_descending(city: &City) -> i64{
	-city.population
}

// Ok Case
fn sort_cities(cities: &mut Vec<City>){
	cities.sort_by_key(city_population_descending);  // Ok 
}

// Closure Case
fn sort_cities(cities: &mut Vec<City>){
	cities.sort_by_key(|city| -city.population );  // Ok 
}

위 예시에서 Closure Case의 경우가 바로 클로저다.

변수 캡쳐하기

클로저는 바깥쪽 함수에 속한 데이터를 사용할 수 있다.

fn sort_by_statistic(cities; &mut Vec<City>, stat: Statistic) {
	cities.sort_by_key(|city| -city.get_statistic(stat));
}

이를 가리켜 클로저가 stat을 캡쳐 한다고 말한다. 이는 클로저의 전형적인 기능중 하나이기 때문에 Rust에서도 지원한다. 그러나 Rust 에서는 이 기능에 조건이 붙는다. 왜냐하면 소유권 문제가 있기 떄문이다.

빌리는 클로저

fn sort_by_statistic(cities; &mut Vec<City>, stat: Statistic) {
	cities.sort_by_key(|city| -city.get_statistic(stat));
}

위 경우에서는 stat의 레퍼런스가 자동으로 차용된다.

훔치는 클로저

use std::thread;

fn start_sorting_thread(mut cities:Vec<City>, stat: Static)
	 -> thread::JoinHandle<Vec<City>>
{
	let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
    thread::spawn(|| {
    	cities.sort_by_key(key_fn); //  Error 발생
        cities
	})
}

thread::spawn은 주어진 클로저를 새 시스템 스레드에서 호출한다. ||는 클로저의 빈 인수 목록이다. 새 스레드는 호출부와 병렬로 실행되며 클로저가 복귀하면 종료된다.

여기서 클로저 key_fn 안에 있는 stat, cities이 소멸되기전에 자신의 일을 끝내리라고 기대할 수 없기 때문에 에러가 발생한다.

use std::thread;

fn start_sorting_thread(mut cities:Vec<City>, stat: Static)
	 -> thread::JoinHandle<Vec<City>>
{
	let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
    thread::spawn(move || {
    	cities.sort_by_key(key_fn); //  Error 발생
        cities
	})
}

이를 해결하기 위해서 move를 사용해 소유권을 클로저 안으로 옮겨 달라고 해야한다.

함수와 클로저 타입

fn count_selected_cities(cities: &Vec<City>, 
	test_fn: fn(&City) -> bool) -> usize
{
	let mut count = 0;
    for city in cities {
    	if test_fn(city) {
        	count+= 1;
        }
    }
    count
}
// Error Case : 타입이 불일치.
let n = count_selected_cities(
	&my_cties,
	|city| city.monster_attack_rist > rist);

함수와 클로저는 엄연히 다른 타입으로 받아들인다. 그래서 함수의 파라미터로 함수를 선언할 경우 함수와 똑같은 클로저를 넘길 경우 에러를 출력하게 된다.
그래서 클로저를 함수의 인자로 넘겨주기 위해서는 함수 매개변수를 아래와 같이 바꿔줘야 한다.

fn count_selected_cities(cities: &Vec<City>, 
	test_fn: F) -> usize
where F: Fn(&City) -> bool
{
	let mut count = 0;
    for city in cities {
    	if test_fn(city) {
        	count+= 1;
        }
    }
    count
}
// Ok Case 
let n = count_selected_cities(
	&my_cties,
	|city| city.monster_attack_rist > rist);

위에서 바뀐 부분은 제네릭으로, 특수 트레이트 Fn(&City) -> bool을 구현하고 있는 임의의 타입 F로 된 test_fn을 받는다.

fn(&City) -> bool // fn 타입, 함수만 받는다.
Fn(&City) -> bool // Fn 트레이트, 함수와 클로저 둘다 받는다.

->와 반환 타입의 경우 생략이 가능하다. 클로저는 호출할 수 있지만 fn은 아니다. 클로저는 자기만의 고유한 타입을 갖는다.

클로저와 안전성

클로저가 캡처된 값을 드롭하거나 수정할 때 벌어지는 일을 설명한다.

FnOnce

Rust에서는 값을 버리는 행위를 하기 위해서 drop()을 호출하면 된다.

let my_str = "hello".to_string();
let f = || drop(my_str);

f(); // Ok
f(); // Error : 이동된 값을 사용한다.

러스트 컴파일러는 여기서 값이 이동되는것을 알고 있다. 이는 인수를 갖지 않는 클로저가 컴파일 과정에서 Fn, FnOnce 트레이트를 따로 구현하기 때문이다.

trait Fn() -> R{
	fn Call(&self) -> R; // 레퍼런스를 자기자신으로 가진다.
}

trait FnOnce() -> R{
	fn call_once(self) -> R;
}

따라서 클로저가 처음 호출될 때만 안전한 경우 call_once()로 확장되고, 여러번 호출되도 되는경우 Call()로 확장된다.

FnMut

변경할 수 있는 데이터나 mut레퍼런스를 가지고 있는 클로저가 있다.

trait FnMut() -> R{
	fn call_once(&mut self) -> R;
}

FnMut 클로저는 mut 레퍼런스를 통해 호출되며, 값의 mut 접근 권한을 필요로 하지만 아무런 값을 drop하지 않는 클로저이다.

따라서 where F: FnMut()을 사용해서 명시적으로 말해주는것이 좋다.

효율적인 클로저 사용법

Rust에서의 클로저는 다른 언어(C#, 자바, 자바스크립트)에 있는 클로저와 다르다. 가장 큰 차이는 GC가 있는 언어의 경우 클로저에서 지역변수를 사용할 때 수명이나 소유권에 대해 걱정할 필요가 없다는 것이다.

그러나 러스트의 경우 위 언어들이 자주 사용하는 객체지향적 접근 방법이 옮은 방법이 아닐수 있다는 것이다.

0개의 댓글