Когда использовать enumerateObjectsUsingBlock по сравнению с for

Помимо очевидных отличий:

  • Используйте enumerateObjectsUsingBlock, когда вам нужен и индекс, и объект
  • Не используйте enumerateObjectsUsingBlock, когда вам нужно изменить локальные переменные (я ошибался, см. ответ bbum)

Считается ли enumerateObjectsUsingBlock лучше или хуже, когда for (id obj in myArray) тоже работает? Каковы преимущества/недостатки (например, более или менее производительно)?


person Paul Wheeler    schedule 20.12.2010    source источник
comment
Мне нравится использовать его, если мне нужен текущий индекс.   -  person Besi    schedule 03.09.2012
comment
См. также: stackoverflow.com/questions/8509662 /   -  person Simon Whitaker    schedule 30.06.2013


Ответы (6)


В конечном счете, используйте любой шаблон, который вы хотите использовать, и он более естественен в контексте.

В то время как for(... in ...) довольно удобен и синтаксически краток, enumerateObjectsUsingBlock: имеет ряд особенностей, которые могут оказаться интересными, а могут и не оказаться:

  • enumerateObjectsUsingBlock: будет таким же быстрым или даже быстрее, чем быстрое перечисление (for(... in ...) использует поддержку NSFastEnumeration для реализации перечисления). Быстрое перечисление требует перевода из внутреннего представления в представление для быстрого перечисления. Там есть накладные расходы. Перечисление на основе блоков позволяет классу коллекции перечислять содержимое так же быстро, как и самый быстрый обход собственного формата хранения. Вероятно, это не имеет значения для массивов, но может иметь огромное значение для словарей.

  • «Не используйте enumerateObjectsUsingBlock, когда вам нужно изменить локальные переменные» — неверно; вы можете объявить своих местных жителей как __block, и они будут доступны для записи в блоке.

  • enumerateObjectsWithOptions:usingBlock: поддерживает параллельное или обратное перечисление.

  • со словарями перечисление на основе блоков является единственным способом одновременного извлечения ключа и значения.

Лично я использую enumerateObjectsUsingBlock: чаще, чем for (... in ...), но - опять же - личный выбор.

