클로저 타입
클로저 타입
클로저는 Rust에서 매우 흔하게 쓰이는데, 정작 그 타입은 글로 쓸 수 없는 고유한 익명 타입이라는 특징이 있어요. 클로저 표현식은 클로저 값을 만들고, 그 값은 캡처한 값들을 담는 구조체와 대략 동등한 타입을 가져요. 예를 들어 아래 클로저를 볼게요.
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point { x: i32, y: i32 }
struct Rectangle { left_top: Point, right_bottom: Point }
fn f<F : FnOnce() -> String> (g: F) {
println!("{}", g());
}
let mut rect = Rectangle {
left_top: Point { x: 1, y: 1 },
right_bottom: Point { x: 0, y: 0 }
};
let c = || {
rect.left_top.x += 1;
rect.right_bottom.x += 1;
format!("{:?}", rect.left_top)
};
f(c); // Prints "Point { x: 2, y: 1 }".
}
이 클로저는 대략 아래와 같은 클로저 타입을 만들어내요.
// 참고: 이건 실제 번역 방식이 아니라, 설명을 위한 것뿐이에요.
struct Closure<'a> {
left_top : &'a mut Point,
right_bottom_x : &'a mut i32,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.left_top.x += 1;
*self.right_bottom_x += 1;
format!("{:?}", self.left_top)
}
}
그래서 f에 대한 호출은 마치 아래처럼 작동해요.
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.right_bottom.x });
본문
캡처 모드
캡처 모드(capture mode) 는 환경의 place 표현식을 클로저로 어떻게 빌려오거나 이동시키는지를 결정해요. 캡처 모드에는 네 가지가 있어요.
- 불변 빌림 (ImmBorrow) — place 표현식이 공유 참조로 캡처돼요.
- 고유 불변 빌림 (UniqueImmBorrow) — 불변 빌림과 비슷하지만 아래 설명처럼 고유(unique) 해야 해요.
- 가변 빌림 (MutBorrow) — place 표현식이 가변 참조로 캡처돼요.
- 이동 (ByValue) — place 표현식이 값을 클로저로 이동시켜 캡처돼요.
환경의 place 표현식은 캡처된 값이 클로저 본문 안에서 어떻게 쓰이는지와 호환되는 첫 번째 모드로 캡처돼요. 이 모드는 클로저 주변의 코드(관련 변수나 필드, 혹은 클로저 자신의 라이프타임)의 영향을 받지 않아요.
Copy 값
클로저로 이동되는 Copy를 구현하는 값은 ImmBorrow 모드로 캡처돼요.
#![allow(unused)]
fn main() {
let x = [0; 1024];
let c = || {
let y = x; // x는 ImmBorrow로 캡처됨
};
}
async 입력 캡처
async 클로저는 본문 안에서 사용되는지 여부와 상관없이 모든 입력 인자를 항상 캡처해요.
캡처 정밀도
캡처 경로(capture path) 는 환경의 변수에서 시작해서 그 변수의 place 투영(projection)이 0개 이상 이어진 수열이에요. place 투영은 필드 접근, 튜플 인덱스, 역참조(및 자동 역참조), 배열/슬라이스 인덱스 표현식, 변수에 적용된 패턴 분해(destructuring)를 말해요.
rustc에서 패턴 분해는 일련의 역참조와 필드/요소 접근으로 디슈가링돼요.
클로저는 캡처 경로를 빌리거나 이동시켜요. 이 경로는 아래 설명할 규칙에 따라 절단(truncate)될 수 있어요. 예시를 볼게요.
#![allow(unused)]
fn main() {
struct SomeStruct {
f1: (i32, i32),
}
let s = SomeStruct { f1: (1, 2) };
let c = || {
let x = s.f1.1; // s.f1.1이 ImmBorrow로 캡처됨
};
c();
}
여기서 캡처 경로는 지역 변수 s, 필드 접근 .f1, 튜플 인덱스 .1이에요. 이 클로저는 s.f1.1의 불변 빌림을 캡처해요.
공유 접두사 (Shared prefix)
캡처 경로와 그 경로의 조상 중 하나가 둘 다 클로저로 캡처되는 경우, 조상 경로는 두 캡처 중 가장 높은 캡처 모드로 캡처돼요.
CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode)
이는 다음의 엄격한 약순서(strict weak ordering)를 사용해요.
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
이것은 재귀적으로 적용되어야 할 수도 있어요.
#![allow(unused)]
fn main() {
// 이 예시에는 공유 조상을 가진 세 개의 서로 다른 캡처 경로가 있어요:
fn move_value<T>(_: T){}
let s = String::from("S");
let t = (s, String::from("T"));
let mut u = (t, String::from("U"));
let c = || {
println!("{:?}", u); // u는 ImmBorrow로 캡처됨
u.1.truncate(0); // u.1은 MutBorrow로 캡처됨
move_value(u.0.0); // u.0.0은 ByValue로 캡처됨
};
c();
}
전체적으로 이 클로저는 u를 ByValue로 캡처해요.
가장 오른쪽 공유 참조 절단 (Rightmost shared reference truncation)
캡처 경로가 공유 참조에 적용된 역참조로 끝난다면, 그 가장 오른쪽 역참조에서 캡처 경로가 절단돼요.
이 절단이 허용되는 이유는, 공유 참조를 통해 읽는 필드는 항상 공유 참조나 복사로 읽히기 때문이에요. 빌림 검사 관점에서 추가 정밀도가 아무 이득도 주지 않을 때 캡처 크기를 줄이는 데 도움이 돼요.
가장 오른쪽 역참조인 이유는, 필요보다 더 짧은 라이프타임을 피하기 위해서예요. 예시를 볼게요.
#![allow(unused)]
fn main() {
struct Int(i32);
struct B<'a>(&'a i32);
struct MyStruct<'a> {
a: &'static Int,
b: B<'a>,
}
fn foo<'a, 'b>(m: &'a MyStruct<'b>) -> impl FnMut() + 'static {
let c = || drop(&m.a.0);
c
}
}
만약 이 클로저가 m을 캡처한다면, m이 'a로 제약되므로 클로저는 더 이상 'static을 아웃라이브하지 못해요. 대신 이 클로저는 (*(*m).a)를 ImmBorrow로 캡처해요.
와일드카드 패턴 바인딩
클로저는 읽어야 하는 데이터만 캡처해요. 와일드카드 패턴으로 값을 바인딩하는 것은 값을 읽지 않으므로 place는 캡처되지 않아요.
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
let x = S;
let c = || {
let _ = x; // `x`를 캡처하지 않아요.
};
let c = || match x {
_ => (), // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
}
튜플, 구조체, 단일 variant enum의 분해는 그 자체로는 읽기나 place 캡처를 일으키지 않아요. 참고로 #[non_exhaustive]로 표시된 enum은 항상 여러 variant를 가진 것으로 취급돼요.
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
// 튜플 분해는 읽기나 캡처를 일으키지 않아요.
let x = (S,);
let c = || {
let (..) = x; // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
// 유닛 구조체 분해는 읽기나 캡처를 일으키지 않아요.
let x = S;
let c = || {
let S = x; // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
// 구조체 분해는 읽기나 캡처를 일으키지 않아요.
struct W<T>(T);
let x = W(S);
let c = || {
let W(..) = x; // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
// 단일 variant enum 분해는 읽기나 캡처를 일으키지 않아요.
enum E<T> { V(T) }
let x = E::V(S);
let c = || {
let E::V(..) = x; // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
}
RestPattern(..) 또는 StructPatternEtCetera(역시 ..)에 매치된 필드는 읽히지 않고, 그 필드들은 캡처되지 않아요.
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
let x = (S, S);
let c = || {
let (x0, ..) = x; // `x.0`을 `ByValue`로 캡처해요.
};
// 첫 번째 튜플 필드만 클로저에 캡처됐어요.
x.1; // OK: 여기서 `x.1`을 이동할 수 있어요.
c();
}
배열과 슬라이스의 부분 캡처는 지원되지 않아요. 와일드카드 패턴 매칭, 인덱싱, 하위 슬라이싱과 함께 쓰여도 슬라이스나 배열 전체가 항상 캡처돼요.
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
let mut x = [S, S];
let c = || {
let [x0, _] = x; // `x` 전체를 `ByValue`로 캡처해요.
};
let _ = &mut x[1]; // ERROR: Borrow of moved value.
}
와일드카드로 매치되는 값도 초기화되어 있어야 해요.
#![allow(unused)]
fn main() {
let x: u8;
let c = || {
let _ = x; // ERROR: Binding `x` isn't initialized.
};
}
discriminant 읽기를 위한 캡처
패턴 매칭이 discriminant를 읽으면, 그 discriminant를 담은 place가 ImmBorrow로 캡처돼요.
variant가 둘 이상인 enum의 variant에 매치하면 discriminant를 읽고, place를 ImmBorrow로 캡처해요.
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
let mut x = (Some(S), S);
let c = || match x {
(None, _) => (),
// ^^^^
// 이 패턴은 discriminant 읽기를 요구하며, 그 때문에
// `x.0`이 `ImmBorrow`로 캡처돼요.
_ => (),
};
let _ = &mut x.0; // ERROR: Cannot borrow `x.0` as mutable.
// ^^^
// 클로저가 아직 살아 있으므로, `x.0`은 여기서 여전히
// 불변으로 빌려져 있어요.
c();
}
#![allow(unused)]
fn main() {
struct S; // `Copy`가 아닌 타입.
let x = (Some(S), S);
let c = || match x { // `x.0`을 `ImmBorrow`로 캡처해요.
(None, _) => (),
_ => (),
};
// `x.0`이 discriminant 읽기 때문에 캡처됐지만,
// `x.1`은 캡처되지 않아요.
x.1; // OK: 여기서 `x.1`을 이동할 수 있어요.
c();
}
단일 variant enum의 유일한 variant에 매치하는 것은 discriminant를 읽지 않고 place를 캡처하지 않아요.
#![allow(unused)]
fn main() {
enum E<T> { V(T) } // 단일 variant enum.
let x = E::V(());
let c = || {
let E::V(_) = x; // `x`를 캡처하지 않아요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
}
enum에 #[non_exhaustive]가 적용되면, 실제로 variant가 하나뿐이더라도 읽기가 발생하는지 결정하는 목적에서는 여러 variant를 가진 것으로 취급돼요.
매치하는 variant를 제외한 모든 variant가 uninhabited(패턴을 반박 불가능하게 만들더라도)여도, 그 외의 경우라면 discriminant는 여전히 읽혀요.
#![allow(unused)]
fn main() {
enum Empty {}
let mut x = Ok::<_, Empty>(42);
let c = || {
let Ok(_) = x; // `x`를 `ImmBorrow`로 캡처해요.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
}
캡처와 범위 패턴
범위 패턴(range pattern) 에 매치하는 것은, 범위가 타입의 모든 가능한 값을 포함하더라도 매치되는 place를 읽고 그 place를 ImmBorrow로 캡처해요.
#![allow(unused)]
fn main() {
let mut x = 0u8;
let c = || {
let 0..=u8::MAX = x; // `x`를 `ImmBorrow`로 캡처해요.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
}
캡처와 슬라이스 패턴
슬라이스를, 단일 rest 패턴만 있는 것(즉 [..]) 을 제외한 슬라이스 패턴에 매치하는 것은 슬라이스의 길이를 읽는 것으로 취급되고, 슬라이스를 ImmBorrow로 캡처해요.
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // `*x`를 `ImmBorrow`로 캡처해요.
&mut [] => (),
// ^^
// 정확히 0개 요소의 슬라이스에 매치해요. 검사 대상이 매치하는지
// 알려면 길이를 읽어야 하므로, 그 슬라이스가 캡처돼요.
_ => (),
};
let _ = &mut *x; // ERROR: Cannot borrow `*x` as mutable.
c();
}
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // `*x`를 캡처하지 않아요.
[..] => (),
// ^^ Rest 패턴.
};
let _ = &mut *x; // OK
c();
}
아마 놀랍게도, 길이가 슬라이스를 가리키는 (wide) 포인터에 들어 있더라도, 포인티(슬라이스)의 place가 읽히는 것으로 취급되고 캡처돼요.
#![allow(unused)]
fn main() {
fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l {
// 클로저는 `**x`를 캡처하기 때문에 `'l`을 아웃라이브해요.
// 만약 `*x`를 캡처했다면, `impl Fn() + 'l` 바운드를
// 만족할 만큼 오래 살지 못했을 거예요.
|| match *x { // `**x`를 `ImmBorrow`로 캡처해요.
&[] => (),
_ => (),
}
}
}
이런 식으로 검사 대상에서 슬라이스까지 역참조하는 것과 동작이 일관돼요.
#![allow(unused)]
fn main() {
fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l {
|| match **x { // `**x`를 `ImmBorrow`로 캡처해요.
[] => (),
_ => (),
}
}
}
자세한 내용은 Rust PR #138961을 참조해요.
배열의 길이는 타입으로 고정되므로, 배열을 슬라이스 패턴에 매치하는 것은 그 자체로는 place를 캡처하지 않아요.
#![allow(unused)]
fn main() {
let x: [u8; 1] = [0];
let c = || match x { // `x`를 캡처하지 않아요.
[_] => (), // 길이는 고정돼 있어요.
};
x; // OK: 여기서 `x`를 이동할 수 있어요.
c();
}
이동 컨텍스트에서의 참조 캡처
참조에서 필드를 이동하는 것은 허용되지 않으므로, move 클로저는 캡처 경로 중 참조의 첫 번째 역참조까지(역참조는 제외) 의 접두사만 캡처해요. 참조 자체는 클로저로 이동돼요.
#![allow(unused)]
fn main() {
struct T(String, String);
let mut t = T(String::from("foo"), String::from("bar"));
let t_mut_ref = &mut t;
let mut c = move || {
t_mut_ref.0.push_str("123"); // `t_mut_ref`를 ByValue로 캡처해요.
};
c();
}
원시 포인터 역참조
원시 포인터를 역참조하는 것은 unsafe이므로, 클로저는 캡처 경로 중 원시 포인터의 첫 번째 역참조까지(역참조는 제외) 의 접두사만 캡처해요.
#![allow(unused)]
fn main() {
struct T(String, String);
let t = T(String::from("foo"), String::from("bar"));
let t_ptr = &t as *const T;
let c = || unsafe {
println!("{}", (*t_ptr).0); // `t_ptr`을 ImmBorrow로 캡처해요.
};
c();
}
union 필드
union 필드에 접근하는 것은 unsafe이므로, 클로저는 캡처 경로 중 union 자신까지의 접두사만 캡처해요.
#![allow(unused)]
fn main() {
union U {
a: (i32, i32),
b: bool,
}
let u = U { a: (123, 456) };
let c = || {
let x = unsafe { u.a.0 }; // `u`를 ByValue로 캡처해요.
};
c();
// 이는 필드에 쓰는 것도 포함해요.
let mut u = U { a: (123, 456) };
let mut c = || {
u.b = true; // `u`를 MutBorrow로 캡처해요.
};
c();
}
정렬되지 않은 구조체로의 참조
구조체의 정렬되지 않은 필드에 참조를 만드는 것은 정의되지 않은 동작(UB)이므로, 클로저는 캡처 경로 중 packed 표현(packed representation)을 쓰는 구조체의 첫 번째 필드 접근까지(그 접근은 제외) 의 접두사만 캡처해요. 이는 구조체의 필드 중 하나가 미래에 바뀔 경우의 호환성 우려를 보호하기 위해, 정렬된 필드까지 포함해 모든 필드를 포함해요.
#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(i32, i32);
let t = T(2, 5);
let c = || {
let a = t.0; // `t`를 ImmBorrow로 캡처해요.
};
// `t`에서 복사하는 것은 괜찮아요.
let (a, b) = (t.0, t.1);
c();
}
마찬가지로, 정렬되지 않은 필드의 주소를 취하는 것도 구조체 전체를 캡처해요.
#![allow(unused)]
fn main() {
#[repr(packed)]
struct T(String, String);
let mut t = T(String::new(), String::new());
let c = || {
let a = std::ptr::addr_of!(t.1); // `t`를 ImmBorrow로 캡처해요.
};
let a = t.0; // ERROR: cannot move out of `t.0` because it is borrowed
c();
}
하지만 packed가 아니면 필드를 정밀하게 캡처하므로 위 코드는 동작해요.
#![allow(unused)]
fn main() {
struct T(String, String);
let mut t = T(String::new(), String::new());
let c = || {
let a = std::ptr::addr_of!(t.1); // `t.1`을 ImmBorrow로 캡처해요.
};
// 여기서 이동은 허용돼요.
let a = t.0;
c();
}
Box와 다른 Deref 구현
Box에 대한 Deref 트레잇 구현은 다른 Deref 구현과 다르게 취급돼요. Box는 특별한 개체로 간주되기 때문이에요.
예를 들어 Rc와 Box를 다루는 예시를 볼게요. *rc는 Rc에 정의된 트레잇 메서드인 deref에 대한 호출로 디슈가링돼요. 하지만 *box는 다르게 취급되므로, Box의 내용물을 정밀하게 캡처하는 것이 가능해요.
non-move 클로저에서 Box의 내용물이 클로저 본문으로 이동되지 않으면, Box의 내용물이 정밀하게 캡처돼요.
#![allow(unused)]
fn main() {
struct S(String);
let b = Box::new(S(String::new()));
let c_box = || {
let x = &(*b).0; // `(*b).0`을 ImmBorrow로 캡처해요.
};
c_box();
// `Box`를 `Deref`를 구현하는 다른 타입과 대조해보세요:
let r = std::rc::Rc::new(S(String::new()));
let c_rc = || {
let x = &(*r).0; // `r`을 ImmBorrow로 캡처해요.
};
c_rc();
}
하지만 Box의 내용물이 클로저로 이동되면 Box가 전체적으로 캡처돼요. 이는 클로저로 이동해야 하는 데이터 양을 최소화하기 위해서예요.
#![allow(unused)]
fn main() {
// 위 예시와 동일하지만, 클로저가 참조 대신 값을 이동해요.
struct S(String);
let b = Box::new(S(String::new()));
let c_box = || {
let x = (*b).0; // `b`를 ByValue로 캡처해요.
};
c_box();
}
non-move 클로저에서 Box 내용물을 이동하는 것과 비슷하게, move 클로저에서 Box 내용물을 읽는 것도 Box를 전체적으로 캡처해요.
#![allow(unused)]
fn main() {
struct S(i32);
let b = Box::new(S(10));
let c_box = move || {
let x = (*b).0; // `b`를 ByValue로 캡처해요.
};
}
캡처에서의 고유 불변 빌림
캡처는 고유 불변 빌림(unique immutable borrow) 이라는 특별한 종류의 빌림으로 발생할 수 있어요. 이 빌림은 언어의 다른 곳에서는 사용할 수 없고 명시적으로 쓸 수도 없어요. 이는 가변 참조의 피참조자(referent)를 수정할 때 발생해요. 아래 예시를 볼게요.
#![allow(unused)]
fn main() {
let mut b = false;
let x = &mut b;
let mut c = || {
// `x`의 ImmBorrow와 MutBorrow.
let a = &x;
*x = true; // `x`는 UniqueImmBorrow로 캡처돼요.
};
// 다음 줄은 에러예요:
// let y = &x;
c();
// 하지만 다음은 OK예요.
let z = &x;
}
이 경우 x를 가변으로 빌리는 것은 불가능한데, x가 mut가 아니기 때문이에요. 하지만 동시에 x를 불변으로 빌리면 할당이 불법이 돼요. & &mut 참조는 고유하지 않을 수 있어서 값을 수정하는 데 안전하게 쓸 수 없거든요. 그래서 고유 불변 빌림이 쓰여요. x를 불변으로 빌리지만, 가변 빌림처럼 고유해야 해요.
위 예시에서 y의 선언 주석을 풀면 에러가 발생해요. 클로저의 x 빌림의 고유성을 위반하기 때문이에요. z의 선언은 유효한데, 클로저의 라이프타임이 블록 끝에서 만료되어 빌림을 해제했기 때문이에요.
호출 트레잇과 강제 변환
모든 클로저 타입은 FnOnce를 구현해요. 즉 클로저의 소유권을 소비해서 한 번 호출할 수 있다는 뜻이에요. 추가로 일부 클로저는 더 구체적인 호출 트레잇을 구현해요.
- 캡처한 변수에서 아무것도 이동하지 않는 클로저는
FnMut를 구현해요. 즉 가변 참조로 호출할 수 있어요. - 캡처한 변수를 수정하거나 이동하지 않는 클로저는
Fn을 구현해요. 즉 공유 참조로 호출할 수 있어요.
참고로 move 클로저도 여전히 Fn이나 FnMut를 구현할 수 있어요. 캡처 변수를 이동으로 캡처하더라도요. 클로저 타입이 구현하는 트레잇은 캡처된 값을 어떻게 캡처하는지가 아니라 무엇을 하는지에 의해 결정되기 때문이에요.
비-캡처 클로저(non-capturing closure) 는 환경에서 아무것도 캡처하지 않는 클로저예요. non-async인 비-캡처 클로저는 일치하는 시그니처를 가진 함수 포인터(예: fn()) 로 강제 변환될 수 있어요.
#![allow(unused)]
fn main() {
let add = |x, y| x + y;
let mut x = add(5,7);
type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);
}
async 클로저 트레잇
async 클로저는 FnMut나 Fn을 구현하는지 여부에 추가적인 제약이 있어요.
async 클로저가 반환하는 Future는 클로저와 비슷한 캡처 특성을 가져요. 캡처된 place 표현식이 어떻게 사용되는지에 따라 async 클로저로부터 place 표현식을 캡처하죠. async 클로저는 다음 속성 중 하나를 가지면 자신의 Future에 대여(lending) 한다고 말해요.
- Future가 가변 캡처를 포함한다.
- async 클로저가 값을 캡처하되, 그 값이 역참조 투영으로 접근되는 경우는 제외.
async 클로저가 Future에 대여하면 FnMut와 Fn은 구현되지 않아요. FnOnce는 항상 구현돼요.
가변 캡처에 대한 첫 번째 조건은 다음과 같이 예시로 나타낼 수 있어요.
#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {}
fn f() {
let mut x = 1i32;
let c = async || {
x = 2; // x는 MutBorrow로 캡처됨
};
takes_callback(c); // ERROR: async closure does not implement `FnMut`
}
}
일반 값 캡처에 대한 두 번째 조건은 다음과 같이 예시로 나타낼 수 있어요.
#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}
fn f() {
let x = &1i32;
let c = async move || {
let a = x + 2; // x는 ByValue로 캡처됨
};
takes_callback(c); // ERROR: async closure does not implement `Fn`
}
}
두 번째 조건의 예외는 역참조를 사용하는 것으로 나타낼 수 있는데, 이 경우 Fn과 FnMut를 구현할 수 있어요.
#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}
fn f() {
let x = &1i32;
let c = async move || {
let a = *x + 2;
};
takes_callback(c); // OK: implements `Fn`
}
}
async 클로저는 일반 클로저가 Fn, FnMut, FnOnce를 구현하는 것과 유사한 방식으로 AsyncFn, AsyncFnMut, AsyncFnOnce를 구현해요. 즉 본문에서 캡처된 변수의 사용에 따라 결정되죠.
다른 트레잇
모든 클로저 타입은 Sized를 구현해요. 추가로 클로저 타입은 저장하는 캡처의 타입이 허용한다면 다음 트레잇도 구현해요.
CloneCopySyncSend
Send와 Sync의 규칙은 일반 구조체 타입과 일치하고, Clone과 Copy는 파생된 것처럼 동작해요. Clone의 경우 캡처된 값이 클로닝되는 순서는 명시되어 있지 않아요.
캡처가 종종 참조로 되기 때문에 다음과 같은 일반 규칙이 생겨요.
- 클로저는 모든 캡처 값이
Sync이면Sync예요. - 클로저는 non-unique 불변 참조로 캡처된 모든 값이
Sync이고, unique 불변 또는 가변 참조, 복사, 이동으로 캡처된 모든 값이Send이면Send예요. - 클로저는 unique 불변 또는 가변 참조로 어떤 값도 캡처하지 않고, 복사나 이동으로 캡처하는 모든 값이 각각
Clone/Copy이면Clone/Copy예요.
드롭 순서
클로저가 구조체, 튜플, enum 같은 합성 타입의 필드를 값으로 캡처하면, 필드의 라이프타임이 이제 클로저에 묶이게 돼요. 그 결과 합성 타입의 서로 다른(disjoint) 필드가 서로 다른 시점에 드롭될 수 있어요.
#![allow(unused)]
fn main() {
{
let tuple =
(String::from("foo"), String::from("bar")); // --+
{ // |
let c = || { // ----------------------------+ |
// tuple.0이 클로저로 캡처돼요 | |
drop(tuple.0); // | |
}; // | |
} // 'c'와 'tuple.0'이 여기서 드롭됨 ------------+ |
} // tuple.1은 여기서 드롭됨 --------------------------+
}
에디션 2018 이전
클로저 타입 차이: 에디션 2018 이전에서는 클로저가 항상 변수 전체를 정밀한 캡처 경로 없이 캡처해요. Closure types 섹션에서 쓴 예시에 대해, 생성되는 클로저 타입이 대신 대략 아래처럼 생겼을 거예요.
struct Closure<'a> {
rect : &'a mut Rectangle,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.rect.left_top.x += 1;
self.rect.right_bottom.x += 1;
format!("{:?}", self.rect.left_top)
}
}
그리고 f에 대한 호출은 다음과 같이 작동했을 거예요.
f(Closure { rect: rect });
캡처 정밀도 차이: 구조체, 튜플, enum 같은 합성 타입은 항상 개별 필드가 아니라 전체로 캡처돼요. 그래서 단일 필드를 캡처하기 위해 지역 변수로 빌려와야 할 수도 있어요.
#![allow(unused)]
fn main() {
use std::collections::HashSet;
struct SetVec {
set: HashSet<u32>,
vec: Vec<u32>
}
impl SetVec {
fn populate(&mut self) {
let vec = &mut self.vec;
self.set.iter().for_each(|&n| {
vec.push(n);
})
}
}
}
클로저가 self.vec를 직접 사용하면 self를 가변 참조로 캡처하려고 할 거예요. 그런데 self.set이 이미 반복을 위해 빌려져 있으므로 그 코드는 컴파일되지 않을 거예요.
move: move 키워드를 쓰면, 빌림이 동작할 수 있는지와 무관하게 모든 캡처가 이동 또는(Copy 타입에 대해) 복사가 돼요. move 키워드는 보통 클로저가 캡처된 값보다 오래 살 수 있게 하기 위해 쓰여요. 클로저가 반환되거나 새 스레드를 생성하는 데 사용되는 경우처럼요.
와일드카드: 데이터가 클로저에 의해 읽힐지 여부(예: 와일드카드 패턴의 경우)와 무관하게, 클로저 밖에서 정의된 변수가 클로저 안에서 언급되면 그 변수는 전체로 캡처돼요.
드롭 순서 차이: 합성 타입이 전체로 캡처되므로, 그런 합성 타입 중 하나를 값으로 캡처하는 클로저는 캡처된 변수 전체를 클로저가 드롭될 때 동시에 드롭해요.
#![allow(unused)]
fn main() {
{
let tuple =
(String::from("foo"), String::from("bar"));
{
let c = || { // --------------------------+
// tuple이 클로저로 캡처돼요 |
drop(tuple.0); // |
}; // |
} // 'c'와 'tuple'이 여기서 드롭됨 ------------+
}
}
더 알아보기 (Learn more)
- Rust Reference - Closure expressions — 클로저 표현식 문법.
- Rust Reference - Destructuring assignment — 와일드카드 패턴.
- Rust Reference - Types