Коли використовувати enumerateObjectsUsingBlock vs. for


150

Крім очевидних відмінностей:

  • Використовуйте, enumerateObjectsUsingBlockколи вам потрібен і індекс, і об'єкт
  • Не використовуйте, enumerateObjectsUsingBlockколи вам потрібно змінювати локальні змінні (я помилявся з цього приводу, див. Відповідь bbum)

Є чи в enumerateObjectsUsingBlockцілому вважається краще або гірше , якщо for (id obj in myArray)б також працювати? Які переваги / недоліки (наприклад, це більш-менш ефективно)?


1
Мені подобається використовувати його, якщо мені потрібен поточний індекс.
Бесі

Відповіді:


350

Зрештою, використовуйте ту схему, яку ви хочете використовувати, і більш природно в контексті.

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

  • enumerateObjectsUsingBlock:буде настільки ж швидким або швидшим, ніж швидке перерахування ( for(... in ...)використовує NSFastEnumerationпідтримку для здійснення перерахування). Швидке перерахування вимагає перекладу з внутрішнього представлення на представлення для швидкого перерахування. У ній над головою. Перерахування на основі блоку дозволяє класу колекції перераховувати вміст так само швидко, як і найшвидше проходження рідного формату зберігання. Ймовірно, не має значення для масивів, але це може бути величезна різниця для словників.

  • "Не використовуйте enumerateObjectsUsingBlock, коли вам потрібно змінити локальні змінні" - не вірно; ви можете оголосити своїх місцевих жителів як __blockі вони будуть писати в блоці.

  • enumerateObjectsWithOptions:usingBlock: підтримує одночасне або зворотне перерахування.

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

Особисто я користуюся enumerateObjectsUsingBlock:частіше for (... in ...), але - знову ж таки - особистим вибором.


16
Нічого собі, дуже інформативно. Я б хотів, щоб я міг прийняти обидві ці відповіді, але я йду з Чак, тому що це трохи більше перегукується зі мною. Також я знайшов ваш блог ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) під час пошуку __block та дізнався ще більше. Дякую.
Пол Вілер

8
Для запису, перерахування на основі блоків не завжди є "настільки швидким чи швидшим" mikeabdullah.net/slow-block-based-dictionary-enumeration.html
Майк Абдулла

2
Блоки @VanDuTran виконуються лише в окремому потоці, якщо ви скажете, що вони будуть виконані в окремому потоці. Якщо ви не використовуєте параметр одночасності перерахування, він виконуватиметься в тому ж потоці, що і дзвінок
bbum

2
Нік Локвуд зробив справді приємну статтю з цього приводу, і, здається enumerateObjectsUsingBlock, все ще значно повільніше, ніж швидке перерахування масивів і наборів. Цікаво, чому? iosdevelopertips.com/objective-c/…
Боб Сприн

2
Хоча це якась деталізація реалізації, у цій відповіді слід зазначити серед відмінностей між двома підходами, які enumerateObjectsUsingBlockобволікають кожен виклик блоку пулом автоматичних випусків (як мінімум на ОС X 10.10 принаймні). Це пояснює різницю продуктивності порівняно з тим, for inщо цього не роблять.
Пол

83

Для простого перерахування, просто використовуючи швидке перерахування (тобто a 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]; . Середні значення: швидкий цикл перерахунку - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009і для блоку - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Дивно, що різниця у часі настільки велика і послідовна, але, очевидно, це дуже специфічний тестовий випадок.
човен

42

Хоча це питання давнє, речі не змінилися, прийнята відповідь невірна.

enumerateObjectsUsingBlockAPI не був призначений , щоб замінити for-in, але зовсім інший варіант використання:

  • Це дозволяє застосовувати довільну, не локальну логіку. тобто вам не потрібно знати, що робить блок, щоб використовувати його в масиві.
  • Одночасне перерахування для великих колекцій або важких обчислень (за допомогою withOptions:параметра)

Швидке перерахування for-inвсе ще є ідіоматичним методом перерахунку колекції.

Швидке перерахування вигоди від стислості коду, читабельності та додаткових оптимізацій що робить його неприродно швидким. Швидше, ніж старий C-цикл!

Швидкий тест дозволяє зробити висновок, що в 2014 році на iOS 7, enumerateObjectsUsingBlock стабільно на 700% повільніше, ніж при вході (на основі 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випадку.
Пол

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 був найбільш повільним на сьогоднішній день. Це зайняло вдвічі більше, ніж швидке перерахування. І підрахуватиObjectsUsingBlock було найшвидшим, воно було на 15-20% швидше, ніж швидка ітерація.

Тому якщо ви дуже стурбовані найкращою можливістю, використовуйте enumerateObjectsUsingBlock. Але майте на увазі, що в деяких випадках час, необхідний для перерахунку колекції, зменшується на час, необхідний для запуску будь-якого коду, який ви хочете виконувати кожен об'єкт.


Чи можете ви вказати на ваш конкретний тест? Здається, ви дали неправильну відповідь.
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++) - для масивів, коли мені потрібно починати з ненульового індексу

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.