person bbum    schedule 20.12.2010
comment
Вау, очень информативно. Хотел бы я принять оба этих ответа, но я выбираю ответ Чака, потому что он мне ближе. Кроме того, я нашел ваш блог (friday.com/bbum/ 29.08.2009/blocks-tips-tricks) во время поиска __block и узнал еще больше. Спасибо. - person Paul Wheeler; 20.12.2010
comment
Просто для чисто информативной ценности я даю этому голос. Хороший ответ. - person imnk; 16.06.2011
comment
Для справки, перечисление на основе блоков не всегда так быстро или быстрее .net/slow-block-based-dictionary-enumeration.html - person Mike Abdullah; 06.11.2012
comment
Хорошая находка. Который, кажется, сломан. Пожалуйста, сообщите об ошибке (и разместите # здесь). Обратите внимание, что он может быть нарушен сейчас по причинам совместимости, по недосмотру или из-за деталей реализации, и он будет исправлен в более позднем выпуске. - person bbum; 06.11.2012
comment
В какой-то момент кто-то отправил его в список рассылки Objective-C, что вызвало следующий ответ: lists.apple.com/archives/objc-language/2012/Sep/msg00012.html - person Mike Abdullah; 08.11.2012
comment
Но разве блоки не выполняются во вторичном потоке? что означает, что строка кода после перечисления будет выполняться ДО окончания блоков перечисления? - person Van Du Tran; 17.06.2013
comment
Блоки @VanDuTran выполняются в отдельном потоке только в том случае, если вы указываете им выполняться в отдельном потоке. Если вы не используете параметр параллелизма перечисления, он будет выполняться в том же потоке, что и вызов. - person bbum; 17.06.2013
comment
Зачем вам нужен __block для этого? Вы не получите цикл сохранения, так как не храните блок. Блокировка какой-либо другой причины не должна сохраняться? - person desudesudesu; 12.01.2014
comment
@desudesudesu Если вам нужно изменить переменную, объявленную локально, в область действия метода в блоке, содержащемся в этом методе, вам нужно будет использовать __block. В противном случае присваивание вызовет ошибку компилятора. Ничего общего с сохранением циклов. - person bbum; 12.01.2014
comment
Ник Локвуд написал об этом очень хорошую статью, и кажется, что enumerateObjectsUsingBlock все еще значительно медленнее, чем быстрое перечисление для массивов и наборов. Интересно, почему? iosdevelopertips.com/objective-c/ - person Bob Spryn; 23.02.2014
comment
Хотя это своего рода деталь реализации, в этом ответе следует упомянуть среди различий между двумя подходами, что enumerateObjectsUsingBlock оборачивает каждый вызов блока пулом автоматического освобождения (по крайней мере, начиная с OS X 10.10). Это объясняет разницу в производительности по сравнению с for in, которая этого не делает. - person Pol; 05.12.2014
comment
Одна вещь, которую следует добавить: блочное перечисление дает вам (правильный) индекс объекта бесплатно, в то время как при быстром перечислении потребовался бы довольно дорогой вызов indexOfObject:, который даже не возвращает правильный индекс наверняка. - person vikingosegundo; 25.11.2015
comment
Утверждения о том, что enumerateObjectsUsingBlock так же быстр, как и for-in, доказуемо ложны. Смотрите мой ответ ниже... Это на самом деле ОЧЕНЬ МЕДЛЕННО по сравнению с ним. Если производительность имеет значение, не используйте enumerateObjectsUsingBlock! На самом деле, не используйте его, если в этом нет необходимости. - person Adam Kaplan; 09.04.2016

Для простого перечисления простое использование быстрого перечисления (т.е. цикл for…in…) является более идиоматичным вариантом. Блочный метод может быть немного быстрее, но в большинстве случаев это не имеет большого значения — немногие программы привязаны к процессору, и даже в этом случае узким местом будет сам цикл, а не вычисление внутри.

Простой цикл также читается более четко. Вот шаблон двух версий:

for (id x in y){
}

[y enumerateObjectsUsingBlock:^(id x, NSUInteger index, BOOL *stop){
}];

Даже если вы добавите переменную для отслеживания индекса, простой цикл будет легче читать.

Итак, когда вы должны использовать enumerateObjectsUsingBlock:? Когда вы сохраняете блок для выполнения позже или в нескольких местах. Это хорошо, когда вы на самом деле используете блок как первоклассную функцию, а не перегруженную замену тела цикла.

person Chuck    schedule 20.12.2010
comment
enumerateObjectsUsingBlock: во всех случаях будет либо с той же скоростью, либо быстрее, чем быстрое перечисление. for(... in ...) использует быстрое перечисление, которое требует от коллекции предоставления некоторого промежуточного представления внутренних структур данных. Как вы заметили, вероятно, не имеет значения. - person bbum; 20.12.2010
comment
@bbum Мои собственные тесты показывают, что enumerateObjects... на самом деле может быть медленнее, чем быстрое перечисление с циклом. Я запускал этот тест несколько тысяч раз; тело блока и цикла были одной и той же строкой кода: [(NSOperation *)obj cancel];. Средние значения: быстрый цикл enum - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009 и для блока - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Странно, что разница во времени такая большая и постоянная, но, очевидно, это очень специфический тестовый случай. - person chown; 30.10.2012

Хотя этот вопрос старый, ничего не изменилось, принятый ответ неверен

enumerateObjectsUsingBlock API не предназначался для замены for-in, но для совершенно другого варианта использования:

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

Быстрое перечисление с for-in по-прежнему является идиоматическим методом перечисления коллекции.

Быстрое перечисление выигрывает от краткости кода, удобочитаемости и дополнительных оптимизаций, которые делают его неестественно быстрым. Быстрее, чем старый цикл for C!

Быстрый тест показывает, что в 2014 году на iOS 7 enumerateObjectsUsingBlock стабильно на 700 % медленнее, чем for-in (на основе 1 мм итераций массива из 100 элементов).

Является ли здесь производительность реальной практической проблемой?

Точно нет, за редким исключением.

Смысл в том, чтобы продемонстрировать, что использование enumerateObjectsUsingBlock: вместо for-in без действительно веской причины малоэффективно. Это не делает код более читабельным... или быстрым... или потокобезопасным. (еще одно распространенное заблуждение).

Выбор зависит от личных предпочтений. Для меня выигрывает идиоматический и читабельный вариант. В данном случае это быстрое перечисление с использованием for-in.

Ориентир:

NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 100; i++) {
    arr[i] = [NSString stringWithFormat:@"%d", i];
}
int i;
__block NSUInteger length;

