7.6 集合(NSSet与NSMutableSet)
NSSet类似于一个罐子,一旦把对象“丢进NSSet”集合,集合里多个对象之间没有明显的顺序。NSSet集合不允许包含相同的元素,如果试图把两个相同的元素放在同一个NSSet集合中,则只会保留一个元素。
7.6.1 NSSet的功能与用法
NSSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
NSSet不能保证元素的添加顺序,顺序有可能发生变化。与NSArray相比,NSSet最大的区别是元素没有索引,因此不能根据索引来操作元素,前面介绍NSArray时介绍的所有有关索引的方法都不适用于NSSet。
实际上,NSArray与NSSet依然有大量的相似之处,NSSet与NSArray在如下方面的调用机制都非常相似:
- 都可通过count方法获取集合元素的数量。
- 都可通过快速枚举来遍历集合元素。
- 通过objectEnumerator方法获取NSEnumerator枚举器对集合元素进行遍历。由于NSSet集合本身就是无序的,因此,提供反向迭代器没有意义。
- 都提供了makeObjectsPerformSelector:、makeObjectsPerformSelector:withObject:方法对集合元素整体调用某个方法,以及enumerateObjectsUsingBlock:、enumerateObjectsWithOptions:usingBlock对集合整体或部分元素迭代执行代码块。
- 都提供了valueForKey:和setValue:forKey:方法对集合元素整体进行KVC编程。
- 都提供了集合的所有元素和部分元素进行KVO编程的方法。
从功能上来看,NSArray相当于NSSet的子类——虽然Objective-C在语法中并没有提供这种支持。
NSArray与NSSet的区别:
NSSet代表集合元素无索引且不允许重复的集合
NSArray则代表集合元素有索引且允许重复的集合
因此,可认为NSSet代表通用的集合,而NSArray则在NSSet基础上扩展了功能:主要是让集合元素有索引,因此,NSArray允许通过索引来操作集合元素。
NSSet的基本用法:
NSSet同样提供了类方法和实例方法来初始化NSSet集合,其中以set开头的方法是类方法,以init开头的方法是实例方法。
获取NSSet对象后,接下来就可以调用NSSet的方法来访问集合元素、遍历集合元素和筛选集合元素。
除了前面介绍的与NSArray相似的方法之外,NSSet包含如下常用的方法:
- setByAddingObject::向集合中添加一个新元素,返回添加元素后的新集合。
- setByAddingObjectsFromSet::使用NSSet集合向集合中添加多个新元素,返回添加元素后的新集合。
- setByAddingObjectsFromArray::使用NSArray集合向集合中添加多个新元素,返回添加元素后的新集合。
- allObjects:返回该集合中所有元素组成的NSArray。
- anyObject:返回该集合中的某个元素。该方法返回的元素是不确定的,但该方法并不保证随机返回集合元素。
虽然该方法每次返回的元素是不确定的,但是不是随机的。只要一个NSSet没有发生改变,无论多少次调用该方法,返回的总是同一个集合元素。
- containsObject::判断集合是否包含指定元素。
- member::判断该集合是否包含与该参数相等的元素,如果包含,返回相等的元素;否则返回nil。
- objectsPassingTest::需要传入一个代码块对集合元素进行过滤,满足该代码块条件的集合元素被保留下来并组成一个新的NSSet集合作为返回值。
- objectsWithOptions:passingTest::与前一个方法的功能基本相似,只是可以额外地传入一个NSEnumerationOptions迭代选项参数。
NSSet集合的基本用法:
//定义一个函数,该函数用于把NSSet集合转换为字符串
NSString* NSCollectionToString(id collection) {
NSMutableString* result = [NSMutableString
stringWithString:@"["];
for (id object in collection) {
[result appendString:[object description]];
[result appendString:@","];
}
NSUInteger len = [result length]; //获取字符串长度
//去掉字符串最后的一个字符
[result deleteCharactersInRange:NSMakeRange(len - 1, 1)];
[result appendString:@"]"];
return result;
}
int main(int argc, char* argv[]) {
@autoreleasepool {
//用4个元素初始化NSSet集合
//故意传入两个相等的元素,NSSet集合只会保留一个元素
NSSet* set1 = [NSSet setWithObjects:@"疯狂iOS讲义", @"疯狂Android讲义",
@"疯狂Ajax讲义", @"疯狂iOS讲义", nil];
//程序输出set1集合中元素的个数为3
NSLog(@"set1集合中的元素个数为:%ld", [set1 count]);
NSLog(@"set1集合:%@", NSCollectionToString(set1));
NSSet* set2 = [NSSet setWithObjects:@"孙悟空", @"疯狂Android讲义",
@"猪八戒", nil];
NSLog(@"set2集合:%@", NSCollectionToString(set2));
//向set1集合中添加单个元素,将添加元素后生成的新集合赋给set1
set1 = [set1 setByAddingObject:@"Struts 2.1权威指南"];
NSLog(@"添加一个元素后:%@", NSCollectionToString(set1));
//使用NSSet集合向set1集合中添加多个元素,相当于计算两个集合的并集
NSSet* s = [set1 setByAddingObjectsFromSet:set2];
NSLog(@"set1与set2的并集:%@", NSCollectionToString(s));
//计算两个NSSet集合是否有交集
BOOL b = [set1 intersectsSet:set2];
//将输出代表YES的1
NSLog(@"set1与set2是否有交集:%d", b);
//判断set2是否是set1的子集
BOOL bo = [set2 isSubsetOfSet:set1];
//将输出代表NO的0
NSLog(@"set2是否为set1的子集:%d", bo);
//判断NSSet集合是否包含指定元素
BOOL bb = [set1 containsObject:@"疯狂Ajax讲义"];
//输出代表YES的1
NSLog(@"set1是否包含“疯狂Ajax讲义”:%d", bb);
//下面两行代码将取出相同的元素,但取出那个元素是不确定的
NSLog(@"set1取出一个元素:%@", [set1 anyObject]);
NSLog(@"set1取出一个元素:%@", [set1 anyObject]);
//使用代码块对集合元素进行过滤
NSSet* filteredSet = [set1 objectsPassingTest:
^BOOL(id obj, BOOL* stop)
{
return (BOOL)([obj length] > 8);
}];
NSLog(@"set1中元素的长度大于8的集合元素有:%@",
NSCollectionToString(filteredSet));
}
return 0;
}
NSArray也提供了isEqualToArray:方法用于判断两个NSArray包含的集合元素是否相等。由于在前面介绍时没有给出方法列表,故此处集中列出。
输出结果为:
注:因为NSSet类似于一个罐子,所以放进去的元素是没有一定顺序的,所以其输出时也是没有顺序的,但元素还是当时录入的元素。
7.6.2 NSSet判断集合元素重复的标准
当向NSSet集合中存入一个元素时,NSSet会调用该对象的Hash方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在底层Hash表中的系统位置,如果根据hashCode计算出来该元素在底层Hash表中的存储位置已经不相同,那么系统自然将它们存在不同的位置。
如果两个元素的hashCode相同,接下来就要通过isEqual:方法判断两个元素是否相等,如果有两个元素通过isEqual:方法比较返回NO,NSSet依然认为它们不相等,NSSet会把他们都存在底层Hash表的同一个位置,只是将在这个位置形成链;如果它们通过isEqual:比较返回YES,那么NSSet认为两个元素相等,后面的元素添加失败。
HashSet集合判断两个元素相等的标准如下:
- 两个对象通过isEqual:方法比较返回YES。
- 两个对象的hash方法返回值也相等。
测试NSSet集合判断两元素相等:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKUser : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* pass;
- (id) initWithName: (NSString*) aName pass: (NSString*) aPass;
- (void) say: (NSString*) content;
@end
NS_ASSUME_NONNULL_END
#import "FKUser.h"
@implementation FKUser
@synthesize name;
@synthesize pass;
- (id) initWithName:(NSString *) aName pass:(NSString *) aPass {
if (self = [super init]) {
self.name = aName;
self.pass = aPass;
}
return self;
}
- (void) say:(NSString *) content {
NSLog(@"%@说:%@", self.name, content);
}
//重写isEqual:方法,重写该方法的比较标准是,
//如果两个FKUser的name、pass相等,即可认为它们相等
- (BOOL) isEqual: (id) other {
if (self == other) {
return YES;
}
//如果两个的类是相同的话就进入
if ([other class] == FKUser.class) {
FKUser* target = (FKUser*)other;
return [self.name isEqualToString:target.name]
&& [self.pass isEqualToString:target.pass];
}
return NO;
}
//重写description方法,可以直接看到FKUser对象的状态
- (NSString*) description {
return [NSString stringWithFormat:
@"<FKUser[name = %@, pass = %@]>"
, self.name, self.pass];
}
@end
//定义一个函数,该函数用于把NSSet集合转换为字符串
NSString* NSCollectionToString(id collection) {
NSMutableString* result = [NSMutableString
stringWithString:@"["];
for (id object in collection) {
[result appendString:[object description]];
[result appendString:@","];
}
NSUInteger len = [result length]; //获取字符串长度
//去掉字符串最后的一个字符
[result deleteCharactersInRange:NSMakeRange(len - 1, 1)];
[result appendString:@"]"];
return result;
}
int main(int argc, char* argv[]) {
@autoreleasepool {
NSSet* set = [NSSet setWithObjects:
[[FKUser alloc] initWithName:@"sun" pass:@"123"],
[[FKUser alloc] initWithName:@"bai" pass:@"345"],
[[FKUser alloc] initWithName:@"sun" pass:@"123"],
[[FKUser alloc] initWithName:@"tang" pass:@"178"],
[[FKUser alloc] initWithName:@"niu" pass:@"155"], nil];
NSLog(@"set集合元素的个数:%ld", [set count]);
NSLog(@"%@", NSCollectionToString(set));
}
return 0;
}
输出结果为:
上面的输出结果中,虽然第一个和第三个的name、pass相等,但NSSet依然将它们当成两个元素。这是因为虽它们两个的isEqual:比较会返回YES,但由于程序并没有重写它们的Hash方法,因此这两个FKUser的hashCode值依然不相等,NSSet依然认为它们不相等,NSSet会同时存储两个元素。
重写Hash方法,重写Hash方法时根据name、pass两个属性的值进行计算,代码如下:
//重写Hash方法,重写该方法的比较标准是,
//如果两个FKUser的name、pass相等,两个FKUser的Hash方法返回值相等
- (NSUInteger) hash {
NSLog(@"===hash===");
NSUInteger nameHash = name == nil ? 0 : [name hash];
NSUInteger passHash = pass == nil ? 0 : [pass hash];
return nameHash * 31 + passHash;
}
NSString已经重写了Hash方法,只要两个字符串所包含的字符序列相同,那么两个NSString的Hash方法返回值就相等。
输出结果如下:
从上面的输出可以看到,程序输出“=== hash ===”5次,这表明NSSet每次添加一个集合元素,总会先调用该元素的Hash方法——所有的对象都继承了NSObject类,因此,所有的对象都有Hash方法。
注意:如果需要把一个对象放入NSSet中,如果重写该对象对应类的isEqual:方法,也应该重写其Hash方法,其 规则 是:如果两个对象通过isEqual:方法比较返回YES时,这两个对象的Hash方法返回值也应该相同。
如果需要某个类的对象保存到NSSet集合中,重写这个类的isEqual:方法和Hash方法时,应该尽量保证两个对象通过isEqual:比较返回YES时,它们的Hash方法返回值也相等。
重写Hash方法的基本规则:
- 程序运行过程中,同一个对象多次调用Hash应该返回相同的值。
- 当两个对象通过isEqual:方法比较返回YES时,这两个对象的Hash应返回相等的值。
- 对象中作为isEqual:比较标准的实例变量,都应该用来计算hashCode值。
重写Hash方法的一般步骤:
- 1. 把对象内每个有意义的实例变量(即每个用作isEqual:比较标准的实例变量)计算出一个int类型的hashCode值。
- 2. 用第一步计算出来的多个hashCode组合计算出一个hashCode值返回。例如如下代码:
return [f1 hash] + [f2 hash];
- 如果为了避免直接相加产生偶然相等(两个对象f1、f2实例变量并不相等,但它们的hashCode和恰好相等),可以通过为各实例变量的hashCode值乘以任意一个质数后再相加,例如如下代码:
return [f1 hash] * 31 + [f2 hash];
7.6.3NSMutableSet的功能与用法
NSMutableSet继承了NSSet,它代表一个集合元素可变的NSSet集合。由于NSMutableSet可以动态添加集合元素,因此,创建NSSet集合时可指定底层Hash表的初始容量。
NSMutableSet在NSSet基础上增加了添加元素、删除元素的方法,并增加了对集合计算交集、并集、差集的方法:
- addObject::向集合中添加单个元素。
- removeObject::从集合中删除单个元素。
- removeAllObjects:删除集合中的所有元素。
- addObjectsFromArray::使用NSArray数组作为参数,向NSSet集合中添加参数数组中的所有元素。
- unionSet::计算两个NSSet集合的并集。
- minusSet::计算两个NSSet集合的差集。
- intersectSet::计算两个NSSet集合的交集。
- setSet::用后一个集合的元素替换已有集合中的所有元素。
NSMutableSet集合的用法:
//定义一个函数,该函数用于把NSSet集合转换为字符串
NSString* NSCollectionToString(id collection) {
NSMutableString* result = [NSMutableString
stringWithString:@"["];
for (id object in collection) {
[result appendString:[object description]];
[result appendString:@","];
}
NSUInteger len = [result length]; //获取字符串长度
//去掉字符串最后的一个字符
[result deleteCharactersInRange:NSMakeRange(len - 1, 1)];
[result appendString:@"]"];
return result;
}
int main(int argc, char* argv[]) {
@autoreleasepool {
//创建一个初始容量为10的Set集合
NSMutableSet* set = [NSMutableSet setWithCapacity:10];
[set addObject:@"疯狂iOS讲义"];
NSLog(@"添加一个元素后:%@", NSCollectionToString(set));
[set addObjectsFromArray:[NSArray
arrayWithObjects:@"疯狂XML讲义",
@"疯狂Ajax讲义", @"疯狂Android讲义", nil]];
NSLog(@"使用NSArray添加3个元素后:%@", NSCollectionToString(set));
[set removeObject:@"疯狂XML讲义"];
NSLog(@"删除一个元素后:%@", NSCollectionToString(set));
//再创建一个Set集合
NSSet* set2 = [NSSet setWithObjects:@"孙悟空", @"疯狂iOS讲义", nil];
//计算两个集合的并集,直接改变set集合的元素
[set unionSet:set2];
//计算两个集合的差集,直接改变set集合的元素
// [set minusSet:set2];
//计算两个集合的交集,直接改变set集合的元素
// [set intersectsSet:set2];
//用set2的集合元素替换set的集合元素,直接改变set集合的元素
// [set setSet:set2];
NSLog(@"%@", NSCollectionToString(set));
}
return 0;
}
输出结果为:
上面最后一行输出的就是两个集合计算并集的结果。除此之外,程序中最后几行代码还可以计算两个集合的交集、差集,以及用后面集合的元素替换现有集合中的所有元素等功能。
7.6.4 NSCountedSet的功能与用法
NSCountedSet是NSMutableSet的子类,NSCountedSet为每个元素额外维护一个添加次数的状态。当程序向NSCountedSet中添加一个元素时,如果NSCountedSet集合中不包含该元素,NSCountedSet真正接纳该元素,并将该元素的添加次数标注为:1;当程序向NSCountedSet中添加一个元素时,如果NSCountedSet集合中已经包含该元素,NSCountedSet不会接纳该元素,但会将该元素的添加次数加1。
当程序从NSCountedSet集合中删除元素时,NSCountedSet只是将该元素的添加次数减1,只有当该元素的添加次数变为0时,该元素才会真正从NSCountedSet集合中删除。
NSCountedSet提供返回某个元素添加次数的方法:
- countForObject::获取指定元素的添加次数。
NSCountedSet的功能和用法:
//定义一个函数,该函数用于把NSArray或NSSet集合转换为字符串
NSString* NSCollectionToString(id collection) {
NSMutableString* result = [NSMutableString
stringWithString:@"["];
for (id object in collection) {
[result appendString:[object description]];
[result appendString:@","];
}
NSUInteger len = [result length]; //获取字符串长度
//去掉字符串最后的一个字符
[result deleteCharactersInRange:NSMakeRange(len - 1, 1)];
[result appendString:@"]"];
return result;
}
int main(int argc, char* argv[]) {
@autoreleasepool {
NSCountedSet* set = [NSCountedSet setWithObjects:@"疯狂iOS讲义",
@"疯狂Android讲义", @"疯狂Ajax讲义", nil];
[set addObject:@"疯狂iOS讲义"];
[set addObject:@"疯狂iOS讲义"];
//输出集合元素
NSLog(@"%@", NSCollectionToString(set));
//获取指定元素的添加顺序
NSLog(@"“疯狂iOS讲义“的添加次数为:%ld",
[set countForObject:@"疯狂iOS讲义"]);
//删除元素
[set removeObject:@"疯狂iOS讲义"];
NSLog(@"删除“疯狂iOS讲义”一次后的结果:%@",
NSCollectionToString(set));
NSLog(@"删除“疯狂iOS讲义”1次后的添加次数为:%ld",
[set countForObject:@"疯狂iOS讲义"]);
//重复删除元素
[set removeObject:@"疯狂iOS讲义"];
[set removeObject:@"疯狂iOS讲义"];
NSLog(@"删除“疯狂iOS讲义”3次后的结果:%@",
NSCollectionToString(set));
}
return 0;
}
输出结果为:
从上面程序可以看出,想要在NSCountedSet集合中真正删除一个元素,就必须让它的添加次数变为0。