Найти тему
Ржавый код

Время жизни и память

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

Типы и перемещения

-2

Память приложений:

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

-- стек (небольшая память с низким уровнем служебных данных, большая часть переменных находится здесь).

-- куча (большая гибкая память, но всегда обрабатывается через прокси стека, например, Box<T>).

-- статическая память (чаще всего используется в качестве места расположения частей &str).

-- код (где находится бит-код ваших функций).

  • Самая сложная часть связана с тем, как работает стек, в текущий момент.
-3

Переменные:

let t = S(1);

  • Резервирует местоположение памяти с именем 't' типа 'S' и значением S(1) хранящимся внутри.
  • Если объявлено с let, то местоположение находится в стеке.
  • Обратите внимание на лингвистическую неоднозначность, в термине переменная, может означать:

-- имя местоположения в исходном файле («именовать эту переменную»).

-- расположение в скомпилированном приложении, 0x7 («сообщить адрес этой переменной»).

-- значение содержащееся в S(1) («заданное значение переменной»).

  • Конкретно у компилятора 't' может означать местоположение 't', в картинке выше 0x7 и значение в 't' S(1).
-4

Перемещение:

  • Это приведет к перемещению значения в пределах 't', перемещению или копированию данных, если 'S' реализована как копируемая.
  • После перемещения расположение 't' является недопустимым и больше не может быть прочитано:

-- биты в этом месте не пусты, но не определены.

-- если у вас все еще был доступ к 't' (через небезопасные ссылки), они могут выглядеть как допустимые 'S', но любая попытка использовать заканчивается неопределенным поведением.

  • Здесь расcмативаем типы явного копирования. Они немного меняют правила, но не сильно:

-- они не сбросят значение.

-- они никогда не оставляют после себя «пустую» переменную.

-5

Безопасность типов:

let c: S = M::new();

  • Тип переменной служит нескольким важным целям:

-- определяет, как следует интерпретировать базовые биты.

-- допускает только четко определенные операции с этими битами.

-- предотвращает запись в это местоположение других случайных значений или битов.

  • Здесь не удаcться скомпилировать значение, так как байты M::new() не могут быть преобразованы в форму типа 'S'.
  • Преобразования между типами всегда будет неправильным, если только явное правило не разрешает это (принуждение, приведение,...).
-6

{
let mut c = S(2);
c = S(3); // <- Удаление `c` перед назначением.
let t = S(1);
let a = t;
} // <- Область действия `a`, `t`, `c` заканчивается здесь и вызывается удаление для `a`, `c`.

  • Как только «имя» неиспользуемой переменной выходит из области, содержащееся значение удаляется.

-- выполнение достигает места, где имя переменной покидает {}-блок.

-- в деталях, это более хитрей.

  • Удаление также вызывается при назначении нового значения существующей переменной.
  • В этом случае для расположения этого значения вызывается Drop::drop().

-- в примере выше drop() вызывается дважды у 'c', но не у 't' (один раз).

  • Большинство значений, не являющихся копиями, удаляются, исключения mem::forget(), Rc-циклы, abort().

Стек вызовов

-7

Границы функций:

fn f(x: S) { … }
let a = S(1); // <- Мы здесь
f(a);

  • При вызове функции память для параметров (и возвращаемых значений) резервируется в стеке.
  • Здесь перед вызовом 'f' значение в a перемещается в стек, и во время работы функции 'f' локальная переменная в ней будет 'x'.
-8

Вложенные функции:

fn f(x: S) {
if once() { f(x) } // <- Мы здесь (перед рекурсией)
}

let a = S(1);
f(a);

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

Перепрофилирование памяти:

fn f(x: S) {
if once() { f(x) }
let m = M::new() // <- We are here (after recursion)
}

let a = S(1);
f(a);

  • Стек, ранее содержавший определенный тип, будет перепрофилирован под функции (даже внутри).
  • Здесь рекурсивность на 'f' 'дала вторую 'х', которая после рекурсии была частично повторно использована для 'm'.

Ссылки и указатели

-10

Ссылки в качестве указателей:

