반복하는 동안 NSMutableArray에서 제거하는 가장 좋은 방법은 무엇입니까?


194

Cocoa에서 NSMutableArray를 반복하고 특정 기준에 맞는 여러 객체를 제거하려면 객체를 제거 할 때마다 루프를 다시 시작하지 않고이를 수행하는 가장 좋은 방법은 무엇입니까?

감사,

편집 : 명확히하기 위해-가장 좋은 방법을 찾고있었습니다. 예를 들어 인덱스를 수동으로 업데이트하는 것보다 더 우아한 것입니다. 예를 들어 C ++에서는 할 수 있습니다.

iterator it = someList.begin();

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

9
뒤에서 앞으로 반복하십시오.
핫 릭

아무도 "WHY"에 답변하지 않습니다
onmyway133

1
@HotLicks 내가 가장 좋아하는 프로그램 중 하나이며 프로그래밍에서 가장 과소 평가 된 솔루션 : D
Julian F. Weinert

답변:


388

명확히하기 위해 삭제할 항목을 수집하는 초기 루프를 만들고 싶습니다. 그런 다음 삭제합니다. Objective-C 2.0 구문을 사용한 샘플은 다음과 같습니다.

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

그런 다음 인덱스가 올바르게 업데이트되는지 또는 기타 작은 부기 세부 정보에 대해 의문의 여지가 없습니다.

추가하기 위해 편집 :

다른 답변에서는 역 공식화가 더 빨라야합니다. 즉, 배열을 반복하고 버릴 객체 대신 유지할 새 객체 배열을 작성하는 경우. 그것은 사실 일 수도 있지만 (새 배열을 할당하고 오래된 배열을 버리는 메모리와 처리 비용은 어떻습니까?) 더 빠르더라도 NSArrays 때문에 순진한 구현만큼 큰 문제는 아닙니다. "정상적인"배열처럼 행동하지 마십시오. 그들은 대화를하지만 다른 길을 걷습니다. 여기에 좋은 분석을보십시오 :

역 제형은 더 빠를 수 있지만, 위의 제형이 항상 내 요구에 충분히 빠르기 때문에 그것이인지 여부를 신경 쓸 필요가 없었습니다.

나를 위해 집으로가는 메시지는 당신에게 가장 분명한 공식을 사용하는 것입니다. 필요한 경우에만 최적화하십시오. 나는 개인적으로 위의 공식을 가장 명확하게 생각하기 때문에 그것을 사용합니다. 그러나 역 공식이 더 명확하다면 그것을 찾으십시오.


47
객체가 배열에 두 번 이상 있으면 버그가 발생할 수 있습니다. 대안으로 NSMutableIndexSet 및-(void) removeObjectsAtIndexes를 사용할 수 있습니다.
Georg Schölly 2016 년

1
실제로는 반대로하는 것이 더 빠릅니다. 성능 차이는 꽤 크지 만 배열이 크지 않으면 시간이 사소한 것입니다.
user1032657 2016 년

리버스를 사용했습니다. 배열을 nil로 만들었습니다. 그런 다음 원하는 항목 만 추가하고 데이터를 다시로드했습니다 ... 완료 ... 기본 배열의 미러 인 dummyArray를 만들어서 실제 실제 데이터를 갖습니다
Fahim Parkar

반대로하려면 추가 메모리를 할당해야합니다. 적절한 알고리즘이 아니며 경우에 따라 좋은 아이디어가 아닙니다. 직접 제거하면 배열 만 이동합니다 (매우 효율적으로 이동하므로 큰 청크를 이동할 수 있기 때문에 매우 효율적 임). 그러나 새 배열을 만들면 모든 할당 및 할당이 필요합니다. 따라서 몇 개의 항목 만 삭제하면 역수가 훨씬 느려집니다. 전형적인 겉보기 올바른 알고리즘입니다.
jack

82

한 가지 더 변형. 따라서 가독성과 성능이 우수합니다.

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

