Я читаю "изучайте ржавчину со слишком большим количеством связанных списков" . В главе 2.7 автор говорит, что "Мы не можем сбросить содержимое ящика после освобождения". Как может быть освобождено значение после его удаления? Не происходит ли освобождение при реализации удаления или это разные понятия?
Для некоторого контекста автор объясняет, почему конкретная реализация связанного списка не является хвостовой рекурсией и, следовательно, не может быть оптимизирована компилятором.
Связанный список:
pub struct List {
head: Link,
}
enum Link {
Empty,
More(Box<Node>),
}
struct Node {
elem: i32,
next: Link,
}
Удалить реализацию:
impl Drop for List {
fn drop(&mut self) {
// NOTE: you can't actually explicitly call `drop` in real Rust code;
// we're pretending to be the compiler!
self.head.drop(); // tail recursive - good!
}
}
impl Drop for Link {
fn drop(&mut self) {
match *self {
Link::Empty => {} // Done!
Link::More(ref mut boxed_node) => {
boxed_node.drop(); // tail recursive - good!
}
}
}
}
impl Drop for Box<Node> {
fn drop(&mut self) {
self.ptr.drop(); // uh oh, not tail recursive!
deallocate(self.ptr);
}
}
impl Drop for Node {
fn drop(&mut self) {
self.next.drop();
}
}

Проблема с хвостовой рекурсией на impl Drop for Box<Node> заключается в том, что мы должны self.ptr.drop() и потомdeallocate(self.ptr); хвостовая рекурсия потребовала бы от нас deallocate(self.ptr) и потомself.ptr.drop() (поэтому drop() рекурсирует в хвостовой позиции функции).
Проблема в том, что когда мы сначала deallocate(self.ptr), содержимое self.ptr сразу же оказывается в неопределенном состоянии, потому что это освобожденная память. Если мы затем self.ptr.drop(), нам понадобится &mut Box<Node>, чтобы выполнить вызов drop() (где это отображается как &mut self).
Это не только означает, что деструктор эффективно обращается к освобожденной памяти. Это также означает, что мы должны создать reference в форме &mut Box<Node>, которая указывает на память, находящуюся в неопределенном состоянии. В Rust это будет Неопределенное поведение, поскольку Rust дает очень строгие гарантии в отношении памяти, на которую указывает reference.
Wouldn't the deallocation occur in the implementation of drop or are these separate concepts?
Это отдельные понятия. В примере кода Drop означает «очистить мой содержание/собственные ресурсы», deallocate означает «вернуть память, в которой я фактически хранился, в пул».
Метод drop запускается со ссылкой на существующий экземпляр. Определение drop на самом деле говорит:
When this method has been called,
selfhas not yet been deallocated. That only happens after the method is over. If this wasn’t the case,selfwould be a dangling reference.
В результате headдолжен будет полностью удален, прежде чем его можно будет освободить. И процесс удаления требует сначала удалить все узлы в списке, каждый из которых освободит связанный узел только после завершения удаления, что приведет к стеку drops, за каждым из которых будем следует deallocate, но только в конце концов drop и deallocates над ними в стеке завершены. Поскольку вы не можете выполнить один deallocate до тех пор, пока drop не будет вызвано по всему стеку (в этот момент drop для хвостового узла возвращается, а хвостовой узел освобождается первым), его нельзя реализовать хвост-рекурсивно.
Примечание: это точно такая же проблема, которую вы видите в C++, пытаясь реализовать связанный список, используя std::shared_ptr или std::unique_ptr для указателя next. Как и в Rust, C++ различает уничтожение (очистка ресурсов в пределах объекта) от освобождения памяти (освобождение памяти, которая проводится объекта). Когда headunique_ptr очищается, прежде чем он сможет освободить память, он должен сказать «head + 1», чтобы очистить свой собственный unique_ptr, который, в свою очередь, говорит «head + 2», чтобы очистить свой unique_ptr и так далее. В любом случае уничтожение (через деструктор) должно предшествовать освобождению (что может произойти через operator delete для материала, выделенного в куче, или просто компилятором, сжимающим стек, фактически не освобождая память для материала, выделенного в стеке).
Я думаю, вас смущает фраза "содержимое". Вы не можете удалить содержание a
Box<Node>до его освобождения, то есть вы не можете удалитьNodeа потом для освобожденияBox. Освобождение является частью реализацииDropдля самBox, а не его содержимого.