Avatar ·

Ошибка "cannot move out of borrowed content" в Rust: как исправить?

Всем привет! Я начал изучать Rust и столкнулся с ошибкой, которую не могу понять. У меня есть структура `User` и функция, которая должна изменять её поле `name`. Вот код: ```rust struct User { name: String, age: u32, } fn update_name(user: &User, new_name: String) { user.name = new_name; // Ошибка здесь } fn main() { let user = User { name: String::from("Alice"), age: 30, }; update_name(&user, String::from("Bob")); println!("{}", user.name); } ``` Компилятор выдаёт: `cannot move out of borrowed content` и `cannot move out of behind a shared reference`. Я понимаю, что передаю `&User` — это неизменяемая ссылка, но я хочу изменить поле `name`. Если я изменю сигнатуру на `&mut User`, то ошибка меняется на `cannot borrow immutable local variable 'user' as mutable`. Я попробовал добавить `mut` перед `user` в `main`, но тогда компилятор говорит, что нельзя заимствовать как изменяемое, потому что оно уже заимствовано как неизменяемое (хотя я не вижу тут заимствования). В общем, я запутался. Как правильно изменить поле структуры через ссылку? И почему Rust такой строгий? Объясните, пожалуйста, на простых примерах. <<>>

Корень проблемы: правила заимствования Rust

Ошибка возникает из-за фундаментальных правил владения и заимствования в Rust. Давайте разберёмся по шагам.

Почему ваш код не работает?

В функции update_name вы принимаете &User — неизменяемую ссылку. Неизменяемая ссылка даёт только право читать данные, но не изменять их. Присваивание user.name = new_name пытается переместить новое значение String в поле name, что требует права на запись. У вас его нет.

Даже если бы это было разрешено, вы бы пытались "вытолкнуть" старое значение String из user.name (поскольку Rust автоматически заменяет старое значение новым). Это называется move out — перемещение владения из-за ссылки, что запрещено, так как ссылка не владеет данными.

Решение 1: Изменяемая ссылка (&mut T)

Если вам нужно изменить данные, используйте изменяемую ссылку &mut User. Но тогда и вызывающий код должен передать изменяемую ссылку, а переменная должна быть объявлена как mut:

struct User {
    name: String,
    age: u32,
}

fn update_name(user: &mut User, new_name: String) {
    user.name = new_name; // Теперь работает!
}

fn main() {
    let mut user = User {          // <-- добавили mut
        name: String::from("Alice"),
        age: 30,
    };
    update_name(&mut user, String::from("Bob")); // <-- &mut
    println!("{}", user.name); // Выведет: Bob
}

Почему это работает? Изменяемая ссылка даёт эксклюзивное право на запись. Вы можете заменить поле name, при этом старое значение будет корректно удалено.

Решение 2: Замена с возвратом старого значения (std::mem::replace)

Если вы не хотите менять сигнатуру, а просто нужно заменить поле, но при этом сохранить старое значение (например, для логирования), используйте std::mem::replace. Эта функция заменяет значение и возвращает старое:

use std::mem;

fn update_name(user: &mut User, new_name: String) -> String {
    let old_name = mem::replace(&mut user.name, new_name);
    old_name // возвращаем старое имя
}

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        age: 30,
    };
    let old = update_name(&mut user, String::from("Bob"));
    println!("Было: {}, стало: {}", old, user.name);
}

Решение 3: Клонирование (для неизменяемых ссылок)

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

fn update_name(user: &User, new_name: String) -> User {
    User {
        name: new_name,
        age: user.age, // копируем остальные поля
    }
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };
    let new_user = update_name(&user, String::from("Bob"));
    println!("{}", new_user.name); // Bob
    // user.name остался Alice
}

Это неэффективно для больших структур, но иногда необходимо.

Почему Rust такой строгий?

  • Безопасность памяти: Запрет на перемещение из-за ссылки предотвращает появление висячих указателей. Если бы вы могли вытолкнуть String через неизменяемую ссылку, ссылка могла бы указывать на освобождённую память.
  • Гонки данных: Rust гарантирует, что в любой момент времени у данных либо одна изменяемая ссылка, либо сколько угодно неизменяемых. Это исключает гонки данных на этапе компиляции.

Запомните главное правило: неизменяемая ссылка — только чтение, изменяемая — чтение и запись. И для изменения данных через ссылку вам всегда нужен &mut.

<<>>

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

Часто проблема решается не борьбой с заимствованием, а изменением архитектуры. Вместо функции, которая принимает ссылку и меняет поле, можно реализовать метод структуры:

struct User {
    name: String,
    age: u32,
}

impl User {
    fn rename(&mut self, new_name: String) {
        self.name = new_name;
    }

    fn new(name: &str, age: u32) -> Self {
        User {
            name: String::from(name),
            age,
        }
    }
}

fn main() {
    let mut user = User::new("Alice", 30);
    user.rename(String::from("Bob"));
    println!("{}", user.name);
}

Метод rename принимает &mut self — это стандартный паттерн для изменения полей. В Rust принято группировать функции, работающие с данными, как методы. Это делает код более читаемым и следует принципу "данные и поведение вместе".

Ещё один полезный приём — использовать внутреннюю изменяемость с Cell или RefCell, если вам нужно изменять данные через неизменяемую ссылку (например, для кэширования). Но это продвинутая тема, для простых случаев достаточно &mut.

Помните: если ваша функция принимает &T, она не должна ничего менять. Это контракт, который вы даёте компилятору и другим разработчикам.