Cocoa 集合类型:NSPointerArray,NSMapTable,NSHashTable
iOS 中有很多种集合类型,最为常见的可能就 NSArray
、NSDictionary
、NSSet
,但其实还有 NSPointerArray
、NSMapTable
、NSHashTable
等类型,虽然后面三个类型不常见,但是它们能在关键时刻,「救你一命」。
环境信息
macOS 10.12.1
Xcode 8.2
iOS 10.2
先来看看传统的集合类型都有哪些短板:
- 放到集合中的对象,只能强引用
- 如果想要弱引用,要先用
NSValue
打包 - 不能放入
nil
而对于 NSPointerArray
、NSMapTable
、NSHashTable
来说,除了以上共有的特点外,还各有各的「特长」,那就一起来看看吧。
NSPointerArray
对应着 NSArray
,先来看看 API 中介绍的特点:
- 和传统 Array 一样,用于有序的插入或移除;
- 与传统 Array 不同的是,可以存储 NULL,并且 NULL 还参与
count
的计算; - 与传统 Array 不同的是,
count
可以 set,如果直接 set count,那么会使用 NULL 占位; - 可以使用 weak 来修饰成员;
- 成员可以是所有指针类型;
- 遵循
NSFastEnumeration
,可以通过for...in
来进行遍历。
初始化
NSPointerArray
与 NSMutableArray
很像,都是可变有序集合。最大的不同就是它们的初始化方法,NSPointerArray
有两个初始化方法:
1 2 | - (instancetype)initWithOptions:( NSPointerFunctionsOptions )options; - (instancetype)initWithPointerFunctions:( NSPointerFunctions *)functions; |
NSPointerFunctionsOptions
首先来看一下 NSPointerFunctionsOptions
,它是个 option,主要分为三大类:
- 内存管理
NSPointerFunctionsStrongMemory
:缺省值,在 CG 和 MRC 下强引用成员NSPointerFunctionsZeroingWeakMemory
:已废弃,在 GC 下,弱引用指针,防止悬挂指针NSPointerFunctionsMallocMemory
与NSPointerFunctionsMachVirtualMemory
: 用于 Mach 的虚拟内存管理NSPointerFunctionsWeakMemory
:在 CG 或者 ARC 下,弱引用成员
- 特性,用于标明对象判等方式
NSPointerFunctionsObjectPersonality
:hash
、isEqual
、对象描述NSPointerFunctionsOpaquePersonality
:pointer 的hash
、直接判等NSPointerFunctionsObjectPointerPersonality
:pointer 的hash
、直接判等、对象描述NSPointerFunctionsCStringPersonality
:string 的hash
、strcmp
函数、UTF-8 编码方式的描述NSPointerFunctionsStructPersonality
:内存hash
、memcmp
函数NSPointerFunctionsIntegerPersonality
:值的hash
- 内存标识
NSPointerFunctionsCopyIn
:根据第二类的选择,来具体处理。
如果是NSPointerFunctionsObjectPersonality
,则根据NSCopying
来拷贝。
所以在使用时,可以多个组合,比如:需要强引用成员、使用对象方式对比、并且 add 时 copy 对象:
1 | NSPointerFunctionsOptions *options = NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality | NSPointerFunctionsCopyIn ; |
NSPointerFunctions
自定义成员的处理方式,如:内存管理、hash
、isEqual
等,可以看到 API 中定义了一系列属性,它们都是函数指针,使用注释分段:
// pointer personality functions
...
// custom memory configuration
可以自行实现函数,然后将函数指针赋给对应属性即可,比如,isEqual
:
1 2 3 4 5 6 | static BOOL IsEqual( const void *item1, const void *item2, NSUInteger (*size)( const void *item)) { return *( const int *)item1 == *( const int *)item2; } NSPointerFunctions *functions = [[ NSPointerFunctions alloc] init]; [functions setIsEqualFunction:IsEqual]; |
compact
之前谈到,NSPointerArray
可以存储 NULL,作为补充,它也提供了 compact
方法,用于剔除数组中为 NULL 的成员。但是 compact
函数有个已经报备的 bug:
1 2 3 4 5 | NSPointerArray *array = [[ NSPointerArray alloc] initWithOptions: NSPointerFunctionsWeakMemory ]; array.count = 10; [array addPointer:[ NSObject new ]]; // compact 调用之后,NULL 并未被剔除 [array compact]; |
出现这种 bug 的原因,和 compact
函数的实现机制有关,当我们主动给 NSPointerArray
添加 NULL 时,数组会标记有空元素插入,此时 compact
函数才会生效,也就是说,compact
函数会先判断是否有标记,之后才会剔除。所以,当直接 set count,或者成员已经释放,自动置空的情况出现时,就会出现这个 bug。解决也很简答:
1 2 3 | // 在调用 compact 之前,手动添加一个 NULL,触发标记 [array addPointer: NULL ]; [array compact]; |
NSMapTable
介绍完 NSPointerArray
,NSMapTable
就方便多了,毕竟都是集合,很多东西都是相通的。
与 NSMapTable
对应的,是 NSMutableDictionary
。除了 集合的共有特点以外,比起传统字典,它还有一些优势:
- key 可以不用遵循
NSCopying
协议; - key 和 value 的内存管理方式可以分开,如:key 是强引用,value 是弱引用;
相比起 NSPointerArray
,NSMapTable
的初始化方法要多得多:
1 2 3 4 5 6 7 8 9 10 11 12 | // 实例方法,虽然有 capacity 参数,但实际没用到 - (instancetype)initWithKeyOptions:( NSPointerFunctionsOptions )keyOptions valueOptions:( NSPointerFunctionsOptions )valueOptions capacity:( NSUInteger )initialCapacity; - (instancetype)initWithKeyPointerFunctions:( NSPointerFunctions *)keyFunctions valuePointerFunctions:( NSPointerFunctions *)valueFunctions capacity:( NSUInteger )initialCapacity; // 便利构造器 + ( NSMapTable <KeyType, ObjectType> *)mapTableWithKeyOptions:( NSPointerFunctionsOptions )keyOptions valueOptions:( NSPointerFunctionsOptions )valueOptions; // 返回指定 key、value 内存管理类型的 map + ( NSMapTable <KeyType, ObjectType> *)strongToStrongObjectsMapTable NS_AVAILABLE (10_8, 6_0); + ( NSMapTable <KeyType, ObjectType> *)weakToStrongObjectsMapTable NS_AVAILABLE (10_8, 6_0); + ( NSMapTable <KeyType, ObjectType> *)strongToWeakObjectsMapTable NS_AVAILABLE (10_8, 6_0); + ( NSMapTable <KeyType, ObjectType> *)weakToWeakObjectsMapTable NS_AVAILABLE (10_8, 6_0); |
其实,这么多的初始化方法就对应着四种搭配:
- key 为 strong,value 为 strong
- key 为 strong,value 为 weak
- key 为 weak,value 为 strong
- key 为 weak,value 为 weak
当用 weak 修饰 key 或 value 时,有一方被释放,则该键值对移除。
NSHashTable
NSHashTable
对应 NSMutableSet
,它的 API 更为简单,与 NSMapTable
同样,初始化方法的 capacity
并未生效。
1 2 | - (instancetype)initWithOptions:( NSPointerFunctionsOptions )options capacity:( NSUInteger )initialCapacity; - (instancetype)initWithPointerFunctions:( NSPointerFunctions *)functions capacity:( NSUInteger )initialCapacity; |
值得注意的是,NSHashTable
有一个 allObjectes
的属性,返回 NSArray
,即使 NSHashTable
是弱引用成员,allObjects
依然会对成员进行强引用。
小结
这类集合类型在 10.5 之后引入,比传统的集合类型更为强大,但是它们的方法却没有传统集合类型多,比如对于 NSPointerArray
来说:
- 操作均基于 index,无法通过 object 来进行操作;
- 无法直接插入 array,或用 array 初始化;
- 查找功能没有
NSArray
强大; - 没有逆序、排序等 API 提供
以上几点仅仅是举的例子,所以 NSPointerArray
也并没有看起来的那么强大,一切选择标准,都应该依据具体需求。
在之前的一个项目中,要用到蓝牙。蓝牙的连接、发送、断开等操作,被我封装成了单例,但是很多地方都需要检测蓝牙当前状态,是否连接、是否发送成功等等,所以需要多个回调。首先考虑传值方式:代理、block、通知。通知我不太考虑,但是代理和 block 对于单例来说,都只能有一个值,一个对象设置以后,另一个就收不到。对于这种情况,就可以选择使用 NSHashTable
。根据 NSHashTable
不持有对象,能添加空对象的特点,几乎完全不会影响成员的生命周期,也不会造成 crash。
所以,还是要先熟悉各种集合的特性,然后去匹配需求,才是最好的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)