Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ссылки и заимствование

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

В этом примере вызов greet перемещает данные из m1 и m2 в параметры greet. Обе строки удаляются в конце greet, и поэтому не могут быть использованы в main. Если попытаться прочитать их, как в операции format!(..), это приведёт к неопределённому поведению. Компилятор Rust поэтому отклоняет эту программу с той же ошибкой, что и в предыдущем разделе:

error[E0382]: borrow of moved value: `m1`
 --> test.rs:5:30
 (...rest of the error...)

Такое поведение с перемещением крайне неудобно. Программы часто должны использовать строку более одного раза. Альтернативный greet мог бы возвращать владение строками, например так:

Однако такой стиль программирования довольно многословен. Rust предоставляет лаконичный стиль чтения и записи без перемещений через ссылки.

Ссылки — это указатели, не владеющие данными

Ссылка — это вид указателя. Вот пример ссылки, который переписывает нашу программу greet более удобным способом:

Выражение &m1 использует оператор амперсанда для создания ссылки на (или “заимствования”) m1. Тип параметра g1 в greet изменён на &String, что означает “ссылка на String”.

Обратите внимание, что на L2 есть два шага от g1 до строки “Hello”. g1 — это ссылка, указывающая на m1 в стеке, а m1 — это String, содержащая коробку, которая указывает на “Hello” в куче.

Хотя m1 владеет данными в куче “Hello”, g1 не владеет ни m1, ни “Hello”. Поэтому после завершения greet и достижения программы L3 никакие данные в куче не освобождаются. Исчезает только стековый фрейм для greet. Этот факт согласуется с нашим Принципом освобождения коробок. Поскольку g1 не владел “Hello”, Rust не освободил “Hello” от имени g1.

Ссылки — это невладеющие указатели, потому что они не владеют данными, на которые указывают.

Разыменование указателя даёт доступ к его данным

Предыдущие примеры с коробками и строками не показывали, как Rust “следует” за указателем к его данным. Например, макрос println! таинственным образом работал как для владеющих строк типа String, так и для ссылок на строки типа &String. Основной механизм — это оператор разыменования, обозначаемый звёздочкой (*). Например, вот программа, использующая разыменования несколькими способами:

Обратите внимание на разницу между r1, указывающим на x в стеке, и r2, указывающим на значение в куче 2.

Вы, вероятно, не увидите оператор разыменования очень часто при чтении кода Rust. Rust неявно вставляет разыменования и ссылки в определённых случаях, например при вызове метода с точечным оператором. Например, эта программа показывает два эквивалентных способа вызова функций i32::abs (абсолютное значение) и str::len (длина строки):

fn main()  {
let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x); // явное разыменование
let x_abs2 = x.abs();      // неявное разыменование
assert_eq!(x_abs1, x_abs2);

let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r); // явное разыменование (дважды)
let r_abs2 = r.abs();       // неявное разыменование (дважды)
assert_eq!(r_abs1, r_abs2);

let s = String::from("Hello");
let s_len1 = str::len(&s); // явная ссылка
let s_len2 = s.len();      // неявная ссылка
assert_eq!(s_len1, s_len2);
}

Этот пример показывает неявные преобразования тремя способами:

  1. Функция i32::abs ожидает на вход тип i32. Чтобы вызвать abs с Box<i32>, можно явно разыменовать коробку, как i32::abs(*x). Можно также неявно разыменовать коробку, используя синтаксис вызова метода, как x.abs(). Точечный синтаксис — это синтаксический сахар для синтаксиса вызова функции.

  2. Это неявное преобразование работает для нескольких слоёв указателей. Например, вызов abs на ссылке на коробку r: &Box<i32> вставит два разыменования.

  3. Это преобразование также работает в обратном направлении. Функция str::len ожидает ссылку &str. Если вызвать len на владеющей String, то Rust вставит один оператор заимствования. (На самом деле, есть дальнейшее преобразование из String в str!)

