Я думаю, что ключевая проблема вашей ментальной модели в том, что вы думаете о Box<T>
как о просто указателе. Ссылки на Rust (и большинство умных указателей, таких как Box<T>
) - это не просто указатели, а действительные указатели. То есть нет нулевых ссылок, и ссылки должны всегда указывать на действительные данные.
Когда мы пытаемся сделать self.list = **bx;
, мы перемещаем данные из bx
в self.list
. Однако bx
не владеет своими данными. Когда изменяемый заем bx
заканчивается, фактический владелец будет содержать недействительные данные.
Так что же нам делать? Самый простой способ - это то, что иногда называют уловка Джонса , где мы переключаем данные в bx
для некоторого фиктивного значения. Теперь фактический владелец данных в bx
не будет содержать недействительные данные. Итак, как мы это сделаем? Это сфера действия функции std::mem::replace
, которая принимает изменяемую ссылку и значение и заменяет данные, стоящие за изменяемой ссылкой, на это значение, возвращая то, что раньше было за изменяемой ссылкой (включая владение!). Это именно то, что мы хотим сделать здесь с self.list = std::mem::replace(&mut **bx, List::Nil)
. Опять же, List::Nil
это просто фиктивные данные;любой List
вообще будет работать точно так же.
enum List {
Cons(u32, Box<List>),
Nil,
}
struct ListHolder {
list: List,
}
impl Iterator for ListHolder {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if let Cons(num, bx) = &mut self.list {
let val = *num;
self.list = std::mem::replace(&mut **bx, List::Nil); // This is the key line
Some(val)
} else {
None
}
}
}
use List::*;
fn main() {
let list_inst = ListHolder {
list: Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))),
};
for i in list_inst.into_iter() {
println!("{}", i); // Prints 1, 2, 3 as expected
}
}
(детская площадка)
Чтобы быть чуть более идиоматичным, вместо &mut **bx
, мы могли быпросто используйте bx.as_mut()
, чтобы получить изменяемую ссылку из коробки. Кроме того, вызов into_iter
для list_inst
не требуется, поскольку ListHolder
уже реализует Iterator
, поэтому его не нужно превращать в один. Вам также может быть интересно узнать о num
и val
и о том, почему мы все еще должны сделать временную переменную для этого.
Причина в том, что это значение по-прежнему является справочным, и мы не владеемдоступ к владельцу (self.list
). Это означает, что мы должны сделать копию, чтобы вернуться. u32
реализует Copy
, так что на самом деле это не проблема, но если вы попытаетесь сделать связанный список общим по типу его элементов, он просто не будет работать. let val = *num;
- это то же самое «перемещение из заемного контента», которое мы не могли сделать раньше.
Решение состоит в том, чтобы использовать std::mem::replace
, чтобы получить право владения не только данными, стоящими за bx
,но из всего списка. Таким образом, если мы используем std::mem::replace(&mut self.list, List::Nil)
перед деструктуризацией, self.list
будет заменен фиктивным значением, и мы будем владеть фактическим списком, включая значение и хвост списка. Это также означает, что мы можем просто иметь self.list = *bx
, как я уверен, что вы изначально хотели.
impl Iterator for ListHolder {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if let Cons(num, bx) = std::mem::replace(&mut self.list, List::Nil) {
self.list = *bx;
Some(num)
} else {
None
}
}
}
(детская площадка)
Результат теперь выможете сделать список общим без особых усилий.
Если вы хотите узнать больше о том, как модель владения Rust влияет на реализацию связанных списков, вы можете сделать не лучше, чем отличная серия Learn Rust со слишком большим количеством связанных списков . Серия охватывает все детали в деталях, а также множество вариаций.