Типы и перемещения
Память приложений:
- Память приложения представляет собой массив байтов на низком уровне.
- Операционная среда обычно разделяется на: — стек (небольшая память с низким уровнем служебных данных, большая часть переменных находится здесь). — куча (большая гибкая память, но всегда обрабатывается через прокси стека, например, Box<T>). — статическая память (чаще всего используется в качестве места расположения частей &str). — код (где находится бит-код ваших функций).
- Самая сложная часть связана с тем, как работает стек, в текущий момент.
Переменные:
let t = S(1);
- Резервирует местоположение памяти с именем ‘t’ типа ‘S’ и значением S(1) хранящимся внутри.
- Если объявлено с let, то местоположение находится в стеке.
- Обратите внимание на лингвистическую неоднозначность, в термине переменная, может означать: — имя местоположения в исходном файле («именовать эту переменную»). — расположение в скомпилированном приложении, 0x7 («сообщить адрес этой переменной»). — значение содержащееся в S(1) («заданное значение переменной»).
- Конкретно у компилятора ‘t’ может означать местоположение ‘t’, в картинке выше 0x7 и значение в ‘t’ S(1).
Перемещение:
let a = t;
- Это приведет к перемещению значения в пределах ‘t’, перемещению или копированию данных, если ‘S’ реализована как копируемая.
- После перемещения расположение ‘t’ является недопустимым и больше не может быть прочитано: — биты в этом месте не пусты, но не определены. — если у вас все еще был доступ к ‘t’ (через небезопасные ссылки), они могут выглядеть как допустимые ‘S’, но любая попытка использовать заканчивается неопределенным поведением.
- Здесь рассмативаем типы явного копирования. Они немного меняют правила, но не сильно: — они не сбросят значение. — они никогда не оставляют после себя «пустую» переменную.
Безопасность типов:
let c: S = M::new();
- Тип переменной служит нескольким важным целям: — определяет, как следует интерпретировать базовые биты. — допускает только четко определенные операции с этими битами. — предотвращает запись в это местоположение других случайных значений или битов.
- Здесь не удаcться скомпилировать значение, так как байты M::new() не могут быть преобразованы в форму типа ‘S’.
- Преобразования между типами всегда будет неправильным, если только явное правило не разрешает это (принуждение, приведение,…).
Область действия и удаление:
{
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().
Стек вызовов
Границы функций:
fn f(x: S) { … }
let a = S(1); // <- Мы здесь
f(a);
- При вызове функции память для параметров (и возвращаемых значений) резервируется в стеке.
- Здесь перед вызовом ‘f’ значение в a перемещается в стек, и во время работы функции ‘f’ локальная переменная в ней будет ‘x’.
Вложенные функции:
fn f(x: S) {
if once() { f(x) } // <- Мы здесь (перед рекурсией)
}
let a = S(1);
f(a);
- Рекурсивный вызов функций или вызов других функций также расширяет кадр стека.
- Вложение слишком большого количества вызовов (например, с помощью неограниченной рекурсии) приведет к росту стека и, в конечном итоге, к переполнению, что приведет к завершению работы приложения.
Перепрофилирование памяти:
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’.
Ссылки и указатели
Ссылки в качестве указателей:
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;
Доступ к памяти, не принадлежащей владельцам:
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 =… старое значение в адресе будет отброшено (не показано выше).
Ссылки на защищенные ссылки:
let mut a = …;
let r = &mut a;
let d = *r; // недопустимое значение для перемещения, «a» будет пустым.
*r = M::new(); // недопустимо для хранения значение, отличного от S, не имеет смысла.
- В то время как привязки гарантируют всегда хранить допустимые данные, ссылки гарантируют всегда указывать на допустимые данные.
- &mut T должен предоставить те же гарантии, что и переменные, поскольку они не могут взять и уничтожить значение: — Они не разрешают запись недопустимых данных. — Они не позволяют перемещать данные (оставят переменную пустой без информации владельца).
Необработанные указатели:
let p: *const S = questionable_origin();
- В отличие от ссылок, указатели почти не имеют гарантий.
- Они могут указывать на недопустимые или несуществующие данные.
- Их разыменование небезопасно, и обращение к недопустимым *p, как если бы оно было допустимым, будет с неопределенным поведением.
Основы жизненного цикла
«Жизнь» событий:
- Каждая сущность в программе имеет некоторую (временную/пространственное) место нахождение, где она актуальна, т.е. жива.
- Мягко говоря, это время жизни может быть: — залочено (строки кода), где доступен элемент (например, имя модуля). — залочено между моментом, когда местоположение инициализировано значением, и моментом, когда местоположение сброшено. — залочено между тем, когда местоположение впервые используется определенным образом, и тем, когда это использование прекращается. — залочено (или фактическое время) между моментом создания значения и моментом отбрасывания этого значения.
- В остальной части этого раздела мы будем ссылаться на вышеперечисленные пункты: — область применения этого элемента, не относящаяся к данному вопросу. — область действия этой переменной или местоположения. — срок службы использования. — время жизни значения может быть полезным при обсуждении дескрипторов открытых файлов, но также не имеющих значений.
- Аналогично, параметры времени жизни в коде, например, r: &‘a S, являются:
— связанные с временем жизни любое местоположение ‘r’ указывает на необходимость быть доступным или заблокированным.
— не связано с «временем существования» (как время жизни) самого ‘r’ (ну, он должен существовать короче, вот и все).
Значение r: &‘c S:
- Предположим, что у вас есть r: &‘c S, это означает: — ‘r’ содержит адрес некоторых ‘S’. — любой адрес ‘r’ указывает как должен и будет существовать по крайней мере для ‘c. — сама переменная ‘r’ не может работать дольше ‘c.
Типичное времени жизни:
{
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.
Заимствованное состояние:
let mut b = S(0);
let r = &mut b;
b = S(4); // Потерпит неудачу, так как b в заимствованном состоянии.
print_byte(r);
- После получения адреса переменной через &b или &mut b переменная помечается как заимствованная.
- При заимствовании содержимое адреса больше не может быть изменено посредством исходной привязки ‘b’.
- Как только адрес, взятый через &b или &mut b, перестает использоваться (как залоченая), оригинальная привязка ‘b’ снова работает.
Время жизни в функциях
Параметры функции:
fn f(x: &S, y:&S) -> &u8 { … }
let b = S(1);
let c = S(2);
let r = f(&b, &c);
- При вызове функций, которые принимают и возвращают ссылки, происходит две интересные вещи: — используемые локальные переменные помещаются в заимствованное состояние. — но во время компиляции неизвестно, какой адрес будет возвращен.
Проблема «заимствованного» распространения:
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’ должны оставаться заблокированными.
- Во многих случаях мы можем добиться улучшения качества времени жизни: — Примечательно, что если известно, что один параметр больше не может использоваться в возвращаемом значении.
Время жизни распространяет заимствованное состояние:
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, мы все еще не могли их различить, и оба должны были оставаться заблокированными.
Разблокирование:
- Местоположение переменной снова разблокируется после окончания последнего использования любой ссылки, которая может указывать на нее.
Продвинутый
Ссылки на ссылки:
// Возвращает ближнюю ('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 во всех случаях всегда терпит неудачу по совокупности причин выше.
Удаление и _:
{
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) исчезнет).