Материал подготовлен в рамках курса «Rust-разработчик. Продвинутый уровень».
Когда borrow checker не пропускает код, самое простое решение: добавить .clone(). Компилятор доволен, код работает. Но вот незадача, что clone() в таком сценарии не решает проблему, а прячет её. Borrow checker сказал, что с владением что‑то не так, а вы вместо исправления просто скопировали данные.
Через полгода в проекте десятки clone(), каждый из которых аллоцирует, и профилировщик показывает, что заметная часть времени уходит на копирование строк.
Clone чтобы угодить borrow checker
fn process(data: &mut Vec<String>) { 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<String>) { let new_items: Vec<String> = 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<String, Vec<i32>> = 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<String, Vec<i32>>, key: &str, ) -> &mut Vec<i32> { 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<RefCell<T>> или Arc<Mutex<T>> дают 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 прячет эту подсказку.

Если хочется разобраться с Rust не на уровне «компилятор снова недоволен», а через реальные инженерные задачи, обратите внимание на курс «Rust‑разработчик. Продвинутый уровень». Он подойдёт тем, кто уже знаком с языком и хочет увереннее работать с владением, асинхронностью, многопоточностью, сетевым взаимодействием и подходами к промышленной разработке.
? Перед стартом можно пройти бесплатное вступительное тестирование — оно поможет оценить текущий уровень и понять, насколько программа подходит под ваши задачи. |
А ещё в рамках курса проходят бесплатные открытые уроки от преподавателей‑практиков: на них можно познакомиться с экспертами, протестировать формат обучения и задать вопросы.
Ближайшие открытые уроки:
➊ 6 мая в 20:00 — «Rust в деле: пишем многопользовательский чат с сервером, клиентом и CLI».
Разберём end‑to‑end проект: от сетевого протокола до работающего чата с сервером, клиентом и CLI.
➋ 19 мая в 20:00 — «Асинхронность под капотом».
Поговорим о том, как Rust представляет асинхронные задачи, что делает компилятор с async fn и почему понимание этих деталей помогает писать более предсказуемый код.
Granulex
Хороший разбор — особенно кейс с Vec-итерацией, его часто упускают. Единственное, чего не хватает: Cow<'_, T> — тип для случая "аллоцирую только если придётся". Статья предлагает либо ссылку, либо clone(), но есть промежуточный вариант: fn normalize(s: &str) -> Cow<'_, str> { if s.contains(' ') { Cow::Owned(s.replace(' ', " ")) } else { Cow::Borrowed(s) } }. Если clone() — "всегда копия", а &str — "никогда копия", то Cow — "копия только если нужна". Без него коллекция антипаттернов неполна.