2013-10-01 5 views
19

Этот вопрос может показаться легким, но я ищу наиболее эффективный и удобный для памяти способ.Извлечение элемента из массива в Objective-C

Предположим, у меня есть массив объектов Person. У каждого человека есть цвет волос, представленный NSString. Давайте скажем, что я хочу удалить все объекты Person из массива, где их цвет волос коричневый.

Как это сделать?

Имейте в виду, что вы не можете удалить объект из массива, который перечисляется.

+3

Сортировка ли? Есть ли дубликаты? Это теоретический вопрос, или у вас есть определенные проблемы с производительностью с вашим кодом? Если последнее, не могли бы вы дать более подробную информацию? Кроме того, чтобы быть ясным, вы конкретно ссылаетесь на мутацию существующего 'NSMutableArray' и не генерируете новый массив, который не содержит всех коричневоволосых людей в оригинале, - это правильно? –

+2

нет, может быть, нет, нет, да –

ответ

25

Существуют два основных подхода. Мы можем протестировать каждый элемент, а затем сразу удалить элемент, если он соответствует критериям тестирования, или мы можем протестировать каждый элемент и сохранить индексы элементов, удовлетворяющих критериям тестирования, и затем удалить сразу все такие элементы. Поскольку использование памяти является реальной проблемой, требования к хранилищу для последнего подхода могут сделать его нежелательным.

С помощью «сохранить все индексы для удаления, а затем удалить их» из таблицы мы должны рассмотреть детали, связанные с первым подходом, и как они будут влиять на правильность и скорость подхода. В этом подходе есть две фатальные ошибки. Первый заключается в том, чтобы удалить оцениваемый объект не на основе его индекса в массиве, а скорее с помощью метода removeObject:. removeObject: выполняет линейный поиск массива, чтобы найти объект для удаления. С большим, несортированным набором данных это приведет к разрушению нашей производительности по мере увеличения времени с квадратом входного размера. Кстати, использование indexOfObject:, а затем removeObjectAtIndex: так же плохо, поэтому нам также следует избегать этого. Вторая фатальная ошибка будет начинаться с нашей итерации с индексом 0. NSMutableArray перестраивает индексы после добавления или удаления объекта, поэтому, если мы начнем с индекса 0, нам будет гарантировано исключение индекса за пределами, если даже один объект удален во время итерация. Итак, мы должны начинать с обратной стороны массива и удалять только те объекты, у которых более низкие индексы, чем каждый проверенный нами индекс.

Отметив это, существует два очевидных варианта: цикл for, который начинается в конце, а не в начале массива, или метод NSArrayenumerateObjectsWithOptions: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 немного быстрее - возможно, около четверти второго быстрее 500000 элементов, что разница между 8.5 и 8.25 секунд, в основном. Поэтому я бы предложил использовать блок-подход, так как он безопаснее и чувствует себя более идиоматично.

+0

в соответствии с этим парнем, перечисление может быть медленным: http://darkdust.net/writings/objective-c/nsarray-enumeration-performance –

+0

@ChoppinBroccoli Как я прочитал эти результаты, перечисление блоков происходит очень быстро. На что конкретно вы ссылаетесь? Возможно, я ошибаюсь в том, что цикл for работает быстрее, но я действительно устал, когда писал эту часть, и сегодня утром мне было интересно, не изменил ли я свои номера. Я снова запустил их и отредактировал свой пост. –

+0

На самом деле я думаю, что ты прав. Я не рассматривал перечисление блоков, просто регулярное перечисление. –

5
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/

+1

Это приведет к тому, что на больших наборах данных будет очень медленно работать из-за вызова 'removeObject:', что требует линейного поиска массива. –

+0

как использовать в swift? –

14

Предполагая, что вы имеете дело с изменяемым массивом и не сортируется/индексируются (т.е. вы должны сканировать через массив), вы можете перебирать массив в обратном порядке, используя enumerateObjectsWithOptions с опцией NSEnumerationReverse:

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 
    // now you can remove the object without affecting the enumeration 
}]; 

Идя в обратном порядке, то вы можете удалить объект из массива быть перечислены.

+3

+1 Это не так хорошо известно, как должно быть! – borrrden

+0

Вау! очень очень умный. – HalR

+0

Это потрясающе, просто дул мой разум haha ​​ – trapper

3

Ключом является использование предикатов для фильтрации массива. См. Код ниже;

- (NSArray*)filterArray:(NSArray*)list 
{ 
    return [list filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings){ 
     People *currentObj = (People*)evaluatedObject; 
     return (![currentObj.hairColour isEqualToString:@"brown"]); 
    }]]; 
} 
+0

предикаты хороши! но обычно это самый медленный способ сделать это. – Cocoadelica

+0

Если вы собираетесь использовать предикаты ...'[NSPredicate predicateWithFormat: @" hairColour ==% @ ", hairColour]' – trapper

3

попробовать, как это,

 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); 
+3

Первый пример - самый эффективный. Люди Objc.io провели сравнение и обнаружили, что сортировка на основе предикатов будет медленной. Если вы подберете первый пример для использования indexesOfObjectsWithOptions: passTest: вместо этого, а в опциях отправит 'NSEnumerationConcurrent', вы получите лучшую производительность. – Cocoadelica

2

Если вы делаете копию массива с некоторыми пунктами отфильтровываются, а затем сделать новую изменяемый массив, итерацию над оригиналом и добавить к копии на лету, как другие предложили для этого ответа. Но ваш вопрос говорит об удалении из существующего (предположительно изменяемого) массива.

Хотя итерации, можно создать массив объектов для удаления, а затем удалить их впоследствии:

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 & Guy English). По-видимому, нет, но близко к этому, необходимо использовать KVC и NSPredicate в этом несколько многословной линии:

NSArray *subsetOfPeople = [allPeople filteredArrayUsingPredicate: 
    [NSPredicate predicateWithFormat:@"SELF.hairColor != %@", hairColorToMatch]]; 

ли идти вперед и создать категорию на NSArray, чтобы сделать вещи более краткими для вашего кода, filterWithFormat: или что-то.

(все проверялось, набранный непосредственно в SO)

+0

Вместо цикла для генерации индексов было бы лучше использовать 'enumerateObjectsWithOptions: usingBlock:'. Поскольку массив не изменяется в блоке, ему не нужно 'NSEnumerationReverse' и может использовать' NSEnumerationConcurrent'. –

Смежные вопросы