Почему нельзя хранить значение и ссылку на него в одной структуре в Rust?
Проблема самоссылающихся структур
Вы столкнулись с фундаментальным ограничением системы владения и заимствования в Rust. Структура, которая содержит ссылки на свои собственные данные, называется самоссылающейся. Rust не позволяет их создавать напрямую, потому что это может привести к неопределённому поведению при перемещении структуры.
Почему это не работает?
Когда вы пишете:
let words: Vec<&str> = data.split_whitespace().collect();
Переменная data передаётся в замыкание split_whitespace по ссылке. Компилятор считает, что data заимствована. Но сразу после этого вы пытаетесь переместить data в структуру (Parser { data, words }). Правило заимствования гласит: нельзя перемещать значение, пока на него есть активные ссылки.
Даже если вы поменяете порядок полей и сначала запишете data, а потом words — компилятор всё равно выдаст ошибку, потому что он не может гарантировать, что ссылки останутся валидными при перемещении всей структуры.
Решение 1: Владеющие типы данных
Самый простой способ — не хранить ссылки, а владеть данными напрямую. Например, вместо &str используйте String:
struct Parser {
data: String,
words: Vec<String>, // теперь это владеющие строки
}
impl Parser {
fn new(input: &str) -> Self {
let data = input.to_string();
let words: Vec<String> = data
.split_whitespace()
.map(|s| s.to_string())
.collect();
Parser { data, words }
}
}
Плюсы: простота, нет проблем с временами жизни.
Минусы: дополнительное выделение памяти под каждое слово.
Решение 2: Индексы вместо ссылок
Вместо ссылок храните индексы начала и конца каждого слова:
struct Parser {
data: String,
words: Vec<(usize, usize)>, // (start, end) для каждого слова
}
impl Parser {
fn new(input: &str) -> Self {
let data = input.to_string();
let words: Vec<(usize, usize)> = data
.split_whitespace()
.map(|word| {
let start = word.as_ptr() as usize - data.as_ptr() as usize;
let end = start + word.len();
(start, end)
})
.collect();
Parser { data, words }
}
fn get_word(&self, index: usize) -> &str {
let (start, end) = self.words[index];
&self.data[start..end]
}
}
Плюсы: эффективно по памяти, нет копирования строк.
Минусы: сложнее в реализации, нужно следить за корректностью индексов.
Решение 3: Использование кратных ссылок с unsafe (не рекомендуется новичкам)
Можно использовать Pin<Box<Self>> и unsafe код, но это сложно и опасно. Лучше избегать этого подхода, пока вы не станете экспертом в Rust.
Решение 4: Разделение на отдельные структуры
Переосмыслите дизайн. Возможно, данные и ссылки на них должны быть в разных структурах:
struct DataHolder {
data: String,
}
impl DataHolder {
fn words(&self) -> Vec<&str> {
self.data.split_whitespace().collect()
}
}
Здесь вы не храните ссылки, а создаёте их по требованию. Это самый идиоматичный подход в Rust.
Вывод
- Самоссылающиеся структуры запрещены в безопасном Rust.
- Используйте владеющие типы (
String,Vec<T>) вместо ссылок. - Храните индексы, если важна производительность.
- Перепроектируйте код так, чтобы данные и их заимствования были разделены.
Помните: ограничения Rust — это не баги, а фичи, которые защищают вас от ошибок памяти. Со временем вы научитесь проектировать код, обходя эти ограничения естественным образом.
<<Альтернативный подход: Использование ouroboros или self_cell
Если вам действительно нужна самоссылающаяся структура (например, для производительности в горячем цикле), существуют крейты, которые позволяют создать её безопасно, используя макросы и unsafe под капотом.
Пример с крейтом ouroboros:
use ouroboros::self_referencing;
#[self_referencing]
struct Parser {
data: String,
#[borrows(data)]
words: Vec<&'this str>,
}
impl Parser {
fn new(input: &str) -> Self {
ParserBuilder {
data: input.to_string(),
words_builder: |data: &String| {
data.split_whitespace().collect()
},
}.build()
}
}
Как это работает: Макрос генерирует безопасный код, который гарантирует, что структура никогда не будет перемещена после создания (используется Pin). Ссылки внутри остаются валидными на всё время жизни структуры.
Когда использовать:
- Когда производительность критична (избегаете аллокаций для каждого слова).
- Когда вы точно знаете, что делаете, и не хотите перепроектировать архитектуру.
Недостатки:
- Усложняет код (макросы, дополнительные зависимости).
- Может затруднить отладку.
- Не все операции над структурой возможны (например, нельзя просто взять и переместить её).
Для учебных проектов и большинства реальных задач лучше использовать первый ответ (владеющие типы или индексы). Но если вы столкнулись с ситуацией, где без самоссылающейся структуры не обойтись — ouroboros или self_cell будут вашими помощниками.