Мы подробнее расскажем о вызовах методов и неявных преобразованиях в следующих главах. Пока важно понять, что эти преобразования происходят при вызовах методов и некоторых макросах, таких как println. Мы хотим распутать всю “магию” Rust, чтобы у вас было чёткое понимание того, как работает Rust.

Rust избегает одновременного алиасинга и мутации

Указатели — мощная и опасная особенность, потому что они позволяют алиасинг. Алиасинг — это доступ к одним и тем же данным через разные переменные. Сам по себе алиасинг безвреден. Но в сочетании с мутацией это рецепт для катастрофы. Одна переменная может “выдернуть ковёр” из-под другой переменной многими способами, например:

  • Освободив алиасированные данные, оставив другую переменную указывать на освобождённую память.
  • Изменив алиасированные данные, нарушив свойства времени выполнения, ожидаемые другой переменной.
  • Одновременно изменяя алиасированные данные, вызывая гонку данных с недетерминированным поведением для другой переменной.

В качестве рабочего примера мы рассмотрим программы, использующие структуру данных вектор, Vec. В отличие от массивов фиксированной длины, векторы имеют переменную длину, храня элементы в куче. Например, Vec::push добавляет элемент в конец вектора, вот так:

Макрос vec! создаёт вектор с элементами между скобками. Вектор v имеет тип Vec<i32>. Синтаксис <i32> означает, что элементы вектора имеют тип i32.

Одна важная деталь реализации: v выделяет массив в куче определённой ёмкости. Мы можем заглянуть во внутренности Vec и увидеть эту деталь:

Примечание: нажмите на значок бинокля в правом верхнем углу диаграммы, чтобы переключить этот детальный вид в любой диаграмме времени выполнения.

Обратите внимание, что вектор имеет длину (len) 3 и ёмкость (cap) 3. Вектор заполнен до ёмкости. Поэтому при push вектор должен создать новое выделение с большей ёмкостью, скопировать все элементы и освободить исходный массив в куче. На диаграмме выше массив 1 2 3 4 находится в (потенциально) другом месте памяти, чем исходный массив 1 2 3.

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

Изначально v указывает на массив с 3 элементами в куче. Затем num создаётся как ссылка на третий элемент, как видно на L1. Однако операция v.push(4) изменяет размер v. Изменение размера освободит предыдущий массив и выделит новый, больший массив. В процессе num остаётся указывать на недействительную память. Поэтому на L3 разыменование *num читает недействительную память, вызывая неопределённое поведение.

В более абстрактных терминах проблема в том, что вектор v одновременно алиасирован (ссылкой num) и мутируется (операцией v.push(4)). Поэтому чтобы избежать подобных проблем, Rust следует базовому принципу:

Принцип безопасности указателей: данные никогда не должны быть алиасированы и мутированы одновременно.

Данные могут быть алиасированы. Данные могут быть мутированы. Но данные не могут быть одновременно алиасированы и мутированы. Например, Rust обеспечивает этот принцип для коробок (владеющих указателей), запрещая алиасинг. Присвоение коробки от одной переменной другой приведёт к перемещению владения, инвалидируя предыдущую переменную. Владеющие данные могут быть доступны только через владельца — без алиасов.

Однако, поскольку ссылки — это указатели, не владеющие данными, им нужны другие правила, чем у коробок, для обеспечения Принципа безопасности указателей. По замыслу ссылки предназначены для временного создания алиасов. В этом разделе мы объясним основы того, как Rust обеспечивает безопасность ссылок через проверку заимствований.

Ссылки изменяют разрешения на места

Основная идея проверки заимствований в том, что переменные имеют три вида разрешений на свои данные:

  • Чтение (R): данные могут быть скопированы в другое место.
  • Запись (W): данные могут быть изменены.
  • Владение (O): данные могут быть перемещены или удалены.

Эти разрешения не существуют во время выполнения, только внутри компилятора. Они описывают, как компилятор “думает” о вашей программе до её выполнения.

