Когда использовать enumerateObjectsUsingBlock или для

150

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

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

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

Пол Уилер
источник
1
Мне нравится использовать его, если мне нужен текущий индекс.
Беси
См. Также: stackoverflow.com/questions/8509662/…
Саймон Уитакер

Ответы:

350

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

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

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

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

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

  • в словарях перечисление на основе блоков - единственный способ получить ключ и значение одновременно.

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

bbum
источник
16
Вау, очень информативно. Хотел бы я принять оба эти ответа, но я иду с Чаком, потому что он резонирует со мной немного больше. Кроме того, я нашел ваш блог ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) во время поиска __block и узнал еще больше. Спасибо.
Пол Уилер
8
Напомним, что
Майк Абдулла,
2
Блоки @VanDuTran выполняются только в отдельном потоке, если вы указываете, что они должны выполняться в отдельном потоке. Если вы не используете опцию параллелизма перечисления, то она будет выполняться в том же потоке, в котором был выполнен вызов
bbum
2
Ник Локвуд написал по этому поводу действительно хорошую статью, и она enumerateObjectsUsingBlockвсе еще значительно медленнее, чем быстрое перечисление массивов и множеств. Интересно, почему? iosdevelopertips.com/objective-c/...
Боб Spryn
2
Хотя это своего рода деталь реализации, в этом ответе следует упомянуть различия между двумя подходами, которые enumerateObjectsUsingBlockзаключают в себе каждый вызов блока с пулом автоматического выпуска (по крайней мере, начиная с OS X 10.10). Это объясняет разницу в производительности по сравнению с тем, for inчего не происходит.
Pol
83

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

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

for (id x in y){
}

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

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

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

цыпленок
источник
5
enumerateObjectsUsingBlock:будет либо одинаковая скорость, либо быстрее, чем быстрое перечисление во всех случаях. for(... in ...)использует быстрое перечисление, которое требует, чтобы коллекция обеспечивала некоторое временное представление внутренних структур данных. Как вы заметили, скорее всего, не имеет значения.
bbum
4
+1When you're storing a block to execute later or in multiple places. It's good for when you're actually using a block as a first-class function rather than an overwrought replacement for a loop body.
Стив
7
@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. Странно, что разница во времени настолько велика и постоянна, но, очевидно, это очень специфический тестовый пример.
chown
42

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

enumerateObjectsUsingBlockAPI не был предназначен , чтобы заменить for-in, но совершенно иной вариант использования:

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

Быстрое перечисление с помощью for-in- все еще идиоматический метод перечисления коллекции.

Быстрое перечисление выигрывает от краткости кода, читабельности и дополнительных оптимизаций, которые делают его неестественно быстрым. Быстрее, чем старый цикл 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
Адам Каплан
источник
4
Я могу подтвердить, что выполнение того же теста на MacBook Pro Retina 2014, enumerateObjectsUsingBlockна самом деле, в 5 раз медленнее. Это, по-видимому, связано с пулом автоматического выпуска, оборачивающим каждый вызов блока, чего не происходит в данном for inслучае.
Pol
2
Я подтверждаю, enumerateObjectsUsingBlock:что все еще в 4 раза медленнее на реальном iPhone 6 iOS9, используя Xcode 7.x для сборки.
Cœur
1
Спасибо за подтверждение! Хотелось бы, чтобы этот ответ не был так похоронен ... некоторым людям просто нравится enumerateсинтаксис, потому что он похож на FP и не хочет слушать анализ производительности.
Адам Каплан
1
Кстати, это не только из-за пула авто-релиза. Там будет намного больше стек толкая и выскакивают сenumerate:
Адам Каплан
24

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

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

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. Но имейте в виду, что в некоторых случаях время, необходимое для перечисления коллекции, значительно меньше времени, которое требуется для выполнения любого кода, который вы хотите, чтобы каждый объект выполнял.

LearnCocos2D
источник
Можете ли вы указать на ваш конкретный тест? Вы, кажется, дали неправильный ответ.
Cœur
3

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

например

[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.

Гейб
источник
1
Или вы можете просто вернуться из метода, как вы это сделали здесь :-D
Адам Каплан
1

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

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

  • enumerateObject...- когда необходим доступ к объекту и индексу. И при доступе к структурам без массивов или словарей (персональные предпочтения)

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

заснеженный
источник