대단해. 배열에서 여러 항목을 제거하려고 시도했지만 완벽하게 작동했습니다. 다른 방법으로 문제가 발생했습니다 :) 고맙습니다
AbhinavVinay

Corey, 이 답변 (아래)은 removeObjectsAtIndexes객체를 제거하는 최악의 방법이라고 동의합니다. 당신의 대답이 너무 오래 되었기 때문에 이것을 묻습니다. 여전히 최선을 선택하는 것이 좋습니까?
Hemang

가독성 측면에서이 솔루션을 매우 좋아합니다. @HotLicks 답변과 비교할 때 성능면에서 어떻게됩니까?
Victor Maia Aldecôa

Obj-C의 배열에서 객체를 삭제하는 동안이 방법을 반드시 권장해야합니다.
boreas

enumerateObjectsUsingBlock:무료로 색인 증분을 얻을 수 있습니다.
pkamb

42

이것은 매우 간단한 문제입니다. 당신은 거꾸로 반복 :

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

이것은 매우 일반적인 패턴입니다.


Jens의 답변을 놓쳤을 것입니다. cuz 그는 코드를 작성하지
않았습니다

3
이것이 가장 좋은 방법입니다. Objective-C의 배열 인덱스가 부호없는 것으로 선언 되었음에도 불구하고 반복 변수는 부호있는 변수 여야한다는 점을 기억해야합니다 (현재 Apple이 어떻게 후회하는지 상상할 수 있습니다).
mojuba

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크기 조정 중에 값을 복사하는 데 시간을 낭비하지 않습니다. 배열을 수정하지 않으므로 Fast Enumeration을 자유롭게 사용할 수 있습니다. 사용 setArray:의 내용을 대체하기 array로하는 것은 itemsToKeep효율적입니다. 코드에 따라 마지막 줄을 다음과 같이 바꿀 수도 있습니다.

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

따라서 값을 복사 할 필요가 없으며 포인터 만 교체하면됩니다.


이 알고리즘은 시간 복잡성 측면에서 좋은 것으로 판명되었습니다. 공간 복잡성 측면에서 이해하려고합니다. 실제 '배열 수'로 'itemsToKeep'의 용량을 설정하는 것과 동일한 구현이 있습니다. 그러나 배열에서 객체를 거의 원하지 않기 때문에 itemsToKeep에 추가하지 않습니다. 내 itemsToKeep의 용량은 10이지만 실제로 6 개의 객체 만 저장합니다. 이것은 내가 4 개체의 공간을 낭비하고 있음을 의미합니까? PS는 결함을 찾지 않고 단지 알고리즘의 복잡성을 이해하려고 노력합니다. :)
tech_human

예, 사용하지 않는 4 개의 포인터를위한 공간이 있습니다. 배열에는 객체 자체가 아닌 포인터 만 포함되므로 16 바이트 (32 비트 아키텍처) 또는 32 바이트 (64 비트)를 의미하는 4 개의 객체에 해당합니다.
benzado

어떤 솔루션이든 최적의 추가 공간이 필요합니다 (항목을 삭제함). 다시 말하지만, 우리는 객체의 전체 사본이 아닌 포인터를 다루기 때문에 대부분의 상황에서 매우 저렴합니다.
benzado

알았어 감사합니다 Benzado !!
tech_human

게시물 에 따르면 배열의 크기에 대한 arrayWithCapacity : 메서드의 힌트는 실제로 사용되지 않습니다.
크렘 크

28

NSpredicate를 사용하여 변경 가능한 배열에서 항목을 제거 할 수 있습니다. for 루프가 필요하지 않습니다.

예를 들어 이름이 NSMutableArray 인 경우 다음과 같은 술어를 작성할 수 있습니다.

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

다음 줄은 b로 시작하는 이름 만 포함 된 배열을 남깁니다.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

필요한 술어를 작성하는 데 문제가있는 경우이 애플 개발자 링크를 사용 하십시오 .


3
보이는 for 루프가 필요하지 않습니다 . 필터 작업에 루프가 있으며 보이지 않습니다.
핫 릭

