반복하는 동안 NSMutableArray에서 제거하는 가장 좋은 방법은 무엇입니까?
-
02-07-2019 - |
문제
Cocoa에서 NSMutableArray를 반복하고 특정 기준에 맞는 여러 개체를 제거하려는 경우 개체를 제거할 때마다 루프를 다시 시작하지 않고 이 작업을 수행하는 가장 좋은 방법은 무엇입니까?
감사해요,
편집하다:명확히하기 위해-나는 가장 좋은 방법을 찾고있었습니다.내가 있는 인덱스를 수동으로 업데이트하는 것보다 더 우아한 것입니다.예를 들어 C++에서는 할 수 있습니다.
iterator it = someList.begin();
while (it != someList.end())
{
if (shouldRemove(it))
it = someList.erase(it);
}
해결책
명확성을 위해 삭제할 항목을 수집하는 초기 루프를 만들고 싶습니다.그런 다음 삭제합니다.다음은 Objective-C 2.0 구문을 사용하는 샘플입니다.
NSMutableArray *discardedItems = [NSMutableArray array];
for (SomeObjectClass *item in originalArrayOfItems) {
if ([item shouldBeDiscarded])
[discardedItems addObject:item];
}
[originalArrayOfItems removeObjectsInArray:discardedItems];
그러면 지수가 올바르게 업데이트되고 있는지 또는 기타 작은 장부 세부 사항이 업데이트되는지 여부에 대해서는 의문의 여지가 없습니다.
다음을 추가하도록 편집되었습니다.
역공식이 더 빨라야한다는 것이 다른 답변에서 언급되었습니다.즉.배열을 반복하고 삭제할 개체 대신 유지할 새 개체 배열을 구성하는 경우.그것이 사실일 수도 있습니다(비록 새로운 배열을 할당하고 이전 배열을 폐기하는 데 드는 메모리 및 처리 비용은 어떻습니까?). 비록 더 빠르더라도 순진한 구현만큼 큰 문제는 아닐 수도 있습니다. 왜냐하면 NSArrays "일반" 배열처럼 동작하지 않습니다.그들은 말은 하지만 다른 길을 걷습니다. 여기에서 좋은 분석을 확인하세요.
역공식이 더 빠를 수도 있지만, 위의 공식이 항상 내 요구에 충분히 빠르기 때문에 그럴 필요가 없었습니다.
나에게 중요한 메시지는 당신에게 가장 명확한 공식을 사용하라는 것입니다.필요한 경우에만 최적화하십시오.나는 개인적으로 위의 공식이 가장 명확하다고 생각하므로 이 공식을 사용합니다.그러나 역공식이 더 명확하다면 그렇게 하십시오.
다른 팁
변형이 하나 더 있습니다.따라서 가독성과 좋은 성능을 얻을 수 있습니다.
NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet];
SomeObjectClass *item;
NSUInteger index = 0;
for (item in originalArrayOfItems) {
if ([item shouldBeDiscarded])
[discardedItems addIndex:index];
index++;
}
[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
이것은 매우 간단한 문제입니다.거꾸로 반복하면 됩니다.
for (NSInteger i = array.count - 1; i >= 0; i--) {
ElementType* element = array[i];
if ([element shouldBeRemoved]) {
[array removeObjectAtIndex:i];
}
}
이것은 매우 일반적인 패턴입니다.
다른 답변 중 일부는 매우 큰 배열에서 성능이 저하될 수 있습니다. 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];
따라서 값을 복사할 필요도 없고 포인터만 교환하면 됩니다.
NSpredicate를 사용하여 가변 배열에서 항목을 제거할 수 있습니다.여기에는 for 루프가 필요하지 않습니다.
예를 들어 NSMutableArray 이름이 있는 경우 다음과 같은 조건자를 만들 수 있습니다.
NSPredicate *caseInsensitiveBNames =
[NSPredicate predicateWithFormat:@"SELF beginswith[c] 'b'"];
다음 줄은 b로 시작하는 이름만 포함하는 배열을 남깁니다.
[namesArray filterUsingPredicate:caseInsensitiveBNames];
필요한 술어를 생성하는 데 문제가 있는 경우 다음을 사용하세요. 애플 개발자 링크.
4가지 방법을 사용하여 성능 테스트를 수행했습니다.각 테스트는 100,000개 요소 배열의 모든 요소를 반복하고 5번째 항목마다 제거했습니다.최적화 유무에 관계없이 결과는 크게 달라지지 않았습니다.이 작업은 iPad 4에서 수행되었습니다.
(1) removeObjectAtIndex:
-- 271ms
(2) removeObjectsAtIndexes:
-- 1010ms (인덱스 세트를 구축하는 데 ~700ms가 걸리기 때문입니다.그렇지 않은 경우 이는 기본적으로 RemoveObjectAtIndex를 호출하는 것과 동일합니다.각 항목마다)
(3) removeObjects:
-- 326ms
(4) 테스트를 통과한 객체로 새 배열을 만듭니다. 17ms
따라서 새 배열을 만드는 것이 가장 빠릅니다.RemoveObjectsAtIndexes를 사용하는 것을 제외하고 다른 방법은 모두 비슷합니다.인덱스 세트를 구축하는 데 필요한 시간으로 인해 제거할 항목이 많아지면 상태가 더 나빠집니다.
인덱스에 대해 루프 카운트다운을 사용하십시오.
for (NSInteger i = array.count - 1; i >= 0; --i) {
또는 보관하려는 개체로 복사본을 만드세요.
특히, for (id object in array)
루프 또는 NSEnumerator
.
iOS 4+ 또는 OS X 10.6+의 경우 Apple이 추가함 passingTest
일련의 API NSMutableArray
, 좋다 – indexesOfObjectsPassingTest:
.이러한 API를 사용하는 솔루션은 다음과 같습니다.
NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];
요즘에는 역방향 블록 기반 열거를 사용할 수 있습니다.간단한 예제 코드:
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"]];
보다 선언적인 방식으로 제거할 항목과 일치하는 기준에 따라 다음을 사용할 수 있습니다.
[theArray filterUsingPredicate:aPredicate]
@Nathan은 매우 효율적이어야 합니다.
쉽고 깨끗한 방법은 다음과 같습니다.나는 빠른 열거 호출에서 바로 배열을 복제하고 싶습니다.
for (LineItem *item in [NSArray arrayWithArray:self.lineItems])
{
if ([item.toBeRemoved boolValue] == YES)
{
[self.lineItems removeObject:item];
}
}
이 방법으로 삭제되는 배열의 복사본을 열거하며 둘 다 동일한 개체를 보유합니다.NSArray는 객체 포인터만 보유하므로 이는 메모리/성능 측면에서 완전히 좋습니다.
제거하려는 개체를 두 번째 배열에 추가하고 루프 후에 -removeObjectsInArray:를 사용합니다.
이렇게 해야 합니다:
NSMutableArray* myArray = ....;
int i;
for(i=0; i<[myArray count]; i++) {
id element = [myArray objectAtIndex:i];
if(element == ...) {
[myArray removeObjectAtIndex:i];
i--;
}
}
도움이 되었기를 바랍니다...
제거할 객체를 다른 NSMutableArray에 추가하는 것이 어떻습니까?반복이 끝나면 수집한 객체를 제거할 수 있습니다.
삭제하려는 요소를 'n'번째 요소, 'n-1'번째 요소 등으로 바꾸는 것은 어떻습니까?
완료되면 배열의 크기를 '이전 크기 - 스왑 수'로 조정합니다.
배열의 모든 개체가 고유하거나 발견된 개체의 모든 항목을 제거하려는 경우 배열 복사본을 빠르게 열거하고 [NSMutableArray RemoveObject:]를 사용하여 원본에서 개체를 제거할 수 있습니다.
NSMutableArray *myArray;
NSArray *myArrayCopy = [NSArray arrayWithArray:myArray];
for (NSObject *anObject in myArrayCopy) {
if (shouldRemove(anObject)) {
[myArray removeObject:anObject];
}
}
위의 benzado의 답변은 사전 형성을 위해 수행해야 할 작업입니다.내 응용 프로그램 중 하나에서 RemoveObjectsInArray의 실행 시간은 1분이었고, 새 배열을 추가하는 데만 .023초가 걸렸습니다.
다음과 같이 블록을 사용하여 필터링할 수 있는 카테고리를 정의합니다.
@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];
}];
더 나은 구현은 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;
}];
거꾸로 반복하는 것은 제가 수년 동안 가장 좋아했지만 오랫동안 '가장 깊은'(가장 높은 개수) 객체가 먼저 제거되는 경우를 본 적이 없습니다.포인터가 다음 인덱스로 이동하기 직전에 아무 것도 없고 충돌이 발생합니다.
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];