MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

学会使用Objective-C集合类(NSSet)

2022-01-064.0k 阅读

1. NSSet 基础概述

在 Objective - C 编程中,集合类是组织和管理数据的强大工具。其中,NSSet 是一种无序且唯一的集合,它不允许有重复的元素。这意味着当你试图向 NSSet 中添加已经存在的元素时,该操作不会产生任何效果,集合的大小也不会改变。

NSSet 的无序特性与数组(NSArray)形成鲜明对比。数组中的元素是按照插入顺序存储的,通过索引可以访问特定位置的元素。而在 NSSet 中,元素没有固定的顺序,不能通过索引来访问单个元素。NSSet 更专注于快速判断某个元素是否存在于集合中,以及执行集合相关的数学操作,如求并集、交集等。

从实现角度来看,NSSet 通常基于哈希表实现。哈希表的使用使得 NSSet 在判断元素是否存在时具有非常高的效率,平均情况下时间复杂度为 O(1)。这使得 NSSet 在需要快速查找元素的场景中表现出色。

2. 创建 NSSet

2.1 使用字面量创建

Objective - C 提供了简洁的字面量语法来创建 NSSet。你可以使用 @ 符号,后跟一对花括号 {},在花括号内列出集合的元素,元素之间用逗号分隔。例如:

NSSet *set1 = @[@"apple", @"banana", @"cherry"];

这里创建了一个包含三个字符串元素的 NSSet。需要注意的是,由于 NSSet 不允许重复元素,如果在字面量中列出了重复的元素,它们会被自动忽略。比如:

NSSet *set2 = @[@"apple", @"banana", @"banana"];
NSLog(@"%lu", (unsigned long)set2.count); // 输出 2

2.2 使用构造函数创建

除了字面量语法,还可以使用 NSSet 的构造函数来创建集合。NSSet 类提供了多个构造函数,其中最常用的是 initWithObjects:count:。例如:

NSString *fruits[] = {@"apple", @"banana", @"cherry"};
NSSet *set3 = [[NSSet alloc] initWithObjects:fruits count:3];

在这个例子中,先定义了一个字符串数组 fruits,然后使用 initWithObjects:count: 构造函数创建了一个 NSSet,构造函数需要传入对象数组和数组中元素的数量。

另外,NSSet 还提供了 initWithArray: 构造函数,用于从一个数组创建 NSSet。这种方式会自动去除数组中的重复元素。例如:

NSArray *array = @[@"apple", @"banana", @"banana"];
NSSet *set4 = [[NSSet alloc] initWithArray:array];
NSLog(@"%lu", (unsigned long)set4.count); // 输出 2

2.3 创建空的 NSSet

有时,你可能需要先创建一个空的 NSSet,然后再逐步添加元素。可以使用 init 方法来创建一个空的 NSSet,之后通过 mutableCopy 方法将其转换为可变的 NSMutableSet,以便添加元素。例如:

NSSet *emptySet = [[NSSet alloc] init];
NSMutableSet *mutableSet = [emptySet mutableCopy];
[mutableSet addObject:@"newElement"];

或者,也可以直接使用 NSMutableSetinit 方法来创建一个空的可变集合:

NSMutableSet *mutableSet2 = [[NSMutableSet alloc] init];
[mutableSet2 addObject:@"anotherElement"];

3. NSSet 的基本操作

3.1 添加元素

如前所述,NSSet 本身是不可变的,无法直接添加元素。如果需要添加元素,需要使用可变的 NSMutableSet 类。NSMutableSet 继承自 NSSet,并提供了添加元素的方法。

使用 addObject: 方法可以向 NSMutableSet 中添加一个元素。例如:

NSMutableSet *mutableSet3 = [[NSMutableSet alloc] init];
[mutableSet3 addObject:@"element1"];
[mutableSet3 addObject:@"element2"];

如果尝试添加已经存在于集合中的元素,addObject: 方法不会产生任何效果,集合的大小也不会改变。例如:

[mutableSet3 addObject:@"element1"];
NSLog(@"%lu", (unsigned long)mutableSet3.count); // 仍然输出 2

3.2 删除元素

NSMutableSet 提供了 removeObject: 方法来删除集合中的元素。例如:

[mutableSet3 removeObject:@"element2"];
NSLog(@"%lu", (unsigned long)mutableSet3.count); // 输出 1

如果尝试删除一个不存在于集合中的元素,removeObject: 方法也不会产生任何错误,只是不会有实际的删除操作。

3.3 判断元素是否存在

NSSet 提供了 containsObject: 方法来判断一个元素是否存在于集合中。该方法返回一个布尔值,YES 表示元素存在,NO 表示不存在。例如:

NSSet *set5 = @[@"one", @"two", @"three"];
BOOL contains = [set5 containsObject:@"two"];
if (contains) {
    NSLog(@"集合中包含 'two'");
} else {
    NSLog(@"集合中不包含 'two'");
}

3.4 获取集合元素个数

