iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🥊

Rust: Understanding Smart Pointers and Box<T>

に公開

Key Points

Overview

  • Data can be stored on the heap instead of the stack
  • What remains on the stack is a pointer to the heap data
Stack                 Heap
+-----------+         +-------------------+
| Box<T>    |  -----> | Data (Heap)       |
| Pointer   |         | e.g., Large Struct|
+-----------+         +-------------------+

Use Cases

When you have a type whose size cannot be known at compile time and you want to use a value of that type in a context that requires an exact size
When you have a large amount of data and you want to transfer ownership but ensure the data is not copied
When you want to own a value and you care only that it’s a type that implements a particular trait rather than being a specific type

Example Usage (Obtaining Recursive Types with Known Sizes)

enum Node {
    Cons(i32, Box<Node>),
    Nil,
}

fn main() {
    let list = Node::Cons(3,Box::new(Node::Cons(2,Box::new(Node::Cons(1,Box::new(Node::Nil))))),);
    println!("Sum of list values: {}", sum(&list));
}

fn sum(list: &Node) -> i32 {
    match list {
        Node::Cons(value, next) => value + sum(next),
        Node::Nil => 0,
    }
}

What is Cons?

  • A type of variant used in an enumeration (enum)
  • A variant refers to the names of the specific cases or possibilities enumerated within an enum.
// For example, like this
enum Color {
    Red,
    Green,
    Blue,
}
  • It is used to hold "a single element (value) and a reference to the next element" in a list.
Cons(i32, Box<Node>) // Cons(value, reference to the next element) 

Why use Box<T>?

Problem when not using it

enum Node {
    Cons(i32, Node),
    Nil,
}

To know the size of Node::Cons, the size of Node is required.
→ However, to know the size of Node, the size of Node::Cons is required recursively.
→ And since that Node contains another Node, the size calculation continues recursively and infinitely...

So, this is why we use Box<T>.

When using it

enum Node {
    Cons(i32, Box<Node>), // Has 8 bytes in a 64-bit environment, 4 bytes in a 32-bit environment
    Nil,
}

To know the size of Node::Cons, the size of Node is required.
→ However, the size of Box<Node> is fixed, so the size of Node does not grow recursively.

How to use Node

  • It's nested, so it's a bit hard to follow, but the end looks like this:
Node::Cons(1, Box::new(Node::Nil)) // Meaning "1 is followed by Nil (the end)"
  • It is stored in the list in a fashion like 3→2→1.
let list = Node::Cons(3,Box::new(Node::Cons(2,Box::new(Node::Cons(1,Box::new(Node::Nil))))),);

sum function

  • Get the pointer of the list and check the content with a match expression.
  • Separate cases for when the content is Cons and when it is Nil.
  • In the case of Cons, it returns value + sum(next). The sum function is called recursively.
  • Here, next is the reference to the next element in Cons(value, reference to the next element).
  • In the case of Nil, it returns 0.
  • A calculation like 3 + (2 + (1 + 0)) = 6 is performed.
fn sum(list: &Node) -> i32 {
    match list {
        Node::Cons(value, next) => value + sum(next),
        Node::Nil => 0,
    }
}

https://doc.rust-jp.rs/book-ja/ch15-01-box.html

Discussion