按属性值从数组中删除项目
Posted
技术标签:
【中文标题】按属性值从数组中删除项目【英文标题】:Removing item from array by property value 【发布时间】:2013-10-01 04:19:54 【问题描述】:我正在寻找最有效和记忆友好的方式。
假设我有一个 Person
对象数组。每个人都有一个由NSString
表示的头发颜色。然后假设我想从数组中删除所有头发颜色为棕色的 Person
对象。
我该怎么做?
请记住,您不能从正在枚举的数组中删除对象。
【问题讨论】:
它们是否已排序?有重复吗?这是一个理论问题,还是您的代码存在特定的性能问题?如果是后者,您能否提供更多细节?另外,需要明确的是,您具体指的是对现有的NSMutableArray
进行变异,而不是生成一个不包含原始中所有棕色头发的人的新数组 - 对吗?
【参考方案1】:
有两种通用方法。我们可以对每个元素进行测试,如果满足测试条件则立即删除该元素,或者我们可以测试每个元素并存储满足测试条件的元素的索引,然后一次性删除所有此类元素。由于内存使用是一个真正的问题,后一种方法的存储要求可能会使其不受欢迎。
对于“存储所有索引以删除,然后删除它们”的方法,我们需要考虑前一种方法所涉及的细节,以及它们将如何影响该方法的正确性和速度。这种方法有两个致命错误在等待。第一种是不根据其在数组中的索引删除评估对象,而是使用removeObject:
方法。 removeObject:
对数组进行线性搜索以找到要删除的对象。对于一个大的、未排序的数据集,随着时间随着输入大小的平方而增加,这将破坏我们的性能。顺便说一句,使用indexOfObject:
然后removeObjectAtIndex:
一样糟糕,所以我们也应该避免它。第二个致命错误是在索引 0 处开始我们的迭代。NSMutableArray
在添加或删除对象后重新排列索引,因此如果我们从索引 0 开始,即使有一个对象,我们也会保证索引越界异常在迭代过程中删除。所以,我们必须从数组的后面开始,只删除索引低于我们目前检查过的每个索引的对象。
考虑到这一点,实际上有两个明显的选择:for
循环从数组的末尾而不是开头开始,或者 NSArray
方法 enumerateObjectsWithOptions:usingBlock:
方法。每个示例如下:
[persons enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Person *p, NSUInteger index, BOOL *stop)
if ([p.hairColor isEqualToString:@"brown"])
[persons removeObjectAtIndex:index];
];
NSInteger count = [persons count];
for (NSInteger index = (count - 1); index >= 0; index--)
Person *p = persons[index];
if ([p.hairColor isEqualToString:@"brown"])
[persons removeObjectAtIndex:index];
我的测试似乎显示for
循环稍微快一些——对于 500,000 个元素,可能快了大约四分之一秒,基本上是 8.5 秒和 8.25 秒之间的差异。所以我建议使用块方法,因为它更安全,感觉更习惯。
【讨论】:
根据这家伙的说法,枚举可能很慢:darkdust.net/writings/objective-c/… @ChoppinBroccoli 我阅读这些结果的方式是,块枚举非常快。你具体指的是什么?不过,对于 for 循环更快,我可能是错的 - 当我写那部分时我真的很累,今天早上想知道我是否没有转置我的数字。我将再次运行它们并编辑我的帖子。 其实我觉得你是对的。我没有看块枚举,只是常规枚举。 将性能与removeObjectsAtIndexes:
和 filteredArrayUsingPredicate:
进行比较会很有趣,请参阅下面的答案
@PierreHouston 针对indexesOfObjectsPassingTest
和removeObjectsAtIndexes
进行了测试,@Carl 的答案是最好的。 :]【参考方案2】:
假设您正在处理一个可变数组并且它没有排序/索引(即您必须扫描数组),您可以使用 enumerateObjectsWithOptions
和 NSEnumerationReverse
选项以相反的顺序遍历数组:
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop)
// now you can remove the object without affecting the enumeration
];
通过逆序,你可以从被枚举的数组中移除一个对象。
【讨论】:
【参考方案3】:NSMutableArray * tempArray = [self.peopleArray mutableCopy];
for (Person * person in peopleArray)
if ([person.hair isEqualToString: @"Brown Hair"])
[tempArray removeObject: person]
self.peopleArray = tempArray;
或者 NSPredicate 也可以:http://nshipster.com/nspredicate/
【讨论】:
由于调用removeObject:
需要对数组进行线性搜索,这将在更大的数据集上真正变慢。【参考方案4】:
关键是使用谓词过滤数组。请看下面的代码;
- (NSArray*)filterArray:(NSArray*)list
return [list filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
People *currentObj = (People*)evaluatedObject;
return (![currentObj.hairColour isEqualToString:@"brown"]);
]];
【讨论】:
如果你要使用谓词...[NSPredicate predicateWithFormat:@"hairColour == %@", hairColour]
【参考方案5】:
试试这样,
NSIndexSet *indices = [personsArray indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop)
return [[obj objectForKey:@"hair"] isEqual:@"Brown Hair"];
];
NSArray *filtered = [personsArray objectsAtIndexes:indices];
或
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.hair=%@ ",@"Brown Hair"];
NSArray* myArray = [personsArray filteredArrayUsingPredicate:predicate];
NSLog(@"%@",myArray);
【讨论】:
第一个例子效率最高。 Objc.io 的人做了一个比较,发现基于谓词的排序很慢。如果您将第一个示例调整为使用 indexOfObjectsWithOptions:passingTest: 并在选项中发送NSEnumerationConcurrent
您将获得最佳性能。【参考方案6】:
如果您要制作过滤掉某些项目的数组副本,则制作一个新的可变数组,迭代原始数组并动态添加到副本中,正如其他人对此答案所建议的那样。但是您的问题是从现有(可能是可变的)数组中删除。
在迭代时,您可以构建一个要删除的对象数组,然后再删除它们:
NSMutableArray *thePeople = ...
NSString *hairColorToMatch = ...
NSMutableArray *matchingObjects = [NSMutableArray array];
for (People *person in thePeople)
if (person.hairColor isEqualToString:hairColorToMatch])
[matchingObjects addObject:person];
[thePeople removeObjects:matchingObjects];
但这会创建一个临时数组,您可能认为这很浪费,更重要的是,很难看到removeObjects:
非常有效。另外,有人提到了一些关于具有重复项的数组,这在这种情况下应该可以工作,但不是最好的,每个重复项也在临时数组中,并且在removeObjects:
中进行冗余匹配。
可以改为按索引进行迭代并在进行时删除,但这会使循环逻辑相当尴尬。相反,我会收集索引集中的索引,然后再次删除:
NSMutableIndexSet *matchingIndexes = [NSMutableIndexSet indexSet];
for (NSUInteger n = thePeople.count, i = 0; i < n; ++i)
People *person = thePeople[i];
if ([person.hairColor isEqualToString:hairColorToMatch])
[matchingIndexes addIndex:i];
[thePeople removeObjectsAtIndexes:matchingIndexes];
我相信索引集的开销非常低,因此这几乎与您将获得的效率一样高,而且很难搞砸。像这样在最后批量删除的另一件事是,Apple 可能已将 removeObjectsAtIndexes:
优化为优于 removeObjectAtIndex:
的序列。因此,即使有创建索引集数据结构的开销,这也可能会在迭代时快速删除。如果数组有重复,这个也很好用。
如果相反,您确实在制作过滤副本,那么我认为您可以使用一些 KVC
集合运算符(我最近正在阅读这些内容,您可以根据 NSHipster 和 @ 做一些疯狂的事情987654322@)。显然没有,但很接近,需要在这个有点罗嗦的行中使用 KVC 和 NSPredicate:
NSArray *subsetOfPeople = [allPeople filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:@"SELF.hairColor != %@", hairColorToMatch]];
请继续在 NSArray
上创建一个类别,以使您的代码更简洁,filterWithFormat:
或其他内容。
(全部未经测试,直接输入SO)
【讨论】:
而不是 for 循环来生成索引,使用enumerateObjectsWithOptions:usingBlock:
可能会更好。由于数组未在块内修改,因此不需要NSEnumerationReverse
,可以使用NSEnumerationConcurrent
。【参考方案7】:
NSMutableArray *arrayForStuff = ...
[arrayForStuff removeObjectAtIndex:[arrayForStuff indexOfObject:objectToRemove]];
【讨论】:
只有代码的答案几乎总是可以通过添加解释来改进。当用六个其他答案回答一个六年前的问题时,必须说明您的答案所针对的问题的新方面,并说明时间的流逝是否改变了答案。以上是关于按属性值从数组中删除项目的主要内容,如果未能解决你的问题,请参考以下文章