Avatar ·

Ошибка "cannot borrow as mutable because it is also borrowed as immutable" в Rust: как исправить?

Привет! Я только начал изучать Rust и столкнулся с проблемой, которую не могу понять. У меня есть простая программа, которая принимает строку и пытается сначала получить её длину, а затем изменить первый символ: ```rust fn main() { let mut text = String::from("hello"); let first = &text[0..1]; // неизменяемая ссылка text.push_str(" world"); // попытка изменяемого заимствования println!("First char: {}", first); } ``` Компилятор выдаёт ошибку: ``` error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable ``` Я понимаю, что в Rust есть правила заимствования: либо одна изменяемая ссылка, либо сколько угодно неизменяемых. Но почему мой случай нарушает это правило? Ведь я сначала беру неизменяемую ссылку на часть строки, а потом пытаюсь её изменить. Когда я использую `first` после изменения, это же может привести к проблемам? Объясните, пожалуйста, как это работает на практике и как правильно решить задачу. Конкретно: мне нужно взять подстроку, изменить исходную строку, а потом использовать обе. Как это сделать без ошибок компиляции? Я слышал про `clone()`, но не уверен, что это правильный путь. Также читал, что можно переупорядочить код, но в моём реальном проекте порядок важен. Спасибо за помощь! <<>>

Почему возникает ошибка?

Ошибка cannot borrow as mutable because it is also borrowed as immutable — это проявление фундаментального правила Rust: безопасность заимствования. Оно гарантирует, что данные не изменятся, пока на них есть активные ссылки. В вашем случае:

  • let first = &text[0..1]; — создаёт неизменяемую ссылку на всю строку (срез &str заимствует владельца String).
  • text.push_str(" world"); — требует изменяемой ссылки на text.
  • Но first всё ещё "жив" до конца области видимости, поэтому компилятор блокирует изменения.

Проблема в том, что Rust не может доказать, что push_str не перераспределит память, сделав first висячей ссылкой. Даже если вы не используете first после изменения — компилятор смотрит на область видимости ссылки, а не на реальное использование.

Решение 1: Переупорядочивание кода (самое простое)

Если вам не нужно использовать first после изменения строки, просто перенесите изменяемую операцию после последнего использования неизменяемой ссылки:

fn main() {
    let mut text = String::from("hello");
    let first = &text[0..1];
    println!("First char: {}", first); // последнее использование first
    text.push_str(" world"); // теперь изменяемое заимствование безопасно
    println!("Full text: {}", text);
}

Здесь first "умирает" после println!, и компилятор разрешает push_str.

Решение 2: Клонирование данных (если нужна копия)

Если вам нужно сохранить подстроку для дальнейшего использования после изменения строки, создайте копию данных:

fn main() {
    let mut text = String::from("hello");
    let first = text[0..1].to_string(); // to_string() создаёт новую String
    text.push_str(" world");
    println!("First char: {}", first); // first — независимая копия
    println!("Full text: {}", text);
}

Недостаток: дополнительное выделение памяти. Но это безопасно и просто.

Решение 3: Использование индексов вместо ссылок

Если вам нужен только первый символ, можно сохранить его индекс, а не ссылку:

fn main() {
    let mut text = String::from("hello");
    let first_index = 0; // сохраняем индекс
    text.push_str(" world");
    let first_char = text.chars().nth(first_index).unwrap();
    println!("First char: {}", first_char);
}

Это работает, потому что индекс — это простой usize, который не заимствует строку.

Решение 4: Разделение на отдельные области видимости

Можно использовать блоки {} для ограничения времени жизни ссылок:

fn main() {
    let mut text = String::from("hello");
    let first = {
        &text[0..1] // ссылка живёт только внутри этого блока
    };
    // здесь first уже недоступен
    text.push_str(" world");
    println!("Full text: {}", text);
}

Что выбрать?

  • Если порядок важен и данные нужны после изменения — используйте to_string() (клонирование).
  • Если можно изменить порядок — переупорядочьте код.
  • Если нужны только индексы — работайте с ними.

Помните: Rust не даёт вам выстрелить себе в ногу. Эта ошибка — защита от неопределённого поведения, которое было бы возможно в C++ или Java.

<<>>

Альтернативный подход: Cell/RefCell для внутренней изменяемости

Если вам нужно обойти правила заимствования в сложном сценарии (например, когда ссылки и изменения пересекаются в разных частях кода), можно использовать RefCell для проверки заимствований во время выполнения:

use std::cell::RefCell;

fn main() {
    let text = RefCell::new(String::from("hello"));
    let first = text.borrow(); // неизменяемое заимствование
    println!("First char: {}", &first[0..1]);
    text.borrow_mut().push_str(" world"); // изменяемое заимствование
    // После drop(first) можно снова использовать
    drop(first);
    println!("Full text: {}", text.borrow());
}

Важно: RefCell проверяет правила заимствования в рантайме, а не на этапе компиляции. Если нарушить правила (например, попытаться взять borrow_mut пока есть активный borrow), программа запаникует. Используйте этот подход только когда статические проверки компилятора невозможно удовлетворить, например, в рекурсивных структурах или графах.

Для вашей простой задачи лучше использовать to_string() или переупорядочивание — это проще и безопаснее.