Почему компилятор Rust удаляет неиспользуемые переменные в порядке, обратном их объявлению?

В книге 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`!

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

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

Schwern 04.07.2024 20:31
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
1
96
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Учитывая, что переменные не используются и на них не ссылаются, не следует ли их удалять сразу после объявления?

Нет. Такое поведение сделало бы 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 будет возможен, поскольку компилятор может отслеживать использование так же, как и время жизни.

Chayim Friedman 04.07.2024 20:11

@ChayimFriedman в этом простом случае. Нет, если после *guard += 1; идет еще код, который не ссылается на guard. Я обновил ответ.

freakish 04.07.2024 20:13

Случай мьютекса, защищающего внешние данные (или код), действительно существует (я даже упомянул об этом в своем собственном ответе), но на самом деле он очень редок. Утверждать, что это «сделает RAII невозможным» из-за этого случая, просто неправильно.

Chayim Friedman 04.07.2024 20:17

@ChayimFriedman Я не знаю, что вы подразумеваете под «неправильным» и почему вы можете предположить, что это единственный случай или даже самый важный случай. Учитывая, что я буквально начал предложение со слов «Например...». Придирки, серьезно.

freakish 04.07.2024 20:25

«Невозможно» — сильно сказано. Гипотетический Rust в альтернативной реальности, который отслеживает использование и немедленно отключается, по-прежнему будет поддерживать побочный эффект RAII, просто он будет менее эргономичным. В вашем примере вам, вероятно, придется написать drop(guard) в конце области видимости, чтобы принудительно получить блокировку до этого момента.

user4815162342 05.07.2024 14:47

@user4815162342 user4815162342, что противоречит цели RAII. И подвержен ошибкам.

freakish 05.07.2024 15:28

@freakish Конечно, это не противоречит цели RAII, за исключением некоторого узкого определения. Суть RAII в том, что конструктор получает ресурс, а деструктор освобождает ресурс, что и делает текущий, и гипотетический Rust. И он подвержен ошибкам только в том случае, если у вас есть побочные эффекты, такие как небезопасный код. Если вы действительно используете guard, он будет удален, когда он вам больше не понадобится, и не позже (или раньше). Rust мог бы сделать это, и это не сделало бы RAII «невозможным», как ошибочно утверждается в вашем ответе.

user4815162342 05.07.2024 16:05

@user4815162342 user4815162342 настоящая цель RAII — автоматическое управление ресурсами, особенно их удаление (что часто менее очевидно). Что, добавив ручное удаление (или любую другую форму искусственного продления срока службы), делает его бессмысленным. Вместо этого я мог бы просто открыть замок. И да, это подвержено ошибкам. Теперь надо подумать, стоит ли мне ставить дроп (или стоит ли его вообще ставить). Посмотрите на мой второй пример. На практике все просто поставят это в конец. Если они это помнят.

freakish 05.07.2024 16:10

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

user4815162342 05.07.2024 16:30

@user4815162342 user4815162342 Я не знаю, что означает «нормальный» код. В любом случае у вас есть два варианта: один, который работает всегда и его легко понять, или другой, который не всегда работает, требует «нормального» кода (что бы это ни значило) и высокого уровня понимания того, когда на самом деле будет использоваться каждая переменная. отбросить, что не всегда очевидно. И компилятор вам не поможет. И в конечном итоге приводит к тому, что первый выбор делается вручную. Извините, но это определение склонности к ошибкам, и для меня это простой выбор.

freakish 05.07.2024 18:56

@freakish Normal означает, что он не зависит от побочных эффектов при удалении неиспользуемой в противном случае защиты блокировки (или другого объекта RAII). Но я даже не хочу спорить по этому поводу — мое возражение касалось утверждения о том, что это сделает RAII «невозможным», что явно неверно.

user4815162342 05.07.2024 21:38

@user4815162342 user4815162342 да, это неверно, если вы допускаете неполный, неинтуитивный и подверженный ошибкам дизайн для RAII. Но я этого не делаю.

freakish 05.07.2024 22:00

Не имеет значения, что переменные не используются; поскольку вы сохранили их в переменных, на них распространяются правила удаления переменных. А именно: «локальные переменные, объявленные в операторе 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], но он нестабильен). , небезопасно и требует корректировки типа и не будет работать, когда деструктору действительно нужен доступ к данным, но лучше в обратном порядке).

Другие вопросы по теме

Проблема стирания элементов в векторе с помощью C
Могу ли я доказать монотонность ассигнований с помощью Rust Borrow Checker
Почему нам следует устанавливать для -XX:InitialRAMPercentage и -XX:MaxRAMPercentage одно и то же значение для облачной среды?
Есть ли что-то вроде указателя для массивов numpy?
C Могу ли я использовать данные, которые я освободил сразу после освобождения?
Веб-скрапинг в R с использованием циклов while. Ошибка в open.connection(x, «rb»): ошибка HTTP 429, когда веб-страница существует
Как обрабатывать данные о ценах на криптовалюту и токены в режиме реального времени на сервере, память моего сервера через некоторое время переполняется
Fread() читает неправильные данные, хотя предыдущий фрагмент был прочитан правильно
Vec-подобные контейнеры разных типов, которые могут использовать один и тот же блок памяти в куче
Как изменить биты в записи таблицы страниц (PTE) в ядре Linux на ARM64?