Владение и заимствование (Rust)
| Язык | Rust |
|---|---|
| Компонент компилятора | borrow checker (проверка заимствований) |
| Что предотвращает | обращение к освобождённой памяти, двойное освобождение, гонки данных |
| Ключевые понятия | владение, перемещение, ссылки, времена жизни |
| Умные указатели | Box, Rc, Arc, RefCell |
| Документация | The Rust Book, глава 4 |
Владение и заимствование — центральная идея языка Rust: набор правил, по которым компилятор во время сборки определяет, кто владеет каждым значением в памяти, кто и как на него ссылается и когда его можно освободить. Это позволяет Rust обходиться без сборщика мусора и при этом исключать целые классы ошибок, типичных для C и C++: использование освобождённой памяти, двойное освобождение, висячие указатели и гонки данных. Проверку выполняет компонент компилятора, который называют borrow checker.
Правила владения
Книга по Rust формулирует три правила:[1]
- У каждого значения есть владелец — переменная.
- В каждый момент владелец может быть только один.
- Когда владелец выходит из области видимости, значение уничтожается (вызывается
drop).
fn main() {
let s1 = String::from("привет");
let s2 = s1; // владение ПЕРЕМЕЩЕНО в s2
// println!("{s1}"); // ошибка E0382: заимствование перемещённого значения
println!("{s2}");
} // здесь s2 выходит из области видимости — память освобождается
Присваивание и передача в функцию по значению перемещают владение. Простые типы фиксированного размера (i32, bool, char) реализуют трейт Copy и копируются вместо перемещения. Глубокую копию данных в куче делают явно методом .clone().
Заимствование
Чтобы использовать значение, не забирая владение, берут ссылку — заимствуют его:
fn length(s: &String) -> usize { // неизменяемое заимствование
s.len()
}
fn shout(s: &mut String) { // изменяемое заимствование
s.push('!');
}
fn main() {
let mut text = String::from("привет");
let n = length(&text);
shout(&mut text);
println!("{text} ({n})");
}
Главное правило заимствования: в каждый момент может существовать либо любое число неизменяемых ссылок &T, либо ровно одна изменяемая &mut T.[1] Это то же правило «много читателей или один писатель», которое в других языках обеспечивают блокировки, только проверенное компилятором. Именно поэтому в безопасном Rust невозможны гонки данных.
let mut v = vec![1, 2, 3];
let first = &v[0]; // неизменяемая ссылка
v.push(4); // ошибка E0502: нельзя изменять, пока жива ссылка first
println!("{first}");
Ошибка здесь не формальная: push может переразместить вектор в памяти, и first указывала бы на освобождённый участок.
Времена жизни
Ссылка не может пережить данные, на которые указывает. Компилятор отслеживает это через времена жизни (lifetimes). Обычно они выводятся автоматически, но в сигнатурах иногда их нужно указать явно:[2]
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
Запись 'a говорит: результат живёт не дольше, чем оба аргумента. С 2018 года действуют нелексические времена жизни (NLL): ссылка считается живой до последнего использования, а не до конца блока, что убрало многие ложные ошибки.[4]
Когда правил мало
Для ситуаций, которые не укладываются в «один владелец», стандартная библиотека предлагает умные указатели:[3]
| Тип | Назначение |
|---|---|
Box<T> |
значение в куче с единственным владельцем |
Rc<T> |
несколько владельцев в одном потоке (подсчёт ссылок) |
Arc<T> |
то же для нескольких потоков (атомарный счётчик) |
RefCell<T> |
проверка правил заимствования во время выполнения («внутренняя изменяемость») |
Mutex<T>, RwLock<T> |
изменяемый доступ из нескольких потоков |
Типичная связка для общих изменяемых данных между потоками — Arc<Mutex<T>>. Безопасность передачи между потоками обеспечивают маркерные трейты Send и Sync, которые компилятор выводит автоматически.[5]
Сравнение подходов к памяти
| Подход | Языки | Цена |
|---|---|---|
| ручное управление | C, C++ | ошибки памяти — частая причина уязвимостей |
| сборка мусора | Java, Go, Python | паузы и накладные расходы во время работы |
| владение и заимствование | Rust | сложнее писать код, проверки — при компиляции |
Подробнее — в статье Сборка мусора vs ручное управление памятью.
Трудности
Новички часто «воюют с borrow checker»: структуры с циклическими ссылками (графы, двусвязные списки) и некоторые шаблоны из других языков приходится перестраивать — использовать индексы вместо ссылок, Rc/Weak или арены. Компилятор консервативен и иногда отвергает корректный код; для таких случаев разрабатывается новый анализатор Polonius, а в крайнем случае есть Unsafe Rust.