通过 count 属性可以获取 NSSet 中元素的个数。例如:

NSSet *set6 = @[@1, @2, @3, @4];
NSLog(@"%lu", (unsigned long)set6.count); // 输出 4

4. NSSet 的遍历

由于 NSSet 是无序的,不能像数组那样通过索引进行遍历。不过,NSSet 提供了几种遍历方式。

4.1 使用 for - in 循环

for - in 循环是一种简洁的遍历 NSSet 的方式。例如:

NSSet *set7 = @[@"red", @"green", @"blue"];
for (NSString *color in set7) {
    NSLog(@"%@", color);
}

for - in 循环中,每次迭代都会从 NSSet 中取出一个元素,直到遍历完所有元素。需要注意的是,由于 NSSet 的无序性,每次遍历的顺序可能不同。

4.2 使用 Enumerator

NSSet 提供了 objectEnumerator 方法来获取一个枚举器。枚举器可以按顺序逐个返回集合中的元素。例如:

NSEnumerator *enumerator = [set7 objectEnumerator];
id object;
while (object = [enumerator nextObject]) {
    NSLog(@"%@", object);
}

在这个例子中,通过 objectEnumerator 获取枚举器,然后使用 nextObject 方法在 while 循环中逐个获取元素,直到 nextObject 返回 nil,表示已经遍历完所有元素。

4.3 使用 Block 进行遍历

NSSet 还支持使用 block 进行遍历。enumerateObjectsUsingBlock: 方法接受一个 block 作为参数,在遍历每个元素时会调用该 block。例如:

[set7 enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
    NSLog(@"%@", obj);
    // 如果需要提前停止遍历,可以设置 *stop = YES;
}];

在 block 中,obj 表示当前遍历到的元素,stop 是一个指向布尔值的指针。如果在 block 中设置 *stop = YES,遍历会提前结束。

5. NSSet 的数学操作

5.1 求并集

两个集合的并集是包含这两个集合中所有不重复元素的集合。在 Objective - C 中,可以通过 unionSet: 方法来求两个 NSSet 的并集。该方法返回一个新的 NSSet,包含了两个集合中的所有元素。例如:

NSSet *set8 = @[@1, @2, @3];
NSSet *set9 = @[@3, @4, @5];
NSSet *unionSet = [set8 setByAddingObjectsFromSet:set9];
NSLog(@"%@", unionSet); // 输出 {1, 2, 3, 4, 5}

这里使用 setByAddingObjectsFromSet: 方法,该方法返回一个新的 NSSet,包含了 set8set9 中的所有元素,重复的元素 3 只出现一次。

5.2 求交集

两个集合的交集是包含这两个集合中共同元素的集合。可以使用 intersectSet: 方法来求两个 NSSet 的交集。例如:

NSSet *intersectionSet = [set8 setByIntersectingSet:set9];
NSLog(@"%@", intersectionSet); // 输出 {3}

setByIntersectingSet: 方法返回一个新的 NSSet,其中的元素是 set8set9 中都有的元素。

5.3 求差集

两个集合的差集是指在一个集合中但不在另一个集合中的元素组成的集合。可以使用 minusSet: 方法来求两个 NSSet 的差集。例如:

NSSet *differenceSet = [set8 setBySubtractingSet:set9];
NSLog(@"%@", differenceSet); // 输出 {1, 2}

这里 setBySubtractingSet: 方法返回一个新的 NSSet,其中的元素是在 set8 中但不在 set9 中的元素。

6. NSSet 的哈希值和相等性

6.1 哈希值

NSSet 中的每个元素都有一个哈希值。哈希值是一个整数,用于在哈希表中快速定位元素。NSSet 使用元素的哈希值来判断元素是否相等,以及在集合中存储和查找元素。

当向 NSSet 中添加元素时,NSSet 会根据元素的哈希值来决定将其存储在哈希表的哪个位置。如果两个元素的哈希值相同,NSSet 会进一步使用 isEqual: 方法来判断它们是否真的相等。

要自定义类作为 NSSet 的元素,需要重写该类的 hash 方法,以提供一个合适的哈希值。一个好的哈希函数应该能够尽量均匀地分布不同对象的哈希值,以提高 NSSet 的查找效率。例如:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
- (NSUInteger)hash {
    return [self.name hash] ^ self.age;
}
@end

在这个例子中,Person 类重写了 hash 方法,将 name 的哈希值和 age 进行异或运算,得到一个相对唯一的哈希值。

6.2 相等性

NSSet 使用 isEqual: 方法来判断两个元素是否相等。默认情况下,NSObjectisEqual: 方法比较的是对象的内存地址,这在大多数情况下不符合实际需求。因此,当自定义类作为 NSSet 的元素时,通常需要重写 isEqual: 方法。

例如,对于上述 Person 类,可以重写 isEqual: 方法如下:

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;
    if (![object isKindOfClass:[Person class]]) return NO;
    Person *otherPerson = (Person *)object;
    return [self.name isEqualToString:otherPerson.name] && self.age == otherPerson.age;
}

