클로저: 자신의 환경을 캡처하는 익명 함수
- 클로저 : 변수에 저장하거나 다른 함수에 인수로 전달할 수 있는 익명 함수
- 한 곳에서 클로저를 만들고 다른 콘텍스트(스코프)에서 이를 호출하여 평가할 수 있음
- 정의된 스코프에서 값을 캡처할 수 있음. 즉, 클로저가 자신이 만들어진 시점의 변수(스코프 안의 값)를 기억해서, 나중에 클로저가 실행될 떄 그 값을 그대로 사용할 수 있음
클로저 타입 추론과 명시
- 클로저는 보통 짧은 콘텍스트 내에서만 사용되고 외부에 노출되지 않으므로, 타입명시를 하지 않아도 됨 (컴파일러가 추론해 줌)
- 클로저의 매개변수와 반환 타입은 최초 사용 시점에 추론 됨
- 한 번 추론된 타입은 그 클로저에 고정 됨 (여러 타입으로 재사용할 수 없음)
참조자를 캡처하거나 소유권 이동하기
- 클로저를 캡처하는 방식 3가지
- 불변 참조로 캡처 (
&T
)- 값을 읽기만 할 때 사용하며, 여러개의 불변 참조자를 동시에 가질 수 있기 때문에 클로저 호출 전 후로 변수를 자유롭게 사용할 수 있음
- 가변 참조로 캡처 (
&mut T
)- 값 수정이 필요할 때 사용되며, 가변 참조는 동시에 다른 참조를 허용하지 않기 때문에 클로저의 정의 ~ 호출 사이에 불변 참조 사용이 불가 (가변 대여가 끝난 시점에서는 사용 가능)
- 소유권 이동으로 캡처 (
T
)- 값 자체를 소유해야 하는 경우 사용되며, 클로저의 정의 앞부분에
move
키워드를 집어넣어 캡처된 값의 소유권을 클로저로 넘겨야 함을 명시 해야 함 (move
키워드를 붙이면 강제로 소유권을 클로저로 이전 함)
- 값 자체를 소유해야 하는 경우 사용되며, 클로저의 정의 앞부분에
- 불변 참조로 캡처 (
캡처된 값을 클로저 밖으로 이동하기와 Fn트레이트
- 클로저가 환경으로부터 값을 캡처하고 다루는 방식은 이 클로저가 구현하는 트레이트에 영향을 주고, 트레이트는 함수와 구조체가 사용할 수 있는 클로저의 종류를 명시할 수 있는 방법
- 러스트에서는 클로저를 작성하면, 그 클로저의 “캡처 방식(외부 변수를 다루는 방식)”에 따라 컴파일러가 자동으로 적절한 트레이트(
FnOnce
,FnMut
,Fn
)를 구현해줌
트레이트 | 클로저의 행동 | 호출 가능 횟수 |
---|---|---|
FnOnce | 캡처된값을 본문 밖으로 이동(move) 시킴 | 1번만 가능 |
FnMut | 캡처된값을 변경(mutate) 함 | 여러 번 가능 |
Fn | 이동, 변경, 캡처를 하지 않음 | 여러 번 가능 |
반복자로 일련의 아이템 처리하기
- 반복자는 각 아이템을 순회하고 언제 시퀀스가 종료될지 결정하는 로직을 담당
- 러스트에서의 반복자는 게으른데, 이는 반복자를 사용하는 메서드를 호출하여 반복자를 소비하기 전까지는 동작을 하지 않는다는 의미 ( 지연평가(lazy evalutaion) )
Iterator 트레이트와 next 메서드
1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 기본 구현이 있는 메서드 ...
}
- 모든 반복자는 표준 라이브러리에 정의된
Iterator
라는 트레이트를 구현 Item
타입은 반복자로부터 반환되는 타입next
메서드는Some
으로 감싼 반복자의 아이템을 하나씩 반환하고, 반복자가 종료될 때는None
을 반환- 반복자에 대한
next
메서드 호출은 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적 ( “반복자로부터 하나의 아이템을 소비 한다”고 말하기도 함 )
반복자의 종류와 동작방식
| 메서드 | 반환하는 값 | 반복자가 가지는 참조 형태 | 사용 목적 | | ————- | ————————— | ————– | ————————— | | iter()
| 각 요소에 대한 불변 참조(\&T) | &T
| 데이터를 읽기만 할 때 사용 | | iter_mut()
| 각 요소에 대한 가변 참조(\&mut T) | &mut T
| 데이터를 변경하고 싶을 때 사용 | | into_iter()
| 각 요소의 소유권(T) | 값 자체 | 데이터를 소유권 이전하면서 소비할 때 사용 |
반복자를 소비하는 메서드
Iterator
트레이트의 메서드 중next
를 호출하는 메서드들을 소비 어댑터 라고 하는데, 호출하면 반복자를 소비하기 때문 (대표적으로sum
메서드가 있음)- 대표적으로
sum
메서드가 있으며, 반복자의 소유권을 가져온 다음 반복적으로next
를 호출하는 방식으로 전체를 순회하여 각 아이템을 더한 합계를 반환함
다른 반복자를 생성하는 메서드
- 반복자 어댑터 는
Iterator
트레이트에 정의된 메서드로 반복자를 소비하지 않고, 대신 원본 반복자의 어떤 측면을 바꿔서 다른 반복자를 제공함 - 대표적으로
map
메서드가 있으며, 클로저를 인수로 받아서 각 아이템에 대해 호출하고 아이템 전체를 순회하여 수정된 아이템들을 생성하는 새로운 반복자를 반환 ( 반복자는 지연 평가 방식을 사용하므로,map
함수는 단순히 어떻게 변환할지만 정의 하고, 이를collect
와 같은 메서드로 소비 해야함 )
성능 비교하기: 루프 vs 반복자
- 반복자는 비용 없는 추상화의 대표적인 예로, 이를 사용하는 것은 추가적인 런타임 오버헤드가 없음
- 비용 없는 추상화(Zero-cost abstraction)라는 개념은 프로그래밍 언어에서 고수준의 문법이나 개념을 사용해도, 런타임 성능에 추가 비용이 전혀 들지 않는 설계를 말함
- 반복자를 사용해도 컴파일 시점에서 루프가 언롤링되고 배열 접근 검사와 오버헤드가 제가되므로 매우 효율적
- 언롤링(unrolling)은 루프 제어 코드의 오버헤드를 제거하고 대신 루프의 각 순회에 해당하는 반복되는 코드를 생성하는 최적화 방법
This post is licensed under CC BY 4.0 by the author.