По умолчанию переменная имеет разрешения чтение/владение (RO) на свои данные. Если переменная аннотирована let mut, то она также имеет разрешение запись (W). Ключевая идея в том, что ссылки могут временно снимать эти разрешения.

Чтобы проиллюстрировать эту идею, давайте посмотрим на разрешения в вариации программы выше, которая на самом деле безопасна. push был перемещён после println!. Разрешения в этой программе визуализированы новым видом диаграммы. Диаграмма показывает изменения разрешений на каждой строке.

Давайте пройдёмся по каждой строке:

  1. После let mut v = (...), переменная v была инициализирована (обозначено ). Она получает разрешения +R+W+O (знак плюс указывает на получение).
  2. После let num = &v[2], данные в v были заимствованы num (обозначено ). Происходят три вещи:
    • Заимствование снимает разрешения
      W
      O
      с v (косая черта указывает на потерю). v не может быть записан или владеем, но всё ещё может быть прочитан.
    • Переменная num получила разрешения RO. num не является изменяемым (отсутствующее разрешение W показано как тире ), потому что оно не было отмечено let mut.
    • Место *num получило разрешение R.
  3. После println!(...), num больше не используется, поэтому v больше не заимствован. Следовательно:
    • v восстанавливает свои разрешения WO (обозначено ).
    • num и *num потеряли все свои разрешения (обозначено ).
  4. После v.push(4), v больше не используется, и он теряет все свои разрешения.

Далее рассмотрим несколько нюансов диаграммы. Во-первых, почему вы видите и num, и *num? Потому что доступ к данным через ссылку — это не то же самое, что манипулирование самой ссылкой. Например, допустим, мы объявили ссылку на число с let mut:

Обратите внимание, что x_ref имеет разрешение W, в то время как *x_ref — нет. Это означает, что мы можем присвоить другую ссылку переменной x_ref (например, x_ref = &y), но не можем изменить данные, на которые она указывает (например, *x_ref += 1).

В более общем смысле, разрешения определены на местах, а не просто на переменных. Место — это всё, что можно поместить в левую часть присваивания. Места включают:

  • Переменные, такие как a.
  • Разыменования мест, такие как *a.
  • Доступы к массиву мест, такие как a[0].
  • Поля мест, такие как a.0 для кортежей или a.field для структур (обсуждается в следующей главе).
  • Любые комбинации вышеперечисленного, такие как *((*a)[0].1).

Во-вторых, почему места теряют разрешения, когда они становятся неиспользуемыми? Потому что некоторые разрешения взаимно исключают друг друга. Если вы пишете num = &v[2], то v не может быть изменён или удалён, пока num используется. Но это не значит, что использовать num снова недействительно. Например, если мы добавим ещё один println! в вышеприведённую программу, то num просто потеряет свои разрешения на строку позже:

Проблема возникает только при попытке использовать num снова после изменения v. Давайте рассмотрим это подробнее.

Проверка заимствований находит нарушения разрешений

Вспомним Принцип безопасности указателей: данные не должны быть алиасированы и мутированы. Цель этих разрешений — гарантировать, что данные не могут быть мутированы, если они алиасированы. Создание ссылки на данные (“заимствование”) приводит к тому, что эти данные временно становятся доступны только для чтения, пока ссылка не перестаёт использоваться.

Rust использует эти разрешения в своей проверке заимствований. Проверка заимствований ищет потенциально небезопасные операции со ссылками. Вернёмся к небезопасной программе, которую мы видели ранее, где push инвалидирует ссылку. На этот раз добавим ещё один аспект к диаграмме разрешений:

Каждый раз, когда место используется, Rust ожидает, что у места будут определённые разрешения в зависимости от операции. Например, заимствование &v[2] требует, чтобы v было читаемым. Поэтому разрешение R показано между операцией & и местом v. Буква заполнена, потому что v имеет разрешение чтения на этой строке.

Напротив, мутирующая операция v.push(4) требует, чтобы v было читаемым и изменяемым. Оба разрешения R и W показаны. Однако v не имеет разрешения записи (оно заимствовано num). Поэтому буква W полая, указывая, что разрешение записи ожидается, но v его не имеет.

