В книге Rust в главе 15, разделе 3 приведен пример, показывающий, когда Rust запускает функцию drop для переменных c и d.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
и результат:
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
Мой вопрос: почему компилятор ржавчины удаляет переменные в таком порядке? Учитывая, что переменные не используются и на них не ссылаются, не следует ли их удалять сразу после объявления и в том порядке, в котором они были объявлены?
Я также попробовал тот же код с другой объявленной переменной.
//snip
let e = CustomSmartPointer {
data: String::from("more stuff"),
};
и выход остается обратным
CustomSmartPointers created.
Dropping CustomSmartPointer with data `more stuff`!
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
Хотя способ удаления переменных напоминает то, как данные помещаются и извлекаются из стека, это должно быть отвлекающим маневром.
Учитывая, что переменные не используются и на них не ссылаются, не следует ли их удалять сразу после объявления?
Нет. Такое поведение сделало бы RAII невозможным. Например это:
let mut lock: Mutex<i32> = Mutex::new(1);
...
{
let mut guard = lock.lock().unwrap();
*guard += 1;
// Something that doesn't reference guard
// but needs to be run under lock
notify_counter_update();
}
guard
здесь не только хранит данные внутри, но, что более важно, это структура, которая разблокирует базовую блокировку при сбросе. Благодаря отбрасыванию, происходящему в конце области, вся эта область автоматически находится в процессе блокировки/разблокировки. Мне не нужно помнить о ручной разблокировке замка (что было бы необходимо, если бы Rust упал guard
в тот момент, когда он больше не используется, я имею в виду: охранники были бы бесполезны). Это может привести к ошибкам, если область содержит, например, ifs или другие вложенные области.
Другой пример, рассмотрим это:
fn custom_fn(value: &String) -> &str;
...
{
let x = "foo".to_owned();
let y = custom_fn(&x);
println!("{}", y);
}
когда должен выпасть Rust x
? Когда он используется в последний раз, т.е. после вызова, но перед println? Что, если custom_fn
реализовано как
fn custom_fn(value: &String) -> &str {
value.as_str()
}
? Удаление x
перед println приведет к тому, что println попытается получить доступ к освобожденной памяти (помните, что в отличие от &str тип String владеет памятью и освобождает ее при удалении). Неопределенное поведение. Краш, если повезет. Вы можете возразить, что Раст может это сделать. Но всегда ли он может это сделать? Что, если:
fn custom_fn(value: &String) -> &str {
if rand() > 0.5 {
value.as_str()
}
else
{
"baz"
}
}
где rand()
возвращает случайное число с плавающей запятой в диапазоне 0..1. Какой вывод Раст может сделать по этому поводу? Или еще хуже: что, если custom_fn
— внешняя функция, вообще не определённая в Rust? Единственный правильный, последовательный и простой для понимания способ — это поставить x
в конце области видимости.
и быть отброшены в том порядке, в котором они были объявлены?
Порядок вторичен, и теоретически подойдет любой (конечно, при условии, что переменные независимы). Но какой-то выбор должен был быть сделан, и полезно, чтобы он был последовательным. Таким образом, здесь есть только два естественных выбора: тот же порядок, что и при объявлении, или обратный.
Однако обратный порядок более естественен. Например вернемся к примеру с замками. Предположим, у нас их два:
{
let mut guard1 = lock1.lock().unwrap();
let mut guard2 = lock2.lock().unwrap();
// do something
}
Вы действительно ожидаете, что замок 1 будет разблокирован раньше, чем замок 2? Я думаю, что большинство людей не стали бы. И на самом деле это единообразно при вложении областей. Приведенный выше код эквивалентен:
{
let mut guard1 = lock1.lock().unwrap();
{
let mut guard2 = lock2.lock().unwrap();
// do something
}
}
что в противном случае (т. е. с другим порядком капель) не было бы.
RAII будет возможен, поскольку компилятор может отслеживать использование так же, как и время жизни.
@ChayimFriedman в этом простом случае. Нет, если после *guard += 1;
идет еще код, который не ссылается на guard
. Я обновил ответ.
Случай мьютекса, защищающего внешние данные (или код), действительно существует (я даже упомянул об этом в своем собственном ответе), но на самом деле он очень редок. Утверждать, что это «сделает RAII невозможным» из-за этого случая, просто неправильно.
@ChayimFriedman Я не знаю, что вы подразумеваете под «неправильным» и почему вы можете предположить, что это единственный случай или даже самый важный случай. Учитывая, что я буквально начал предложение со слов «Например...». Придирки, серьезно.
«Невозможно» — сильно сказано. Гипотетический Rust в альтернативной реальности, который отслеживает использование и немедленно отключается, по-прежнему будет поддерживать побочный эффект RAII, просто он будет менее эргономичным. В вашем примере вам, вероятно, придется написать drop(guard)
в конце области видимости, чтобы принудительно получить блокировку до этого момента.
@user4815162342 user4815162342, что противоречит цели RAII. И подвержен ошибкам.
@freakish Конечно, это не противоречит цели RAII, за исключением некоторого узкого определения. Суть RAII в том, что конструктор получает ресурс, а деструктор освобождает ресурс, что и делает текущий, и гипотетический Rust. И он подвержен ошибкам только в том случае, если у вас есть побочные эффекты, такие как небезопасный код. Если вы действительно используете guard
, он будет удален, когда он вам больше не понадобится, и не позже (или раньше). Rust мог бы сделать это, и это не сделало бы RAII «невозможным», как ошибочно утверждается в вашем ответе.
@user4815162342 user4815162342 настоящая цель RAII — автоматическое управление ресурсами, особенно их удаление (что часто менее очевидно). Что, добавив ручное удаление (или любую другую форму искусственного продления срока службы), делает его бессмысленным. Вместо этого я мог бы просто открыть замок. И да, это подвержено ошибкам. Теперь надо подумать, стоит ли мне ставить дроп (или стоит ли его вообще ставить). Посмотрите на мой второй пример. На практике все просто поставят это в конец. Если они это помнят.
Мы разговариваем мимо друг друга. Цель ручного удаления, которое я описал, — расширить область действия в тех редких случаях, когда вам нужно выполнить удаление позже из-за побочного эффекта. Обычный код не потребует удаления вручную, как и сейчас.
@user4815162342 user4815162342 Я не знаю, что означает «нормальный» код. В любом случае у вас есть два варианта: один, который работает всегда и его легко понять, или другой, который не всегда работает, требует «нормального» кода (что бы это ни значило) и высокого уровня понимания того, когда на самом деле будет использоваться каждая переменная. отбросить, что не всегда очевидно. И компилятор вам не поможет. И в конечном итоге приводит к тому, что первый выбор делается вручную. Извините, но это определение склонности к ошибкам, и для меня это простой выбор.
@freakish Normal означает, что он не зависит от побочных эффектов при удалении неиспользуемой в противном случае защиты блокировки (или другого объекта RAII). Но я даже не хочу спорить по этому поводу — мое возражение касалось утверждения о том, что это сделает RAII «невозможным», что явно неверно.
@user4815162342 user4815162342 да, это неверно, если вы допускаете неполный, неинтуитивный и подверженный ошибкам дизайн для RAII. Но я этого не делаю.
Не имеет значения, что переменные не используются; поскольку вы сохранили их в переменных, на них распространяются правила удаления переменных. А именно: «локальные переменные, объявленные в операторе let
, связаны с областью действия блока, который содержит оператор let
» и «Когда поток управления покидает область удаления, все переменные, связанные с этой областью, удаляются в порядке, обратном объявлению [... ]" из деструкторов в справочнике по Rust.
Если вы знаете, что они не используются, и хотите, чтобы они были немедленно удалены, вы можете не сохранять их в переменной или использовать привязку _
(поскольку это не переменная):
// the value is created and dropped immediately
let _ = CustomSmartPointer {
data: String::from("my stuff"),
};
Почему они отбрасываются в обратном порядке? В этих вопросах и ответах по C++ есть несколько довольно агностических причин, но суть в том, что более поздние переменные часто зависят от более ранних переменных, и, таким образом, если более ранние переменные были уничтожены раньше, то удаление логики для более поздних переменных, скорее всего, приведет к потере данных. раса/конфликт. По, казалось бы, глупым причинам это, скорее всего, будет выглядеть как ошибка проверки заимствований в Rust.
Вы утверждаете две вещи: переменные должны удаляться после их последнего использования, а не в конце блока, и что удаление должно происходить по порядку, а не в обратном порядке. Ответ на каждое из этих утверждений различен.
В первом утверждении удаление переменных в конце области видимости является почти необходимостью.
Проблема в том, что компилятор не может точно определить использование.
Даже если компилятор может отслеживать время жизни, он не может отслеживать необработанные указатели. И доступ через них все равно должен быть действительным.
Вот пример:
fn main() {
let v = vec![123];
let p: *const i32 = v.as_ptr();
unsafe {
println!("{}", *p);
}
}
Согласно предложенному вами закону, компилятор упадет v
сразу после вызова as_ptr()
, но это приведет к зависанию p
и печати UB.
Более того, иногда даже необработанные указатели не означают жизнеспособности. Например, люди могут использовать Mutex<()>
для защиты некоторых внешних данных. Компилятор не имеет возможности связать внешние данные с MutexGuard
; если он упадет (таким образом, разблокируется) слишком рано, у вас могут возникнуть гонки данных.
Что касается порядка, обратный порядок вполне естественен. Это позволяет вам делать такие вещи, как следующие:
let mutex = std::sync::Mutex::new("abc".to_owned());
let lock = mutex.lock().unwrap();
Мьютекс и блокировка находятся в одном блоке. Если бы мьютекс был удален раньше, это не скомпилировалось бы, поскольку данные были освобождены при удалении мьютекса, но деструктор MutexGuard
может получить к нему доступ (есть атрибут, помогающий в подобных случаях - #[may_dangle]
, но он нестабильен). , небезопасно и требует корректировки типа и не будет работать, когда деструктору действительно нужен доступ к данным, но лучше в обратном порядке).
«Хотя способ удаления переменных напоминает то, как данные помещаются и извлекаются из стека, это должно быть отвлекающим маневром». Нет, это, пожалуй, все. Языки программирования часто хранят локальные переменные в стеке.