Appearance
20.6 迭代器
我们知道,可以通过Vec::iter()
方法创建一个动态数组的迭代器。但是我们在源码中却没有见到这个方法的存在。这是因为这个方法实际上是 slice 类型的方法,Vec 只是自动 deref 后调用了原生数组的迭代器而已。
但是,Vec 类型本身也是可以用于 for 循环中的:
rust
fn main() {
let x = vec![0_i32, 1, 2];
for item in x { println!("{}", item); }
}
这是因为 Vec 实现了 IntoIterator trait。标准库中的 IntoIterator 就是编译器留下来的一个扩展内置 for 循环语法的接口。任何自定义类型,只要合理地实现了这个 trait,就可以被用在内置的 for 循环里面。关于迭代器的更多内容,在本书第三部分继续讲述。
关于迭代器,有一个Vec::drain
方法实现得比较特殊,这里专门拿出来讲一下。它的功能是从动态数组中把一个范围的数据“移除”出去,返回的还是一个“迭代器”。我们还可以遍历一次这个迭代器,使用已经被移除的那些元素。示例如下:
rust
fn main() {
let mut origin = vec![0, 1, 2, 3, 4, 5];
println!("Removed:");
for i in origin.drain(1..3) {
println!("{}", i);
}
println!("Left:");
for i in origin.iter() {
println!("{}", i);
}
}
drain()
方法返回的类型就是一个普通的迭代器,在标准库中,这个方法的源码如下所示:
rust
pub fn drain<R>(&mut self, range: R) -> Drain<T>
where R: RangeArgument<usize>
{
let len = self.len();
let start = match range.start() {
Included(&n) => n,
Excluded(&n) => n + 1,
Unbounded => 0,
};
let end = match range.end() {
Included(&n) => n + 1,
Excluded(&n) => n,
Unbounded => len,
};
assert!(start <= end);
assert!(end <= len);
unsafe {
self.set_len(start);
let range_slice = slice::from_raw_parts_mut(self.as_mut_ptr().offset (start as isize), end - start);
Drain {
tail_start: end,
tail_len: len - end,
iter: range_slice.iter(),
vec: Shared::from(self),
}
}
}
返回的这个 Drain 类型实现了 Iterator trait,具体源码就不详细列出了。总之遍历 Drain 这个迭代器,会把所有应该被删除的元素遍历一遍。而 Drain 类型还实现了一个析构函数。当它自己被销毁的时候,它会去修改原始的 Vec 的内容,把这些应该被删除的元素从原始数组中真正删掉。
大致原理就是这样。特别需要注意的是,在Vec::drain
方法中创建迭代器之前,先调用了self.set_len(start)
方法。那么这个设置的目的是什么呢?
这个设计实际上是为了防止另一种情况下的内存不安全。
我们假设用户写了这样的代码:
rust
fn main() {
let mut origin = vec![
"0".to_string(), "1".to_string(), "2".to_string(),
"3".to_string(), "4".to_string(), "5".to_string()];
{
let mut d = origin.drain(1..3);
let s: Option<String> = d.next();
println!("{:?}", s);
let s: Option<String> = d.next();
println!("{:?}", s);
let s: Option<String> = d.next();
println!("{:?}", s);
std::mem::forget(d);
}
println!("Left:");
for i in origin.iter() {
println!("{:?}", i);
}
}
前面讲泄露的时候已经说过了,std::mem::forget
函数是不带 unsafe 修饰的。它可以阻止一个类型的析构函数调用,析构函数是不能保证一定会被调用的。在这种情况下,Drain 类型没有机会执行它的析构函数,所以它没有机会修改原始的 Vec,把数据从 Vec 中移除。
假设没有self.set_len(start);
这个函数调用,在上面的例子中会出现某些字符串元素已经被 Drain 迭代器取出来消费掉了,但是 Vec 中还存有一份“副本”,而这个副本本身处于一种“未初始化状态”,它们从逻辑上已经被移走了,但依然被认为是 Vec 的正常数据。这是典型的内存不安全的情况。
所以,在标准库中,drain()
方法内部在返回迭代器之前,先把当前 Vec 的大小设置为一个比较小的绝对安全的值。如果说这个drain()
方法返回的迭代器因为某种原因未能成功析构,那么最坏的结果也就是,原数组中仅剩下了 start 之前的元素。至少我们可以肯定,任何情况下,数组中的数据都是符合“内存安全”的。如果这个迭代器的析构函数成功执行了,那么 end 之后的元素会向前移动,数组的长度会被重置,这时候这个逻辑才算执行完整。
析构函数泄漏绝对不是我们期望发生的事情,我们只是无法阻止这种情况而已。所以,在写库的时候要注意,我们的底线是,即便析构函数泄漏会导致逻辑错误,也不会发生“内存不安全”。