러스트는 소유권과 대여의 개념을 가지고 있다. 참조자는 데이터를 빌리기만 하는 반면, 스마트 포인터는 가리킨 데이터를 소유한다.
Box
가장 직관적인 스마트 포인터로, 힙에 데이터를 저장할 수 있게 해준다.
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
위와 같이 사용하면 b는 일반적인 코드와 똑같이 5라는 값을 나타낼 것이다. 5가 힙에 할당된다는 점만 다르다.
재귀적 타입
recursive type은 자기 안에 동일한 타입의 또 다른 값을 담을 수 있다. 컴파일할 때 모든 정보를 알아야 하는 러스트에 있어서 재귀적 타입은 문제를 일으킬 여지가 많다. 박스는 알려진 크기를 갖고 있으므로 박스에 넣으면 이를 허용할 수 있다.
콘스 리스트(cons list)는 Lisp에서의 연결 리스트이다. cons에 어떤 값과 다른 쌍으로 구성된 쌍을 넣어 호출하면, 재귀적인 쌍으로 이루어진 리스트를 구성할 수 있다.
(1, (2, (3, Nil)))
각 아이템은 두 개의 요소를 담고 있다. Nil은 재귀의 기본 케이스를 의미한다.
이를 열거형으로 구현해보자.
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
이 코드를 실행하면 recursive type 'List' has infinite size
라는 오류가 발생한다. 마치 자신을 무한히 복사하여 꼬리에 붙이는 모양이기 때문에 컴파일러가 크기를 알아낼 수 없다.
오류를 해결하려면 List
의 크기를 컴파일러가 알 수 있게 해야 한다.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Box<T>
는 포인터다. 포인터의 크기는 무엇을 가리키든지 정해져 있다. 즉, List
를 박스에 넣음으로써 크기를 알릴 수 있다.
Deref: 포인터를 참조자처럼 쓰기
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
포인터가 가리키는 값을 얻어내기 위해서는 역참조를 해야 한다. 원래라면 y
는 &x
로 정의되어 역참조해 x
값을 얻어낼 수 있겠지만, y
에 Box
로 감싼 x
를 할당해 동일한 연산을 할 수 있다. 이는 Deref
트레이트를 통해서 구현할 수 있다.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
MyBox<T>
는 Box<T>
의 동작을 따라하는 커스텀 구조체이다.
트레이트를 구현하려면 그 트레이트가 요구하는 메서드에 대한 구현체를 제공해야 한다. Deref
트레이트는 deref
라는 메서드를 구현하도록 요구한다. 이 함수는 self
를 빌려와서 내부 데이터의 참조자를 반환한다.
&self.0
으로 채워진 deref
는 *
연산자를 이용해 접근하려는 값의 참조자를 반환한다. MyBox
는 튜플 구조체이므로, &self.0
은 인스턴스의 첫 번째 값에 접근한다.
역참조 강제(deref coercion)
&String
은 &Str
타입으로 변환될 수 있는데, 이는 역참조 강제 덕분이다. 역참조 강제는 Deref를 구현한 어떤 타입의 참조자를 다른 타입의 참조자로 바꿔준다. 인수로 넘겨준 타입과 매개변수로 정의된 타입이 다르면 자동으로 발생한다. 이를 통해 명시적으로 참조와 역참조를 하지 않아도 코드를 작성할 수 있게 된다.
Drop 트레이트
Drop
트레이트의 drop
메서드를 구현함으로써 어떤 값이 스코프 밖으로 벗어날 때 할 일을 커스터마이징 할 수 있다.
강제로 drop을 호출하는 건 불가능하다. 스코프가 벗어날 때 자동으로 drop을 호출하기 때문에 중복 해제가 발생할 수 있기 떄문이다.
강제로 메모리를 해제하려면 std::mem::drop
함수를 호출해야 한다.
Rc: 참조 카운트 스마트 포인터
그래프에서 여러 개의 edge가 동일한 node를 가리키는 등 복수 소유권이 필요할 때 사용한다. Rc는 참조 카운팅(reference counting)의 약자로, 어떤 값의 참조자 개수를 추적한다.
두 개의 콘스 리스트를 만들고, 모두 세 번째 리스트의 소유권을 공유하게 해보자.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
b 리스트를 만들 때 a는 b 안으로 이동해 b의 소유가 된다. c를 생성해 다시 a를 사용하려 하면 a는 이미 이동된 상태이므로 오류가 발생한다. Rc
를 사용해 수정해보자.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
b를 만들 때 a의 소유권을 얻는 대신 a를 가진 Rc<List>
를 clone하여 참조자를 2개로 늘리고 소유권을 공유하도록 한다. c를 만들면 참조자는 3개가 된다. 참조자가 0개가 되지 않으면 메모리는 정리되지 않을 것이다. clone한다고 해서 모든 데이터를 복사하는 것이 아니라, 참조자의 개수를 늘리기만 한다.
RefCell
내부 가변성(interior mutability)은 어떤 데이터에 대한 불변 참조자가 있어도 데이터를 변경할 수 있게 해주는 패턴이다. RefCell<T>
를 이용하면 불변성 검사는 런타임에 실행된다. 즉, RefCell<T>
가 불변일 때라도 내부의 값을 변경할 수 있게 된다.
'프로그래밍 > Rust' 카테고리의 다른 글
[Rust] 스레드 (0) | 2024.05.19 |
---|---|
[Rust] 반복자 (0) | 2024.05.19 |
[Rust] 클로저 (0) | 2024.05.19 |
[Rust] 테스트 (0) | 2024.05.19 |
[Rust] 제네릭과 트레이트 (0) | 2024.05.19 |