소멸자
소멸자 (Destructors)
변수가 스코프를 벗어날 때 그 뒤를 정리해 주는 게 러스트의 소멸자(destructor)예요. 소유권 때문에 항상 마주치게 되는 주제라서, 어떤 순서로 무엇이 드롭되는지를 정확히 아는 게 중요해요. 이 페이지에서 소멸자의 규칙을 차근차근 정리해볼게요.
출처: Rust Reference
본문
초기화된 변수나 임시 값이 스코프를 벗어날 때, 그 소멸자가 실행되거나 값이 드롭됩니다(dropped). 할당(assignment)은 또한 그 왼쪽 피연산자에 대해서도, 만약 초기화되어 있다면 소멸자를 실행해요. 변수가 부분적으로 초기화되었다면, 초기화된 필드만 드롭돼요.
소멸자의 동작 (The destructor of a type)
타입 T의 소멸자는 다음으로 구성돼요.
T: Drop이면<T as core::ops::Drop>::drop호출- 모든 필드의 소멸자를 재귀적으로 실행
필드들은 다음 순서로 드롭돼요.
- 구조체의 필드는 선언 순서대로 드롭돼요.
- 활성 열거형 변형(variant)의 필드는 선언 순서대로 드롭돼요.
- 튜플의 필드는 순서대로 드롭돼요.
- 배열이나 소유 슬라이스의 요소들은 첫 번째 요소부터 마지막까지 드롭돼요.
- 클로저가 move로 캡처한 변수들은 지정되지 않은 순서로 드롭돼요.
- 트레이트 객체는 기저 타입의 소멸자를 실행해요.
- 다른 타입들은 추가 드롭을 일으키지 않아요.
소멸자를 수동으로 실행해야 한다면, 예를 들어 직접 스마트 포인터를 구현할 때처럼, core::ptr::drop_in_place를 사용할 수 있어요.
몇 가지 예시를 볼게요.
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("{}", self.0);
}
}
let mut overwritten = PrintOnDrop("drops when overwritten");
overwritten = PrintOnDrop("drops when scope ends");
let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second"));
let moved;
// 할당에서는 소멸자가 실행되지 않아요.
moved = PrintOnDrop("Drops when moved");
// 지금 드롭되고 나서, 초기화되지 않은 상태가 돼요.
moved;
// 초기화되지 않은 것은 드롭되지 않아요.
let uninitialized: PrintOnDrop;
// 부분 이동 후에는 남은 필드만 드롭돼요.
let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten"));
// 부분 이동을 수행하고, `partial_move.0`만 초기화된 채로 남겨요.
core::mem::forget(partial_move.1);
// partial_move의 스코프가 끝나면 첫 번째 필드만 드롭돼요.
드롭 스코프 (Drop scopes)
각 변수나 임시 값은 드롭 스코프(drop scope)와 연관돼요. 제어 흐름이 드롭 스코프를 벗어나면, 그 스코프와 연관된 모든 변수는 선언 순서(변수의 경우) 또는 생성 순서(임시 값의 경우)의 역순으로 드롭돼요.
드롭 스코프는 for, if, while 표현식을 match, loop, break를 사용한 동등한 표현식으로 바꾸면 결정할 수 있어요. 오버로드된 연산자는 내장 연산자와 구별되지 않고, 바인딩 모드도 고려되지 않아요.
함수나 클로저가 주어졌을 때, 드롭 스코프는 다음과 같은 곳들에 있어요.
- 전체 함수 — 함수의 전체 드롭 스코프는 가장 바깥 스코프예요.
- 각 문장(statement) — 각 문장에 대한 스코프.
- 각 표현식(expression) — 각 표현식에 대한 스코프.
- 각 블록(block) — 함수 본문을 포함한 각 블록.
블록 표현식의 경우, 블록의 스코프와 표현식의 스코프는 같은 스코프예요.
- match 표현식의 각 arm — 각 arm에 대한 스코프.
드롭 스코프는 다음과 같이 서로 중첩돼요. 함수에서 반환할 때처럼 여러 스코프를 한 번에 벗어날 때, 변수들은 안쪽에서 바깥쪽으로 드롭돼요.
- 전체 함수 스코프가 가장 바깥 스코프예요.
- 함수 본문 블록은 전체 함수의 스코프 안에 포함돼요.
- 표현식 문장에서 표현식의 부모는 문장의 스코프예요.
let문장의 초기화식의 부모는 그let문장의 스코프예요.- 문장 스코프의 부모는 그 문장을 포함하는 블록의 스코프예요.
- match 가드의 표현식의 부모는 그 가드가 속한 arm의 스코프예요.
match표현식에서=>뒤의 표현식의 부모는 그 표현식이 있는 arm의 스코프예요.- arm 스코프의 부모는 그 arm이 속한
match표현식의 스코프예요. - 다른 모든 스코프의 부모는 즉시 둘러싸는 표현식의 스코프예요.
함수 매개변수의 스코프 (Scopes of function parameters)
모든 함수 매개변수는 전체 함수 본문의 스코프 안에 있어서, 함수를 평가할 때 가장 마지막에 드롭돼요. 각 실제 함수 매개변수는 그 매개변수의 패턴에 도입된 바인딩들 이후에 드롭돼요.
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
// `y`, 두 번째 매개변수, `x`, 첫 번째 매개변수 순서로 드롭돼요
fn patterns_in_parameters(
(x, _): (PrintOnDrop, PrintOnDrop),
(_, y): (PrintOnDrop, PrintOnDrop),
) {}
// 드롭 순서는 3 2 0 1
patterns_in_parameters(
(PrintOnDrop("0"), PrintOnDrop("1")),
(PrintOnDrop("2"), PrintOnDrop("3")),
);
지역 변수의 스코프 (Scopes of local variables)
let 문장에서 선언된 지역 변수는 그 let 문장을 포함하는 블록의 스코프와 연관돼요.
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("drop({})", self.0);
}
}
let declared_first = PrintOnDrop("Dropped last in outer scope");
{
let declared_in_block = PrintOnDrop("Dropped in inner scope");
}
let declared_last = PrintOnDrop("Dropped first in outer scope");
match 표현식이나 패턴 매칭 match 가드에서 선언된 지역 변수는 그들이 선언된 match arm의 arm 스코프와 연관돼요.
match PrintOnDrop("Dropped last in the first arm's scope") {
// 가드 평가가 성공하면 제어 흐름이 arm 안에 머물고, 값이 검사 대상에서
// arm의 바인딩으로 이동할 수 있어서 arm의 스코프에서 드롭돼요.
x if let y = PrintOnDrop("Dropped second in the first arm's scope")
&& let z = PrintOnDrop("Dropped first in the first arm's scope") =>
{
let declared_in_block = PrintOnDrop("Dropped in inner scope");
// 패턴 매칭 가드의 바인딩과 임시 값은 역순으로 드롭되고, 각 가드 조건
// 피연산자의 바인딩이 그 임시 값보다 먼저 드롭돼요. 마지막으로 arm의
// 패턴이 바인딩한 변수들이 드롭돼요.
}
_ => unreachable!(),
}
match PrintOnDrop("Dropped in the enclosing temporary scope") {
// 가드 평가가 실패하면 제어 흐름이 arm 스코프를 벗어나, 이전 패턴 매칭
// 가드 조건 피연산자들의 바인딩과 임시 값이 드롭돼요. 이는 다음 arm의
// 가드나 본문을 평가하기 전에 발생해요.
_ if let y = PrintOnDrop("Dropped in the first arm's scope")
&& false => unreachable!(),
// 자체 겹치는 or-패턴 때문에 가드가 여러 번 실행되면, 가드가 실패할 때
// arm 스코프를 벗어났다가 가드를 다시 실행하기 전에 arm 스코프로
// 다시 들어와요.
_ | _ if let y = PrintOnDrop("Dropped in the second arm's scope twice")
&& false => unreachable!(),
_ => {},
}
패턴 안의 변수들은 패턴 안에서 선언된 순서의 역순으로 드롭돼요.
let (declared_first, declared_last) = (
PrintOnDrop("Dropped last"),
PrintOnDrop("Dropped first"),
);
드롭 순서를 위한 목적에서, or-패턴은 첫 번째 하위 패턴이 주는 순서로 바인딩을 선언해요.
// `x`를 `y`보다 먼저 드롭해요.
fn or_pattern_drop_order<T>(
(Ok([x, y]) | Err([y, x])): Result<[T; 2], [T; 2]>
// ^^^^^^^^^^ ^^^^^^^^^^^ 이건 두 번째 하위 패턴이에요.
// |
// 이건 첫 번째 하위 패턴이에요.
//
// 첫 번째 하위 패턴에서는 `x`가 `y`보다 먼저 선언돼요. 첫 번째 하위
// 패턴이니, 바인딩이 반대 순서로 선언된 두 번째 하위 패턴이 매치되더라도
// 그 순서가 사용돼요.
) {}
// 여기서 첫 번째 하위 패턴을 매치하고, 드롭은 첫 번째 하위 패턴의
// 선언 순서대로 일어나요.
or_pattern_drop_order(Ok([
PrintOnDrop("Declared first, dropped last"),
PrintOnDrop("Declared last, dropped first"),
]));
// 여기서 두 번째 하위 패턴을 매치하지만, 드롭은 여전히 첫 번째 하위
// 패턴의 선언 순서대로 일어나요.
or_pattern_drop_order(Err([
PrintOnDrop("Declared last, dropped first"),
PrintOnDrop("Declared first, dropped last"),
]));
임시 스코프 (Temporary scopes)
표현식의 **임시 스코프(temporary scope)**는, 프로모션(promoted)되지 않는 한, 그 표현식이 위치 문맥에서 사용될 때 결과를 담는 임시 변수에 사용되는 스코프예요.
수명 연장(lifetime extension)을 제외하면, 표현식의 임시 스코프는 그 표현식을 포함하는 가장 작은 스코프이면서 다음 중 하나여야 해요.
- 전체 함수.
- 문장(statement).
if,while또는loop표현식의 본문.if표현식의else블록.if또는while표현식의 비패턴 매칭 조건 표현식, 또는 비패턴 매칭 match 가드 조건 피연산자.- match arm의 패턴 매칭 가드(있을 경우)와 본문 표현식.
- 지연 부울 표현식의 각 피연산자.
if의 패턴 매칭 조건(들)과 그에 따른 본문 (2024 에디션).while의 패턴 매칭 조건과 루프 본문.- 블록 꼬리 표현식의 전체 (2024 에디션).
참고:
match표현식의 검사 대상은 임시 스코프가 아니므로, 검사 대상의 임시 값은match표현식 이후에도 드롭될 수 있어요. 예를 들어match 1 { ref mut z => z };에서1의 임시 값은 문장이 끝날 때까지 살아있어요.
참고: 구조 분해 할당(destructuring assignment)의 디슈가링은 그 할당된 값 피연산자(오른쪽)의 임시 스코프를 제한해요.
2024 에디션 차이
2024 에디션은 두 가지 새로운 임시 스코프 축소 규칙을 추가했어요. if let 임시 값이 else 블록 전에 드롭되는 것과, 블록 꼬리 표현식의 임시 값이 꼬리 표현식이 평가된 직후 드롭되는 것이에요.
몇 가지 예시를 볼게요.
let local_var = PrintOnDrop("local var");
// 조건이 평가되면 드롭돼요
if PrintOnDrop("If condition").0 == "If condition" {
// 블록 끝에서 드롭돼요
PrintOnDrop("If body").0
} else {
unreachable!()
};
if let "if let scrutinee" = PrintOnDrop("if let scrutinee").0 {
PrintOnDrop("if let consequent").0
// `if let consequent` 여기서 드롭돼요
}
// `if let scrutinee` 여기서 드롭돼요
else {
PrintOnDrop("if let else").0
// `if let else` 여기서 드롭돼요
};
while let x = PrintOnDrop("while let scrutinee").0 {
PrintOnDrop("while let loop body").0;
break;
// `while let loop body` 여기서 드롭돼요.
// `while let scrutinee` 여기서 드롭돼요.
}
// 첫 번째 || 앞에서 드롭돼요
(PrintOnDrop("first operand").0 == ""
// ) 앞에서 드롭돼요
|| PrintOnDrop("second operand").0 == "")
// ; 앞에서 드롭돼요
|| PrintOnDrop("third operand").0 == "";
// 검사 대상은 함수 끝에서, 지역 변수들보다 먼저 드롭돼요
// (함수 본문 블록의 꼬리 표현식이기 때문이에요).
match PrintOnDrop("Matched value in final expression") {
// 비패턴 매칭 가드의 임시 값은 조건이 평가되면 드롭돼요
_ if PrintOnDrop("guard condition").0 == "" => (),
// 패턴 매칭 가드의 임시 값은 arm 스코프를 벗어날 때 드롭돼요
_ if let "guard scrutinee" = PrintOnDrop("guard scrutinee").0 => {
let _ = &PrintOnDrop("lifetime-extended temporary in inner scope");
// `lifetime-extended temporary in inner scope` 여기서 드롭돼요
}
// `guard scrutinee` 여기서 드롭돼요
_ => (),
}
피연산자 (Operands)
임시 값은 다른 피연산자들이 평가되는 동안 표현식의 피연산자 결과를 담기 위해서도 생성돼요. 이 임시 값들은 그 피연산자를 가진 표현식의 스코프와 연관돼요. 표현식이 평가되면 임시 값은 이동되므로, 표현식의 피연산자 중 하나가 표현식에서 벗어나거나(break), 반환하거나, 패닉하지 않는 한 이들을 드롭하는 것은 아무런 효과가 없어요.
loop {
// 튜플 표현식이 평가를 끝내지 않아 피연산자들이 역순으로 드롭돼요
(
PrintOnDrop("Outer tuple first"),
PrintOnDrop("Outer tuple second"),
(
PrintOnDrop("Inner tuple first"),
PrintOnDrop("Inner tuple second"),
break,
),
PrintOnDrop("Never created"),
);
}
상수 프로모션 (Constant promotion)
값 표현식이 'static 슬롯으로 프로모션되는 것은, 그 표현식을 상수에 쓰고 빌려올 수 있고, 그 빌림을 원래 표현식이 쓰여진 곳에서 역참조할 수 있으며, 런타임 동작을 바꾸지 않을 때 발생해요. 즉, 프로모션된 표현식은 컴파일 타임에 평가될 수 있고, 결과 값은 내부 가변성(interior mutability)이나 소멸자를 포함하지 않아요. (이 속성들은 가능하면 값에 기반해 결정돼요. 예를 들어 &None은 항상 &'static Option<_> 타입을 갖는데, 허용되지 않는 것을 담고 있지 않기 때문이에요.)
임시 수명 연장 (Temporary lifetime extension)
참고: 임시 수명 연장의 정확한 규칙은 변경될 수 있어요. 이 문서는 현재 동작만을 서술하는 것이에요.
let 문장의 표현식에 대한 임시 스코프는 때때로 그 let 문장을 포함하는 블록의 스코프까지 연장돼요. 이는 특정 문법 규칙에 기반해, 보통의 임시 스코프가 너무 작을 때 수행돼요. 예를 들어:
let x = &mut 0;
// 보통은 임시 값이 지금쯤 드롭됐겠지만, `0`의 임시 값은 블록 끝까지 살아있어요.
println!("{}", x);
수명 연장은 static과 const 아이템에도 적용되며, 임시 값이 프로그램 끝까지 살아있게 만들어요. 예를 들어:
const C: &Vec<i32> = &Vec::new();
// 보통은 `Vec`이 `C`의 초기화식 안에서만 존재하므로 이는 댕글링 참조가
// 되겠지만, 대신 빌림이 수명 연장되어 사실상 `'static` 수명을 갖게 돼요.
println!("{:?}", C);
빌림, 역참조, 필드, 또는 튜플 인덱싱 표현식이 연장된 임시 스코프를 가지면, 그 피연산자도 마찬가지로 갖게 돼요. 인덱싱 표현식이 연장된 임시 스코프를 가지면, 인덱싱되는 표현식도 마찬가지로 연장된 임시 스코프를 가져요.
패턴 기반 연장 (Extending based on patterns)
**연장 패턴(extending pattern)**은 다음 중 하나예요.
- 참조 또는 가변 참조로 바인딩하는 식별자 패턴.
fn temp() {}
let ref x = temp(); // 참조로 바인딩.
x;
let ref mut x = temp(); // 가변 참조로 바인딩.
x;
- 직접 하위 패턴 중 적어도 하나가 연장 패턴인 구조체, 튜플, 튜플 구조체, 슬라이스, 또는 or-패턴.
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
static X: AtomicU64 = AtomicU64::new(0);
struct W<T>(T);
impl<T> Drop for W<T> { fn drop(&mut self) { X.fetch_add(1, Relaxed); } }
let W { 0: ref x } = W(()); // 구조체 패턴.
x;
let W(ref x) = W(()); // 튜플 구조체 패턴.
x;
let (W(ref x),) = (W(()),); // 튜플 패턴.
x;
let [W(ref x), ..] = [W(())]; // 슬라이스 패턴.
x;
let (Ok(W(ref x)) | Err(&ref x)) = Ok(W(())); // Or 패턴.
x;
//
// 위의 모든 임시 값은 여기서도 여전히 살아있어요.
assert_eq!(0, X.load(Relaxed));
그래서 ref x, V(ref x), [ref x, y]는 모두 연장 패턴이지만, x, &ref x, &(ref x,)는 아니에요.
let 문장의 패턴이 연장 패턴이면 초기화 표현식의 임시 스코프가 연장돼요.
fn temp() {}
// 이건 연장 패턴이므로, 임시 스코프가 연장돼요.
let ref x = *&temp(); // OK
x;
fn temp() {}
// 이건 연장 패턴도 연장 표현식도 아니므로, 임시 값이 세미콜론에서 드롭돼요.
let &ref x = *&&temp(); // ERROR
x;
fn temp() {}
// 이건 연장 패턴은 아니지만 연장 표현식이므로, 임시 값이 `let` 문장을
// 넘어 살아있어요.
let &ref x = &*&temp(); // OK
x;
표현식 기반 연장 (Extending based on expressions)
초기화식을 가진 let 문장에 대해, **연장 표현식(extending expression)**은 다음 중 하나인 표현식이에요.
- 초기화 표현식.
- 연장 빌림 표현식의 피연산자.
- 연장 super 매크로 호출 표현식의 super 피연산자.
- 연장 배열, 캐스트, 중괄호 구조체, 또는 튜플 표현식의 피연산자(들).
- 연장 튜플 구조체 또는 튜플 열거형 변형 생성자 표현식의 인자들.
async블록 표현식을 제외한 연장 블록 표현식의 꼬리 표현식.- 연장
if표현식의 결과 본문,else if, 또는else블록의 꼬리 표현식. - 연장
match표현식의 arm 표현식.
참고: 구조 분해 할당의 디슈가링은 그 할당된 값 피연산자(오른쪽)를 새로 도입된 블록 안에서 연장 표현식으로 만들어요.
그래서 &mut 0, (&1, &mut 2), Some(&mut 3)의 빌림 표현식은 모두 연장 표현식이에요. &0 + &1과 f(&mut 0)의 빌림은 아니에요.
연장 빌림 표현식의 피연산자는 임시 스코프가 연장돼요. 연장 super 매크로 호출 표현식의 super 임시 값도 스코프가 연장돼요.
참고: rustc는 연장 배열 표현식의 배열 반복 피연산자를 연장 표현식으로 취급하지 않아요. 그래야 하는지는 열린 질문이에요.
연장된 임시 스코프를 가진 예시:
use core::pin::pin;
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
static X: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)] struct S;
impl Drop for S { fn drop(&mut self) { X.fetch_add(1, Relaxed); } }
const fn temp() -> S { S }
let x = &temp(); // 빌림의 피연산자.
x;
let x = &raw const *&temp(); // 원시 빌림의 피연산자.
assert_eq!(X.load(Relaxed), 0);
let x = &temp() as &dyn Send; // 캐스트의 피연산자.
x;
let x = (&*&temp(),); // 튜플 생성자의 피연산자.
x;
struct W<T>(T);
let x = W(&temp()); // 튜플 구조체 생성자의 인자.
x;
let x = Some(&temp()); // 튜플 열거형 변형 생성자의 인자.
x;
let x = { [Some(&temp())] }; // 블록의 꼬리 표현식.
x;
let x = const { &temp() }; // `const` 블록의 꼬리 표현식.
x;
let x = unsafe { &temp() }; // `unsafe` 블록의 꼬리 표현식.
x;
let x = if true { &temp() } else { &temp() };
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// `if`/`else` 블록의 꼬리 표현식.
x;
let x = match () { _ => &temp() }; // `match` arm 표현식.
x;
let x = pin!(temp()); // super 매크로 호출 표현식의 super 피연산자.
x;
let x = pin!({ &mut temp() }); // 위와 같이.
x;
let x = format_args!("{:?}", temp()); // 위와 같이.
x;
//
// 위의 모든 임시 값은 여기서도 여전히 살아있어요.
assert_eq!(0, X.load(Relaxed));
연장된 임시 스코프를 가지지 않는 예시:
fn temp() {}
// 함수 호출의 인자는 연장 표현식이 아니에요. 임시 값이 세미콜론에서 드롭돼요.
let x = core::convert::identity(&temp()); // ERROR
x;
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// 메서드 호출의 수신자는 연장 표현식이 아니에요.
let x = (&temp()).use_temp(); // ERROR
x;
fn temp() {}
// match 표현식의 검사 대상은 연장 표현식이 아니에요.
let x = match &temp() { x => x }; // ERROR
x;
fn temp() {}
// `async` 블록의 꼬리 표현식은 연장 표현식이 아니에요.
let x = async { &temp() }; // ERROR
x;
fn temp() {}
// 클로저의 꼬리 표현식은 연장 표현식이 아니에요.
let x = || &temp(); // ERROR
x;
fn temp() {}
// 루프 break의 피연산자는 연장 표현식이 아니에요.
let x = loop { break &temp() }; // ERROR
x;
fn temp() {}
// 레이블로의 break의 피연산자는 연장 표현식이 아니에요.
let x = 'a: { break 'a &temp() }; // ERROR
x;
use core::pin::pin;
fn temp() {}
// `pin!`의 인자는 호출이 연장 표현식일 때만 연장 표현식이에요. 여기서는
// 아니므로, 안쪽 블록이 연장 표현식이 아니고, 꼬리 표현식의 임시 값들이
// 즉시 드롭돼요.
pin!({ &temp() }); // ERROR
fn temp() {}
// 위와 같이.
format_args!("{:?}", { &temp() }); // ERROR
소멸자를 실행하지 않기 (Not running destructors)
소멸자를 수동으로 억제하기 (Manually suppressing destructors)
core::mem::forget는 변수의 소멸자가 실행되지 않게 막는 데 사용할 수 있고, core::mem::ManuallyDrop는 변수나 필드가 자동으로 드롭되지 않게 하는 래퍼를 제공해요.
참고:
core::mem::forget나 다른 수단으로 소멸자가 실행되지 않게 막는 것은, 그 타입이'static이 아니더라도 안전해요. 이 문서가 정의한 대로 소멸자가 보장되는 장소를 제외하면, 타입은 건전성(soundness)을 위해 소멸자가 실행되는 것에 안전하게 의존할 수 없어요.
언와인딩 없는 프로세스 종료 (Process termination without unwinding)
언와인딩(unwinding) 없이 프로세스를 종료하는 몇 가지 방법이 있는데, 그 경우 소멸자는 실행되지 않아요. 표준 라이브러리는 이를 명시적으로 하는 std::process::exit와 std::process::abort를 제공해요. 게다가 패닉 핸들러가 abort로 설정되어 있으면, 패닉은 항상 소멸자 없이 프로세스를 종료해요. 주의할 추가 사례가 하나 있어요. 패닉이 non-unwinding ABI 경계에 도달하면, 소멸자가 하나도 실행되지 않거나, ABI 경계까지의 모든 소멸자가 실행돼요.