注:本文摘自《编写高质量iOS与OS X代码的52个有效方法》第7章第48条并进行了相关整理和扩充
我们在编程中,经常性的需要列举
collection
中的元素,在OC中,有多种形式来实现该功能:
- C语言中的for循环
- Objective-C 1.0 的
NSEnumerator
- Objective-C 2.0 的快速遍历
- 基于块的遍历方式
开发中,我们常常使用第一种和第三种遍历方式,而往往忽略其他几种遍历方式,但实际上,某些情况下使用其他的方法会大幅度简化编码的过程。
注:本文中所讲的
collection
,包含NSArray
,NSDictionary
,NSSet
这几个频繁使用的类型。
遍历数组我们最常采用的方法就是采用for循环:
NSArray *anArray = /*...*/;
for (int i = 0 ; i < anArray.count; i ++){
id object = anArray[i];
//Do something with 'object'
}
NSDictionary *aDictionary = /*...*/;
NSArray *keys = [aDictionary allKeys];
for(int i = 0; i < keys.count; i ++){
id key = keys[i];
id value = aDictionary[key];
//Do something with 'key' and 'value'
}
NSSet *aSet = /*...*/;
NSArray *objects = [aSet allObjects];
for(int i = 0; i < objects.count; i ++){
id object = objects[i];
//Do something with 'object'
}
可以看到,遍历数组的过程还比较简便。但是字典和set相对来说比较复杂。由于字典与set都是无序的,无法根据特定的整数下标来直接访问其中的值,必须先获取字典里的所有的键或是set里的全部对象,为了保存字典里所有的键或是set中的全部对象,我们必须要创建额外的一个数组对象,用于保留collection
中的这些元素对象。相对的,释放操作时,也要将这些额外的对象释放掉,执行了本来不需要执行的方法。相比较而言,其他的方法则无需创建这种中介数组。
当然for循环也可以实现反向遍历,只需要将计数器i
的值从元素个数递减。就反向遍历而言,相比较其他的方法简便许多。
NSArray *anArray = /*...*/;
for (int i = anArray.count ; i >= 0; i --){
id object = anArray[i];
//Do something with 'object'
}
NSEnumerator
是一个抽象的基类,定义中只提供了一个公共方法和一个公共属性供子类来实现。
@property (readonly , copy)NSArray *allObjects;
-(nullable ObjectTye)nextObject;
其中,nextObject
方法返回的是枚举里的下一个对象。每次调用该方法,其内部的数据结构都会更新,使下次调用发放时能返回下一个对象。如果枚举中的全部对象都已返回,再次调用就会返回nil,表示枚举结束。
Foundation
框架中的collection
类都实现了这种遍历方式。
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil){
//Do something with 'object'
}
这种方式的优势在于:不论遍历哪种collection,都可以采用如上类似的语法。
NSDictionary *aDictionary = /*...*/;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil){
//Do something with 'key' and 'value'
}
遍历字典的方式与数组和set有所不同,由于字典中既有键也有值,需要根据给定的键将对应的值取出来。
NSSet *aSet = /*...*/;
NSEnumerator *enumerator = [aSet objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil){
//Do something with 'object'
}
NSEnumerator
具有多种枚举器,如,反向遍历的枚举器,使用它就可以按反方向进行遍历了。
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [aSet reverseObjectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil){
//Do something with 'object'
}
相较for循环而言,上面的代码更具可读性。
Objective-C 2.0加入了快速遍历的功能,其语法比较简洁,它为for循环添加了in关键字。
如果某一个类支持快速遍历,只需遵从NSFastEnumeration
协议,该协议只定义了一个方法:
-(NSUInteger)countByEnumeratingWithStat:
(NSFastEnumerationState*)state
objects:(id*)stackbuffer
count:(NSUInteger)length;
该方法允许类实例同时返回多个对象,这就使得循环遍历操作更为高效了。
NSArray *anArray = /*...*/;
for(id object in anArray){
//Do something with 'object'
}
NSDictionary *aDictionary = /*...*/;
for(id key in aDictionary){
id value = aDictionary[key];
//Do something with 'key' and 'value'
}
NSSet *aSet = /*...*/;
for(id object in aSet){
//Do something with 'object'
}
由于NSEnumerator
对象也实现了NSFastEnumeration
协议,所以能用来执行反向遍历。
NSArray *anArray = /*...*/;
for(id object in [anArray reverseObjectEnumerator]){
//Do something with 'object'
}
后来的Objective-C中,又引入了一种新的做法,它可以实现最基本的遍历功能,这就是基于块的遍历方式。除此之外,还有一系列类似的遍历方法,他们可以接受各种选项,以控制遍历操作。
在遍历数组以及set时,每次迭代都要执行由block参数所传入的,这个块中有三个参数,分别是当前迭代所针对的对象object
,当前迭代所针对的下标idx
,以及指向布尔值的指针*stop
。通过第三个参数所提供的机制,开发者可以用其终止遍历操作。
NSArray
中定义了如下的方法:
-(void)enumerateObjectsUsingBlock:
(void(^)(id object,NSUInteger idx,BOOL *stop))block;
遍历方式如下:
NSArray *anArray = /*...*/;
[anArray enumerateObjectsUsingBlock:^(id object,NSUInteger idx,BOOL *stop){
//Do something with 'object'
if(needStop){
*stop = YES;
}
}];
虽然,该种方法代码略多一些,但是思路却很清晰,而且在遍历时可以获取对象和下标。开发者可以通过设定stop变量值来终止遍历操作,当然,使用其他几种遍历方式时,也可以通过break
来终止循环。
遍历字典所用的方法略有不同:
-(void)enumerateKeysAndObjectsUsingBlock:
(void(^)(id key , id object, BOOL *stop))block;
遍历方式如下:
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key,id object,BOOL *stop){
//Do something with 'key' and 'value'
if(needStop){
*stop = YES;
}
}];
遍历set的方法与array相同。
NSSet *aSet = /*...*/;
[aSet enumerateObjectsUsingBlock:^(id object,BOOL *stop){
//Do something with 'object'
if(needStop){
*stop = YES;
}
}];
这种方式的优点在于:遍历时可以直接从块里获取更多的信息。遍历数组和有序set时可以知道当前所针对的下标,而在遍历字典时,无需额外编码,即可同事获取键与值,省去了根据给定的键来获取对应值这一步。
另一个好处是能够修改块的方法签名,以此免除类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给了块方法的签名去做。比如,使用“快速遍历”,将字典中的对象转化为字符串,我们可以这样做:
for(NSString * key in aDictionary){
NSString *object = (NSString *)aDictionary[key];
//Do something with 'key' and 'object'
}
如果改用块的方式来遍历,就可以直接在块的方法签名中直接转换:
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:
^(NSString *key, NSString *obj,BOOL *stop){
//Do something with 'key' and 'obj'
}];
之所以能够如此,是因为id类型相当特殊,它可以被其他类型所复写。要是原来的块签名把键值都定义成NSObject*,那就不可以了。该技巧看似不显眼,实际上非常有用,指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不具备的方法,并在发现这种问题时报错。
用此方式也可以执行反向遍历。数组、字典、set都实现里前述方法的另一个版本,令开发者可以向其中传入“选项掩码”(option mask):
-(void)enumerateObjectsWithOptions:
(NSEnumerationOptions)options
usingBlock:(void(^)(id obj,NSUInteger idx,BOOL *stop))block;
-(void)enumerateKeysAndObjectsWithOptions:
(NSEnumerationOptions)options
usingBlock:(void(^)(id key,id obj,BOOL *stop))block;
NSEnumerationOptions
的定义如下:
typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
NSEnumerationConcurrent = (1UL << 0),//并发
NSEnumerationReverse = (1UL << 1),//反向
};
NSEnumerationOptions
类型是一个enum,其取值可用“按或位”链接。开发者可以请求以并发执行各轮迭代,通过NSEnumerationConcurrent
选项即可开启此功能。反向遍历则是通过NSEnumerationReverse
选项来实现,当然,只有在遍历数组或有序set等有循序的collection
时,这个选项才有意义。
总体来说,基于块的遍历方式拥有其他的遍历方式都具备的优势,能提供遍历时所针对的下标,在遍历字典时也可以同时提供键与值,而且还有选项可以开启并发迭代功能,因此,代码多一些也是值得的。
NSEnumerator
相关方法进行遍历NSEnumerator
思路清晰,阅读性更佳NSEnumerator
实现反向遍历collection
中含何种的对象,可以修改块签名,指出对象的具体类型