i = 1000 * 1000;
uint64_t a1 = mach_absolute_time();
while (--i > 0) {
    for (NSString *s in arr) {
        length = s.length;
    }
}
NSLog(@"For-in %llu", mach_absolute_time()-a1);

i = 1000 * 1000;
uint64_t b1 = mach_absolute_time();
while (--i > 0) {
    [arr enumerateObjectsUsingBlock:^(NSString *s, NSUInteger idx, BOOL *stop) {
        length = s.length;
    }];
}
NSLog(@"Enum %llu", mach_absolute_time()-b1);

Результаты:

2014-06-11 14:37:47.717 Test[57483:60b] For-in 1087754062
2014-06-11 14:37:55.492 Test[57483:60b] Enum   7775447746
person Adam Kaplan    schedule 11.06.2014
comment
Я могу подтвердить, что выполнение того же теста на MacBook Pro Retina 2014 enumerateObjectsUsingBlock на самом деле в 5 раз медленнее. Очевидно, это связано с тем, что пул автоосвобождения оборачивает каждый вызов блока, чего не происходит в случае for in. - person Pol; 05.12.2014
comment
Я подтверждаю, что enumerateObjectsUsingBlock: все еще в 4 раза медленнее на реальном iPhone 6 iOS9, используя для сборки Xcode 7.x. - person Cœur; 25.11.2015
comment
Спасибо за подтверждение! Я бы хотел, чтобы этот ответ не был так похоронен ... некоторым людям просто нравится синтаксис enumerate, потому что он похож на FP, и они не хотят слушать анализ производительности. - person Adam Kaplan; 09.04.2016
comment
Кстати, дело не только в пуле авторелиза. С помощью enumerate: можно гораздо больше толкать и выталкивать стек. - person Adam Kaplan; 08.01.2017

Чтобы ответить на вопрос о производительности, я провел несколько тестов, используя мой проект тестирования производительности< /а>. Я хотел узнать, какой из трех вариантов отправки сообщения всем объектам в массиве самый быстрый.

Варианты были:

1) makeObjectsPerformSelector

[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) быстрое перечисление и регулярная отправка сообщений

for (id item in arr)
{
    [item _stubMethod];
}

3) enumerateObjectsUsingBlock и отправка обычного сообщения

[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
 {
     [obj _stubMethod];
 }];

Оказывается, makeObjectsPerformSelector был самым медленным. Это заняло в два раза больше времени, чем быстрое перечисление. И enumerateObjectsUsingBlock был самым быстрым, примерно на 15-20% быстрее, чем быстрая итерация.

Поэтому, если вы очень заинтересованы в максимально возможной производительности, используйте enumerateObjectsUsingBlock. Но имейте в виду, что в некоторых случаях время, необходимое для перечисления коллекции, ничтожно по сравнению со временем, которое требуется для запуска любого кода, который вы хотите, чтобы каждый объект выполнял.

person LearnCocos2D    schedule 05.02.2012
comment
Можете ли вы указать на ваш конкретный тест? Вы, кажется, дали неверный ответ. - person Cœur; 06.08.2015

Довольно полезно использовать enumerateObjectsUsingBlock в качестве внешнего цикла, когда вы хотите разорвать вложенные циклы.

e.g.

[array1 enumerateObjectsUsingBlock:^(id obj1, NSUInteger idx, BOOL * _Nonnull stop) {
  for(id obj2 in array2) {
    for(id obj3 in array3) {
      if(condition) {
        // break ALL the loops!
        *stop = YES;
        return;
      }
    }
  }
}];

Альтернативой является использование операторов goto.

person Gabe    schedule 28.08.2015
comment
Или вы можете просто вернуться из метода, как вы сделали здесь :-D - person Adam Kaplan; 09.04.2016

Спасибо @bbum и @Chuck за всестороннее сравнение производительности. Рад узнать, что это банально. Кажется, я пошел с:

  • for (... in ...) - мой путь по умолчанию. Для меня более интуитивно понятен, здесь больше истории программирования, чем любые реальные предпочтения - повторное использование на разных языках, меньше ввода для большинства структур данных из-за автоматического завершения IDE: P.

  • enumerateObject... - когда нужен доступ к объекту и индексу. И при доступе к структурам, не являющимся массивами или словарями (личные предпочтения)

  • for (int i=idx; i<count; i++) - для массивов, когда мне нужно начать с ненулевого индекса

person snowbound    schedule 30.12.2015