let a = S(1);
let r: &S = &a;

  • Ссылочный тип, такой как &S или &mut S, может содержать место нахождения каких нибудь 's'.
  • Здесь тип &S, связанный именем 'r', содержит местоположение переменной 'a' (0x3), которая должна быть типом 'S', полученной через &a.
  • Если переменная 'a' рассматривается как определенное местоположение, ссылка 'r' является коммутатором для местоположения.

let r: &S = &a;
let r = &a;

-11

Доступ к памяти, не принадлежащей владельцам:

let mut a = S(1);
let r = &mut a;
let d = r.clone(); // Допустимо для клонирования (или копирования) из r.
*r = S(2); // Допустимо установить новое значение S в r.

  • Ссылки могут считываться из (&S), а также записываться в (&mut S), адрес на которое они указывают.
  • Разыменование - *r означает не использовать адрес расположения значения, а использовать само значение по адресу 'r'.
  • В приведенном выше примере клон d создается из *r, а также S(2) записывается в *r.

-- метод Clone::clone(& T) ожидает ссылку, поэтому мы можем использовать r, а не *r.

-- при назначении *r =... старое значение в адресе будет отброшено (не показано выше).

-12

Ссылки на защищенные ссылки:

let mut a = …;
let r = &mut a;
let d = *r; // недопустимое значение для перемещения, «a» будет пустым.
*r = M::new(); // недопустимо для хранения значение, отличного от S, не имеет смысла.

  • В то время как привязки гарантируют всегда хранить допустимые данные, ссылки гарантируют всегда указывать на допустимые данные.
  • &mut T должен предоставить те же гарантии, что и переменные, поскольку они не могут взять и уничтожить значение:

-- Они не разрешают запись недопустимых данных.

-- Они не позволяют перемещать данные (оставят переменную пустой без информации владельца).

-13

Необработанные указатели:

let p: *const S = questionable_origin();

  • В отличие от ссылок, указатели почти не имеют гарантий.
  • Они могут указывать на недопустимые или несуществующие данные.
  • Их разыменование небезопасно, и обращение к недопустимым *p, как если бы оно было допустимым, будет с неопределенным поведением.

Основы жизненного цикла

-14

«Жизнь» событий:

  • Каждая сущность в программе имеет некоторую (временную/пространственное) место нахождение, где она актуальна, т.е. жива.
  • Мягко говоря, это время жизни может быть:

-- залочено (строки кода), где доступен элемент (например, имя модуля).

-- залочено между моментом, когда местоположение инициализировано значением, и моментом, когда местоположение сброшено.
-- залочено между тем, когда местоположение впервые используется определенным образом, и тем, когда это использование прекращается.
-- залочено (или фактическое время) между моментом создания значения и моментом отбрасывания этого значения.

  • В остальной части этого раздела мы будем ссылаться на вышеперечисленные пункты:

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

  • Аналогично, параметры времени жизни в коде, например, r: &'a S, являются:

-- связанные с временем жизни любое местоположение 'r' указывает на необходимость быть доступным или заблокированным.
-- не связано с «временем существования» (как время жизни) самого 'r' (ну, он должен существовать короче, вот и все).

-15

Значение r: &'c S:

  • Предположим, что у вас есть r: &'c S, это означает:

-- 'r' содержит адрес некоторых 'S'.
-- любой адрес 'r' указывает как должен и будет существовать по крайней мере для 'c.
-- сама переменная 'r' не может работать дольше 'c.

-16

Типичное времени жизни:

{
let b = S(3);
{
let c = S(2);
let r: &'c S = &c; // не совсем работает, так как мы не можем назвать локальное время жизни
{ // переменные в теле функции, но применяется тот же принцип
let a = S(0); // для выполнения функций.

r = &a; // расположение 'a' не имеет достаточного количества строк жизни - > не ok.
r = &b; // расположение 'b' содержит все строки жизни 'c' и более - > ok.
}
}
}

Предположим, что вы получили mut r: & mut 'c S откуда-то:

