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的产物,它看的是:
- 对于Swift结构体,系统会自动把它桥接成Objective-C的包装对象,但默认的
isEqual:实现是比较结构体的所有存储属性(包括你的id、age、address这些); - 哪怕你自定义了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

