Ссылки и заимствование
Эта глава является одной из трёх, описывающих систему владения ресурсами Rust. Эта система представляет собой наиболее уникальную и привлекательную особенность Rust, о которой разработчики должны иметь полное представление. Владение — это то, как Rust достигает своей главной цели — безопасности памяти. Система владения включает в себя несколько различных концепций, каждая из которых рассматривается в своей собственной главе:
- владение, ключевая концепция
- заимствование, её вы читаете сейчас
- время жизни, расширение понятия заимствования
Эти три главы взаимосвязаны, и их порядок важен. Вы должны будете освоить все три главы, чтобы полностью понять систему владения.
Мета
Прежде чем перейти к подробностям, отметим два важных момента в системе владения.
Rust сфокусирован на безопасности и скорости. Это достигается за счёт «абстракций с нулевой стоимостью» (zero-cost abstractions). Это значит, что в Rust стоимость абстракций должна быть настолько малой, насколько это возможно без ущерба для работоспособности. Система владения ресурсами — это яркий пример абстракции с нулевой стоимостью. Весь анализ, о котором мы будем говорить в этом руководстве, выполняется во время компиляции. Во время исполнения вы не платите за какую-либо из возможностей ничего.
Тем не менее, эта система всё же имеет определённую стоимость: кривая обучения. Многие новые пользователи Rust «борются с проверкой заимствования» — компилятор Rust отказывается компилировать программу, которая по мнению автора является абсолютно правильной. Это часто происходит потому, что мысленное представление программиста о том, как должно работать владение, не совпадает с реальными правилами, которыми оперирует Rust. Вы, наверное, поначалу также будете испытывать подобные трудности. Однако существует и хорошая новость: более опытные разработчики на Rust говорят, что чем больше они работают с правилами системы владения, тем меньше они борются с компилятором.
Имея это в виду, давайте перейдём к изучению системы владения.
Заимствование
В конце главы Владение у нас была убогая функция, которая выглядела так:
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
Однако, этот код не является идиоматичным с точки зрения Rust, так как он не использует заимствование. Вот первый шаг:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
// return the answer
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
// we can use v1 and v2 here!
Вместо того, чтобы принимать Vec<i32>
в качестве аргументов, мы будем
принимать ссылки: &Vec<i32>
. И вместо передачи v1
и v2
напрямую, мы будем
передавать &v1
и &v2
. Мы называем тип &T
«ссылка», и вместо того, чтобы
забирать владение ресурсом, она его заимствует. Имена, которые заимствуют что-
то, не освобождают ресурс, когда они выходят из области видимости. Это означает,
что, после вызова foo()
, мы снова можем использовать наши исходные имена.
Ссылки являются неизменяемыми, как и имена. Это означает, что внутри foo()
векторы не могут быть изменены:
fn foo(v: &Vec<i32>) {
v.push(5);
}
let v = vec![];
foo(&v);
выдаёт ошибку:
error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^
Добавление значения изменяет вектор, и поэтому компилятор не позволил нам это сделать.
Ссылки &mut
Вот второй вид ссылок: &mut T
. Это «изменяемая ссылка», которая позволяет
изменять ресурс, который вы заимствуете. Например:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
Этот код напечатает 6
. Мы создали y
, изменяемую ссылку на x
, а затем
добавили единицу к значению, на которое указывает y
. Следует отметить, что x
также должно быть помечено как mut
. Если бы этого не было, то мы не могли бы
получить изменяемую ссылку неизменяемого значения.
Во всем остальном изменяемые ссылки (&mut
) такие же, как и неизменяемые (&
).
Однако, существует большая разница между этими двумя концепциями, и тем, как они
взаимодействуют. Вы можете сказать, что в приведённом выше примере есть что-то
подозрительное, потому что нам зачем-то понадобилась дополнительная область
видимости, созданная с помощью {
и }
. Если мы уберем эти скобки, то получим
ошибку:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
^
note: previous borrow ends here
fn main() {
}
^
Оказывается, есть определённые правила создания ссылок.
Правила
Вот правила заимствования в Rust.
Во-первых, область видимости любой ссылки должна находиться в пределах области видимости владельца. Во-вторых, одновременно у вас может быть только один из двух перечисленных ниже видов заимствования, но не оба сразу:
- одна или более неизменяемых ссылок (
&T
) на ресурс; - ровно одна изменяемая ссылка (
&mut T
) на ресурс.
Вы можете заметить, что это похоже, хотя и не соответствует точно, определению состояния гонки данных:
Состояние «гонки данных» возникает, когда два или более указателей осуществляют доступ к одной и той же области памяти одновременно, по крайней мере один из них производит запись, и операции не синхронизированы.
Что касается неизменяемых ссылок, то вы можете иметь их столько, сколько хотите,
так как ни одна из них не производит запись. Если же вы производите запись, и
вам нужно два или больше указателей на одну и ту же область памяти, то вы можете
иметь только одну &mut
одновременно. Так Rust предотвращает возникновение
состояния гонки данных во время компиляции: мы получим ошибку компиляции, если
нарушим эти правила.
Имея это в виду, давайте рассмотрим наш пример еще раз.
Осмысливаем области видимости (Thinking in scopes)
Вот код:
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
Этот код выдает нам такую ошибку:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
Это потому, что мы нарушили правила: у нас есть изменяемая ссылка &mut T
,
указывающая на x
, и поэтому мы не можем создать какую-либо &T
. Одно из двух.
Примечание подсказывает как следует рассматривать эту проблему:
note: previous borrow ends here
fn main() {
}
^
Другими словами, изменяемая ссылка сохраняется до конца нашего примера. А мы
хотим, чтобы изменяемое заимствование заканчивалось до того, как мы пытаемся
вызвать println!
и создать неизменяемое заимствование. В Rust заимствование
привязано к области видимости, в которой оно является действительным. И эти
области видимости выглядят следующим образом:
let mut x = 5;
let y = &mut x; // -+ заимствование x через &mut начинается здесь
// |
*y += 1; // |
// |
println!("{}", x); // -+ - пытаемся позаимствовать x здесь
// -+ заимствование x через &mut заканчивается здесь
Конфликт областей видимости: мы не можем создать &x
до тех пор, пока y
находится в области видимости.
Поэтому, когда мы добавляем фигурные скобки:
let mut x = 5;
{
let y = &mut x; // -+ заимствование через &mut начинается здесь
*y += 1; // |
} // -+ ... и заканчивается здесь
println!("{}", x); // <- пытаемся позаимствовать x здесь
Никаких проблем нет. Наша изменяемая ссылка выходит из области видимости до создания неизменяемой. Но область видимости является ключом к определению того, как долго длится заимствование.
Проблемы, которые предотвращает заимствование
Почему нужны эти ограничивающие правила? Ну, как мы уже отметили, эти правила предотвращают гонки данных. Какие виды проблем могут привести к состоянию гонки данных? Вот некоторые из них.
Недействительный итератор
Одним из примеров является «недействительный итератор». Такое может произойти, когда вы пытаетесь изменить коллекцию, которую в данный момент обходите. Проверка заимствования Rust предотвращает это:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
Этот код печатает числа от одного до трёх. Когда мы обходим вектор, мы получаем
лишь ссылки на элементы. И сам v
заимствован как неизменяемый, что означает,
что мы не можем изменить его в процессе обхода:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
v.push(34);
}
Вот ошибка:
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^
Мы не можем изменить v
, потому что он уже заимствован в цикле.
Использование после освобождения (use after free)
Ссылки должны жить так же долго, как и ресурс, на который они ссылаются. Rust проверяет области видимости ваших ссылок, чтобы удостовериться, что это правда.
Если Rust не будет проверять это свойство, то мы можем случайно использовать ссылку, которая будет недействительна. Например:
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
Мы получим следующую ошибку:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
Другими словами, y
действителен только для той области видимости, где
существует x
. Как только x
выходит из области видимости, ссылка на него
становится недействительной. Таким образом, ошибка сообщает, что заимствование
«не живет достаточно долго» («does not live long enough»), потому что оно не
является действительным столько времени, сколько требуется.
Такая же проблема возникает, когда ссылка объявлена перед значением, на которое она ссылается. Это происходит потому что ресурсы в одном блоке освобождаются в порядке, противоположном порядку их объявления:
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
Мы получим такую ошибку:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}
В примере выше y
объявлена перед x
, т.е. живёт дольше x
, а это запрещено.