Вы неправильно используете clone() в Rust

Страницы:  1

Ответить
 

Professor Seleznov


Материал подготовлен в рамках курса«Rust-разработчик. Продвинутый уровень».
Когда borrow checker не пропускает код, самое простое решение: добавить .clone(). Компилятор доволен, код работает. Но вот незадача, что clone() в таком сценарии не решает проблему, а прячет её. Borrow checker сказал, что с владением что‑то не так, а вы вместо исправления просто скопировали данные.
Через полгода в проекте десятки clone(), каждый из которых аллоцирует, и профилировщик показывает, что заметная часть времени уходит на копирование строк.
Clone чтобы угодить borrow checker
fn process(data: &mut Vec) {
for item in data.clone() { // clone всего вектора!
if item.starts_with("error") {
data.push(format!("found: {}", item));
}
}
}
Borrow checker не даёт итерироваться по data и одновременно его мутировать. Разработчик клонирует весь вектор. Если в data тысяча строк, это тысяча аллокаций на пустом месте.
Решение простое: собрать результат отдельно и добавить после цикла.
fn process(data: &mut Vec) {
let new_items: Vec = data.iter()
.filter(|item| item.starts_with("error"))
.map(|item| format!("found: {}", item))
.collect();
data.extend(new_items);
}
Одна аллокация на новый вектор вместо копирования всего старого.
String вместо &str в сигнатуре
fn connect(url: String) { /* ... */ }
fn main() {
let config = load_config();
connect(config.db_url.clone()); // clone, потому что config нужен дальше
println!("key: {}", config.api_key);
}
Функция connect принимает String по значению, хотя внутри, скорее всего, только читает URL. Clone появляется из‑за того, что API требует владения, хотя ему достаточно ссылки.
fn connect(url: &str) { /* ... */ }
fn main() {
let config = load_config();
connect(&config.db_url); // без clone
println!("key: {}", config.api_key);
}
Если функция не сохраняет значение и не передаёт его дальше во владение, она должна принимать ссылку. Для строк это почти всегда &str вместо String.
Clone в замыканиях
let name = String::from("Alice");
let make_message = move || {
format!("Hello, {}!", name)
};
// println!("{}", name); // ошибка: name moved в замыкание
Часто замыкание только читает данные, и ему достаточно ссылки:
let name = String::from("Alice");
let make_message = || {
format!("Hello, {}!", &name) // захват по ссылке
};
println!("{}", make_message());
println!("{}", name); // работает
Clone в замыканиях реально нужен в двух случаях: когда замыкание уходит в другой поток (требуется move + Send) и когда замыкание переживает данные, на которые ссылается. Во всех остальных случаях стоит попробовать ссылку.
Clone из‑за HashMap
let mut map: HashMap> = HashMap::new();
let key = String::from("scores");
map.entry(key.clone()).or_insert_with(Vec::new).push(42);
// clone потому что entry забирает key, а он нужен дальше
Entry API забирает ключ по значению, потому что может вставить его в map. Если ключ уже есть, владение пропадёт зря.
Решение:проверить наличие отдельно и аллоцировать только при вставке.
fn get_or_create(
map: &mut HashMap>,
key: &str,
) -> &mut Vec {
if !map.contains_key(key) {
map.insert(key.to_owned(), Vec::new());
}
map.get_mut(key).unwrap()
}
Аллокация только при вставке нового ключа. Для существующих ключей ноль аллокаций.
Arc вместо clone
Когда одни и те же данные нужны нескольким потокам, clone копирует всё содержимое. На большой структуре (конфигурация, кеш, справочник) это дорого.
let config = load_config();
let handle1 = thread::spawn({
let config = config.clone(); // полная копия
move || use_config(&config)
});
Если потоки только читают данные, Arc решает задачу без копирования:
let config = Arc::new(load_config());
let handle1 = thread::spawn({
let config = Arc::clone(&config); // инкремент счётчика, не копия
move || use_config(&config)
});
Arc::clone увеличивает атомарный счётчик ссылок, это одна атомарная операция вместо глубокого копирования. Данные существуют в одном экземпляре.
Для однопоточного кода Arc не нужен, есть Rc (без атомарных операций, дешевле). Если данные нужно мутировать из нескольких мест, Rc> или Arc> дают shared ownership с внутренней мутабельностью. Это сложнее, чем просто clone(), но зато вы не копируете данные на каждый чих:
use std::rc::Rc;
use std::cell::RefCell;
let shared_state = Rc::new(RefCell::new(Vec::new()));
let state_for_callback = Rc::clone(&shared_state);
let callback = move || {
state_for_callback.borrow_mut().push(42);
};
callback();
println!("{ }", shared_state.borrow()); // [42]
Как найти лишние clone() в проекте
Clippy ловит часть случаев. cargo clippy выдаёт предупреждения вроде redundant_clone (clone() на значении, которое больше не используется) и clone_on_copy (clone() на Copy‑типе, где достаточно копирования). Но Clippy не ловит архитектурные clone(), где копирование формально корректно, но не нужно.
Для тех случаев помогает grep:
grep -rn "\.clone()" src/ | wc -l
Если в проекте на 10К строк больше 50 вызовов clone(), стоит пройтись по ним и спросить себя по каждому: зачем здесь копия? Если ответ «чтобы компилятор замолчал», это кандидат на рефакторинг.
Ещё один приём: временно заменить clone() на todo!() и посмотреть, где компилятор ругается. Те места, где после замены ошибка говорит «value moved here», показывают реальные конфликты владения, которые clone() прятал:
// вместо
let data = original.clone();
// временно поставьте
let data = todo!("why clone?");
Проект не скомпилируется, зато вы увидите, почему borrow checker требовал копию в каждом конкретном месте.
Когда clone нормален
Не каждый clone — это ошибка.
Маленькие Copy‑типы (i32, f64, bool) клонируются бесплатно. Маленькие строки и вектора на несколько элементов стоят наносекунды, и усложнять архитектуру ради этого бессмысленно. При прототипировании clone позволяет быстро проверить идею и вернуться к оптимизации позже.
Проблема не в clone как таковом, а в clone как затычке для borrow checker. Каждый раз, когда вы добавляете clone, чтобы компилятор замолчал, стоит остановиться и спросить: «Я копирую данные, потому что мне нужна копия, или потому что не разобрался с владением?» Если второе, компилятор подсказывает вам, что архитектура требует внимания, а clone прячет эту подсказку.
pic
Если хочется разобраться с Rust не на уровне «компилятор снова недоволен», а через реальные инженерные задачи, обратите внимание на курс «Rust‑разработчик. Продвинутый уровень». Он подойдёт тем, кто уже знаком с языком и хочет увереннее работать с владением, асинхронностью, многопоточностью, сетевым взаимодействием и подходами к промышленной разработке.
📌 Перед стартом можно пройти бесплатное вступительное тестирование — оно поможет оценить текущий уровень и понять, насколько программа подходит под ваши задачи.

А ещё в рамках курса проходят бесплатные открытые уроки от преподавателей‑практиков: на них можно познакомиться с экспертами, протестировать формат обучения и задать вопросы.
Ближайшие открытые уроки:
6 мая в 20:00 «Rust в деле: пишем многопользовательский чат с сервером, клиентом и CLI».
Разберём end‑to‑end проект: от сетевого протокола до работающего чата с сервером, клиентом и CLI.
19 мая в 20:00 — «Асинхронность под капотом».
Поговорим о том, как Rust представляет асинхронные задачи, что делает компилятор с async fn и почему понимание этих деталей помогает писать более предсказуемый код.-Источник
 
Loading...
Error