Владение и заимствование (Rust)

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Владение и заимствование (Rust)
ЯзыкRust
Компонент компилятораborrow checker (проверка заимствований)
Что предотвращаетобращение к освобождённой памяти, двойное освобождение, гонки данных
Ключевые понятиявладение, перемещение, ссылки, времена жизни
Умные указателиBox, Rc, Arc, RefCell
ДокументацияThe Rust Book, глава 4

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

Правила владения

Книга по Rust формулирует три правила:[1]

  1. У каждого значения есть владелец — переменная.
  2. В каждый момент владелец может быть только один.
  3. Когда владелец выходит из области видимости, значение уничтожается (вызывается 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.

См. также

Источники

  1. The Rust Book — Understanding Ownership
  2. The Rust Book — Validating References with Lifetimes
  3. The Rust Book — Smart Pointers
  4. RFC 2094 — Non-lexical lifetimes
  5. The Rustonomicon — Send and Sync