在这个 isEqual: 方法中,首先判断两个对象是否是同一个对象(内存地址相同),如果是则直接返回 YES。然后判断传入的对象是否是 Person 类的实例,如果不是则返回 NO。最后比较两个 Person 对象的 nameage 属性,如果都相等则返回 YES,否则返回 NO

7. NSSet 在实际开发中的应用场景

7.1 去重

在处理数据时,经常会遇到需要去除重复元素的情况。例如,从网络请求中获取到一组数据,可能会包含重复的记录。使用 NSSet 可以很方便地去除这些重复数据。例如:

NSArray *duplicateArray = @[@"item1", @"item2", @"item1", @"item3"];
NSSet *uniqueSet = [[NSSet alloc] initWithArray:duplicateArray];
NSArray *uniqueArray = [uniqueSet allObjects];
NSLog(@"%@", uniqueArray); // 输出 ["item1", "item2", "item3"]

通过将数组转换为 NSSet,重复元素被自动去除,然后再将 NSSet 转换回数组,就得到了去重后的数组。

7.2 快速查找

在需要频繁判断某个元素是否存在的场景中,NSSet 非常有用。比如,在一个游戏中,需要快速判断某个角色是否在某个区域内。可以将区域内的所有角色存储在一个 NSSet 中,然后使用 containsObject: 方法来快速判断。例如:

NSSet *charactersInArea = @[character1, character2, character3];
if ([charactersInArea containsObject:targetCharacter]) {
    // 执行相应操作
}

这种方式比在数组中逐个查找元素要高效得多,特别是当集合中的元素数量较大时。

7.3 集合运算

在数据分析或算法实现中,经常需要执行集合的数学运算,如求并集、交集、差集等。NSSet 提供了相应的方法来方便地执行这些运算。例如,在分析两个用户群体的共同兴趣爱好时,可以将每个用户群体的兴趣爱好存储在 NSSet 中,然后通过求交集来得到共同的兴趣爱好。

NSSet *user1Interests = @[@"reading", @"traveling", @"coding"];
NSSet *user2Interests = @[@"traveling", @"photography", @"hiking"];
NSSet *commonInterests = [user1Interests setByIntersectingSet:user2Interests];
NSLog(@"%@", commonInterests); // 输出 {"traveling"}

8. 与其他集合类的比较

8.1 NSSet 与 NSArray

NSArray 是有序的集合,可以通过索引访问特定位置的元素。而 NSSet 是无序且唯一的集合,不能通过索引访问元素,但在判断元素是否存在时效率更高。

如果需要按照特定顺序存储和访问元素,如显示列表数据,NSArray 是更好的选择。如果只关心元素的唯一性和快速查找,NSSet 则更为合适。例如,在一个音乐播放列表中,歌曲的顺序很重要,应该使用 NSArray。而在一个记录已播放歌曲的集合中,只需要知道某首歌是否已经播放过,使用 NSSet 更合适。

8.2 NSSet 与 NSDictionary

NSDictionary 是键值对的集合,通过键来访问对应的值。NSSet 则是单一元素的集合,更专注于元素的唯一性和集合运算。

如果数据是以键值对的形式存在,并且需要通过键来查找值,NSDictionary 是首选。例如,存储用户信息,使用用户名作为键,用户详细信息作为值。而当只需要处理一组唯一的元素,不涉及键值对关系时,NSSet 更适用。

9. 注意事项

9.1 不可变与可变

在使用 NSSet 时,要清楚地区分不可变的 NSSet 和可变的 NSMutableSet。不可变的 NSSet 在创建后不能再添加或删除元素,这使得它在多线程环境中更安全,因为不用担心其他线程修改集合内容。而可变的 NSMutableSet 提供了添加和删除元素的方法,但在多线程环境中需要注意同步问题,以避免数据竞争。

9.2 元素的哈希值和相等性

如前所述,当自定义类作为 NSSet 的元素时,必须正确重写 hashisEqual: 方法。否则,可能会出现元素无法正确存储、查找或判断相等性的问题。同时,要注意 hash 方法的实现质量,尽量保证不同对象的哈希值均匀分布,以提高 NSSet 的性能。

9.3 内存管理

在使用 NSSet 时,要注意内存管理。当向 NSSet 中添加对象时,NSSet 会对对象进行 retain(在 ARC 环境下等效于增加引用计数)。当对象从 NSSet 中移除或 NSSet 被释放时,对象会被 release(在 ARC 环境下等效于减少引用计数)。因此,要确保对象的生命周期得到正确管理,避免内存泄漏或悬空指针的问题。

通过深入理解和掌握 NSSet 的特性、操作方法以及应用场景,开发者能够在 Objective - C 编程中更高效地组织和处理数据,提升程序的性能和质量。无论是处理简单的数据去重,还是复杂的集合运算,NSSet 都能成为强大的工具。