我想知道为什么这不会在GNU Smalltalk中终止:s := Set new. s add: s
从理论上讲,s
应该只是一个包含空集的集。但是执行该操作将永远循环下去,炸毁堆。
有趣的是,((s := Set with: 4 with: 5 with: 6) add: s) size.
终止并求值为4。
最佳答案
一个介绍Set
是专门用于快速成员资格检查的一种HashedCollection
。内部Set
具有HashTable
,这是一个稀疏数组,具有许多空插槽,以最大程度地减少冲突次数。当我们将元素#add:
编码为Set
时,将index
的HashTable
计算为(hash \\ size) + 1
,其中#\\
是mod操作,而size
是表的长度。如果index
处的插槽已被占用,则index
会递增,直到找到可用插槽为止,然后将元素存储在此处(N.B.,+ 1
,因为Smalltalk数组是基于1
的。) How does Set actually work]
我们的情况
现在,让我们看看问题代码会发生什么:
1. s := Set new.
2. s add: s.
如上所述,在步骤2中,
add: s
将计算:s hash \\ p + 1
其中
p
是s
的内部表的初始插槽数(素数,在初次创建该集时设置为5
或7
,然后根据需要增加。)到现在为止还挺好。但是可能会有一些
问题
在哪里?根据Smalltalk方言的不同,
#printOn:
或元素的下一个add:
位置可能会出现问题。打印问题
#printOn:
可能发生的问题是无限递归。当打印s
时,人们也想打印其元素,而在我们的案例中,这种方法将递归地尝试在此过程中打印s
,从而产生无尽的圆度。为了防止这种情况的发生,
Pharo
使用了LimitedWriteStream
,它将在经过一定数量的迭代后停止写操作,并破坏了递归(如果存在)。我还没有亲自检查过,但这是GNU Smalltalk中似乎正在发生的问题(根据问题)。
请注意,仅在
Set
中仅打印最大数量的元素是不够的。实际上,我们的集合仅包含一个元素(本身),足以创建递归。附加问题
正如@ aka.nice在评论中所观察到的,第二次添加
s
时也必须小心。为什么?因为,正如我们在上面的简介中所看到的,消息add: s
将需要计算s hash …
。以及如何定义s hash
?这是个有趣的问题。 Smalltalkers通常会遇到在某些类中实现良好的#hash
的问题。由于s
是一个集合,因此很容易考虑其元素的hash
以获取最终结果,对吗? Pharo采用这种方法,请看:hash
| hash |
hash := self species hash.
self size <= 10 ifTrue:
[self do: [:elem | hash := hash bitXor: elem hash]].
^hash bitXor: self size hash
吞下了诱饵。为什么?因为集合
s
(本身)有1个元素,所以条件self size <= 10
是true
,该方法将尝试再次递归计算s hash
,导致堆栈溢出。结论
Collection >> #printOn:
时要小心。即使该集合不包含其自身,但是如果存在从元素之一到包含它的集合的间接引用,则可能会发生递归。 Collection >> #hash
时要小心(相同的原因)Collection
时要小心。更一般而言,当集合包含一个带有(可能是间接)引用的元素时要小心,因为枚举这样的集合可能很棘手。