Skip to the content.

第七章 A Bunch of Silly Lists(一堆整活链表)

本章是”living document”,收录各种离谱但真实可用的链表,展示它们与 Rust 类型系统的互动。书中实现了两个:The Double Single 和 The Stack Allocated List(另预留 Self-Referential Arena List、GhostCell List 两节未写)。

7.1 The Double Singly-Linked List(双单链表)

设计动机与内存布局

第四章的双向链表之所以难写,是因为我们假设所有链接都朝同一方向,导致没有任何节点独占拥有另一个节点。换一个思路:把链表从中间劈成两半,一半向左、一半向右,各是一个普通的单链表栈:

pub struct List<T> {
    left: Stack<T>,
    right: Stack<T>,
}

两个 Stack 之间就是”当前位置”(finger)。这样所有权完全清晰:每个 Stack 独占拥有自己那半边的所有节点,不需要 Rc/RefCell,更不需要 unsafe。

Stack 的改造

复制第二章的安全栈实现,但把 push/pop 拆出内部辅助函数,暴露按节点(而非按值)的操作,这样”走动”时可以整体搬移节点、避免重新分配:

pub fn push(&mut self, elem: T) {
    let new_node = Box::new(Node { elem, next: None });
    self.push_node(new_node);
}

fn push_node(&mut self, mut node: Box<Node<T>>) {
    node.next = self.head.take();
    self.head = Some(node);
}

pub fn pop(&mut self) -> Option<T> {
    self.pop_node().map(|node| node.elem)
}

fn pop_node(&mut self) -> Option<Box<Node<T>>> {
    self.head.take().map(|mut node| {
        self.head = node.next.take();
        node
    })
}

List 的 API

常规操作全部是对左右栈的直接转发:push_left/push_right/pop_left/pop_right/peek_left/peek_right/peek_left_mut/peek_right_mut

核心创新是”走动”操作:从一边弹出整个节点,推到另一边,返回 bool 表示是否真的移动了:

pub fn go_left(&mut self) -> bool {
    self.left.pop_node().map(|node| {
        self.right.push_node(node);
    }).is_some()
}

pub fn go_right(&mut self) -> bool {
    self.right.pop_node().map(|node| {
        self.left.push_node(node);
    }).is_some()
}

测试要点

测试 walk_aboot 用注释标注每步的状态,用 _ 表示 finger 位置,例如 [0, 2, 3, _, 4, 1]。覆盖:左右 push/peek、while list.go_left() {} 把 finger 移到最左端后 pop_left() 返回 None(此时所有元素都在右栈)、混合 push/pop 的序列化遍历、最终两侧都弹空。

概念:finger 数据结构

这是一个极端的 finger 数据结构:对 finger 附近位置的修改是 O(1),远处操作的开销与 finger 到目标的距离成正比。对比:&mut 引用也能沿链接向下走做临时修改,但 &mut 无法往回走(借用规则决定),而 finger 可以来回移动。

7.2 The Stack-Allocated Linked List(栈分配链表)

设计动机

堆分配不是唯一选择。”在栈上动态分配”的简单做法就是:调用函数、获得新的栈帧。任何递归过程如果把当前步骤状态的指针传给下一步,而这个指针又是状态的一部分,就天然构成一个栈分配的链表。本书用 callback 风格把它写成显式的链表。

布局与 push

节点本身(不是指针)就是链表,每个节点持有一个指向前一个节点的共享引用,零堆分配:

pub struct List<'a, T> {
    pub data: T,
    pub prev: Option<&'a List<'a, T>>,
}

唯一的操作是 push:接收旧链表的引用、当前节点数据和一个 callback;在函数栈帧上构造新节点,把它的引用传给 callback。callback 的返回值原样返回,所以嵌套 callback 可以逐层向外传值:

