Разборка
У pop_front должна быть та же самая базовая логика, что и у push_front, но обращённая.
Попробуем:
pub fn pop_front(&mut self) -> Option<T> {
// надо получить старую голову, убедимся, что количество ссылок -2
self.head.take().map(|old_head| { // -1 old_head
match old_head.borrow_mut().next.take() {
Some(new_head) => { // -1 new_head
// не пустой список
new_head.borrow_mut().prev.take(); // -1 old_head
self.head = Some(new_head); // +1 new_head
// всего: -2 старых, +0 новых
}
None => {
// пустой список
self.tail.take(); // -1 old_head
// всего: -2 старых, (новых нет)
}
}
old_head.elem
})
}
> cargo build
error[E0609]: no field `elem` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
--> src/fourth.rs:64:22
|
64 | old_head.elem
| ^^^^ unknown field
ТАК.
RefCells.
Чувствую, нам снова нужен borrow_mut...
pub fn pop_front(&mut self) -> Option<T> {
self.head.take().map(|old_head| {
match old_head.borrow_mut().next.take() {
Some(new_head) => {
new_head.borrow_mut().prev.take();
self.head = Some(new_head);
}
None => {
self.tail.take();
}
}
old_head.borrow_mut().elem
})
}
cargo build
error[E0507]: cannot move out of borrowed content
--> src/fourth.rs:64:13
|
64 | old_head.borrow_mut().elem
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot move out of borrowed content
вздох
cannot move out of borrowed content
Не могу извлечь заимствованное содержимое.
Грм...
Похоже, на самом деле Box нас баловал.
borrow_num возвращает нам &mut Nude<T>, но мы не можем извлечь оттуда ничего!
Нам нужно что-то, что возьмёт у нас RefCell<T> и вернёт нам T.
Давайте проверим доки на предмет наличия чего-то похожего:
fn into_inner(self) -> TПолучает RefCell, возвращает завёрнутое значение.
Выглядит многообещающе!
old_head.into_inner().elem
> cargo build
error[E0507]: cannot move out of an `Rc`
--> src/fourth.rs:64:13
|
64 | old_head.into_inner().elem
| ^^^^^^^^ cannot move out of an `Rc`
Вот блин.
into_inner хочет извлечь RefCell, но мы не можем её предоставить, потому что она находится в Rc.
В предыдущей главе мы видели, что Rc<T> разрешает доступ к своему значению только по разделяемой ссылке.
И это правильно, потому что главный смысл указателей с подсчётом ссылок в том, что они разделяемые!
Это стало для нас проблемой, когда мы писали drop для списка с подсчётом ссылок.
Воспользуемся тем же решением, а именно, методом Rc::try_unwrap, который возвращает содержимое Rc, если счётчик ссылок равен 1.
Rc::try_unwrap(old_head).unwrap().into_inner().elem
Rc::try_unwrap возвращает Result<T, Rc<T>>.
Значения типа Result — расширенный тип Option в котором у варианта None появляются связанные с ним данные.
В нашем случае — Rc, из которого мы пытаемся достать значение.
Поскольку нас не беспокоят возможные ошибки (если мы правильно написали программу, она обязана завершиться успешно), мы просто вызываем unwrap.
В любом случае, давайте посмотрим, когда ошибка компиляции возникнет на этот раз (будем честными — она обязательно возникнет).
> cargo build
error[E0599]: no method named `unwrap` found for type `std::result::Result<std::cell::RefCell<fourth::Node<T>>, std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>>` in the current scope
--> src/fourth.rs:64:38
|
64 | Rc::try_unwrap(old_head).unwrap().into_inner().elem
| ^^^^^^
|
= note: the method `unwrap` exists but the following trait bounds were not satisfied:
`std::rc::Rc<std::cell::RefCell<fourth::Node<T>>> : std::fmt::Debug`
Да что ты будешь делать?
unwrap у Result требует, чтобы тип ошибки мог печатать себя при отладке.
А RefCell<T> реализует Debug только тогда, когда его реализует T.
А Node не реализует Debug.
Вместо того, чтобы добавлять реализацию, выполним обходной маневр, преобразовав Result в Options с помощью вызова ok:
Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem
НУ, ПОЖАЛУЙСТА.
cargo build
ДА.
уф
Получилось.
Мы реализовали push и pop.
Давайте протестируем реализацию с помощью нашего старого базового теста для стека (потому что он единственный, который мы пока написали):
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// Проверяем, что пустой список ведёт себя правильно
assert_eq!(list.pop_front(), None);
// Заполняем список
list.push_front(1);
list.push_front(2);
list.push_front(3);
// Проверяем обычное удаление
assert_eq!(list.pop_front(), Some(3));
assert_eq!(list.pop_front(), Some(2));
// Вставляем новые значения, просто чтобы проверить, что ничего не сломается
list.push_front(4);
list.push_front(5);
// Проверяем обычное удаление
assert_eq!(list.pop_front(), Some(5));
assert_eq!(list.pop_front(), Some(4));
// Проверяем граничный случай
assert_eq!(list.pop_front(), Some(1));
assert_eq!(list.pop_front(), None);
}
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 9 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::basics ... ok
test fifth::test::iter_mut ... ok
test third::test::basics ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test second::test::into_iter ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured
В яблочко.
Теперь, научившись корректно удалять элементы из списка, мы можем реализовать и типаж Drop. На этот раз Drop чуть более интересен с концептуальной точки зрения. Раньше мы реализовывали Drop для стека, чтобы избавиться от неограниченной рекурсии. Сейчас нам надо реализовать Drop, чтобы хоть-нибудь вообще заработало.
Rc не умеет работать с циклическими ссылками.
Если есть цикл, объекты поддерживают существование друг друга.
А двусвязный список, как выяснилось — одна большая цепочка из крошечных циклов!
Так что когда мы уничтожаем наш список, два конечных узла уменьшают свои счётчики до 1... а затем ничего не происходит.
Ладно, когда наш список содержит ровно один узел, счётчик ссылок сбрасывается но нуля.
Но в идеале список должен корректно работать даже когда нём больше одного элемента.
Возможно, это только моё мнение.
Как мы видели, удаление элементов оказалось довольно болезненным.
Поэтому простейший способ для нас — это просто вызывать pop, пока мы не получим None:
impl<T> Drop for List<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
}
cargo build
(На самом деле мы могли бы сделать это с помощью наших изменяемых стеков, но короткий путь только для тех, кто знает, куда идти!)
Можно было заняться реализацией версий _back для методов push и pop, но это всего лишь копи-паста.
К ней мы вернёмся позже в этой главе.
А сейчас давайте посмотрим на более интересные вещи!