소멸자

러스트에서는 일반적인 소멸자는 없지만, 러스트에서 제공하는 것은 Drop 트레잇을 통한 완전 자동화된 소멸자입니다. 이 트레잇은 다음의 메서드를 제공합니다:

fn drop(&mut self);

이 메서드는 타입에 하던 일을 끝낼 시간을 줍니다.

drop이 실행된 후, 러스트는 self의 모든 필드들을 재귀적으로 해제하려 시도할 겁니다.

이것은 여러분이 필드들을 해제하기 위해 "소멸자 코드 노가다"를 하지 않아도 되도록 하는 편의 기능입니다. 만약 구조체가 해제될 때 그 필드들을 해제하는 것 외에 다른 특별한 논리가 없다면, Drop 구현이 아예 없어도 된다는 뜻입니다!

러스트 1.0에서는 이것을 막을 안정적인 방법은 존재하지 않습니다.

또한 &mut self를 취한다는 것은 여러분이 어떻게 재귀적인 해제를 막는다고 해도, self에서 필드를 이동하는 등의 작업을 러스트가 막는다는 것을 의미합니다. 대부분의 타입에 있어서 이것은 전혀 문제가 없습니다.

예를 들어, Box의 어떤 구현은 Drop을 이렇게 작성할 수도 있겠습니다:

#![feature(ptr_internals, allocator_api)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::mem;
use std::ptr::{drop_in_place, NonNull, Unique};

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>())
        }
    }
}
fn main() {}

그리고 이것은 잘 작동하는데, 러스트가 ptr 필드를 해제하려 할 때, 실제 Drop 구현이 없는 Unique를 보기 때문입니다. 마찬가지로 ptr을 누구도 해제 후 사용할 수 없는데, drop이 종료하고 나면 접근할 수 없기 때문입니다.

하지만 다음의 코드는 동작하지 않을 겁니다:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Box<T> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // 슈-퍼 최적화: `my_box`를 `drop`하지 않고 
            // 그 내용물을 해제합니다
            let c: NonNull<T> = self.my_box.ptr.into();
            Global.deallocate(c.cast::<u8>(), Layout::new::<T>());
        }
    }
}
fn main() {}

SuperBox의 소멸자에서 my_boxptr을 해제한 후에, 러스트는 해맑게 my_box에게 스스로를 해제하라고 말할 것이고, 그럼 해제 후 사용과 이중 해제로 모든 것이 폭발할 겁니다.

이러한 재귀적인 해제 동작은 Drop을 구현하든 하지 않든 모든 구조체와 열거형에 적용됩니다. 따라서 이러한 구조체는

#![allow(unused)]
fn main() {
struct Boxy<T> {
    data1: Box<T>,
    data2: Box<T>,
    info: u32,
}
}

그 자체로 Drop을 구현하지 않더라도, data1data2의 필드들이 해제될 "것 같을 때" 그 소멸자를 호출할 것입니다. 우리는 이런 타입이 Drop이 필요하다고 합니다, 그 자체로는 Drop이 아니더라도요.

마찬가지로,

#![allow(unused)]
fn main() {
enum Link {
    Next(Box<Link>),
    None,
}
}

이 열거형은 오직 그 값이 Next일 때만 그 안의 Box 필드를 해제할 것입니다.

일반적으로는 여러분이 데이터 구조를 바꿀 때마다 drop을 추가/삭제하지 않아도 되기 때문에 이것은 매우 좋게 동작합니다. 하지만, 여전히 소멸자를 가지고 좀더 복잡한 작업을 할 때의 많은 올바른 사용 사례가 확실히 있습니다.

재귀적 해제를 수동으로 바꿔서 drop 중에 Self에서 필드를 이동하는 것을 허용하는 고전적인 안전한 방법은 Option을 사용하는 것입니다:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, GlobalAlloc, Global, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Option<Box<T>> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // 슈-퍼 최적화: `my_box`를 `drop`하지 않고
            // 그 내용물을 해제합니다. 러스트가 `my_box`를 `drop`하지 않도록
            // 이 필드를 `None`으로 설정해야 합니다.
            let my_box = self.my_box.take().unwrap();
            let c: NonNull<T> = my_box.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
            mem::forget(my_box);
        }
    }
}
fn main() {}

하지만 이것은 좀 이상한 의미를 담고 있습니다: 항상 Some이어야 하는 필드가 None수도 있다는 겁니다 (소멸자에서 이런 일이 일어나기 때문에요). 당연히 역으로는 말이 됩니다: 소멸자에서는 self에 임의의 메서드를 호출할 수 있고, 이렇게 하는 것이 필드를 비초기화한 후에 그 필드를 건드리지 못하게 합니다. 하지만 이렇게 할 때 그 필드에 다른 임의의 잘못된 상태를 만드는 것은 막지 못합니다.

득과 실을 비교했을 때 이것은 괜찮은 선택입니다. 분명히 기본적으로 해야 하는 방법이죠. 하지만 미래에는 필드가 자동으로 해제되면 안된다고 말하는, 공식적인 방법이 있기를 바랍니다.