Ошибка "cannot move out of borrowed content" в 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, она не должна ничего менять. Это контракт, который вы даёте компилятору и другим разработчикам.