Avatar ·

Разбираемся с Box, ref, & и * в Rust: в чём разница и как их использовать?

Всем привет! Я начал изучать Rust недавно и постоянно путаюсь в этих четырёх вещах: `Box`, `ref`, `&` и `*`. Понимаю, что все они как-то связаны с указателями и ссылками, но не могу уловить, когда что применять. Например, я пишу такую программу: ```rust fn main() { let x = 42; let y = &x; // ссылка let z = Box::new(x); // Box println!("{} {}", y, z); } ``` Тут всё работает. Но когда я пытаюсь сделать что-то посложнее, например, сопоставление с образцом, всё ломается: ```rust fn main() { let maybe_number = Some(10); match maybe_number { Some(ref x) => println!("Получили {}", x), None => println!("Ничего нет"), } } ``` Почему тут нужен `ref`, а не просто `x`? И чем `*` отличается от `&`? Я читал, что `*` — это разыменование, но зачем оно нужно, если можно просто использовать `&`? Объясните, пожалуйста, на пальцах, как эти четыре сущности связаны и когда каждую из них применять. Особенно интересует практика: в каких ситуациях `Box` лучше обычной ссылки, и зачем вообще придумали `ref` в match. Заранее спасибо! <<>>

Ключевое различие: владение и заимствование

Чтобы понять разницу, нужно запомнить главное правило Rust: каждое значение имеет ровно одного владельца. Box, &, ref и * — это инструменты для управления этим владением.

1. & — ссылка (заимствование)

&x создаёт ссылку на значение x. Это значит, что мы заимствуем значение, но не становимся его владельцем. Когда ссылка выходит из области видимости, значение не удаляется — оно остаётся у владельца.

let x = 42;
let y = &x; // y — это &i32, ссылка на x
println!("{}", y); // 42 — автоматическое разыменование при печати
// x всё ещё жив, так как y только заимствовал его

2. * — оператор разыменования

* делает обратное &: он берёт ссылку и даёт доступ к самому значению. В большинстве случаев Rust автоматически применяет * (например, при вызове методов), но иногда нужно явно:

let x = 42;
let y = &x;
let z = *y; // z = 42, теперь z — это i32, а не ссылка
// Но! z — это копия, так как i32 реализует Copy

Важно: *y даёт доступ к значению, но не передаёт владение, если это ссылка. Можно только читать или копировать (если тип реализует Copy).

3. ref — захват по ссылке в сопоставлении с образцом

ref используется только в match и деструктуризации. Он говорит: «не забирай владение, а возьми ссылку». Без ref вы попытаетесь переместить значение из Option или Result:

let maybe_number = Some(10);
match maybe_number {
    Some(x) => println!("{}", x), // ОШИБКА! x перемещается, maybe_number больше нельзя использовать
    None => ()
}
// println!("{:?}", maybe_number); // ошибка компиляции

С ref мы берём ссылку на внутреннее значение:

match maybe_number {
    Some(ref x) => { // x — это &i32
        println!("{}", x); // автоматически разыменовывается
    }
    None => ()
}
println!("{:?}", maybe_number); // ОК, владение не было передано

Также есть ref mut для изменяемых ссылок.

4. Box<T> — умный указатель с владением

Box — это владеющий указатель на данные в куче (heap). В отличие от &, Box является владельцем. Когда Box удаляется, данные в куче тоже удаляются.

let a = Box::new(42); // a — владелец числа 42 в куче
let b = a; // владение перемещается в b
// println!("{}", a); // ОШИБКА! a больше не владеет данными

Сравните с ссылкой:

let x = 42;
let y = &x; // y — ссылка, x остаётся владельцем
let z = y; // копируется ссылка, x всё ещё владеет
println!("{}", x); // ОК

Когда что использовать?

  • & — когда нужно временно «посмотреть» на данные, не забирая их. 90% случаев.
  • * — когда нужно явно получить доступ к значению за ссылкой (редко, обычно автоматически).
  • ref — только в match и деструктуризации, чтобы не перемещать данные.
  • Box — когда нужно владеть данными, но хранить их в куче (например, для рекурсивных типов, трейт-объектов или больших данных, которые не должны копироваться на стеке).

Пример: рекурсивный тип без Box не скомпилируется

enum List {
    Cons(i32, Box<List>), // Box обязателен, иначе размер бесконечен
    Nil,
}

Итог

Запомните цепочку: & создаёт ссылку, * её разыменовывает, ref — это & внутри match, а Box — это владелец, который хранит данные в куче. Начните с & для заимствования, и используйте Box только когда компилятор явно попросит.

<<>>

Практический взгляд: паттерны и производительность

Добавлю несколько важных моментов, которые помогут на практике.

Автоматическое разыменование (deref coercion)

Rust часто применяет * за вас. Например, при вызове методов:

let s = String::from("hello");
let r = &s;
println!("{}", r.len()); // работает без явного *r
// Rust автоматически разыменовывает r до String, чтобы вызвать len()

То же с Box:

let b = Box::new(String::from("world"));
println!("{}", b.len()); // автоматическое разыменование Box -> String

ref vs & в match — важный нюанс

Можно использовать & прямо в match, если вы сопоставляете со ссылкой:

let maybe_ref = &Some(10);
match maybe_ref {
    &Some(x) => println!("{}", x), // x — это i32, потому что мы "разобрали" ссылку
    &None => ()
}
// Но если maybe_ref — это не ссылка, а владелец, то &Some(x) не сработает

ref удобнее, потому что он работает независимо от того, владеете вы значением или нет.

Когда Box не нужен

Многие новички злоупотребляют Box, думая, что это «ссылка на куче». На самом деле, если вам нужно просто передать ссылку на функцию — используйте &. Box оправдан только когда:

  • Нужно владение, но тип не имеет известного размера на этапе компиляции (трейт-объекты).
  • Рекурсивные структуры данных.
  • Вы хотите явно управлять временем жизни данных в куче.

В остальных случаях & будет быстрее и проще.

Идиоматический пример

fn process(data: &Vec<i32>) { // заимствование
    for x in data.iter() {
        println!("{}", x);
    }
}

fn main() {
    let v = vec![1, 2, 3];
    process(&v); // передаём ссылку
    println!("{:?}", v); // v всё ещё доступен