197

在 Cocoa 中,如果我想遍历 NSMutableArray 并删除符合特定条件的多个对象,那么在每次删除对象时不重新启动循环的最佳方法是什么?

谢谢,

编辑:只是为了澄清-我正在寻找最好的方法,例如比手动更新我所在的索引更优雅的方法。例如在 C++ 中我可以做到;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
4

20 回答 20

391

为了清楚起见,我喜欢创建一个初始循环,在其中收集要删除的项目。然后我删除它们。这是一个使用 Objective-C 2.0 语法的示例:

NSMutableArray *discardedItems = [NSMutableArray array];

for (SomeObjectClass *item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addObject:item];
}

[originalArrayOfItems removeObjectsInArray:discardedItems];

那么就没有问题是否正确更新了索引或其他小的簿记细节。

编辑添加:

在其他答案中已经指出,逆公式应该更快。即,如果您遍历数组并组成一个新的对象数组来保留,而不是要丢弃的对象。这可能是真的(尽管分配新数组并丢弃旧数组的内存和处理成本如何?)但即使它更快,它也可能不像幼稚的实现那样重要,因为 NSArrays不要表现得像“正常”数组。他们谈论谈话,但他们走的是不同的路。在这里看到一个很好的分析:

逆公式可能更快,但我从来不需要关心它是否是,因为上面的公式总是足够快以满足我的需要。

对我来说,带回家的信息是使用你最清楚的任何表述。仅在必要时进行优化。我个人觉得上面的公式最清楚,这就是我使用它的原因。但是,如果逆公式对您来说更清楚,那就去吧。

于 2008-09-21T23:23:49.080 回答
83

还有一种变体。因此,您可以获得可读性和良好的性能:

NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet];
SomeObjectClass *item;
NSUInteger index = 0;

for (item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addIndex:index];
    index++;
}

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
于 2009-06-19T20:33:29.977 回答
47

这是一个非常简单的问题。您只需向后迭代:

for (NSInteger i = array.count - 1; i >= 0; i--) {
   ElementType* element = array[i];
   if ([element shouldBeRemoved]) {
       [array removeObjectAtIndex:i];
   }
}

这是一种非常常见的模式。

于 2013-08-27T04:24:18.827 回答
39

其他一些答案在非常大的数组上性能很差,因为方法removeObject:removeObjectsInArray:涉及对接收器进行线性搜索,这是一种浪费,因为您已经知道对象在哪里。此外,任何调用removeObjectAtIndex:都必须一次将值从索引复制到数组末尾一个插槽。

更有效的方法如下:

NSMutableArray *array = ...
NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];
for (id object in array) {
    if (! shouldRemove(object)) {
        [itemsToKeep addObject:object];
    }
}
[array setArray:itemsToKeep];

因为我们设置了容量itemsToKeep,我们不会在调整大小时浪费任何时间复制值。我们不会就地修改数组,所以我们可以自由地使用快速枚举。setArray:用于替换arraywith的内容将itemsToKeep是有效的。根据您的代码,您甚至可以将最后一行替换为:

[array release];
array = [itemsToKeep retain];

所以甚至不需要复制值,只需要交换一个指针。

于 2008-09-24T08:51:49.947 回答
28

您可以使用 NSpredicate 从可变数组中删除项目。这不需要 for 循环。

例如,如果你有一个 NSMutableArray 名称,你可以创建一个这样的谓词:

NSPredicate *caseInsensitiveBNames = 
[NSPredicate predicateWithFormat:@"SELF beginswith[c] 'b'"];

以下行将为您留下一个仅包含以 b 开头的名称的数组。

[namesArray filterUsingPredicate:caseInsensitiveBNames];

如果您无法创建所需的谓词,请使用此Apple 开发人员链接

于 2008-09-24T20:59:56.250 回答
18

我使用 4 种不同的方法进行了性能测试。每个测试都会遍历 100,000 个元素数组中的所有元素,并每隔 5 项删除一次。结果在有/没有优化的情况下变化不大。这些是在 iPad 4 上完成的:

(1) removeObjectAtIndex:-- 271 毫秒

(2) removeObjectsAtIndexes:-- 1010 毫秒(因为构建索引集大约需要 700 毫秒;否则这与为每个项目调用 removeObjectAtIndex: 基本相同)

(3) removeObjects:-- 326 毫秒

(4) 用通过测试的对象创建一个新数组 -- 17 ms

因此,创建一个新数组是迄今为止最快的。其他方法都是可比较的,除了使用 removeObjectsAtIndexes: 会更糟,因为要删除更多项目,因为构建索引集需要时间。

于 2013-06-03T21:04:53.577 回答
17

要么对索引使用循环倒计时:

for (NSInteger i = array.count - 1; i >= 0; --i) {

或使用您要保留的对象制作副本。

特别是,不要使用for (id object in array)循环或NSEnumerator.

于 2008-09-21T19:52:07.170 回答
12

对于 iOS 4+ 或 OS X 10.6+,ApplepassingTest在. 具有此类 API 的解决方案是:NSMutableArray– indexesOfObjectsPassingTest:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];
于 2012-08-18T16:41:11.723 回答
12