18

4 가지 방법으로 성능 테스트를 수행했습니다. 각 테스트는 100,000 개의 요소 배열에서 모든 요소를 ​​반복하고 5 번째 항목마다 제거했습니다. 결과는 최적화 유무에 관계없이 크게 변하지 않았습니다. 이것은 iPad 4에서 수행되었습니다.

(1) removeObjectAtIndex:- (271) MS

(2) removeObjectsAtIndexes:- 1010 MS (인덱스 세트를 구축하는 것은 ~ 700 밀리 필요하기 때문에, 그렇지 않으면이 기본적으로 호출 removeObjectAtIndex과 동일 : 각 항목에 대한)

(3) removeObjects:- (326) MS

(4) 테스트를 통과 한 객체로 새로운 배열을 만듭니다 -17ms

따라서 새 어레이를 만드는 것이 훨씬 빠릅니다. removeObjectsAtIndexes :를 사용하면 인덱스 세트를 빌드하는 데 필요한 시간 때문에 제거 할 항목이 많을수록 더 나빠진다는 점을 제외하고 다른 메소드는 모두 비슷합니다.


1
새로운 배열을 만들고 나중에 할당을 해제하는 시간을 세었습니까? 나는 그것이 17ms 안에 혼자 할 수 있다고 생각하지 않습니다. 플러스 75,000 과제?
jack

새로운 배열을 생성하고 할당을 해제하는 데 필요한 시간을 최소화
user1032657

리버스 루프 방식을 측정하지 않은 것 같습니다.
핫 릭

첫 번째 테스트는 역방향 루프 체계와 동일합니다.
user1032657

newArray가 남아 있고 prevArray가 널이면 어떻게됩니까? newArray를 PrevArray의 이름에 복사하거나 이름을 바꾸어야합니다. 그렇지 않으면 다른 코드에서 어떻게 참조합니까?
aremvee

17

인덱스에 대해 루프 카운트 다운을 사용하십시오.

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

보관하려는 개체로 복사하십시오.

특히 for (id object in array)루프 또는을 사용하지 마십시오 NSEnumerator.


3
코드 m8에 답을 써야합니다. haha는 거의 그것을 놓쳤다.
FlowUI. SimpleUITesting.com 2013 년

이것은 옳지 않습니다. "for (배열의 id 객체)"는 "for (NSInteger i = array.count-1; i> = 0; --i)"보다 빠르며 빠른 반복이라고합니다. 반복자를 사용하는 것은 색인 작성보다 확실히 빠릅니다.
jack

@jack-그러나 반복자 아래에서 요소를 제거하면 일반적으로 혼란이 생깁니다. (그리고 "빠른 반복"이 항상 그렇게 빠른 것은 아닙니다.)
핫 릭

답은 2008 년입니다. 빠른 반복이 없었습니다.
Jens Ayton

12

iOS 4+ 또는 OS X 10.6+의 경우 Apple은와 같은 passingTestAPI 시리즈를 추가했습니다 . 이러한 API를 사용하는 솔루션은 다음과 같습니다.NSMutableArray– indexesOfObjectsPassingTest:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];

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"]];

배열이 재 할당되지 않을 것이라는 보장이 있습니까?
Cfr

재 할당이란 무엇입니까?
vikingosegundo

선형 구조에서 요소를 제거하는 동안 새로운 연속 메모리 블록을 할당하고 모든 요소를 ​​그곳으로 옮기는 것이 효과적 일 수 있습니다. 이 경우 모든 포인터가 무효화됩니다.
Cfr

삭제 작업은 마지막에 얼마나 많은 요소가 삭제되는지 알지 못하므로 제거 중에는 의미가 없습니다. 어쨌든 : 구현 세부 사항, 합리적인 코드를 작성하려면 사과를 신뢰해야합니다.
vikingosegundo

