Добро пожаловать в шестую лекцию нашего курса по Rust! Сегодня мы разберём одну из самых важных и уникальных особенностей языка — систему владения и заимствования. Эти концепции лежат в основе безопасности памяти Rust, позволяя избежать ошибок вроде "use-after-free" или "data race" без использования сборщика мусора. Мы рассмотрим владение (ownership), правила заимствования (borrowing), ссылки и срезы, основы времени жизни (lifetimes), разберём типичные ошибки компиляции и их исправление, а завершится лекция упражнением по реализации структуры с заимствованием.
Пояснение:
Mutex
) т.е. нет никакого "порядка" или "правил", которые бы говорили: "Сначала ты, потом я". Оба потока лезут к переменной одновременно, и никто их не останавливает. Синхронизация — это как "замок" или "очередь", чтобы потоки не мешали друг другу. В Rust для этого используют специальные инструменты (Mutex
, RwLock
).
Эта глава самодостаточна, но мы будем опираться на базовые знания из предыдущих лекций (переменные, функции). Более сложные темы, такие как продвинутые времена жизни или многопоточность, раскроются позже. Наша цель — дать вам глубокое понимание того, как Rust управляет памятью, и научить вас писать безопасный код. Приступим!
Владение — центральная идея Rust. Правила:
fn main() {
let s = String::from("hello"); // s владеет строкой
println!("{}", s);
} // s выходит из области видимости, память освобождается
При присваивании владение перемещается:
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()
для копирования, если нужно.&
и &mut
Заимствование (borrowing) в Rust — это когда ты даёшь доступ к данным (переменной) другому куску кода, но не отдаёшь их полностью, а только "одалживаешь". При этом оригинальный владелец данных остаётся, и есть строгие правила:
&
) сколько угодно раз.&mut
), но только один раз за раз и без одновременного чтения.Это нужно, чтобы избежать ошибок вроде изменения данных, пока кто-то их использует. Rust следит за этим через компилятор.
Заимствование — как дать другу посмотреть или поправить твою тетрадь, но она остаётся твоей.
Правила в кратце:
&mut
), либо много неизменяемых (&
).&x
или &mut x
), эти данные должны оставаться "живыми" и доступными всё время, пока ссылка используется. Проще говоря, нельзя ссылаться на то, что уже исчезло или было уничтожено.
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
.Срез — ссылка на часть данных:
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]
}
..
(исключает конец) или ..=
(включает).Время жизни — это как 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);
}
В 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
позже, либо вы должны передать её владение явно.{}
.std::mem::drop
для явного освобождения памяти..clear()
или замену на пустое значение, если тип это поддерживает.Создайте строку, выведите её, "удалите" с 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"
}
fn main() {
let s = String::from("hello");
let s2 = s;
println!("{}", s); // Ошибка
}
Исправление:
fn main() {
let s = String::from("hello");
let s2 = &s;
println!("{}", s);
}
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);
}
Задача: Создайте структуру 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. Вы научились управлять памятью через перемещение и ссылки, работать со срезами и понимать основы времён жизни. Эти знания помогут вам писать безопасный и эффективный код, избегая ошибок на этапе компиляции.
Практикуйтесь с примерами и упражнением, чтобы закрепить материал. В следующих лекциях мы углубимся в типы данных, трейты и многопоточность. До встречи!