Ownership и borrowing в Rust простыми словами

Ownership в Rust лучше понимать не как теорию, а как правило владения: у значения есть владелец, владелец отвечает за время жизни значения, а передача владения может сделать старую переменную недоступной. Borrowing — это способ дать доступ к значению без передачи владения

В этом уроке мы разберем move, &, &mut и первые ошибки borrow checker на маленьких примерах. Наша цель не выучить все lifetime-случаи, а перестать пугаться сообщений компилятора

Что получится в конце

Создайте проект:

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x