You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NSCountedSet如何判定Swift结构体的唯一性?

NSCountedSet如何判定Swift结构体的唯一性?

这坑我之前刚踩过!你肯定是把Swift原生Set的逻辑和Objective-C的NSCountedSet搞混了——这俩判断元素是否相等的规则完全不是一回事儿,你的自定义Hashable实现对NSCountedSet来说等于白写!

核心原因:NSCountedSet只认Objective-C那套规则

NSCountedSet是Foundation框架里的Objective-C老古董,它判断两个元素是否“相同”,只认Objective-C的isEqual:和hash方法,根本不鸟Swift的Hashable协议。

你给Person结构体实现了hash(into:)只哈希name,但这只会影响Swift原生的Set、Dictionary这些容器的判断逻辑。而NSCountedSet作为Objective-C的产物,它看的是:

  1. 对于Swift结构体,系统会自动把它桥接成Objective-C的包装对象,但默认的isEqual:实现是比较结构体的所有存储属性(包括你的id、age、address这些);
  2. 哪怕你自定义了Swift的Hashable,Objective-C的hash方法默认还是基于所有属性生成的,和你写的hash(into:)没关系。

所以你那些同name但其他属性不同的Person实例,在NSCountedSet眼里就是完全不同的对象——比如三个Bob,因为age、address不一样,isEqual:返回false,自然会被当成三个独立元素,计数都是1,最后就有8个“唯一项”了。

怎么解决?两种方案任你选

方案1:把结构体改成类,重写Objective-C的isEqual:和hash

如果一定要用NSCountedSet,那把Person从struct改成class,然后重写Objective-C的相等判断方法:

class Person: NSObject, CustomStringConvertible {
    var name: String
    var id: Int?
    var age: Int?
    var address: String?
    
    init(name: String, id: Int? = nil, age: Int? = nil, address: String? = nil) {
        self.name = name
        self.id = id
        self.age = age
        self.address = address
        super.init()
    }
    
    // 重写Objective-C的isEqual:,只比较name
    override func isEqual(_ object: Any?) -> Bool {
        guard let otherPerson = object as? Person else {
            return false
        }
        return self.name == otherPerson.name
    }
    
    // 重写Objective-C的hash,返回name的哈希值
    override var hash: Int {
        return name.hashValue
    }
    
    var description: String {
        return "Person(name: \(name), id: \(String(describing: id)), age: \(String(describing: age)), address: \(String(describing: address)))"
    }
}

再跑你原来的测试代码,就会得到你预期的结果:5个唯一项,Bob计数3,John计数3,其他都是1。

方案2:放弃NSCountedSet,用Swift原生容器模拟计数

如果不想改结构体,那直接用Swift的Dictionary来模拟计数集合就好——因为Swift的Dictionary会遵守你自定义的Hashable规则:

// 保持你原来的Person结构体,补全Hashable协议的==实现
struct Person: Hashable, CustomStringConvertible {
    var name: String
    var id: Int?
    var age: Int?
    var address: String?
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
    
    // 必须补这个!Swift要求hash相等的元素,==必须返回true
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name
    }
    
    var description: String {
        return "Person(name: \(name), id: \(String(describing: id)), age: \(String(describing: age)), address: \(String(describing: address)))"
    }
}

// 用Dictionary模拟计数集合
let people = [
    Person(name: "Bob"),
    Person(name: "Charlotte"),
    Person(name: "John"),
    Person(name: "Bob", address: "456 Elm St"),
    Person(name: "James"),
    Person(name: "John", age: 47),
    Person(name: "Sophie"),
    Person(name: "John"),
    Person(name: "Bob", age: 33, address: "123 Main St")
]

var countDict: [Person: Int] = [:]
for person in people {
    countDict[person, default: 0] += 1
}

print("Dictionary contains \(countDict.count) unique items")
for (person, count) in countDict {
    print("\(person.description). 计数:\(count)")
}

这里要注意:Swift的Hashable协议有个硬性要求——如果两个元素==返回true,它们的hash值必须相等。所以只写hash(into:)还不够,必须补一个==运算符的重载,不然Swift的容器可能会出各种莫名其妙的问题。

最后敲个黑板

  • 用Swift容器(Set、Dictionary)→ 遵守Swift的Hashable协议,实现hash(into:)和==;
  • 用Objective-C容器(NSCountedSet、NSSet)→ 要么改成NSObject子类,重写isEqual:和hash;要么直接换Swift容器,省得来回折腾。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 11:18:05