恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
掌握Objective-C字典:NSDictionary与NSMutableDictionary完全指南
首页
资讯中心
/
掌握Objective-C字典:NSDictionary与NSMutableDictionary完全指南
掌握Objective-C字典:NSDictionary与NSMutableDictionary完全指南
发布时间:2026/10/15 7:25:44
聊Objective-C的基础十个人里有九个绕不开NSDictionary和NSMutableDictionary。数组解决的是“按位置找东西”字典解决的是“按名字找东西”后者在实际开发里用得甚至更频繁接口返回的JSON是字典页面传参是字典缓存配置也是字典。这篇文章我打算从设计思路、初始化、常用API、遍历、内存语义到真实场景把字典这个容器从头到尾捋一遍。适合刚学OC的学生、从Swift或Java转过来的朋友也适合写了好几年代码但没仔细抠过细节的开发者。标题里虽然写的是“学习”但内容我尽量按工程实践的标准来写——不光是“怎么用”还要讲清楚“为什么这么用”以及“哪些坑一定别踩”。1. 先搞清楚为什么需要字典这种容器1.1 字典和数组的差别不是“换了个语法”那么简单数组存数据靠下标[array objectAtIndex:0]拿第一个元素。问题是下标没有任何业务含义——你看到一个array[3]完全不知道里面是什么。字典不一样它存的是“键值对”每个value都有一个明确的key比如userName - 张三、age - 18。这种“按名取值”的模型更贴近真实业务。与Java的HashMap、Python的dict相比OC的字典有两点很特殊。第一它严格区分可变和不可变版本NSDictionary创建后不能增删元素NSMutableDictionary才允许修改。第二它的整体设计风格是“线程安全优先给只读场景”——不可变字典创建后内容固定多线程同时读没有任何问题这在做缓存、配置快照时特别有用。初学者最容易犯的错是把NSDictionary当成“能改的字典”来用结果在运行时报unrecognized selector。其实这和NSString/NSMutableString是同一个设计哲学不可变对象保证内容稳定可变对象提供修改能力。两者各管一摊想清楚了就会觉得很合理。1.2 哈希表原理字典为什么取数快字典的底层实现是哈希表不是线性表。往字典里插入一个键值对时系统会对key调用hash方法得到一个哈希值再通过哈希值找到对应的存储桶。取值时同样走一遍哈希计算直接定位到目标桶。这个过程的平均时间复杂度是O(1)和数组中“遍历找元素”的O(n)是量级差别。你用字典最大的收益就在这里数据量小的时候数组遍历和字典查找看起来差不多但数据量上百、上千之后字典的优势立刻拉开。比如一个商品列表有10000条按ID找一个商品数组可能平均要循环5000次字典只需要一次哈希计算一次桶内比对。哈希冲突也不需要过度担心底层会用拉链法处理多个key落到同一个桶时在桶里串成一个链表或树结构。OC的NSDictionary经过这么多年的迭代底层实现已经非常成熟尤其是iOS 14之后全面采用新的存储布局性能比早期版本稳得多。1.3 key和value的规则这比想象中重要字典不是随便什么东西都能往里面塞的。首先是key必须遵循NSCopying协议。因为字典在插入时会对key执行一次copy操作——不是把指针存进去而是把key拷贝一份作为内部使用的键。这就带来一个非常重要的推论如果key是可变对象比如NSMutableString你传进字典时它被拷贝成了不可变副本。但如果这个可变对象在加入字典之后、查找之前被修改了而字典内部存的还是旧副本就很容易出现“明明加进去了但取不出来”的怪问题。所以我在项目里有一个不成文的规定能用NSString、NSNumber这种不可变对象做key就不要碰自定义对象和可变对象。如果非要自定义对象做key必须重写isEqual:和hash而且这两个方法的返回值不能依赖任何可变状态。value的约束相对宽松任何OC对象都可以存唯一的大坑是nil。nil在字典里表示“结束标记”你试图把nil作为value存进字典大概率直接崩溃。如果需要“空值”语义用[NSNull null]。2. 从初始化到常用操作手把手过一遍API2.1 创建字典的四种姿势与踩坑点创建NSDictionary的方式不少我挑最常用的几种列出来对比创建方式写法示例适用场景字面量{key : value}最常见可读性好单键值[NSDictionary dictionaryWithObject:forKey:]创建单元素字典多键值[NSDictionary dictionaryWithObjects:forKeys:]批量构建可变字典[NSMutableDictionary dictionary]需要增删改时日常开发里用字面量足够但有两个细节值得注意。第一个是dictionaryWithObjectsAndKeys:方法它的参数顺序是“value在前key在后”而且必须用nil结尾。这个顺序反直觉很多新手写反了导致数据错乱官方后来已经标记废弃新代码不要再用。第二个是字面量{}创建出来的永远是NSDictionary就算你把它赋值给NSMutableDictionary类型的变量运行时本质还是不可变字典此时调用setObject:forKey:会直接崩。想创建可变字典老老实实用[NSMutableDictionary dictionary]或[NSMutableDictionary new]。批量场景下我会更推荐dictionaryWithObjects:forKeys:它接收两个数组一个放value一个放key按顺序一一对应。用之前注意两个数组长度必须一致否则传nil或数组越界都容易出问题。2.2 读取与基础操作这些API太常用了创建完字典日常干得最多的就是取值。老接口是objectForKey:新语法是dict[key]两者等价。取值时key不存在不会崩溃返回nil这点和数组越界完全不同也是字典用起来比数组“温和”的原因之一。看一下最基础的操作清单NSDictionary *dict { name : iPhone, price : 5999, count : 10 }; // 基础信息 NSUInteger count dict.count; // 键值对数量 // 取值 id name [dict objectForKey:name]; // 老接口 id price dict[price]; // 新下标语法 id notExist dict[xxx]; // 返回nil // 获取所有key和value NSArray *allKeys dict.allKeys; NSArray *allValues dict.allValues; // 判断是否存在 BOOL hasKey [dict objectForKey:name] ! nil; // 内容比较 NSDictionary *other { name : iPhone, price : 5999, count : 10 }; BOOL isEqual [dict isEqualToDictionary:other]; // YES比较的是内容allKeys有个小坑需要注意它返回的数组顺序不受你写入顺序控制因为NSDictionary本身是无序容器。后续如果有遍历需求要先排序再展示否则用户看到的数据顺序可能和预期不一致。还有一个很多人不知道的APIobjectsForKeys:notFoundMarker:。它可以一次性批量取多个key对应的value如果某个key不存在就用notFoundMarker填充。这个接口在做批量查询时能省不少循环代码尤其是从大字典里取一组配置项的时候。2.3 NSMutableDictionary的增删改与细节可变字典的接口基本围绕“增、删、改”展开。这里我直接给一个包含完整操作的示例NSMutableDictionary *mDict [NSMutableDictionary dictionary]; // 添加/更新单个键值 [mDict setObject:value1 forKey:key1]; [mDict setObject:1 forKey:number]; // 使用下标语法 mDict[key2] value2; // 批量添加 [mDict addEntriesFromDictionary:{ a : 1, b : 2 }]; // 删除 [mDict removeObjectForKey:key1]; [mDict removeObjectsForKeys:[a, b]]; [mDict removeAllObjects]; // 覆盖性地替换内容清空再写入 [mDict setDictionary:{ x : 10 }];setObject:forKey:有两个大坑。第一value传nil会崩溃第二key传nil也是崩溃。很多新人在解析服务端数据时直接把可能为空的字段塞进去然后程序莫名其妙的挂掉。记住一个习惯所有存进字典的value都先判空为空就用NSNull null代替。setObject:forKey:还有一个隐藏行为如果key已经存在它会把旧值覆盖掉所以“更新”和“新增”用的是同一个方法。你要是想判断“这个key之前有没有”需要先objectForKey:检查一遍。另外注意setValue:forKey:和setObject:forKey:的区别。表面上两者差不多但setValue:forKey:是KVC方法如果value传nil它不会崩溃而是把对应的key从字典里移除。这个差异在实际开发中很容易造成“我明明存了空值结果取的时候key直接没了”的困惑。2.4 copy与mutableCopy深浅拷贝的迷思关于NSDictionary的拷贝网上说法有很多我直接把结论讲清楚。copy对不可变字典是浅拷贝返回的指针可能和原对象相同。mutableCopy会创建一个新的可变字典但内部的元素对象只是指针引用不会递归拷贝。举个例子NSDictionary *dict { key : [NSMutableString stringWithString:value] }; NSMutableDictionary *mDict [dict mutableCopy]; NSString *innerValue mDict[key];这里的innerValue和原字典里的value指向同一个对象。你通过mDict修改这个字符串原字典取出来的也会变——因为底层是同一个对象。这就是“单层深拷贝”外层容器是新对象内层元素仍然共享。开发中很容易忽略的是NSArray/NSDictionary嵌套结构的“深拷贝”。copy和mutableCopy都不会深入容器内部。如果你需要“容器和元素都彻底独立”的拷贝要自己实现递归拷贝通常用归档解档NSData *data [NSKeyedArchiver archivedDataWithRootObject:dict requiringSecureCoding:NO error:nil]; NSDictionary *newDict [NSKeyedUnarchiver unarchiveObjectWithData:data];这个方法做的是真正的深拷贝代价是性能开销比较大只适合数据量不大、且确实需要完全独立的场景。3. 遍历、排序与KVC让字典真正好用起来3.1 三种遍历方式与枚举时修改的问题NSDictionary最常用的遍历方式有三种快速枚举、block枚举、迭代器枚举。NSDictionary *dict { name : 小明, age : 18, city : 北京 }; // 方式一快速枚举遍历key for (NSString *key in dict) { id value dict[key]; NSLog(% - %, key, value); } // 方式二block枚举同时拿到key和value [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSLog(% - %, key, obj); if ([key isEqualToString:age]) { *stop YES; // 可以提前终止 } }]; // 方式三keyEnumerator迭代器 NSEnumerator *enumerator [dict keyEnumerator]; NSString *key; while ((key [enumerator nextObject])) { id value dict[key]; NSLog(% - %, key, value); }三种方式在实际使用中差别不大但有一个极度重要的禁忌快速枚举过程中不能修改字典哪怕你是NSMutableDictionary也不可以。一旦在for-in循环里执行removeObjectForKey:或setObject:forKey:程序会在运行时抛出“collection was mutated while being enumerated”的异常。解决思路很简单先把要改的key收集到数组里循环结束后再把数组里的key逐个移除。block枚举方法里同样有这个限制虽然它提供了*stop指针让你能退出但“边遍历边修改”依然是危险操作。我习惯的做法是NSMutableDictionary *mDict [NSMutableDictionary dictionaryWithDictionary:{a:1, b:2, c:3}]; NSMutableArray *keysToRemove [NSMutableArray array]; [mDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if ([obj integerValue] 2) { [keysToRemove addObject:key]; } }]; [mDict removeObjectsForKeys:keysToRemove];3.2 排序与过滤处理业务数据的关键字典本身无序但很多展示场景要求有序。常见做法是把allKeys取出来用sortedArrayUsingSelector:或sortedArrayUsingComparator:排序再按排序后的key顺序取值。NSDictionary *scoreDict { 张三 : 85, 李四 : 92, 王五 : 76 }; // 按key字符串排序 NSArray *sortedKeys [scoreDict.allKeys sortedArrayUsingSelector:selector(compare:)]; // 按value数值排序从高到低 NSArray *sortedKeysByValue [scoreDict keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj2 compare:obj1]; }];过滤和筛选则可以用keysOfEntriesPassingTest:它和block枚举很像返回符合条件的key集合NSSet *passKeys [scoreDict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return [obj integerValue] 80; }];这类API适合在内存中对小批量的配置数据做筛选比如从权限字典里挑出所有可访问的模块。需要提醒的是keysOfEntriesPassingTest:返回的是NSSet不是NSArray需要遍历时记得先转成数组避免顺序问题。3.3 字典与KVCvalueForKey的隐藏行为NSDictionary支持KVC也就是可以直接用[dict valueForKey:key]取值。很多老代码喜欢这么写看起来简洁但有个非常隐蔽的坑valueForKey:会先检查字典有没有对应名称的方法如果方法存在它返回的是方法调用的结果而不是字典里存储的值。举个例子如果你有个字典key是count值为10然后执行[dict valueForKey:count]返回的很可能不是10而是字典里键值对的个数——因为NSDictionary本身有count方法。类似冲突的方法名还有description、allKeys等。所以我的建议是日常取值优先用objectForKey:或下标语法KVC只在绑定场景比如KVO、模型转换中使用。有一个相对实用的小技巧给NSDictionary发setValue:forKey:当value为nil时不会崩溃而是执行移除操作。如果需要向可变字典中显式写入“空值”必须使用[NSNull null]否则写进去的key会直接被移除和预期完全相反。4. 实战案例把字典用在真实业务里4.1 场景一解析接口返回的JSONJSON解析是NSDictionary出现频率最高的场景。NSJSONSerialization把一段JSON data转换后通常会返回NSDictionary或NSArray取决于顶层是{}还是[]。解析时第一件事是类型判断不能假设一定是字典NSError *error nil; id jsonObj [NSJSONSerialization JSONObjectWithData:data options:0 error:error]; if (!jsonObj) { NSLog(JSON解析失败: %, error); return; } if (![jsonObj isKindOfClass:[NSDictionary class]]) { NSLog(顶层不是字典); return; } NSDictionary *jsonDict (NSDictionary *)jsonObj; NSString *name [jsonDict objectForKey:name]; NSNumber *status [jsonDict objectForKey:status];这里有三个经常踩的坑。第一个是服务端返回的字段名可能是nullJSON中的null转换后是NSNull对象objectForKey:不会返回nil而是返回[NSNull null]。直接拿它做字符串拼接或判断会异常需要对NSNull做一次判断。第二个是接口返回的数值可能是字符串也可能是数字直接用objectForKey:拿到的类型不一定是你想要的。第三个是嵌套层级较深时每一层都要判空否则很容易出现“多层字典取值崩溃”的连锁问题。我习惯封装一个小工具方法专门处理JSON字典的取值- (id)safeObjectForKey:(NSString *)key fromDict:(NSDictionary *)dict { id obj dict[key]; if ([obj isKindOfClass:[NSNull class]]) { return nil; } return obj; }4.2 场景二用字典做分组统计字典非常适合做计数和分组。比如我有一个单词数组要统计每个单词出现的次数NSArray *words [apple, banana, apple, orange, banana, apple]; NSMutableDictionary *counter [NSMutableDictionary dictionary]; for (NSString *word in words) { NSNumber *oldCount counter[word]; NSUInteger newCount oldCount ? oldCount.integerValue 1 : 1; counter[word] (newCount); } // 输出结果 [counter enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSLog(% 出现 % 次, key, obj); }];这里用了一个小技巧如果counter[word]不存在返回的是nilnil的任何方法调用都返回0或NO所以可以写成([counter[word] integerValue] 1)当旧值不存在时就是0 1逻辑依然正确。这种“对nil发消息返回0”是OC的特性也是很多人觉得OC“神”的一个点不过为了可读性我还是推荐显式判空。分组统计也一样比如把一批学生按班级分组key是班级名value是可变的数组往里面追加成员即可。这个场景里尤其要注意value必须是可变容器而且用之前要判断容器存不存在不存在就先创建一个。4.3 场景三用字典做轻量缓存字典还常被当作最简单的内存缓存来用。比如图片加载模块以URL字符串为key把下载好的图片数据缓存到NSMutableDictionary里interface ImageCache : NSObject property (nonatomic, strong) NSMutableDictionary *cache; - (UIImage *)imageForKey:(NSString *)key; - (void)setImage:(UIImage *)image forKey:(NSString *)key; end这种轻量缓存的优点是实现简单、读取快适合中等规模的数据缓存。但它不是为“大缓存”设计的原因主要有两个一是字典没有淘汰策略缓存会无限增长二是没有线程安全保证多线程同时读写需要加锁。如果只是单线程、数据量不大用字典做缓存完全够用。一旦涉及多线程、内存压力大就应该考虑专用缓存工具了。对字典缓存做多线程保护时我的做法是给访问方法加一个串行队列所有读写都丢进队列里执行既保证线程安全又不会让代码复杂到失控。5. 高频问题与排查技巧实录5.1 nil导致的崩溃字典最经典的坑字典相关的崩溃90%都和nil有关。总结一下最常见的三种场景原因解决方案setObject:forKey:传入nilvalue为nil导致内部异常判空后用NSNull兜底setObject:forKey:key为nilkey为nil导致内部异常判空后跳过或使用默认keydict[nil]下标取值nil下标触发异常先判断key是否存在我在团队review代码时只要看到“往字典里塞值”的代码一定会追问一句“这个value有没有可能为nil”服务端接口凡是没返回的字段转成NSNull是常态而新手往往默认它是nil两者处理逻辑完全是两回事。5.2 线程安全多线程读写的正确姿势NSMutableDictionary不是线程安全的。多个线程同时写入轻则数据丢失重则直接崩溃。常见错误是以为“只是加个值没什么问题”实际运行时会因为内部存储结构被并发修改而触发异常。我常用的保护方案有两个。第一种是串行队列包装property (nonatomic, strong) dispatch_queue_t ioQueue; property (nonatomic, strong) NSMutableDictionary *cacheDict; - (void)setObject:(id)obj forKey:(NSString *)key { dispatch_sync(self.ioQueue, ^{ [self.cacheDict setObject:obj forKey:key]; }); }第二种是加锁property (nonatomic, strong) NSLock *lock; - (id)objectForKey:(NSString *)key { [self.lock lock]; id obj self.cacheDict[key]; [self.lock unlock]; return obj; }两种方案都能用区别在于并发粒度不同。串行队列更简单锁则可以做更细粒度的控制。真正要记住的是不要裸用NSMutableDictionary做跨线程共享数据。5.3 字典与属性声明的选择strong还是copy自定义类里的属性如果是NSDictionary类型我几乎一律写copyproperty (nonatomic, copy) NSDictionary *config;原因是防止外部传入一个NSMutableDictionary后在外部又被悄悄修改导致内部数据不完整。用copy修饰后赋值时系统会自动拷贝一份不可变副本从源头上隔离外部影响。这个细节在模型层尤其重要。比如一个配置模型外部很可能先构建一个可变字典再赋值给模型的属性。如果属性用strong外部后续的增删改会直接污染模型内部数据排查起来非常痛苦。用copy虽然多了一次拷贝开销但换来的是数据稳定性完全值得。5.4 选型对比什么时候别用NSDictionary字典虽好用但并不是所有“键值场景”都适合它。简单做一个对比容器特点适用场景NSDictionary键强引用拷贝key无序业务数据大部分场景NSMapTablekey可以弱引用支持自定义相等性对象关联、内存敏感的观察者列表NSCache自带淘汰策略适合缓存图片缓存、大对象缓存NSMapTable比较冷门但它在某些场景确实是更好的选择。比如一个组件想要“监听某些对象”又不想让监听关系阻止对象释放NSMapTable的弱引用key正好解决这个问题。它牺牲了一点点性能换来了灵活的内存管理这在做工具类框架时很有用。日常开发我建议还是先考虑NSDictionary性能足够好、API丰富、团队熟悉。只有明确遇到“内存关联”“自动淘汰”需求时才需要换容器。最后分享一个我自己用字典的心得写代码前养成“假设value永远是未知类型”的习惯每次取值后都做一次类型或nil判断看似繁琐但能避免大量线上问题。字典这个容器看起来简单拆开讲也有哈希、拷贝、线程安全这些硬知识点。你把这些细节吃透了后面写模型层、网络层都会顺畅很多。