|
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 прячет эту подсказку.
 Если хочется разобраться с Rust не на уровне «компилятор снова недоволен», а через реальные инженерные задачи, обратите внимание на курс «Rust‑разработчик. Продвинутый уровень». Он подойдёт тем, кто уже знаком с языком и хочет увереннее работать с владением, асинхронностью, многопоточностью, сетевым взаимодействием и подходами к промышленной разработке.
| 📌 Перед стартом можно пройти бесплатное вступительное тестирование — оно поможет оценить текущий уровень и понять, насколько программа подходит под ваши задачи. |
А ещё в рамках курса проходят бесплатные открытые уроки от преподавателей‑практиков: на них можно познакомиться с экспертами, протестировать формат обучения и задать вопросы. Ближайшие открытые уроки: ➊ 6 мая в 20:00 — «Rust в деле: пишем многопользовательский чат с сервером, клиентом и CLI».
Разберём end‑to‑end проект: от сетевого протокола до работающего чата с сервером, клиентом и CLI. ➋ 19 мая в 20:00 — «Асинхронность под капотом».
Поговорим о том, как Rust представляет асинхронные задачи, что делает компилятор с async fn и почему понимание этих деталей помогает писать более предсказуемый код.-Источник
|