impl<'a, T> List<'a, T> {
    pub fn push<U>(
        prev: Option<&'a List<'a, T>>,
        data: T,
        callback: impl FnOnce(&List<'a, T>) -> U,
    ) -> U {
        let list = List { data, prev };
        callback(&list)
    }
}

使用方式是嵌套闭包,每层闭包里 list 都只在当前栈帧存活:

List::push(None, 3, |list| {
    List::push(Some(list), 5, |list| {
        List::push(Some(list), 13, |list| {
            println!("{}", list.data);
        })
    })
})

Iter

常规迭代器,沿 prev 引用走:

pub struct Iter<'a, T> {
    next: Option<&'a List<'a, T>>,
}

impl<'a, T> List<'a, T> {
    pub fn iter(&'a self) -> Iter<'a, T> {
        Iter { next: Some(self) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.prev;
            &node.data
        })
    }
}

测试要点

elegance 测试:三层嵌套 push,每层用 list.iter().copied().sum::<i32>() 断言从当前节点到链尾所有元素之和(3、5+3、13+5+3),验证遍历方向和 lifetime 正确。

踩坑:换成 &mut 后编译失败 —— variance

尝试把 prev 改成 Option<&'a mut List<'a, T>> 以支持修改数据,结果连简化测试都报 error[E0521]: borrowed data escapes outside of closure(”list is a reference that is only valid in the closure body”),无限递归式报错。

根因:这段代码(共享引用版)能编译,其实是悄悄依赖了 variance。每个节点里存的是”与自己类型完全相同”的 List<'a, T>,即所有节点被声明成同一个 'a;但客观上每层节点活在严格嵌套的作用域里,外层节点的 lifetime 比内层长。共享引用下编译器会悄悄”收缩”较长的 lifetime 去匹配内层类型,这是安全的——大 lifetime 是小 lifetime “and more”,忘掉多余部分没问题(类比继承体系中把 Cat 传给期望 Animal 的位置)。

但对 &mut 这么做就不安全了。如果允许收缩,内层 callback 可以写出 use-after-free:

List::push(None, 3, |list| {
    List::push(Some(list), 5, |list| {
        List::push(Some(list), 13, |list| {
            // 所有 lifetime 相同,于是可以让父节点持有指向我自己的可变引用!
            *list.prev.as_mut().unwrap().prev = Some(list);
        })
    })
})

问题本质:遗忘细节之所以危险,是因为别处可能还记着这些细节并要求它们成立——一旦有 mutation,写入方以为类型被”缩短”了,读取方却还期待原来的类型。继承体系类比:

let mut my_kitty = Cat;                  // 长 lifetime
let animal: &mut Animal = &mut my_kitty; // 收缩掉它是 Cat 的信息
*animal = Dog;                           // 写入短 lifetime 的值
my_kitty.meow();                         // 会叫的 Dog!use-after-free

形式化结论:&'a mut T'acovariant,对 Tinvariant——嵌套进另一个引用内部后不允许再收缩 lifetime,因此 &mut &'big mut T 不能转成 &mut &'small mut T。共享引用 &'a T'aT 都 covariant,所以共享引用版能编译。(趣闻:Java 数组允许这种协变,靠运行时检查抛 ArrayStoreException 来防”会叫的狗”。)

解决方案:interior mutability

回到共享引用版本,要修改数据就用 Cell 包裹数据,向编译器声明”只改数据、不动引用”:

#[test]
fn cell() {
    use std::cell::Cell;

    List::push(None, Cell::new(3), |list| {
        List::push(Some(list), Cell::new(5), |list| {
            List::push(Some(list), Cell::new(13), |list| {
                // 把链表中每个值乘以 10
                for val in list.iter() {
                    val.set(val.get() * 10)
                }

                let mut vals = list.iter();
                assert_eq!(vals.next().unwrap().get(), 130);
                assert_eq!(vals.next().unwrap().get(), 50);
                assert_eq!(vals.next().unwrap().get(), 30);
                assert_eq!(vals.next(), None);
            })
        })
    })
}

测试同时验证了 Cell::set/get 的就地修改和迭代器穷尽后持续返回 None

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。