успіх: / провал: блоки проти завершення: блок


23

Я бачу два поширених шаблони для блоків у Objective-C. Один - це пара успіху: / fail: блоки, інше - єдине завершення: block.

Наприклад, скажімо, що у мене є завдання, яке поверне об'єкт асинхронно, і це може бути невдалим. Перша закономірність така -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Другий візерунок є -taskWithCompletion:(void (^)(id object, NSError *error))completion.

успіх: / провал:

[target taskWithSuccess:^(id object) {
    // W00t! I've got my object
} failure:^(NSError *error) {
    // Oh noes! report the failure.
}];

завершення:

[target taskWithCompletion:^(id object, NSError *error) {
    if (object) {
        // W00t! I've got my object
    } else {
        // Oh noes! report the failure.
    }
}];

Який кращий зразок? Які сильні та слабкі сторони? Коли ви використовуєте одне над іншим?


Я впевнений, що у Objective-C є обробка виключень із кидком / ловом, чи є причина, що ви не можете це використати?
FrustratedWithFormsDesigner

Будь-який із цих дозволів дозволяє ланцюжок викликів асинхронізації, які винятки вам не дають.
Френк Ширар

5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - ідіоматичне ObjC не використовує TRY / зловити для управління потоком.
Мураха

1
Подумайте, будь ласка, перенести свою відповідь з питання на відповідь ... адже це відповідь (і ви можете відповісти на власні запитання).

1
Нарешті я прихилився до тиску однолітків і перейшов свою відповідь до фактичної відповіді.
Джеффі Томас

Відповіді:


8

Зворотний виклик завершення (на відміну від пари успіху / відмови) є більш загальним. Якщо вам потрібно підготувати деякий контекст, перш ніж вирішувати стан повернення, ви можете зробити це безпосередньо перед пунктом "if (object)". У випадку успіху / відмови вам потрібно дублювати цей код. Це, звичайно, залежить від семантики зворотного виклику.


Не можу коментувати оригінальне запитання ... Винятки не є дійсним регулюванням потоку в target-c (ну, какао) і не повинні використовуватися як такі. Викинутий виняток слід виловлювати лише для того, щоб граціозно припинити.

Так, я це бачу. Якщо ви -task…могли б повернути об’єкт, але об'єкт не в правильному стані, вам все одно знадобиться обробка помилок в умові успіху.
Джеффі Томас

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

Я не розумію, як обробник завершення є більш загальним. Завершення в основному перетворює кілька парамметів методу в одну - у вигляді блокових парам. Також родове означає краще? У MVC у вас часто є дублікат коду і в контролері подання, це є необхідним злом через розділення проблем. Я не думаю, що це причина не залишатися подалі від MVC.
Бун

@Boon Однією з причин я вважаю, що один обробник є більш загальним - це випадки, коли ви вважаєте за краще, щоб виклик / обробник / блок визначав, чи вдала операція чи не вдалася. Розглянемо випадки часткового успіху, коли у вас, можливо, є об’єкт з частковими даними, а ваш об'єкт помилки - це помилка, яка вказує на те, що не всі дані були повернуті. Блок міг би вивчити самі дані та перевірити, чи достатньо їх. Це неможливо при сценарії зворотного виклику двійкових успіхів / збоїв.
Тревіс

8

Я б сказав, чи надає API один обробник завершення або пара блоків успіху / відмови, це насамперед питання особистої переваги.

Обидва підходи мають плюси і мінуси, хоча існують лише незначні відмінності.

Враховуйте, що є й інші варіанти, наприклад, коли один обробник завершення може мати лише один параметр, що поєднує кінцевий результат або потенційну помилку:

typedef void (^completion_t)(id result);

- (void) taskWithCompletion:(completion_t)completionHandler;

[self taskWithCompletion:^(id result){
    if ([result isKindOfError:[NSError class]) {
        NSLog(@"Error: %@", result);
    }
    else {
        ...
    }
}]; 

Мета цього підпису полягає в тому, що обробник завершення може бути використаний загалом в інших API.

Наприклад, у категорії для NSArray є метод, forEachApplyTask:completion:який послідовно викликає завдання для кожного об'єкта і розбиває цикл IFF, сталася помилка. Оскільки цей метод також є асинхронним, він також має обробник завершення:

typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);

