我想知道为什么这不会在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时,将indexHashTable计算为(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

其中ps的内部表的初始插槽数(素数,在初次创建该集时设置为57,然后根据需要增加。)

到现在为止还挺好。但是可能会有一些

问题

在哪里?根据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 <= 10true,该方法将尝试再次递归计算s hash,导致堆栈溢出。

结论
  • 实现Collection >> #printOn:时要小心。即使该集合不包含其自身,但是如果存在从元素之一到包含它的集合的间接引用,则可能会发生递归。
  • 实现Collection >> #hash时要小心(相同的原因)
  • 向自身添加Collection时要小心。更一般而言,当集合包含一个带有(可能是间接)引用的元素时要小心,因为枚举这样的集合可能很棘手。
  • 在数学(科学)中,集合不能是其本身的元素(集合理论公理明确禁止这样做)。因此,在违反源自极其成熟和发展的科学知识体系的规则之前,请重新考虑您的模型。
  • 09-30 13:55
    查看更多