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

Заметка первая · 7 минут чтения

Система владения — то, чем Rust отличается от всех остальных распространённых языков. Она заменяет и сборщик мусора, и ручные malloc с free.

Три правила

  1. У каждого значения есть владелец — переменная, которая за него отвечает.
  2. Владелец в каждый момент времени ровно один.
  3. Когда владелец выходит из области видимости, значение уничтожается, а его память освобождается.

Всё остальное — следствия. Компилятор проверяет эти правила при сборке, поэтому в готовой программе нет ни счётчиков, ни пауз на сборку мусора.

Перемещение

Присваивание и передача в функцию по умолчанию перемещают значение: прежняя переменная перестаёт быть владельцем, и пользоваться ею больше нельзя.

let a = String::from("привет");
let b = a;             // строка переехала в b

// println!("{a}");    // ошибка: borrow of moved value: `a`
println!("{b}");

Так Rust исключает двойное освобождение: у буфера строки всегда один хозяин, и освободит его только он.

Простые типы фиксированного размера — числа, bool, char, кортежи из таких же типов — реализуют типаж Copy и при присваивании копируются побитово. С ними исходная переменная остаётся рабочей. Если нужна настоящая копия строки или вектора, её делают явно: a.clone().

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

Передавать владение туда-сюда неудобно, поэтому значением можно поделиться на время — отдать ссылку. Ссылка &T позволяет читать, ссылка &mut T — ещё и изменять.

fn length(s: &str) -> usize {
    s.chars().count()
}

fn shout(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut greeting = String::from("привет");
    let n = length(&greeting);   // заимствуем для чтения
    shout(&mut greeting);         // заимствуем для изменения
    println!("{greeting} ({n} букв)");
}

Главное правило заимствований: одновременно может существовать либо сколько угодно ссылок для чтения, либо ровно одна ссылка для изменения. Не то и другое сразу.

На практике это правило чаще всего напоминает о себе вот так:

let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);             // ошибка: v уже заимствован для чтения
println!("{first}");

Компилятор прав: push может перенести содержимое вектора в новый буфер, и тогда first указывала бы на освобождённую память. В C++ такой код компилируется и иногда даже работает. В Rust его достаточно переставить: сначала push, потом взять ссылку.

Времена жизни

Ссылка не должна пережить то, на что она указывает. Обычно компилятор выводит это сам, но если функция возвращает ссылку и получает несколько, ему нужна подсказка — параметр времени жизни.

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

Запись 'a ничего не продлевает и не создаёт. Она лишь сообщает: результат живёт не дольше, чем каждый из аргументов. Проверять это будет компилятор в месте вызова.

Что делать, когда компилятор не пускает