Глава 6: Владение и заимствование

Содержание: Концепция владения (Ownership) Правила заимствования (Borrowing): & и &mut Ссылки и срезы (References & Slices) Время жизни (Lifetimes): основы Очистка и удаление переменных в Rust Примеры: ошибки компиляции и их исправление Упражнение: Реализовать структуру с заимствованием

Добро пожаловать в шестую лекцию нашего курса по Rust! Сегодня мы разберём одну из самых важных и уникальных особенностей языка — систему владения и заимствования. Эти концепции лежат в основе безопасности памяти Rust, позволяя избежать ошибок вроде "use-after-free" или "data race" без использования сборщика мусора. Мы рассмотрим владение (ownership), правила заимствования (borrowing), ссылки и срезы, основы времени жизни (lifetimes), разберём типичные ошибки компиляции и их исправление, а завершится лекция упражнением по реализации структуры с заимствованием.

Пояснение:

Эта глава самодостаточна, но мы будем опираться на базовые знания из предыдущих лекций (переменные, функции). Более сложные темы, такие как продвинутые времена жизни или многопоточность, раскроются позже. Наша цель — дать вам глубокое понимание того, как Rust управляет памятью, и научить вас писать безопасный код. Приступим!


1. Концепция владения (Ownership)

Основы

Владение — центральная идея Rust. Правила:

  1. Каждое значение имеет владельца (переменную).
  2. У значения может быть только один владелец.
  3. Когда владелец (переменная) выходит из области видимости, значение освобождается.
fn main() {
    let s = String::from("hello"); // s владеет строкой
    println!("{}", s);
} // s выходит из области видимости, память освобождается

Перемещение (Move)

При присваивании владение перемещается:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Владение перемещается
    // println!("{}", s1); // Ошибка
    println!("{}", s2);
}

Типы с Copy копируются:

fn main() {
    let x = 5;
    let y = x; // Копирование
    println!("x = {}, y = {}", x, y);
}
Совет: Используйте .clone() для копирования, если нужно.

2. Правила заимствования (Borrowing): & и &mut

Заимствование

Заимствование (borrowing) в Rust — это когда ты даёшь доступ к данным (переменной) другому куску кода, но не отдаёшь их полностью, а только "одалживаешь". При этом оригинальный владелец данных остаётся, и есть строгие правила:

Это нужно, чтобы избежать ошибок вроде изменения данных, пока кто-то их использует. Rust следит за этим через компилятор.

Заимствование — как дать другу посмотреть или поправить твою тетрадь, но она остаётся твоей.

Правила в кратце:

  1. Либо одна изменяемая ссылка (&mut), либо много неизменяемых (&).
  2. Ссылки должны быть валидны. Это означает, что когда ты "одалживаешь" данные через ссылку (например, &x или &mut x), эти данные должны оставаться "живыми" и доступными всё время, пока ссылка используется. Проще говоря, нельзя ссылаться на то, что уже исчезло или было уничтожено.
    Представь, что ты дал другу адрес своего дома, чтобы он зашёл в гости. Если ты переедешь, пока он идёт, адрес станет "невалидным" — друг придёт в никуда. В Rust компилятор следит, чтобы такого не случилось: ссылка всегда должна указывать на реальные, существующие данные.
let x = 5;
let y = &x; // y "ссылается" на x
// Пока y используется, x должен существовать

Если x пропадёт (например, выйдет из области видимости), а ты попытаешься использовать y, Rust не позволит — это "невалидная" ссылка. Это защищает от ошибок вроде обращения к "мёртвой" памяти.

fn main() {
    let s = String::from("hello");
    let r = &s; // Неизменяемая ссылка
    println!("s = {}, r = {}", s, r);
}

Изменяемая ссылка:

fn main() {
    let mut s = String::from("hello");
    let r = &mut s; // Изменяемая ссылка
    r.push_str(", world");
    println!("{}", r);
}

Ошибка: нарушение правил

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s; // Ошибка
    r1.push_str(", world");
}
Совет: Используйте & для чтения, минимизируйте область &mut.

3. Ссылки и срезы (References & Slices)

Срезы

Срез — ссылка на часть данных:

fn main() {
    let s = String::from("hello, world");
    let hello = &s[0..5]; // Срез
    println!("Срез: {}", hello); // Вывод: hello
}

Срез массива:

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..4];
    println!("Срез: {:?}", slice); // Вывод: [2, 3, 4]
}
Совет: Используйте .. (исключает конец) или ..= (включает).

4. Время жизни (Lifetimes): основы

Что такое время жизни?

Время жизни — это как Rust следит за памятью или как "срок годности" для данных в твоей программе. В Rust компьютер должен знать, как долго можно использовать какую-то штуку (например, число или слово), чтобы не запутаться и не оставить "мусор" в памяти. Это помогает избежать ошибок, когда программа пытается взять что-то, чего уже нет. Представь, что ты даёшь другу игрушку поиграть, но говоришь: "Верни, когда закончишь" — вот время жизни и есть такие правила для данных.