Насправді, completion_tяк визначено вище, є загальним і достатнім, щоб обробити всі сценарії.

Однак існують інші засоби для асинхронної задачі, щоб подати повідомлення про її завершення на сайт виклику:

Обіцянки

Обіцянки, які також називаються «майбутніми», «відкладеними» або «відкладеними», є кінцевим результатом асинхронного завдання (див. Також: Вікі Майбутнє та обіцянки ).

Спочатку обіцянка знаходиться у стані "очікування". Тобто, його "значення" ще не оцінено та ще не доступне.

У Objective-C обіцянка буде звичайним об'єктом, який буде повернуто з асинхронного методу, як показано нижче:

- (Promise*) doSomethingAsync;

! Початковий стан Обіцянки - "очікує на розгляд".

Тим часом асинхронні завдання починають оцінювати його результат.

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

Асинхронна задача, яка створила об'єкт обіцянки, ОБОВ'ЯЗКОВО повинна "вирішити" свою обіцянку. Це означає, що оскільки завдання може бути успішним або невдалим, воно ОБОВ'ЯЗКОВО або "виконає" обіцянку, передаючи їй оцінений результат, або ОБОВ'ЯЗКОВО "відхилити" обіцянку, передаючи їй помилку, вказуючи причину відмови.

! Завдання має врешті-решт вирішити свою обіцянку.

Коли обіцянка буде вирішена, вона більше не може змінити стан, включаючи його значення.

! Обіцянку можна вирішити лише один раз .

Після того, як обіцянка була вирішена, сайт-дзвінок може отримати результат (невдалий чи успішний). Як це буде виконано, залежить від того, реалізується обіцянка за допомогою синхронного чи асинхронного стилю.

Обіцянка може бути реалізована в синхронному або асинхронному стилі, що призводить до блокування відповідної неблокуючої семантики.

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

В асинхронному стилі сайт викликів реєстрував би зворотні дзвінки або блоки обробника, які дзвонять одразу після того, як обіцянка буде вирішена.

Виявилося, що синхронний стиль має ряд істотних недоліків, які фактично перемагають достоїнства асинхронних завдань. Цікаву статтю про недосконалу реалізацію "ф'ючерсів" у стандартному C ++ 11 lib можна прочитати тут: Нескороті обіцянки – C ++ 0x ф'ючерси .

Яким чином у Objective-C сайт для викликів отримає результат?

Ну, мабуть, найкраще показати кілька прикладів. Є кілька бібліотек, які реалізують Обіцянку (див. Посилання нижче).

Однак для наступних фрагментів коду я буду використовувати конкретну реалізацію бібліотеки Promise, доступної на GitHub RXPromise . Я автор RXPromise.

Інші реалізації можуть мати подібний API, але можуть бути невеликі та, можливо, тонкі відмінності в синтаксисі. RXPromise - це версія специфікації Specific -C специфіки Promise / A +, яка визначає відкритий стандарт для надійної та сумісної реалізації обіцянок в JavaScript.

Усі бібліотеки обіцянок, перелічені нижче, реалізують асинхронний стиль.

Існують досить істотні відмінності між різними реалізаціями. RXPromise внутрішньо використовує диспетчерську вкладку, повністю безпечний для потоків, надзвичайно легкий, а також надає ряд додаткових корисних функцій, таких як скасування.

Сайт виклику отримує кінцевий результат асинхронного завдання за допомогою "реєстрації" обробників. "Специфікація Promise / A +" визначає метод then.

Метод then

З RXPromise це виглядає так:

promise.then(successHandler, errorHandler);

де successHandler - це блок, який викликається, коли обіцянка виконана ", а errorHandler - це блок, який викликається, коли обіцянку було" відхилено ".

! thenвикористовується для отримання можливого результату та визначення успіху чи обробника помилок.

У RXPromise блоки обробника мають такий підпис:

typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);

У success_handler є результат параметра, який, очевидно, є результатом асинхронної задачі. Аналогічно, error_handler має помилку параметра, яка є помилкою, про яку повідомляє асинхронна задача, коли вона не вдалася.

Обидва блоки мають повернене значення. Про що ця повернена вартість, стане зрозуміло незабаром.

