Avatar ·

Почему нельзя хранить значение и ссылку на него в одной структуре в Rust?

Всем привет! Я начал изучать Rust и столкнулся с проблемой, которую не могу решить уже несколько дней. Я пишу парсер для конфигурационных файлов. У меня есть структура `Parser`, которая считывает данные из строки и предоставляет итератор по словам. Вот упрощённый код: ```rust struct Parser { data: String, words: Vec<&str>, // ссылки на data } impl Parser { fn new(input: &str) -> Self { let data = input.to_string(); let words: Vec<&str> = data.split_whitespace().collect(); Parser { data, words } // ошибка! } } ``` Компилятор выдаёт ошибку: `cannot move out of 'data' because it is borrowed`. Я понимаю, что `words` содержит ссылки на `data`, но ведь `data` и `words` находятся в одной структуре! Разве Rust не должен понимать, что они живут вместе? Я пробовал разные варианты: использовать `Rc`, менять порядок полей, добавлять lifetime — ничего не помогает. Может, я неправильно проектирую структуру? Как вообще в 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 будут вашими помощниками.