Почему Rust не даёт вам хранить ссылку на самого себя: разбираем на примерах
Представьте ситуацию: вы пишете на Rust программу, и вам нужно создать структуру, которая хранит какие-то данные, а также ссылку на эти же данные. Логично? Ещё бы! Но компилятор Rust вдруг выдаёт ошибку, и вы в недоумении. Знакомо? Этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров), и сегодня мы разберём его от и до.
В этом пошаговом туториале мы не только поймём, почему нельзя хранить значение и ссылку в одной структуре Rust, но и научимся обходить это ограничение безопасными способами. Поехали!
Проблема: наивная попытка
Допустим, мы хотим создать структуру, которая хранит строку и её первую букву (ссылку на первый символ). Вот как это выглядит в коде:
struct MyStruct {
data: String,
first_char: &str, // ссылка на data
}
fn main() {
let s = String::from("Hello");
let first = &s[0..1]; // ссылка на первый символ
let my_struct = MyStruct {
data: s,
first_char: first, // пытаемся сохранить ссылку
};
}
Компилятор выдаёт ошибку, которая сбивает с толку многих новичков. Давайте разберёмся, что именно пошло не так.
Почему Rust запрещает селф-референсные структуры?
Корень проблемы — в системе владения (ownership) и заимствования (borrowing) Rust. Когда вы перемещаете (move) значение s в поле data, оно физически копируется в новое место в памяти. Ссылка first при этом всё ещё указывает на старый адрес, который теперь недействителен.
В Rust каждая ссылка должна быть действительной всё время своего существования. Селф-референсные структуры Rust нарушают это правило, потому что ссылка и значение живут в одном объекте, и при перемещении структуры ссылка становится висячей (dangling reference).
Что происходит под капотом?
Когда вы пишете такой код:
let s = String::from("Hello");
let first = &s[0..1];
let my_struct = MyStruct { data: s, first_char: first };
Происходит следующее:
sвладеет строкой "Hello" в куче.first— ссылка на часть этой строки.- При создании структуры
data: s— право владения строкой передаётся структуре. Строка может переместиться в памяти. - Ссылка
firstвсё ещё указывает на старый адрес — это висячая ссылка. Rust не может этого допустить.
Borrow checker Rust видит эту потенциальную небезопасность и блокирует код на этапе компиляции. Это не баг, а фича, которая спасает от множества проблем в рантайме.
Реальные сценарии, где нужны селф-референсные структуры
Несмотря на запрет, такие структуры бывают полезны. Вот типичные случаи:
- Парсеры и AST: когда узлы дерева ссылаются друг на друга.
- Игровые движки: сущности, которые содержат ссылки на свои компоненты.
- Сетевые протоколы: буферы с внутренними указателями.
- Системы плагинов: когда объект содержит ссылку на свой конфиг.
Хорошая новость: есть безопасные способы обойти это ограничение!
Решение 1: Использование индексов вместо ссылок (самый простой способ)
Самый простой и безопасный способ — хранить не ссылку, а индекс или ключ. Вместо &str храните usize (позицию первого символа).
struct MyStruct {
data: String,
first_char_index: usize, // храним позицию, а не ссылку
}
impl MyStruct {
fn first_char(&self) -> &str {
&self.data[self.first_char_index..self.first_char_index + 1]
}
}
fn main() {
let s = String::from("Hello");
let my_struct = MyStruct {
data: s,
first_char_index: 0, // индекс первого символа
};
println!("Первый символ: {}", my_struct.first_char());
}
Этот подход работает отлично для строк, векторов и других коллекций с произвольным доступом. Вы получаете ссылку только при вызове метода, когда borrow checker может проверить её валидность.
Плюсы и минусы:
- ✅ Полностью безопасно, без
unsafe - ✅ Простота и понятность
- ❌ Нужно следить за синхронизацией индексов при изменении данных
- ❌ Не подходит для сложных структур данных
Решение 2: Использование Rc и RefCell (для сложных случаев)
Если вам нужно разделяемое владение и внутренняя изменяемость, на помощь приходят умные указатели Rc (счётчик ссылок) и RefCell (проверка заимствования в рантайме).
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
parent: Option<Rc<RefCell<Node>>>, // ссылка на родителя
}
fn main() {
let root = Rc::new(RefCell::new(Node {
value: 1,
children: vec![],
parent: None,
}));
let child = Rc::new(RefCell::new(Node {
value: 2,
children: vec![],
parent: Some(Rc::clone(&root)), // ссылка на родителя
}));
root.borrow_mut().children.push(Rc::clone(&child));
println!("Дочерний узел {} имеет родителя с значением {}",
child.borrow().value,
child.borrow().parent.as_ref().unwrap().borrow().value);
}
Rc позволяет иметь несколько владельцев одного значения, а RefCell даёт возможность изменять данные даже при наличии нескольких ссылок. Это идеальное решение для графов, деревьев и других структур с циклическими ссылками.
Важно знать:
- ✅ Безопасно (проверки в рантайме, а не в compile-time)
- ✅ Подходит для сложных графов и деревьев
- ❌ Небольшое снижение производительности (runtime checks)
- ❌ Возможны паники при нарушении правил заимств