Владение и заимствование
Система владения — то, чем Rust отличается от всех остальных распространённых языков. Она заменяет и сборщик мусора, и ручные malloc с free.
Три правила
- У каждого значения есть владелец — переменная, которая за него отвечает.
- Владелец в каждый момент времени ровно один.
- Когда владелец выходит из области видимости, значение уничтожается, а его память освобождается.
Всё остальное — следствия. Компилятор проверяет эти правила при сборке, поэтому в готовой программе нет ни счётчиков, ни пауз на сборку мусора.
Перемещение
Присваивание и передача в функцию по умолчанию перемещают значение: прежняя переменная перестаёт быть владельцем, и пользоваться ею больше нельзя.
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 ничего не продлевает и не создаёт. Она лишь сообщает: результат живёт не дольше, чем каждый из аргументов. Проверять это будет компилятор в месте вызова.
Что делать, когда компилятор не пускает
- Сократите жизнь ссылки: возьмите её позже или заключите работу с ней в отдельный блок.
- Передавайте в функции срезы
&strи&[T], а не&Stringи&Vec<T>— так код гибче. - Не бойтесь
clone(), пока учитесь. Лишнюю копию легко убрать потом, когда программа уже работает. - Если данным действительно нужно несколько владельцев, для этого есть
Rc<T>, а в многопоточном коде —Arc<T>.