Время жизни гарантирует валидность ссылок:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("short");
    let s2 = String::from("longer string");
    let result = longest(&s1, &s2);
    println!("Самая длинная строка: {}", result);
}
Совет: Пока просто следите, чтобы ссылки не пережили данные.

5. Очистка и удаление переменных в Rust

В Rust переменные автоматически "удаляются" (освобождаются из памяти), когда они выходят из области видимости (scope). Это контролируется механизмом владения (ownership) и временем жизни (lifetimes). Вместо явного "удаления" переменно вы просто позволяете компилятору самому убрать её, либо вручную ограничиваете её область видимости.

Естественное удаление при выходе из области видимости

Когда переменная выходит из области видимости (закрывающая фигурная скобка }), её память освобождается автоматически, если она владеет данными (например,String, Vec).

Пример:

fn main() {
    let text = String::from("Hello");
    println!("{}", text); // "Hello"
    // Здесь text всё ещё существует
} // text выходит из области видимости и память освобождается
  // println!("{}", text); // Ошибка: text больше не существует

Явное ограничение области видимости

Если вы хотите "удалить" переменную раньше конца функции, можно использовать вложенный блок {}:

fn main() {
    let text = String::from("Hello");
    {
        let inner = String::from("World");
        println!("{}", inner); // "World"
    } // inner освобождается здесь
    // println!("{}", inner); // Ошибка: inner не существует
    println!("{}", text); // "Hello" — text всё ещё жив
} // text освобождается здесь

Присвоение значения для "очистки"

Если вы хотите явно "очистить" переменную, не дожидаясь конца области видимости, можно переместить её владение или заменить на что-то пустое.

Например:

fn main() {
    let mut text = String::from("Hello");
    let moved = text; // владение переходит к moved, text становится недоступным
    // println!("{}", text); // Ошибка: text больше не владеет данными
    println!("{}", moved); // "Hello"
}
fn main() {
    let mut text = String::from("Hello");
    text.clear(); // очищает содержимое, но переменная остаётся
    println!("{}", text); // "" (пустая строка)
}
fn main() {
    let mut text = String::from("Hello");
    text = String::new(); // заменяем на новую пустую строку
    println!("{}", text); // "" (пустая строка)
}

Это не совсем "удаление" переменной а освобождение её содержимого или перемещение данных. Переменная остаётся в области видимости, но её значение может быть недоступным или пустым.

Использование std::mem::drop

Для явного "удаления" переменной (освобождения её памяти) до конца области видимости можно использовать функцию std::mem::drop:

fn main() {
    let mut text = String::from("Hello");
    println!("{}", text); // "Hello"
    std::mem::drop(text); // text "удаляется" (перестаёт существовать)
    // println!("{}", text); // Ошибка: text больше не существует
}

drop принимает владение переменной и немедленно освобождает её память. После этого переменная становится недоступной.

Ограничение: Переменная должна быть объявлена с mut, если вы хотите использоватьdrop позже, либо вы должны передать её владение явно.

Итого:

Пример

Создайте строку, выведите её, "удалите" с drop, затем создайте новую переменную с тем же именем:

fn main() {
    let mut data = String::from("Important data");
    println!("До: {}", data); // "Important data"
    std::mem::drop(data);     // "удаляем" data
    // println!("{}", data);  // Ошибка
    let data = "New";         // можно переиспользовать имя
    println!("После: {}", data); // "New"
}

6. Примеры: ошибки компиляции и их исправление

Ошибка 1: Использование после перемещения

fn main() {
    let s = String::from("hello");
    let s2 = s;
    println!("{}", s); // Ошибка
}

Исправление:

fn main() {
    let s = String::from("hello");
    let s2 = &s;
    println!("{}", s);
}

Ошибка 2: Одновременные изменяемые ссылки

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s; // Ошибка
    r1.push_str(" world");
}

Исправление:

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    r1.push_str(" world");
    println!("{}", r1);
}
Совет: Читайте сообщения компилятора — они помогают.

7. Упражнение: Реализовать структуру с заимствованием

Задача: Создайте структуру Book с полями title (ссылка) и pages. Реализуйте функцию описания.

struct Book<'a> {
    title: &'a str,
    pages: u32,
}

fn describe_book<'a>(book: &Book<'a>) -> String {
    format!("Книга '{}', {} страниц", book.title, book.pages)
}

fn main() {
    let title = String::from("Rust in Action");
    let my_book = Book {
        title: &title,
        pages: 450,
    };
    let description = describe_book(&my_book);
    println!("{}", description); // Вывод: Книга 'Rust in Action', 450 страниц
}

Улучшение: Добавьте метод в impl для печати описания.


Заключение

Мы изучили владение и заимствование — сердце системы безопасности Rust. Вы научились управлять памятью через перемещение и ссылки, работать со срезами и понимать основы времён жизни. Эти знания помогут вам писать безопасный и эффективный код, избегая ошибок на этапе компиляции.

Практикуйтесь с примерами и упражнением, чтобы закрепить материал. В следующих лекциях мы углубимся в типы данных, трейты и многопоточность. До встречи!