Если вы попытаетесь скомпилировать эту программу, компилятор Rust вернёт следующую ошибку:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> test.rs:4:1
  |
3 | let num: &i32 = &v[2];
  |                  - immutable borrow occurs here
4 | v.push(4);
  | ^^^^^^^^^ mutable borrow occurs here
5 | println!("Third element is {}", *num);
  |                                 ---- immutable borrow later used here

Сообщение об ошибке объясняет, что v не может быть изменено, пока ссылка num используется. Это поверхностная причина — основная проблема в том, что num может быть инвалидировано push. Rust ловит это потенциальное нарушение безопасности памяти.

Изменяемые ссылки предоставляют уникальный и невладеющий доступ к данным

Ссылки, которые мы видели до сих пор, — это доступные только для чтения неизменяемые ссылки (также называемые общими ссылками). Неизменяемые ссылки разрешают алиасинг, но запрещают мутацию. Однако также полезно временно предоставить изменяемый доступ к данным без их перемещения.

Механизм для этого — изменяемые ссылки (также называемые уникальными ссылками). Вот простой пример изменяемой ссылки с accompanying изменениями разрешений:

Примечание: когда ожидаемые разрешения не строго релевантны примеру, мы будем сокращать их точками, как
R
W
. Вы можете навести курсор на кружки (или коснуться на сенсорном экране), чтобы увидеть соответствующие буквы разрешений.

Изменяемая ссылка создаётся оператором &mut. Тип num записывается как &mut i32. По сравнению с неизменяемыми ссылками, вы можете увидеть две важные разницы в разрешениях:

  1. Когда num была неизменяемой ссылкой, v всё ещё имел разрешение R. Теперь, когда num — изменяемая ссылка, v потерял все разрешения, пока num используется.
  2. Когда num была неизменяемой ссылкой, место *num имело только разрешение R. Теперь, когда num — изменяемая ссылка, *num также получил разрешение W.

Первое наблюдение делает изменяемые ссылки безопасными. Изменяемые ссылки позволяют мутацию, но предотвращают алиасинг. Заимствованное место v временно становится непригодным для использования, поэтому эффективно не является алиасом.

Второе наблюдение делает изменяемые ссылки полезными. v[2] может быть изменён через *num. Например, *num += 1 изменяет v[2]. Обратите внимание, что *num имеет разрешение W, но num — нет. num ссылается на саму изменяемую ссылку, например num не может быть переназначен на другую изменяемую ссылку.

Изменяемые ссылки также могут быть временно “понижены” до ссылок, доступных только для чтения. Например:

Примечание: когда изменения разрешений не релевантны примеру, мы будем их скрывать. Вы можете просмотреть скрытые шаги, нажав “»”, и вы можете просмотреть скрытые разрешения в рамках шага, нажав “● ● ●”.

В этой программе заимствование &*num снимает разрешение W с *num, но не разрешение R, поэтому println!(..) может читать и *num, и *num2.

Разрешения возвращаются в конце времени жизни ссылки

Мы сказали выше, что ссылка изменяет разрешения, пока она “используется”. Фраза “используется” описывает время жизни ссылки, или диапазон кода от её рождения (где ссылка создаётся) до её смерти (последний раз(ы), когда ссылка используется).

Например, в этой программе время жизни y начинается с let y = &x и заканчивается с let z = *y:

Разрешение W на x возвращается x после окончания времени жизни y, как мы видели ранее.

В предыдущих примерах время жизни было непрерывной областью кода. Однако, когда мы вводим управляющие конструкции, это не обязательно так. Например, вот функция, которая делает первую букву в векторе символов ASCII заглавной:

Переменная c имеет разное время жизни в каждой ветви if-выражения. В then-блоке c используется в выражении c.to_ascii_uppercase(). Поэтому *v не восстанавливает разрешение W до после этой строки.

Однако в else-блоке c не используется. *v немедленно восстанавливает разрешение W при входе в else-блок.

