Box 타입

앞에서 배운 원시 타입과 구조체 타입들은 모두 크기가 일정했습니다. 그런데 만일 어떤 타입의 크기를 컴파일 타임에 미리 알 수 없다면 어떨까요? 어떤 타입의 크기가 런타임에 정해지는 경우가 있을까요? 다음과 같이 자기 자신을 필드값의 타입으로 갖는 재귀 형태의 구조체(Recursive type)를 정의해 보겠습니다.

Node 타입의 크기를 컴파일 타임에 미리 알 수 없다.

struct Node {
    value: i32,
    next: Option<Node>,
}

fn main() {
    let mut head = Node {
        value: 1,
        next: None,
    };
    head.next = Some(Node {
        value: 2,
        next: None,
    });
    println!("{}", head.value);
}
error[E0072]: recursive type `Node` has infinite size
 --> src/main.rs:1:1
  |
1 | struct Node {
  | ^^^^^^^^^^^
2 |     value: i32,
3 |     next: Option<Node>,
  |                  ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
3 |     next: Option<Box<Node>>,
  |                  ++++    +

Box 를 사용하라고 함

struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

fn main() {
    let mut head = Node {
        value: 1,
        next: None,
    };
    head.next = Some(Box::new(Node {
        value: 2,
        next: None,
    }));
    println!("{}", head.value);
}

문제없이 컴파일

Box<T>

Box가 대체 무엇일까?

Box를 사용하면 스택이 아닌 힙에 데이터를 저장할 수 있습니다. 스택에 남는 것은 힙 데이터에 대한 포인터입니다.

Box 사용하기

아래 예제는 Box를 사용하여 i32 값을 힙에 저장하는 방법을 보여줍니다.

fn main() {
    let my_box = Box::new(5);
    println!("my_box = {}", my_box);
}

변수 b가 힙에 할당된 값 5를 가리키는 Box의 값을 갖도록 정의합니다. 이 프로그램은 b = 5를 출력합니다. 이 경우 이 데이터가 스택에 있을 때와 유사하게 상자에 있는 데이터에 액세스할 수 있습니다. 다른 소유 값과 마찬가지로 상자가 범위를 벗어나면, 메인 끝에서 b가 그러하듯이, 상자는 할당 해제됩니다. 할당 해제는 상자(스택에 저장됨)와 상자가 가리키는 데이터(힙에 저장됨) 모두에 대해 발생합니다. 여기까지는 스택에 값을 저장하는 것과 크게 다르지 않습니다.

Box는 주로 다음과 같은 상황에 사용됩니다.

  • 컴파일 시 크기를 알 수 없는 타입 내부의 값에 접근해야 하는 경우
  • 크기가 큰 값의 소유권을 이전하고 싶지만, 메모리 효율성을 위해 전체 값이 복사되지 않도록 해야 하는 경우
  • 특정 타입이 아닌, 특정 트레이트를 구현하는 타입의 변수의 소유권을 가져오고 싶은 경우

첫 번째 상황은 위에서 이미 살펴본 Node의 경우입니다. 이제 나머지 각각의 경우를 자세히 살펴보겠습니다.

소유권을 효율적으로 전달하기

레퍼런스 대신

fn transfer_box(_data: Box<Vec<i32>>) {}

fn transfer_vec(_data: Vec<i32>) {}

fn main() {
    let data = vec![0; 10_000_000];

    transfer_vec(data.clone());

    let boxed = Box::new(data);
    transfer_box(boxed);
}

dynBox로 트레이트 타입 표현하기

cargo add rand
struct Dog {}
struct Cat {}

trait Animal {
    fn noise(&self) -> &'static str;
}

impl Animal for Dog {
    fn noise(&self) -> &'static str {
        "🐶멍멍!"
    }
}

impl Animal for Cat {
    fn noise(&self) -> &'static str {
        "🐱야옹!"
    }
}

fn random_animal() -> impl Animal {
    if rand::random::<f64>() < 0.5 {
        Dog {}
    } else {
        Cat {}
    }
}

fn main() {
    for _ in 0..10 {
        println!("{}", random_animal().noise());
    }
}

실행 결과

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:24:9
   |
21 | /     if rand::random::<f64>() < 0.5 {
22 | |         Dog {}
   | |         ------ expected because of this
23 | |     } else {
24 | |         Cat {}
   | |         ^^^^^^ expected struct `Dog`, found struct `Cat`
25 | |     }
   | |_____- `if` and `else` have incompatible types
   |
help: you could change the return type to be a boxed trait object
   |
20 | fn random_animal() -> Box<dyn Animal> {
   |                       ~~~~~~~       +
help: if you change the return type to expect trait objects, box the returned expressions
   |
22 ~         Box::new(Dog {})
23 |     } else {
24 ~         Box::new(Cat {})
   |
struct Dog {}
struct Cat {}

trait Animal {
    fn noise(&self) -> &'static str;
}

impl Animal for Dog {
    fn noise(&self) -> &'static str {
        "🐶멍멍!"
    }
}

impl Animal for Cat {
    fn noise(&self) -> &'static str {
        "🐱야옹!"
    }
}

fn random_animal() -> Box<dyn Animal> {
    if rand::random::<f64>() < 0.5 {
        Box::new(Dog {})
    } else {
        Box::new(Cat {})
    }
}

fn main() {
    for _ in 0..10 {
        println!("{}", random_animal().noise());
    }
}

실행 결과

🐱야옹!
🐶멍멍!
🐶멍멍!
🐶멍멍!
🐱야옹!
🐱야옹!
🐶멍멍!
🐶멍멍!
🐶멍멍!
🐶멍멍!