У RXPromise then- це властивість, яка повертає блок. Цей блок має два параметри, блок обробника успіху та блок обробника помилок. Обробники повинні бути визначені сайтом виклику.

! Обробники повинні бути визначені сайтом виклику.

Отже, вираз promise.then(success_handler, error_handler);- це коротка форма

then_block_t block promise.then;
block(success_handler, error_handler);

Ми можемо написати ще більш стислий код:

doSomethingAsync
.then(^id(id result){
    
    return @“OK”;
}, nil);

У коді написано: "Виконати doSomethingAsync, коли це вдасться, а потім виконати обробник успіху".

Тут, обробник помилок - nilце означає, що в разі помилки він не буде оброблятися в цій обіцянці.

Ще одним важливим фактом є те, що виклик блоку, повернутого з власності then, поверне Обіцянку:

! then(...)повертає Обіцянку

При виклику блоку, поверненого з власності then, "приймач" повертає нову Обіцянку, обіцянку дитини . Одержувач стає батьківською обіцянкою.

RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);

Що це означає?

Ну, завдяки цьому ми можемо «ланцюжок» асинхронних завдань, які ефективно виконуються послідовно.

Крім того, повернене значення будь-якого обробника стане «значенням» повернутої обіцянки. Отже, якщо завдання вдалося досягти з кінцевим результатом @ "ОК", повернута обіцянка буде "вирішена" (тобто "виконано") зі значенням @ "ОК":

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return @"OK";
}, nil);

...
assert([[returnedPromise get] isEqualToString:@"OK"]);

Так само, коли асинхронна задача не вдається, повернута обіцянка буде вирішена (тобто "відхилена") з помилкою.

RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
    return error;
});

...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);

Обробник також може повернути ще одну обіцянку. Наприклад, коли цей обробник виконує інше асинхронне завдання. За допомогою цього механізму ми можемо «ланцюжок» асинхронних завдань:

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return asyncB(result);
}, nil);

! Повернене значення блоку обробника стає значенням дитячої обіцянки.

Якщо дитячої обіцянки немає, повернене значення не впливає.

Більш складний приклад:

Тут ми виконуємо asyncTaskA, asyncTaskB, asyncTaskCі asyncTaskD послідовно - і кожна наступна задача приймає результат попередньої задачі в якості вхідних даних:

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);

Такий «ланцюжок» також називають «продовженням».

Помилка обробки

Обіцяння особливо спрощують поводження з помилками. Помилки будуть "перенаправлені" від батька до дитини, якщо в обітниці батьків не буде визначено обробника помилок. Помилка буде спрямована вгору по ланцюгу, поки дитина не обробляє її. Таким чином, маючи вищевказану ланцюжок, ми можемо реалізувати обробку , просто додавши ще один «продовження» , яка має справу з потенційною помилки , які можуть статися в будь-якому місці помилки вище :

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);
.then(nil, ^id(NSError*error) {
    NSLog(@“”Error: %@“, error);
    return nil;
});

Це схоже на, мабуть, більш звичний синхронний стиль із обробкою виключень:

try {
    id a = A();
    id b = B(a);
    id c = C(b);
    id d = D(c);
    // handle d
}
catch (NSError* error) {
    NSLog(@“”Error: %@“, error);
}

Обіцянки загалом мають інші корисні функції:

Наприклад, маючи посилання на обіцянку, через thenних можна «зареєструвати» стільки обробників, скільки бажається. У RXPromise реєстрація обробників може відбуватися в будь-який час та з будь-якої потоку, оскільки вона повністю захищена від потоку.

RXPromise має ще кілька корисних функціональних функцій, які не вимагаються специфікацією Promise / A +. Одне - «скасування».

Виявилося, що "скасування" - неоціненна і важлива особливість. Наприклад, сайт для дзвінків, що містить посилання на обіцянку, може надіслати йому cancelповідомлення, щоб вказати, що він більше не зацікавлений у можливому результаті.

Просто уявіть собі асинхронну задачу, яка завантажує зображення з Інтернету і яке повинно відображатися в контролері перегляду. Якщо користувач відходить від поточного контролера перегляду, розробник може реалізувати код, який надсилає повідомлення про скасування в imagePromise , що, в свою чергу, запускає оброблювач помилок, визначений операцією запиту HTTP, коли запит буде скасовано.