-- то есть изменяемое местоположение, которое может содержать изменяемую ссылку.

  • Как уже упоминалось, эта ссылка должна защищать целевую память.
  • Однако часть 'c, как и тип, также охраняет то, что разрешено в 'r'.
  • Здесь назначение &b (0x6) для 'r' является допустимым, но &a (0x3) не допустимо, так как только &b живет равно или длиннее, чем &c.
-17

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

let mut b = S(0);
let r = &mut b;

b = S(4); // Потерпит неудачу, так как b в заимствованном состоянии.

print_byte(r);

  • После получения адреса переменной через &b или &mut b переменная помечается как заимствованная.
  • При заимствовании содержимое адреса больше не может быть изменено посредством исходной привязки 'b'.
  • Как только адрес, взятый через &b или &mut b, перестает использоваться (как залоченая), оригинальная привязка 'b' снова работает.

Время жизни в функциях

-18

Параметры функции:

fn f(x: &S, y:&S) -> &u8 { … }

let b = S(1);
let c = S(2);

let r = f(&b, &c);

  • При вызове функций, которые принимают и возвращают ссылки, происходит две интересные вещи:

-- используемые локальные переменные помещаются в заимствованное состояние.
-- но во время компиляции неизвестно, какой адрес будет возвращен.

-19

Проблема «заимствованного» распространения:

let b = S(1);
let c = S(2);

let r = f(&b, &c);

let a = b; // Мы можем это сделать?
let a = c; // Какой из них действительно заимствован?

print_byte(r);

  • Поскольку f может возвращать только один адрес, не во всех случаях 'b' и 'c' должны оставаться заблокированными.
  • Во многих случаях мы можем добиться улучшения качества времени жизни:

-- Примечательно, что если известно, что один параметр больше не может использоваться в возвращаемом значении.

-20

Время жизни распространяет заимствованное состояние:

fn f<'b, 'c>(x: &'b S, y: &'c S) -> &'c u8 { … }

let b = S(1);
let c = S(2);

let r = f(&b, &c); // Мы знаем, что возвращенная ссылка основана на c, которая должна оставаться заблокированной,
// в то время как b может свободно перемещаться.

let a = b;

print_byte(r);

  • Параметры времени жизни в сигнатурах, как 'c выше, решают эту проблему.
  • Их основная цель заключается в следующем:

-- за пределами функции, чтобы объяснить, на основе какого входного адреса может быть сгенерирован выходной адрес.
-- внутри функции, чтобы гарантировать только адреса, которые живут, по крайней мере 'c.

  • Фактические времена жизни 'b, 'c прозрачно выбираются компилятором в месте вызова на основе заимствованных переменных, которые дал разработчик.
  • Области не равны (которые были бы залочены от инициализации до уничтожения) 'b' или 'c', 'а' лишь минимальное подмножество их области, называемое временем жизни, то есть залоченно к минимальному набору, основанному на том, как долго необходимо заимствовать 'b' и 'c' для выполнения этого вызова и использования полученного результата.
  • В некоторых случаях, например, если у 'f' было 'c и 'b, мы все еще не могли их различить, и оба должны были оставаться заблокированными.
-21

Разблокирование:

  • Местоположение переменной снова разблокируется после окончания последнего использования любой ссылки, которая может указывать на нее.

Продвинутый

-22

Ссылки на ссылки:

// Возвращает ближнюю ('b) ссылку.
fn f1sr<'b, 'a>(rb: &'b &'a S) -> &'b S { *rb }
fn f2sr<'b, 'a>(rb: &'b &'a mut S) -> &'b S { *rb }
fn f3sr<'b, 'a>(rb: &'b mut &'a S) -> &'b S { *rb }
fn f4sr<'b, 'a>(rb: &'b mut &'a mut S) -> &'b S { *rb }

// Возвращает ближнюю ('b) изменяемую ссылку.
// f1sm<'b, 'a>(rb: &'b &'a S) -> &'b mut S { *rb } // M
// f2sm<'b, 'a>(rb: &'b &'a mut S) -> &'b mut S { *rb } // M
// f3sm<'b, 'a>(rb: &'b mut &'a S) -> &'b mut S { *rb } // M
fn f4sm<'b, 'a>(rb: &'b mut &'a mut S) -> &'b mut S { *rb }

