Swift:使用Set.insert()的间歇,不一致,意外行为

时间:2019-01-05 04:25:15

标签: swift set

编辑:我从没想过在删除派生数据后重新启动Xcode。现在一切都按预期运行。

我的应用程序遇到间歇性故障。我已将其缩小到Set.insert()有点奇怪。有时,插入会导致对我的==函数的调用,有时没有原因,没有明显的原因。这是我可以拿出的最好的精简样本。它在操场上跑。

// Results are the same whether I use Equatable or not
struct ID: Hashable, Equatable {
    let idNumber: Int

    // Results are the same whether I implement this or not
    func hash(into hasher: inout Hasher) {
        hasher.combine(idNumber)
    }

    // Always return true; it doesn't matter. What matters
    // is that sometimes the Set.insert() doesn't even
    // call this function.
    static func == (_ lhs: ID, _ rhs: ID) -> Bool {
        print("(eq)", terminator: ""); return true
    }
}

let id0 = ID(idNumber: 0)
let id1 = ID(idNumber: 1)

var uniqueIDs = Set<ID>()

print("a", terminator: "")
uniqueIDs.insert(id0)
print("b", terminator: "")
uniqueIDs.insert(id1)
print("c", terminator: "")

如果我在操场上跑步十次,大约一半的时间我会在输出中看到eq,而另一半则没有。也就是说,大约有一半的时间Set.insert()在尝试插入之前没有呼叫我的==

我阅读了Swift的书,却没有发现任何东西。我有点认为,如果这是预期的行为,则将在其上附加一个大的警告标志来记录该行为。缺少此类警告表明我正在滥用Sets,但是我不知道自己在做什么错。我错过了什么文档或答案?

2 个答案:

答案 0 :(得分:2)

如果没有值冲突,

Set没有理由在您的类型上调用==。我这是一条红鲱鱼。

Set对您的值调用hash(into hasher: inout Hasher),然后对集合的内部数组大小取模。结果是一个索引,该值(如果该值已存在于集合中)应位于该索引处。自然地,此过程使散列和取模后的多个值可以在同一数组插槽中结束。

为了弥补这一点,不是将元素直接存储在数组插槽中,而是通过链接列表存储它们。从概念上讲,同一插槽内的项目称为“存储桶”。在查找元素时,Set使用哈希值来查找正确的存储桶,但需要遍历链接列表以查找确切的元素。此时,散列不再对标识元素有用,因此Set使用==检查,直到找到正确的匹配为止。这通常非常有效,因为Set应该使数组足够大,以使存储桶很小并且包含很少的冲突。

因为在存储桶中找到一个元素是O(N),所以如果您可以强制执行许多哈希冲突,则可以强制Set的{​​{1}}的插入/删除/检查操作退化为O(1)遍历整个O(N)的元素(因为您可以使所有元素映射到单个存储桶。为了抵抗DOS漏洞,现代关联数据结构使用每次都会随机选择的“种子”运行该应用程序,并使用它来打乱哈希值,这样就很难制作具有相同哈希值的有效负载(这会导致存储桶过大)。这就是不确定性的来源。 PSA: The stdlib now uses randomly seeded hash values

从根本上讲,Set实际上只是类型为Set<T>的{​​{1}}。因此,如果您阅读了基于散列的关联数据结构(其他通用名称如Dictionary,Hash,HashMaps等)的工作原理,则同样适用。

答案 1 :(得分:0)

看起来像操场一样不知所措。这是因为,我在空白应用程序项目中输入的相同代码具有与操场上相同代码不同的输出。请看一下:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        doSmth()
        return true
    }

    func doSmth(){


        // Results are the same whether I use Equatable or not
        struct ID: Hashable, Equatable {
            let idNumber: Int

            // Results are the same whether I implement this or not
            func hash(into hasher: inout Hasher) {
                hasher.combine(idNumber)
            }

            // Always return true; it doesn't matter. What matters
            // is that sometimes the Set.insert() doesn't even
            // call this function.
            static func == (_ lhs: ID, _ rhs: ID) -> Bool {
                // print("(eq)", terminator: "");
                // return false
                return true
            }
        }

        let id0 = ID(idNumber: 0)
        let id1 = ID(idNumber: 1)
        let id2 = ID(idNumber: 2)

        var uniqueIDs = Set<ID>([id0, id1])

        uniqueIDs.insert(id0)
        uniqueIDs.insert(id1)
        uniqueIDs.insert(id2)
        uniqueIDs.insert(id0)
        uniqueIDs.insert(id1)

        uniqueIDs.forEach{ value in
            print("value - \(value)")
        } 
    }
}

它打印出来:

value - ID(idNumber: 0)
value - ID(idNumber: 1)
value - ID(idNumber: 2)

没有重复项(即使我尝试添加重复项也是如此)。