첫 번째는 더 좋지 않습니다 (처음에는 그것이 어떻게 작동하는지에 대해 많이 생각해야하기 때문에). 그리고 두 번째는 안전한 리팩토링이 아닙니다. 결국 나는 실제로 읽을 수있는 고전적인 승인 된 솔루션으로 끝났습니다.
Renetik

8

보다 선언적인 방법으로 제거 할 항목과 일치하는 기준에 따라 다음을 사용할 수 있습니다.

[theArray filterUsingPredicate:aPredicate]

@Nathan은 매우 효율적이어야합니다


6

쉽고 깨끗한 방법이 있습니다. 빠른 열거 형 호출에서 배열을 바로 복제하고 싶습니다.

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

이 방법을 사용하면 삭제되는 배열의 사본을 통해 동일한 객체를 보유 할 수 있습니다. NSArray는 객체 포인터 만 가지고 있으므로 메모리 / 성능이 좋습니다.


2
또는 더 간단합니다 –for (LineItem *item in self.lineItems.copy)
Alexandre G

5

제거하려는 객체를 두 번째 배열에 추가하고 루프 후에 -removeObjectsInArray :를 사용하십시오.


5

이 작업을 수행해야합니다.

    NSMutableArray* myArray = ....;

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

도움이 되었기를 바랍니다...


정통적이지는 않지만 깨끗하고 간단한 솔루션이 되려면 뒤로 반복하고 삭제하는 것을 발견했습니다. 일반적으로 가장 빠른 방법 중 하나입니다.
rpetrich

이게 뭐가 문제 야? 깨끗하고 빠르며 쉽게 읽을 수 있으며 매력처럼 작동합니다. 나에게 그것은 최고의 답변처럼 보입니다. 왜 부정적인 점수를 얻습니까? 여기에 뭔가 빠졌습니까?
Steph Thirion

1
@Steph : 질문은 "수동으로 인덱스를 업데이트하는 것보다 더 우아한 것"이라고 말합니다.
Steve Madsen

1
오. 나는 절대적으로 색인을 수동으로 업데이트하고 싶지 않습니다. 고마워 스티브. IMO이 솔루션은 선택한 솔루션보다 더 우아하므로 (일시적인 배열이 필요하지 않음) 반대 투표는 불공평합니다.
Steph Thirion

@Steve : 편집 내용을 확인하면 답변을 제출 한 후에 해당 부분이 추가되었습니다 .... 그렇지 않으면 "반복 반복은 가장 우아한 해결책입니다"라고 대답했습니다. :). 좋은 하루 보내세요!
Pokot0

1

제거 할 객체를 다른 NSMutableArray에 추가하지 않겠습니까? 반복이 완료되면 수집 한 객체를 제거 할 수 있습니다.


1

'n'번째 요소, 'n-1'번째 요소 등으로 삭제하려는 요소를 바꾸는 것은 어떻습니까?

완료되면 배열의 크기를 '이전 크기-스왑 수'로 조정하십시오


1

배열의 모든 객체가 고유하거나 발견 될 때 발생하는 모든 객체를 제거하려는 경우 어레이 사본을 빠르게 열거하고 [NSMutableArray removeObject :]를 사용하여 원본에서 객체를 제거 할 수 있습니다.

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

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}

원본 myArray+arrayWithArray실행되는 동안 업데이트 되면 어떻게됩니까 ?
bioffe

1
@ bioffe : 그런 다음 코드에 버그가 있습니다. NSMutableArray는 스레드로부터 안전하지 않으므로 잠금을 통해 액세스를 제어해야합니다. 이 답변
dreamlax

1

위의 벤 자도의 대답은 당신이 preformace를 위해해야 ​​할 일입니다. 내 응용 프로그램 중 하나에서 removeObjectsInArray의 실행 시간은 1 분이 걸렸으며 새 배열에 추가하는 데 .023 초가 걸렸습니다.


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];
}];

1

NSMutableArray에서 아래의 category 메소드를 사용하는 것이 더 좋은 구현 일 수 있습니다.

@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;
}];

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];
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.