让我们一步一步来尝试。
根据经验,在创建不可变对象时,所有构造函数参数都应该在实例化时就知道,但是让我们作弊并按名称传递构造函数参数,然后使用惰性字段来延迟评估,这样我们就可以创建双向链接元素之间:
// p and n are passed by name
// and won't be evaluated until prev and next are accessed
// for the first time
class Element [T] (val value: T, p : => Element[T], n : => Element [T]) {
lazy val prev = p
lazy val next = n
}
val e1:Element[Int] = new Element [Int] (1,null,e2)
val e2:Element[Int] = new Element [Int] (2,e1,e3)
val e3:Element[Int] = new Element [Int] (3,e2,e4)
val e4:Element[Int] = new Element [Int] (4,e3,null)
一旦我们运行代码,我们将收到一个不可变的双向链表:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null
并且将能够来回遍历它:
println(e1.next.next.next.value)
println(e4.prev.prev.prev.value)
4
1
现在,假设我们要在列表末尾添加第五个元素,使其看起来像这样:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) ↔ e5(5) → null
val e5:Element[Int] = new Element [Int] (5,e4,null)
此时我们得到:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null
↖ ↑
e5(5)
等一下,看起来不太对劲! e4 应该指向 e5 而不是指向 null,但是 e4 是不可变的,我们无法更改元素本身,所以看起来唯一的选择是制作一个副本并将其指向 e3 和 e5。让我们尝试将这种新方法应用于初始列表:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null
val e4_b: Element[Int] = new Element [Int] (e4.value, // keeping original value
e3,e5)
val e5 : Element[Int] = new Element [Int] (5,e4_b,null)
这样更好,e4_b 通向 e5 又通向 e4_b:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null
↖ ↑
e4_b(4) ↔ e5(5)
但是现在我们有同样的原始问题,只是 e3 仍然指向 e4。你能看到正在出现的趋势吗?如果我们继续复制元素以很快解决问题,我们最终会得到:
null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null
↑ ↑
e1_b(1) ↔ e2_b(2) ↔ e3_b(3) ↔ e4_b(4) ↔ e5(5)
原来的列表没有任何改变(事实证明我们并没有无缘无故地称其为“不可变”),相反,我们最终得到了一个全新的列表,尽管它具有相同的值。因此,每当我们尝试对不可变的双向链接数据结构进行更改时,我们都需要从头开始重建整个事物,并保留值。
让我们来看看 Scala 标准的单链不可变列表:
e1(1) → e2(2) → e3(3) → e4(4) → 无
我们会注意到,我们能够更轻松地派生新列表,而无需从头开始重建整个数据结构,例如要删除第二个元素,我们只需复制第一个元素并指向它到第三个:
e1(1) → e2(2) → e3(3) → e4(4) → Nil
↗
e1_b(1)
当然,因为原始列表是不可变的,所以它并没有真正改变。