У RXPromise повідомлення про скасування передаватиметься лише від батьків своїм дітям, але не навпаки. Тобто, «коренева» обіцянка скасує всі дитячі обіцянки. Але дитяча обіцянка скасує «гілку» лише там, де вона є батьком. Повідомлення про скасування також буде передано дітям, якщо обіцянка вже вирішена.

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

Ось ще кілька реалізацій Promises в Objective-C, знайдених на GitHub:

https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https: // github.com/KptainO/Rebelle

і моя власна реалізація: RXPromise .

Цей список, ймовірно, не повний!

Вибираючи третю бібліотеку для свого проекту, уважно перевірте, чи реалізація бібліотеки відповідає переліченим нижче умовам:

  • Надійна бібліотека з обіцянками БУДЕ безпечна для потоків!

    Вся справа в асинхронній обробці, і ми хочемо використовувати кілька ЦП та виконувати на різних потоках одночасно, коли це можливо. Будьте уважні, більшість реалізацій не є безпечними для потоків!

  • Обробники БУДЬ викликатися асинхронно, що стосується сайту виклику! Завжди, і незважаючи ні на що!

    Будь-яка гідна реалізація також повинна слідувати дуже суворій схемі, коли викликає асинхронні функції. Багато виконавців прагнуть "оптимізувати" випадок, коли обробник буде викликаний синхронно, коли обіцянка вже вирішена, коли обробник зареєструється. Це може викликати всілякі проблеми. Дивіться Не звільняйте Zalgo! .

  • Також має бути механізм для скасування обіцянки.

    Можливість скасувати асинхронну задачу часто стає вимогою з високим пріоритетом в аналізі вимог. Якщо ні, напевно користувач буде надсилати запит на покращення через деякий час після виходу програми. Причина повинна бути очевидною: будь-яке завдання, яке може затриматись або зайняти занадто багато часу для завершення, повинно бути скасовано користувачем або затримкою. Гідна бібліотека обіцянок повинна підтримувати скасування.


1
Це отримує приз за найдовший невідповідь коли-небудь. Але A для зусиль :-)
Мандруючий чоловік

3

Я усвідомлюю, що це давнє питання, але я мушу відповісти на нього, тому що моя відповідь відрізняється від інших.

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

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

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


Зауважте, що з Swift все змінюється. У ньому ми можемо реалізувати поняття Eitherперерахунку, щоб гарантовано, що в блоці єдиного завершення є або об'єкт, або помилка, і він повинен мати саме один з них. Так що для Swift кращий один блок.


1

Я підозрюю, що це зрештою стане особистим уподобанням ...

Але я віддаю перевагу окремим блокам успіху / невдачі. Мені подобається розділяти логіку успіху / невдачі. Якби ви вклали успіх / невдачі, ви б закінчилися чимось читальним (принаймні, на мою думку).

Як відносно екстремальний приклад такого гніздування, ось Рубі показав цю модель.


1
Я бачив вкладені ланцюжки обох. Я думаю, що вони обидва виглядають жахливо, але це моя особиста думка.
Джеффі Томас

1
Але як ще можна ланцюг асинхронних дзвінків?
Френк Ширар

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

Звичайно. Ви закінчуєте писати свій код у стилі продовження, що не дуже дивно. (Haskell має свою нотацію саме з цієї причини: дозволяючи писати в нібито прямому стилі.)
Frank Shearar

Можливо, вас зацікавить реалізація цієї програми ObjC Обіцяє: github.com/couchdeveloper/RXPromise
e1985

0

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

Я думаю, що остаточний код буде виглядати приблизно так

[target taskWithCompletion:^(id object, NSError *error) {
    if (error) {
        // Oh noes! report the failure.
    } else if (![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
    } else {
        // W00t! I've got my object
    }
}];

або просто

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    // W00t! I've got my object
}];

Не найкращий фрагмент коду, і вкладення стає гірше

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    [object objectTaskWithCompletion:^(id object2, NSError *error) {
        if (error || ![object validateObject2:&object2 error:&error]) {
            // Oh noes! report the failure.
            return;
        }

        // W00t! I've got object and object 2
    }];
}];

Думаю, я на деякий час піду на мопе.

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