홈강좌Rust 입문·중급 47편 중 38편째
Rust Box — 값을 힙에 두고 크기가 정해진 포인터로 다룬다 (E0072)
Rust 의 Box<T> 는 값을 힙에 두고, 그 자리를 가리키는 크기가 정해진 포인터다. 자기 자신을 칸으로 갖는 재귀 타입은 크기가 끝없이 커져서 E0072 로 막히는데, 그 칸을 Box 로 감싸면 크기가 정해져 컴파일된다.
01 스택과 힙
프로그램이 값을 두는 곳은 크게 둘이다.
| 스택 | 힙 | |
|---|---|---|
| 두는 것 | 크기가 컴파일할 때 정해진 값 | 크기가 실행 중에 정해지거나 큰 값 |
| 예 | i32 · bool · 고정 길이 배열 | String · Vec 의 내용 |
| 빠르기 | 아주 빠르다 | 자리를 찾는 만큼 느리다 |
15편부터 본 String 은 사실 글자를 힙에 두고, 변수에는 그 자리를 가리키는 작은 정보만 둔다. 주인이 블록을 나갈 때 힙의 글자를 치우는 것이 소유권 규칙이었다.
02 Box::new 로 힙에 둔다
Box 는 그 일을 아무 값에나 해 준다.
fn main() {
let n = Box::new(5);
let doubled = *n * 2;
println!("{n} {doubled}");
} Compiling boxes v0.1.0 (~/projects/boxes)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/boxes`
5 10Box::new(5) 는 5 를 힙에 두고, n 은 그 자리를 가리킨다. 찍을 때는 그냥 {n} 으로 되고, 계산할 때는 *n 으로 가리키는 값을 꺼낸다(28편의 *s 와 같다). n 이 블록을 나가면 힙의 5 도 치워진다. 정수 하나를 힙에 둘 일은 거의 없다. Box 가 꼭 필요한 곳은 아래 같은 경우다.
03 자기 자신을 칸으로 가지면 E0072
연결 목록을 열거형으로 만들어 보자. 한 칸은 숫자 하나와 "나머지 목록" 을 갖거나, 끝(End)이다.
#[derive(Debug)]
enum List {
Node(i32, List),
End,
}
fn main() {
let list = List::Node(1, List::Node(2, List::End));
println!("{list:?}");
}error[E0072]: recursive type `List` has infinite size
--> src/main.rs:2:1
|
2 | enum List {
| ^^^^^^^^^
3 | Node(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
3 | Node(i32, Box<List>),
| ++++ +- 무엇 — 재귀 타입
List의 크기가 무한하다(infinite size). - 어디 — 3번째 줄의
List. 간접 참조 없이 자기 자신을 품었다(recursive without indirection). - 고치는 법 —
help:가Box나Rc,&를 끼우라고 하고,Box<List>로 고친 줄을 보여 준다.
Rust 는 컴파일할 때 타입의 크기를 알아야 한다. List 의 크기는 i32 + List 의 크기인데, 그 안의 List 는 또 i32 + List 다. 끝이 없다. 실제로는 뒤에 이어지는 오류(E0391)도 같은 원인이라 함께 고쳐진다.
04 실습 — Box 로 감싼 연결 목록
Box<List> 는 "힙 어딘가에 있는 List 를 가리키는 포인터" 다. 포인터의 크기는 늘 같으니 List 의 크기가 정해진다.
#[derive(Debug)]
enum List {
Node(i32, Box<List>),
End,
}
fn sum(list: &List) -> i32 {
match list {
List::Node(value, rest) => value + sum(rest),
List::End => 0,
}
}
fn main() {
let list = List::Node(1, Box::new(List::Node(2, Box::new(List::Node(3, Box::new(List::End))))));
println!("{list:?}");
println!("sum = {}", sum(&list));
} Compiling list v0.1.0 (~/projects/list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/list`
Node(1, Node(2, Node(3, End)))
sum = 6List::Node(1, Box::new(…))처럼 다음 칸을Box::new로 감싸 만든다.sum은 자기 자신을 부르는 재귀 함수다.Node면 값에 나머지의 합을 더하고,End면 0 이다(25편의match).rest는&Box<List>인데,sum(rest)에 그대로 넘겨도&List로 맞춰진다.Box가 가리키는 값처럼 행동하기 때문이다.
목록 끝에 4 를 하나 더 이어 보자. 괄호가 하나씩 늘어난다. 이런 모양은 트리·그래프 같은 자료 구조에 그대로 쓰인다. 다만 Box 는 주인이 하나뿐이라, 한 칸을 둘이 함께 가리킬 수는 없다. 그것은 다음 편의 Rc 가 한다.