Данные должны переживать все свои ссылки

Как часть Принципа безопасности указателей, проверка заимствований обеспечивает, чтобы данные переживали любые ссылки на них. Rust обеспечивает это свойство двумя способами. Первый способ имеет дело со ссылками, которые создаются и удаляются в пределах одной функции. Например, допустим, мы попытались удалить строку, держа ссылку на неё:

Чтобы поймать такие ошибки, Rust использует разрешения, которые мы уже обсуждали. Заимствование &s снимает разрешение O с s. Однако drop ожидает разрешение O, что приводит к несоответствию разрешений.

Ключевая идея в том, что в этом примере Rust знает, как долго живёт s_ref. Но Rust нужен другой механизм обеспечения, когда он не знает, как долго живёт ссылка. А именно, когда ссылки либо являются входом в функцию, либо выходом из функции. Например, вот безопасная функция, возвращающая ссылку на первый элемент в векторе:

Этот фрагмент вводит новый вид разрешения, разрешение потока F. Разрешение F ожидается всякий раз, когда выражение использует входную ссылку (как &strings[0]) или возвращает выходную ссылку (как return s_ref).

В отличие от разрешений RWO, F не меняется на протяжении тела функции. Ссылка имеет разрешение F, если ей разрешено использоваться (то есть протекать) в конкретном выражении. Например, давайте изменим first на новую функцию first_or, которая включает параметр default:

Эта функция больше не компилируется, потому что выражения &strings[0] и default не имеют необходимого разрешения F для возврата. Но почему? Rust даёт следующую ошибку:

error[E0106]: missing lifetime specifier
 --> test.rs:1:57
  |
1 | fn first_or(strings: &Vec<String>, default: &String) -> &String {
  |                      ------------           -------     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `strings` or `default`

Сообщение “missing lifetime specifier” немного загадочно, но справочное сообщение даёт некоторый полезный контекст. Если Rust просто посмотрит на сигнатуру функции, он не знает, является ли выход &String ссылкой на strings или default. Чтобы понять, почему это важно, давайте скажем, что мы использовали first_or так:

fn main() {
    let strings = vec![];
    let default = String::from("default");
    let s = first_or(&strings, &default);
    drop(default);
    println!("{}", s);
}

Эта программа небезопасна, если first_or позволяет default протекать в возвращаемое значение. Как и в предыдущем примере, drop может инвалидировать s. Rust позволит компилировать эту программу только если он уверен, что default не может протекать в возвращаемое значение.

Чтобы указать, может ли default быть возвращён, Rust предоставляет механизм, называемый параметрами времени жизни. Мы объясним эту особенность позже в Главе 10.3, “Проверка ссылок с помощью времени жизни”. Пока достаточно знать, что: (1) входные/выходные ссылки обрабатываются иначе, чем ссылки внутри тела функции, и (2) Rust использует другой механизм, разрешение F, для проверки безопасности этих ссылок.

Чтобы увидеть разрешение F в другом контексте, скажем, вы попытались вернуть ссылку на переменную в стеке, вот так:

Эта программа небезопасна, потому что ссылка &s будет инвалидирована при возврате return_a_string. И Rust отклонит эту программу с похожей ошибкой “missing lifetime specifier”. Теперь вы можете понять, что эта ошибка означает, что s_ref отсутствует соответствующее разрешение потока.

Резюме

Ссылки предоставляют возможность читать и записывать данные без потребления владения ими. Ссылки создаются заимствованиями (& и &mut) и используются с разыменованиями (*), часто неявно.

Однако ссылки могут быть легко использованы неправильно. Проверка заимствований Rust обеспечивает систему разрешений, которая гарантирует безопасное использование ссылок:

  • Все переменные могут читать, владеть и (опционально) записывать свои данные.
  • Создание ссылки передаст разрешения из заимствованного места ссылке.
  • Разрешения возвращаются, как только время жизни ссылки заканчивается.
  • Данные должны переживать все ссылки, которые на них указывают.

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