现在你可以使用反向的基于块的枚举。一个简单的示例代码:

NSMutableArray *array = [@[@{@"name": @"a", @"shouldDelete": @(YES)},
                           @{@"name": @"b", @"shouldDelete": @(NO)},
                           @{@"name": @"c", @"shouldDelete": @(YES)},
                           @{@"name": @"d", @"shouldDelete": @(NO)}] mutableCopy];

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if([obj[@"shouldDelete"] boolValue])
        [array removeObjectAtIndex:idx];
}];

结果:

(
    {
        name = b;
        shouldDelete = 0;
    },
    {
        name = d;
        shouldDelete = 0;
    }
)

仅一行代码的另一种选择:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];
于 2013-08-28T02:05:49.933 回答
8

以更具声明性的方式,根据与要删除的项目匹配的条件,您可以使用:

[theArray filterUsingPredicate:aPredicate]

@Nathan 应该非常高效

于 2008-09-21T20:51:06.093 回答
6

这是简单而干净的方法。我喜欢在快速枚举调用中复制我的数组:

for (LineItem *item in [NSArray arrayWithArray:self.lineItems]) 
{
    if ([item.toBeRemoved boolValue] == YES) 
    {
        [self.lineItems removeObject:item];
    }
}

通过这种方式,您可以枚举要从中删除的数组的副本,两者都包含相同的对象。NSArray 仅保存对象指针,因此这在内存/性能方面非常好。

于 2013-08-28T01:42:06.800 回答
5

将要删除的对象添加到第二个数组中,并在循环之后使用 -removeObjectsInArray:。

于 2008-09-21T19:50:41.963 回答
5

这应该这样做:

    NSMutableArray* myArray = ....;

    int i;
    for(i=0; i<[myArray count]; i++) {
        id element = [myArray objectAtIndex:i];
        if(element == ...) {
            [myArray removeObjectAtIndex:i];
            i--;
        }
    }

希望这可以帮助...

于 2008-09-21T19:55:11.110 回答
1

为什么不将要删除的对象添加到另一个 NSMutableArray。完成迭代后,您可以移除已收集的对象。

于 2008-09-21T19:51:40.620 回答
1

如何将要删除的元素与第 n 个元素、第 n-1 个元素等交换?

完成后,将数组大小调整为“以前的大小 - 交换次数”

于 2008-09-21T21:12:19.100 回答
1

如果数组中的所有对象都是唯一的,或者您想在找到对象时删除所有出现的对象,则可以快速枚举数组副本并使用 [NSMutableArray removeObject:] 从原始对象中删除该对象。

NSMutableArray *myArray;
NSArray *myArrayCopy = [NSArray arrayWithArray:myArray];

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}
于 2008-09-22T05:06:19.507 回答
1

上面的 benzado 的 anwser 是你应该为 preformace 做的事情。在我的一个应用程序中,removeObjectsInArray 的运行时间为 1 分钟,仅添加到新数组需要 0.023 秒。

于 2010-06-17T21:31:00.180 回答
1

我定义了一个类别,让我可以使用块进行过滤,如下所示:

@implementation NSMutableArray (Filtering)

- (void)filterUsingTest:(BOOL (^)(id obj, NSUInteger idx))predicate {
    NSMutableIndexSet *indexesFailingTest = [[NSMutableIndexSet alloc] init];

    NSUInteger index = 0;
    for (id object in self) {
        if (!predicate(object, index)) {
            [indexesFailingTest addIndex:index];
        }
        ++index;
    }
    [self removeObjectsAtIndexes:indexesFailingTest];

    [indexesFailingTest release];
}

@end

然后可以这样使用:

[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
    return [self doIWantToKeepThisObject:obj atIndex:idx];
}];
于 2012-01-23T13:10:36.610 回答
1

更好的实现可能是在 NSMutableArray 上使用下面的类别方法。

@implementation NSMutableArray(BMCommons)

- (void)removeObjectsWithPredicate:(BOOL (^)(id obj))predicate {
    if (predicate != nil) {
        NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:self.count];
        for (id obj in self) {
            BOOL shouldRemove = predicate(obj);
            if (!shouldRemove) {
                [newArray addObject:obj];
            }
        }
        [self setArray:newArray];
    }
}

@end

可以实现谓词块来对数组中的每个对象进行处理。如果谓词返回 true,则删除对象。

一个日期数组的示例,用于删除过去的所有日期:

NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
    NSDate *date = (NSDate *)obj;
    return [date timeIntervalSinceNow] < 0;
}];
于 2015-08-27T09:58:43.713 回答
0

多年来,我最喜欢向后迭代,但很长一段时间以来,我从未遇到过首先删除“最深”(最高计数)对象的情况。在指针移动到下一个索引之前的瞬间,没有任何东西并且它崩溃了。

Benzado 的方式最接近我现在所做的,但我从未意识到每次移除后都会重新洗牌。

在 Xcode 6 下这工作

NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];

    for (id object in array)
    {
        if ( [object isNotEqualTo:@"whatever"]) {
           [itemsToKeep addObject:object ];
        }
    }
    array = nil;
    array = [[NSMutableArray alloc]initWithArray:itemsToKeep];
于 2015-08-10T06:55:40.960 回答