JS, TS랑 비교하며 Rust 공부하기 (10)
스마트 포인터
스마트 포인터는 포인터처럼 작동할 뿐만 아니라 추가적인 메타데이터와 능력들도 가지고 있는 데이터 구조이다.
참조자와는 다르게 대부분의 스마트 포인터는 가리킨 데이터를 소유한다.
스마트 포인터는 보통 구조체로 구현되어 있으며 Dref와 Drop 트레이트를 구현한다.
Box<T>
Box<T>는 값을 스택 대신 힙에 저장하고, 이를 가리키는 포인터만 스택에 유지하는 단순한 스마트 포인터이다.
Deref 트레이트를 구현해서 일반 값처럼 사용할 수 있으며 스코프를 벗어나면 내부 힙 데이터도 자동으로 해제된다.
Box<T>는 다음과 같은 상황에서 자주 쓰인다.
- 컴파일 타임에 크기를 예측할 수 없는 타입을 크기가 고정된 컨텍스트에서 사용해야 할 때 (재귀적 타입)
- 대용량 데이터가 복사되지 않으면서 소유권을 옮기고 싶을 때
- 구체적인 타입보다 특정 트레이트를 구현한 타입으로 값을 다루고 싶을 때 (트레이트 객체)
let b = Box::new(5);
println!("b = {}", b);
Deref 트레이트
Deref 트레이트를 구현하면 역참조 연산자인 * 동작의 커스터마이징을 가능하게 해준다.
Deref 트레이트가 구현된 스마트 포인터는 참조자처럼 취급되며 참조자에 작동하도록 작성된 코드에서도 사용할 수 있다.
Deref 트레이트는 표준 라이브러리에서 제공되며 deref 라는 이름의 메서드 하나를 구현하면 된다. 이 메서드는 self를 빌려와서 내부 데이터의 참조자를 반환한다.
Deref 트레이트가 구현된 스마트 포인터를 *로 역참조하면 컴파일러가 deref()를 자동 호출해 그 반환값(보통 &T)을 다시 역참조한다.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, *y); // *y == *(y.deref())
매개변수가 특정 참조 타입으로 정의된 함수나 메서드가 Deref를 구현한 타입의 참조자를 인수로 받으면, 컴파일러가 deref를 호출해 해당 참조 타입과 호환되는 경우 자동으로 변환한다. 이를 역참조 강제 변환이라 하며 호환되지 않으면 컴파일 에러가 발생한다.
러스트는 다음의 세 가지 경우에 해당하는 타입과 트레이트 구현을 찾았을 때 역참조 강제 변환은 수행한다.
T: Deref<Target=U>일 때&T에서&U로T: DerefMut<Target=U>일 때&mut T에서&mut U로T: Deref<Target=U>일 때&mut T에서&U로
fn hello(name: &str) {
println!("Hello, {name}!");
}
let m = MyBox::new(String::from("Rust"));
hello(&m); // &String -> &str
Drop 트레이트
Drop 트레이트를 구현하면 어떤 값이 스코프 밖으로 벗어났을 때 실행되는 동작을 지정할 수 있다.
Drop 트레이트는 프렐루드에 포함되어 있으며 drop 이라는 이름의 메서드를 구현하면 된다. 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖는다.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
러스트는 수동으로 Drop 트레이트의 drop 메서드를 호출을 허용하지 않지만, 프렐루드에 포함된 drop 함수를 호출하여 스코프가 끝나기 전에 강제로 값을 버리도록 할 수 있다.
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
Rc<T>
Rc<T>는 참조 카운트를 통해 하나의 값을 여러 소유자가 가질 수 있도록 해주는 스마트 포인터이다.
Rc<T>는 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶은데 컴파일 타임에는 어떤 부분이 그 데이터를 마지막에 이용하게 될지 알 수 없는 경우에 사용하면 된다.
Rc<T>는 싱글스레드 환경에서만 사용할 수 있으며, 스코프를 벗어날 때 Drop 트레이트의 구현이 자동으로 참조 카운트를 감소시킨다.
Rc::new()로 생성된 Rc<T>는 내부적으로 RcInner<T>를 가리키는 포인터를 보관하며, Rc::clone()을 호출하면 참조 카운트만 증가시키고 같은 데이터를 가리키는 또 다른 Rc<T>를 만든다. 이 때문에 Rc::new()로 생성된 Rc<T>를 보관한 변수가 먼저 drop되더라도, 다른 Rc<T> 복제본이 남아 있다면 해당 데이터에 계속 접근할 수 있다.
use std::rc::Rc;
let a = Rc::new(1);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("{}", Rc::strong_count(&a));
drop(a);
println!("{}", Rc::strong_count(&b));
println!("{}", Rc::strong_count(&c));
RefCell<T>
RefCell<T>는 런타임에 가변 대여를 검사하여, 불변 참조자가 존재해도 내부 데이터를 변경할 수 있게 해준다.
RefCell<T>는 단일 소유만 가능하며, 컴파일 타임이 아닌 런타임에 대여 규칙을 검사한다.
RefCell<T>는 컴파일러가 보장하지 못하는 대여를 작성자가 확신할 때 사용하면 된다.
RefCell<T>에는 borrow와 borrow_mut 메서드가 있다. borrow 메서드는 불변 참조자 역할을 하는 Ref<T>를 반환하고 borrow_mut 메서드는 가변 참조자 역할을 하는 RefMut<T>를 반환한다.
컴파일 타임의 대여 규칙과 동일하게, RefCell<T>는 여러 개의 불변 대여 또는 하나의 가변 대여만 허용한다.
Rc<T>와 함께 사용하면, 여러 소유자가 있으면서도 내부 데이터를 가변적으로 변경할 수 있다.
use std::rc::Rc;
use std::cell::RefCell;
// RefCell로 내부 가변성 허용
let data = Rc::new(RefCell::new(5));
// 불변 대여 (읽기)
{
let val = data.borrow();
println!("불변 대여: {}", *val); // 5
}
// 가변 대여 (쓰기)
{
let mut val_mut = data.borrow_mut();
*val_mut += 10;
println!("가변 대여 후 값: {}", *val_mut); // 15
}
// 여러 소유자와 함께 내부 값 변경 가능
let data_clone = Rc::clone(&data);
{
let mut val_mut = data_clone.borrow_mut();
*val_mut *= 2;
}
println!("최종 값: {}", data.borrow()); // 30
Weak<T>
Weak<T>는 Rc<T>와 함께 사용되는 약한 참조로, 순환 참조를 방지하기 위해 사용한다.
Weak<T>는 참조 카운트에 포함되지 않아, Weak<T>만 남아 있으면 해당 값은 해제된다.
Rc::downgrade를 호출하면 Weak<T> 타입의 약한 참조를 얻는다.
Weak<T>가 가리키는 값이 이미 해제됐을 수도 있으므로, 값에 접근하려면 upgrade 메서드를 사용해 확인해야 한다. upgrade는 Option<Rc<T>>를 반환하며, 값이 살아 있으면 Some(Rc<T>), 해제됐으면 None을 반환한다.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 부모는 약한 참조
children: RefCell<Vec<Rc<Node>>>, // 자식은 강한 참조
}
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
); // leaf strong = 1, weak = 0
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// leaf의 부모를 branch의 약한 참조로 설정
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
); // branch strong = 1, weak = 1
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
); // leaf strong = 2, weak = 0
} // branch가 여기서 drop 되어 메모리 해제 가능
// leaf의 parent를 upgrade하여 존재 여부 확인
println!(
"leaf parent = {:?}",
leaf.parent.borrow().upgrade()
); // leaf parent = None
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
); // leaf strong = 1, weak = 0