Материал подготовлен в рамках курса «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 и почему понимание этих деталей помогает писать более предсказуемый код.

Комментарии (1)


  1. Granulex
    01.05.2026 09:17

    Хороший разбор — особенно кейс с Vec-итерацией, его часто упускают. Единственное, чего не хватает: Cow<'_, T> — тип для случая "аллоцирую только если придётся". Статья предлагает либо ссылку, либо clone(), но есть промежуточный вариант: fn normalize(s: &str) -> Cow<'_, str> { if s.contains(' ') { Cow::Owned(s.replace(' ', " ")) } else { Cow::Borrowed(s) } }. Если clone() — "всегда копия", а &str — "никогда копия", то Cow — "копия только если нужна". Без него коллекция антипаттернов неполна.