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