// Возвращает дальнюю ('a) ссылку.
fn f1lr<'b, 'a>(rb: &'b &'a S) -> &'a S { *rb }
// f2lr<'b, 'a>(rb: &'b &'a mut S) -> &'a S { *rb } // L
fn f3lr<'b, 'a>(rb: &'b mut &'a S) -> &'a S { *rb }
// f4lr<'b, 'a>(rb: &'b mut &'a mut S) -> &'a S { *rb } // L

// Возвращает дальнюю ('a) изменяемую ссылку.
// f1lm<'b, 'a>(rb: &'b &'a S) -> &'a mut S { *rb } // M
// f2lm<'b, 'a>(rb: &'b &'a mut S) -> &'a mut S { *rb } // M
// f3lm<'b, 'a>(rb: &'b mut &'a S) -> &'a mut S { *rb } // M
// f4lm<'b, 'a>(rb: &'b mut &'a mut S) -> &'a mut S { *rb } // L

// Теперь предположим, что у нас где-то есть `ra`.
let mut ra: &'a mut S = …;

let rval = f1sr(&&*ra); // Нормально
let rval = f2sr(&&mut *ra);
let rval = f3sr(&mut &*ra);
let rval = f4sr(&mut ra);

// rval = f1sm(&&*ra); // Было бы плохо, так как 'rval' будет изменяемой
// rval = f2sm(&&mut *ra); // ссылкой, полученной из нарушаемой мутабельной
// rval = f3sm(&mut &*ra); // цепочки.
let rval = f4sm(&mut ra);

let rval = f1lr(&&*ra);
// rval = f2lr(&&mut *ra); // Если бы это сработало, у нас были бы 'rval' и 'ra' …
let rval = f3lr(&mut &*ra);
// rval = f4lr(&mut ra); // … теперь (mut) псевдоним 'S' в вычислении ниже.

// rval = f1lm(&&*ra); // То же, что и выше, не удается по причинам мутабельной цепочки.
// rval = f2lm(&&mut *ra); // "
// rval = f3lm(&mut &*ra); // "
// rval = f4lm(&mut ra); // То же, что и выше, не удается из-за наложения псевдонимов.

// Какое-то вымышленное место, где мы используем 'ra' и 'rval', обе действующие.
compute(ra, rval);

Здесь (M) означает сбой компиляции из-за ошибки мутабельности, (L) ошибки времени жизни. Кроме того, разыменовывание *rb не является строго необходимым, просто добавлен для ясности.

  • f_sr все случаи всегда работают, всегда может быть получена краткая ссылка (время жизни 'b).
  • f_sm в некоторых случаях получает ошибку просто потому, что изменяемая цепочка к 'S' обязана была вернуть &mut S.
  • f_lr в некоторых случаях может завершиться ошибкой, поскольку возврат &'a S из &'mut S означает, что теперь будут существовать две ссылки (одна изменяемая) на одну и ту же S, что является недопустимым.
  • f_lm во всех случаях всегда терпит неудачу по совокупности причин выше.
-23

Удаление и _:

{
let f = |x, y| (S(x), S(y)); // Функция, возвращающая два «Droppables».

let ( x1, y) = f(1, 4); // S(1) - EoS S(4) - EoS
let ( x2, _) = f(2, 5); // S(2) - EoS S(5) - немедленно сброшено
let (ref x3, _) = f(3, 6); // S(3) - EoS S(6) - EoS

println!("…");
}

Здесь EoS означает, что время жизни будет до конца срока действия, т.е. после println!().

  • Функции или выражения, создающие перемещения значения, должны быть обрабатаны вызывом.
  • Значения, хранящиеся в «обычных» привязках (переменных), сохраняются до конца области действия, а затем удаляются.
  • Значения, хранящиеся в _, обычно отбрасываются сразу.
  • Однако иногда ссылки (например, ссылка x3) могут сохранять значение (например, кортеж (S(3), S (6))) дольше, так что S(6), будучи частью этого кортежа, может быть удален только после того, как ссылка на S(3) исчезнет).

Статья на list-site.