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

23

Я вижу два общих шаблона для блоков в Objective-C. Один из них - пара успехов: / fail: блоки, другой - одно завершение: блок.

Например, допустим, у меня есть задача, которая будет возвращать объект асинхронно, и эта задача может завершиться ошибкой. Первый шаблон -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 есть обработка исключений с помощью throw / catch, есть причина, по которой вы не можете использовать это?
FrustratedWithFormsDesigner
Любое из этих разрешений позволяет объединять в цепочку асинхронные вызовы, которые исключение не дает.
Фрэнк Шеарар
5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - идиоматический объект не использует try / catch для управления потоком.
Муравей
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.

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

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 будет обычным объектом, который будет возвращен из асинхронного метода, как показано ниже:

- (Promise*) doSomethingAsync;

! Начальное состояние Обещания «ожидает».

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

Также обратите внимание, что нет обработчика завершения. Вместо этого Обещание предоставит более мощные средства, с помощью которых сайт вызова может получить конечный результат асинхронной задачи, который мы скоро увидим.

Асинхронная задача, которая создала объект обещания, ДОЛЖНА в конечном итоге «разрешить» его обещание. Это означает, что, поскольку задача может быть выполнена успешно или не выполнена, она ДОЛЖНА либо «выполнить» обещание, передав ему оцененный результат, либо ДОЛЖНА «отклонить» обещание, передав ему ошибку, указывающую причину ошибки.

! Задача должна в конечном итоге решить свое обещание.

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

! Обещание может быть выполнено только один раз .

После того, как обещание было выполнено, сайт вызова может получить результат (независимо от того, провалился он или успешно). Как это будет сделано, зависит от того, реализовано ли обещание с использованием синхронного или асинхронного стиля.

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

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

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

Оказалось, что синхронный стиль имеет ряд существенных недостатков, которые эффективно побеждают достоинства асинхронных задач. Интересная статья о в настоящее время некорректной реализации «фьючерсов» в стандартной C ++ 11 lib может быть прочитана здесь: Broken promises - C ++ 0x futures .

Как, в Objective-C, сайт вызова может получить результат?

Ну, наверное, лучше показать несколько примеров. Есть несколько библиотек, которые реализуют Обещание (см. Ссылки ниже).

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

Другие реализации могут иметь похожий API, но могут быть небольшие и, возможно, тонкие различия в синтаксисе. RXPromise - это версия спецификации Promise / A + для Objective-C, которая определяет открытый стандарт для надежных и совместимых реализаций обещаний в JavaScript.

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

Между различными реализациями есть довольно существенные различия. RXPromise внутренне использует диспетчеризацию lib, является полностью поточно-ориентированным, чрезвычайно легким, а также предоставляет ряд дополнительных полезных функций, таких как отмена.

Сайт вызова получает конечный результат асинхронной задачи через «регистрацию» обработчиков. «Обещание / спецификация 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 имеет параметр error, который является ошибкой, сообщаемой асинхронной задачей в случае ее сбоя.

Оба блока имеют возвращаемое значение. О чем это возвращаемое значение, скоро станет ясно.

В RXPromise thenэто свойство, которое возвращает блок. Этот блок имеет два параметра: блок обработчика успеха и блок обработчика ошибок. Обработчики должны быть определены call-сайтом.

! Обработчики должны быть определены call-сайтом.

Таким образом, выражение 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, вернет Promise:

! then(...)возвращает обещание

При вызове блока, возвращенного из свойства then, «получатель» возвращает новое обещание, дочернее обещание. Получатель становится родительским обещанием.

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

Что это значит?

Что ж, благодаря этому мы можем «связывать» асинхронные задачи, которые эффективно выполняются последовательно.

Кроме того, возвращаемое значение любого из обработчиков станет «значением» возвращаемого обещания. Таким образом, если задача выполнена успешно с возможным результатом @ «OK», возвращенное обещание будет «разрешено» (то есть «выполнено») со значением @ «OK»:

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 .

Этот список, вероятно, не полный!

При выборе третьей библиотеки для вашего проекта, пожалуйста, внимательно проверьте, соответствует ли реализация библиотеки предварительным условиям, перечисленным ниже:

  • Надежная библиотека обещаний ДОЛЖНА быть потокобезопасной!

    Все дело в асинхронной обработке, и мы хотим использовать несколько процессоров и выполнять их в разных потоках одновременно, когда это возможно. Будьте осторожны, большинство реализаций не являются потокобезопасными!

  • Обработчики ДОЛЖНЫ вызываться асинхронно в отношении call-сайта! Всегда и ни на что!

    Любая достойная реализация также должна следовать очень строгому шаблону при вызове асинхронных функций. Многие разработчики стремятся «оптимизировать» случай, когда обработчик будет вызываться синхронно когда обещание уже разрешено, когда обработчик будет зарегистрирован. Это может вызвать все виды проблем. Смотри Не отпускай Залго! ,

  • Должен также быть механизм отмены обещания.

    Возможность отмены асинхронной задачи часто становится требованием с высоким приоритетом в анализе требований. Если нет, то наверняка будет подан запрос на улучшение от пользователя через некоторое время после выпуска приложения. Причина должна быть очевидна: любая задача, которая может остановиться или занять слишком много времени, должна быть отменена пользователем или по таймауту. Приличная библиотека обещаний должна поддерживать отмену.

CouchDeveloper
источник
1
Это получает приз за самый длинный не ответ. Но A для усилий :-)
Путешествующий человек
3

Я понимаю, что это старый вопрос, но я должен ответить на него, потому что мой ответ отличается от других.

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

В случае завершения вашему блоку передаются два объекта, один представляет успех, а другой - отказ ... Так что вы будете делать, если оба равны нулю? Что вы делаете, если оба имеют значение? Это вопросы, которых можно избежать во время компиляции и как таковые они должны быть. Вы избегаете этих вопросов, имея два отдельных блока.

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


Обратите внимание, что с Swift все меняется. В нем мы можем реализовать понятие Eitherперечисления так, чтобы в одном блоке завершения гарантированно присутствовал либо объект, либо ошибка, и должен быть ровно один из них. Так что для Свифта лучше один блок.

Даниэль Т.
источник
1

Я подозреваю, что в конечном итоге это будет личное предпочтение ...

Но я предпочитаю отдельные блоки успеха / неудачи. Мне нравится разделять логику успеха / неудачи. Если бы у вас были вложенные успехи / неудачи, вы бы получили что-то, что было бы более читабельным (по крайней мере, на мой взгляд).

В качестве относительно экстремального примера такого вложения, вот несколько примеров Ruby, демонстрирующих этот паттерн.

Фрэнк Шиарар
источник
1
Я видел вложенные цепи обоих. Я думаю, что они оба выглядят ужасно, но это мое личное мнение.
Джеффри Томас
1
Но как еще можно связать асинхронные вызовы?
Фрэнк Шиарар
Я не знаю человека ... я не знаю. Отчасти я спрашиваю, потому что мне не нравится, как выглядит мой асинхронный код.
Джеффри Томас
Конечно. Вы заканчиваете тем, что пишете свой код в стиле передачи продолжения, что неудивительно. (Haskell имеет обозначение do именно по этой причине: позволяет писать в якобы прямом стиле.)
Фрэнк Шеарар
Возможно, вас заинтересует реализация ObjC Promises: 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
    }];
}];

Я думаю, что я пойду на некоторое время.

Джеффри Томас
источник