Ownership в Rust лучше понимать не как теорию, а как правило владения: у значения есть владелец, владелец отвечает за время жизни значения, а передача владения может сделать старую переменную недоступной. Borrowing — это способ дать доступ к значению без передачи владения
В этом уроке мы разберем move, &, &mut и первые ошибки borrow checker на маленьких примерах. Наша цель не выучить все lifetime-случаи, а перестать пугаться сообщений компилятора
- Что получится в конце
- Move: почему значение может переехать
- Borrowing: доступ без переезда
- Mutable borrowing: дать право изменить
- Главное правило ссылок
- Как исправлять ошибку borrow checker
- Мини-практика: нормализуем заголовок
- Функция может вернуть владение обратно
- &String и &str
- Когда clone оправдан
- Почему borrow checker ругается на порядок строк
- Частые ошибки и порядок проверки
- Что может быть еще интересно по этой теме
- Что почитать дальше по Rust
Что получится в конце
Создайте проект:
cargo new rust_ownership
cd rust_ownership
В src/main.rs вставьте:
fn print_length(text: &String) {
println!("Length: {}", text.len());
}
fn add_suffix(text: &mut String) {
text.push_str(" is safe");
}
fn main() {
let mut title = String::from("Rust");
print_length(&title);
add_suffix(&mut title);
println!("{title}");
}
Запуск:
cargo run
Ожидаемый вывод:
Length: 4
Rust is safe
Здесь есть два вида borrowing: обычная ссылка &String для чтения и изменяемая ссылка &mut String для изменения
Move: почему значение может переехать
Посмотрите на пример:
fn main() {
let first = String::from("Rust");
let second = first;
println!("{second}");
}
String хранит данные в heap. Когда мы пишем let second = first;, владение строкой переходит к second. После этого first больше нельзя использовать
Если добавить:
println!("{first}");
компилятор остановит код. Это не наказание, а защита от двойного освобождения памяти и неясного владения. Rust говорит: у значения должен быть один активный владелец
Для типов вроде i32 поведение другое:
let a = 10;
let b = a;
println!("{a} {b}");
Число копируется, потому что простой тип реализует Copy. Поэтому ownership особенно заметен на String, Vec и других типах с данными в heap
Borrowing: доступ без переезда
Если функция должна только прочитать строку, ей не нужно забирать владение:
fn print_length(text: &String) {
println!("Length: {}", text.len());
}
Вызов:
print_length(&title);
println!("{title}");
&title создает ссылку. Функция получает доступ к строке, но не становится владельцем. После вызова title остается доступной
В Rust такую передачу называют borrow: функция как будто берет значение «почитать» и возвращает доступ обратно
Mutable borrowing: дать право изменить
Если функция должна менять строку, нужна изменяемая ссылка:
fn add_suffix(text: &mut String) {
text.push_str(" is safe");
}
Переменная тоже должна быть объявлена как изменяемая:
let mut title = String::from("Rust");
add_suffix(&mut title);
Rust явно разделяет чтение и изменение. Это уменьшает количество ситуаций, где одна часть кода читает значение, а другая в тот же момент меняет его
Главное правило ссылок
В один момент времени можно иметь либо много обычных ссылок, либо одну изменяемую ссылку. Смешивать активное чтение и активное изменение нельзя
Рабочий пример:
let title = String::from("Rust");
let a = &title;
let b = &title;
println!("{a} {b}");
Нерабочая идея:
let mut title = String::from("Rust");
let a = &title;
let b = &mut title;
println!("{a}");
println!("{b}");
Компилятор не даст иметь обычную ссылку и изменяемую ссылку одновременно, если они пересекаются по использованию. На практике это помогает избегать гонок и неожиданного изменения данных
Как исправлять ошибку borrow checker
Первый способ — сократить область использования обычной ссылки:
let mut title = String::from("Rust");
let length = title.len();
println!("Length: {length}");
title.push_str(" language");
println!("{title}");
Здесь мы не храним ссылку дольше, чем нужно. Сначала получили длину, потом меняем строку
Второй способ — сделать отдельный блок:
let mut title = String::from("Rust");
{
let view = &title;
println!("{view}");
}
title.push_str(" language");
После блока view больше не используется, и изменяемый доступ становится возможным
Мини-практика: нормализуем заголовок
Напишите функцию, которая принимает изменяемую строку и добавляет префикс:
fn make_lesson_title(title: &mut String) {
title.insert_str(0, "Lesson: ");
}
fn main() {
let mut title = String::from("Ownership");
make_lesson_title(&mut title);
println!("{title}");
}
Проверьте:
cargo run
Теперь временно уберите mut из let mut title. Компилятор объяснит, что переменную нельзя заимствовать как изменяемую. Это хороший учебный момент: Rust просит явно назвать места, где данные могут меняться
Функция может вернуть владение обратно
Если функция забрала String, она может вернуть ее обратно. Это не лучший стиль для простого чтения, но полезно для понимания ownership:
fn take_and_return(text: String) -> String {
println!("Inside: {text}");
text
}
fn main() {
let title = String::from("Rust");
let title = take_and_return(title);
println!("After: {title}");
}
Первый title переезжает в функцию. Затем функция возвращает строку, и мы снова привязываем ее к имени title. Это показывает, что move не уничтожает значение сам по себе. Он передает владение
В реальном коде для простого чтения лучше использовать ссылку &String или чаще &str. Но модель «забрал — вернул» помогает увидеть механику без магии
&String и &str
В учебных примерах часто пишут &String, потому что так проще связать тему со значением String. Но в реальном Rust функции, которые только читают текст, обычно принимают &str:
fn print_title(title: &str) {
println!("{title}");
}
Такую функцию можно вызвать и со строковым литералом, и со String:
let owned = String::from("Rust");
print_title("literal");
print_title(&owned);
&str гибче, потому что означает «ссылка на строковый срез», а не обязательно ссылка на владеющую строку. Для новичка порядок такой: сначала понять String, ownership и &, затем постепенно привыкнуть к &str
Когда clone оправдан
Иногда компилятор предлагает использовать clone, и это действительно решает проблему:
let first = String::from("Rust");
let second = first.clone();
println!("{first}");
println!("{second}");
Теперь есть две строки с одинаковым содержимым. Но clone создает копию данных, а не просто новое имя. Если строка большая или операция повторяется часто, это может быть лишней работой
Здоровая привычка: если хочется поставить clone, сначала спросить, нужна ли настоящая копия. Если нужно только прочитать значение, ссылка лучше. Если нужны два независимых владельца данных, clone может быть нормальным решением
Почему borrow checker ругается на порядок строк
Rust анализирует, где ссылка используется в последний раз. Иногда достаточно поменять порядок:
let mut title = String::from("Rust");
let view = &title;
println!("{view}");
title.push_str(" language");
Это работает, потому что после println!("{view}") обычная ссылка больше не нужна. Но если попробовать изменить строку до последнего использования view, компилятор остановит код
Не воспринимайте это как каприз. Rust защищает момент, когда кто-то еще смотрит на значение, а вы пытаетесь его изменить. Чем точнее вы ограничиваете область ссылки, тем легче договориться с компилятором
Частые ошибки и порядок проверки
Использовали String после move Если передали владение другой переменной или функции, старое имя больше недоступно. Передавайте ссылку, если владение не нужно
Забыли mut у переменной &mut title возможен только если сама переменная объявлена как let mut title
Держите ссылку слишком долго Не храните ссылку, если можно сразу получить результат и отпустить borrow. Часто помогает вынести значение в отдельную переменную
Пытаетесь понять lifetime раньше времени Сначала освойте move, & и &mut на простых функциях. Lifetime-синтаксис нужен реже, чем кажется на старте
Что может быть еще интересно по этой теме
Borrow checker мешает или помогает? На старте мешает темпу, но ловит реальные ошибки владения. Со временем он становится похож на строгого напарника по ревью
Почему Rust не делает сборщик мусора? Rust управляет памятью через владение и время жизни значений. Это дает безопасность памяти без постоянного runtime GC
Когда нужен clone? Когда вам действительно нужна отдельная копия данных. Но не используйте clone как универсальный пластырь, сначала подумайте, достаточно ли ссылки
Что открыть дальше? После ownership удобно разбирать Option и Result, потому что Rust часто выражает отсутствие значения и ошибку через типы, а не через null и исключения
Что почитать дальше по Rust
- Rust с нуля: cargo new и первая программа
- Типы, match, Option и Result в Rust
- Vec, String и HashMap в Rust
- Struct, enum и impl в Rust



