성공 : / 실패 : 블록 vs 완료 : 블록


23

Objective-C에서 블록에 대한 두 가지 공통 패턴이 있습니다. 하나는 성공 : 실패 / 블록 : 한 쌍이고 다른 하나는 단일 완료 : 블록입니다.

예를 들어, 객체를 비동기 적으로 반환하는 작업이 있고 해당 작업이 실패 할 수 있다고 가정하겠습니다. 첫 번째 패턴은 -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

이 중 하나는 체인 호출 비동기 호출을 허용하지만 예외는 아닙니다.
Frank Shearar

5
@FrustratedWithFormsDesigner : stackoverflow.com/a/3678556/2289- 관용 objc는 흐름 제어에 try / catch를 사용하지 않습니다.
Ant

1
답을 질문에서 답으로 옮기는 것을 고려하십시오 ... 결국 답입니다 (자신의 질문에 대답 할 수 있습니다).

1
나는 마침내 동료의 압력에 굴복하여 내 대답을 실제 대답으로 옮겼습니다.
Jeffery Thomas

답변:


8

완료 콜백 (성공 / 실패 쌍과 반대)이 더 일반적입니다. 리턴 상태를 처리하기 전에 일부 컨텍스트를 준비해야하는 경우 "if (object)"절 직전에 수행 할 수 있습니다. 성공 / 실패의 경우이 코드를 복제해야합니다. 이것은 물론 콜백 의미에 달려 있습니다.


원래 질문에 대해서는 언급 할 수 없습니다 ... 예외는 objective-c (웰, 코코아)에서 유효한 흐름 제어가 아니므로 그대로 사용해서는 안됩니다. 예외를 정상적으로 처리하려면 예외를 처리해야합니다.

응, 알 겠어 경우 -task…개체를 반환하지만 개체가 올바른 상태가 아닌 수, 당신은 여전히 성공 조건에서 오류 처리가 필요합니다.
Jeffery Thomas

예, 그리고 블록이 제자리에 있지 않지만 컨트롤러에 인수로 전달되면 두 블록을 던져야합니다. 콜백을 여러 계층으로 통과해야하는 경우 지루할 수 있습니다. 그래도 언제든지 분할 / 구성 할 수 있습니다.

완료 처리기가 더 일반적인 방법을 이해하지 못합니다. 완성은 기본적으로 여러 메소드 매개 변수를 하나의 블록 매개 변수 형식으로 바꿉니다. 또한 일반적인 의미가 더 낫습니까? MVC에서 종종 뷰 컨트롤러에 중복 코드가있는 경우가 종종 있습니다. 이는 우려의 분리로 인해 필요한 악입니다. 나는 그것이 MVC에서 멀리 떨어져있는 이유라고 생각하지 않습니다.
Boon

@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 for 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위에서 정의한대로 모든 시나리오를 처리하기에 충분하고 충분합니다.

그러나 비동기 작업이 완료 알림을 호출 사이트에 알리는 다른 방법이 있습니다.

약속

"미래", "지연"또는 "지연" 이라고도하는 약속 은 비동기 작업 의 최종 결과를 나타냅니다 (wiki 미래 및 약속 참조 ).

처음에는 약속이 "보류 중"상태입니다. 즉, "값"은 아직 평가되지 않았으며 아직 사용할 수 없습니다.

Objective-C에서 Promise는 다음과 같이 비동기 메소드에서 리턴되는 일반 오브젝트입니다.

- (Promise*) doSomethingAsync;

! 약속의 초기 상태는 "대기 중"입니다.

한편, 비동기 작업은 결과를 평가하기 시작합니다.

또한 완료 핸들러가 없습니다. 대신, 약속은 콜 사이트가 비동기 작업의 결과를 얻을 수있는보다 강력한 수단을 제공 할 것입니다.

promise 객체를 생성 한 비동기 작업은 결국 해당 약속을 "해결"해야합니다. 즉, 작업이 성공 또는 실패 할 수 있기 때문에 평가 된 결과를 전달하는 약속을 "수행"해야하거나 실패 이유를 나타내는 오류를 전달한 약속을 "거부"해야합니다.

! 작업은 결국 약속을 해결해야합니다.

약속이 해결되면 더 이상 값을 포함하여 상태를 변경할 수 없습니다.

! 약속은 한 번만 해결할 수 있습니다 .

약속이 해결되면 콜 사이트는 결과 (실패 또는 성공 여부)를 얻을 수 있습니다. 이것이 달성되는 방법은 약속이 동기식 스타일을 사용하는지 아니면 비동기식 스타일을 사용하여 구현되는지에 따라 다릅니다.

약속 중 하나에있는 리드 동기 또는 비동기 스타일로 구현 될 수있다 블로킹 각각 비 차단 의미.

약속 값을 검색하기 위해 동기식 스타일에서 콜 사이트는 약속이 비동기 작업으로 해결되고 최종 결과가 제공 될 때까지 현재 스레드 를 차단 하는 메소드를 사용 합니다.

비동기 스타일에서 콜 사이트는 약속이 해결 된 후 즉시 호출되는 콜백 또는 핸들러 블록을 등록합니다.

동기식 스타일에는 비동기 작업의 장점을 효과적으로 극복 할 수있는 여러 가지 중요한 단점이 있습니다. 표준 C ++ 11 lib에서 현재 결함이있는 "미래"구현에 대한 흥미로운 기사는 여기에서 읽을 수 있습니다 : Broken promises–C ++ 0x futures .

Objective-C에서 콜 사이트는 어떻게 결과를 얻습니까?

글쎄, 아마도 몇 가지 예를 보여주는 것이 가장 좋습니다. Promise를 구현하는 몇 가지 라이브러리가 있습니다 (아래 링크 참조).

그러나 다음 코드 스 니펫에는 GitHub RXPromise 에서 제공되는 Promise 라이브러리의 특정 구현을 사용합니다 . 저는 RXPromise의 저자입니다.

다른 구현에는 비슷한 API가있을 수 있지만 구문에는 작고 미묘한 차이가있을 수 있습니다. RXPromise는 Promise / A + 사양 의 Objective-C 버전으로 JavaScript에서 강력하고 상호 운용 가능한 약속 구현을위한 공개 표준을 정의합니다.

아래에 나열된 모든 promise 라이브러리는 비동기 스타일을 구현합니다.

서로 다른 구현 간에는 상당한 차이가 있습니다. RXPromise는 디스패치 lib를 내부적으로 사용하고, 스레드 안전하고, 매우 가벼우 며, 취소와 같은 여러 가지 유용한 기능을 제공합니다.

콜 사이트는“등록”핸들러를 통해 비동기 작업의 최종 결과를 얻습니다. "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에서, thenA는 특성 블록을 반환합니다. 이 블록에는 성공 처리기 블록과 오류 처리기 블록의 두 매개 변수가 있습니다. 핸들러는 호출 사이트에서 정의해야합니다.

! 핸들러는 호출 사이트에서 정의해야합니다.

따라서 표현 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(...)약속을 돌려줍니다

property 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, asyncTaskCasyncTaskD 순차 - 각각의 후속 작업이 입력으로 이전 작업의 결과를 취

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에서 취소 메시지는 부모에서 자식에게만 전달되며 그 반대도 마찬가지입니다. 즉,“뿌리”약속은 모든 어린이 약속을 취소합니다. 그러나 어린이 약속은 부모 인“지점”만 취소합니다. 약속이 이미 해결 된 경우 취소 메시지도 어린이에게 전달됩니다.

비동기 작업은 자체 약속을 위해 처리기를 자체 등록 할 수 있으므로 다른 사람이이를 취소 한시기를 감지 할 수 있습니다. 그러면 시간이 오래 걸리고 비용이 많이 드는 작업 수행이 조기에 중단 될 수 있습니다.

GitHub에서 찾은 Objective-C의 Promises 구현은 다음과 같습니다.

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 .

이 목록은 완전하지 않을 수 있습니다!

프로젝트의 세 번째 라이브러리를 선택할 때 라이브러리 구현이 아래 나열된 전제 조건을 따르는 지주의 깊게 확인하십시오.

  • 신뢰할 수있는 약속 라이브러리는 스레드 안전해야합니다!

    비동기 처리에 관한 것이므로 여러 CPU를 활용하고 가능할 때마다 다른 스레드에서 동시에 실행하려고합니다. 대부분의 구현은 스레드로부터 안전하지 않습니다.

  • 콜 사이트와 관련하여 핸들러를 비동기식 으로 호출해야합니다! 항상, 그리고 무엇이든!

    적절한 구현은 비동기 함수를 호출 할 때 매우 엄격한 패턴을 따라야합니다. 많은 구현 자는 처리기가 등록 될 때 약속이 이미 해결 될 때 처리기가 동 기적 으로 호출되는 경우를 "최적화"하는 경향이 있습니다 . 이로 인해 모든 종류의 문제가 발생할 수 있습니다. Zalgo를 놓지 마십시오를 참조하십시오 ! .

  • 약속을 취소하는 메커니즘도 있어야합니다.

    비동기 작업을 취소 할 수있는 가능성은 종종 요구 사항 분석에서 우선 순위가 높은 요구 사항이됩니다. 그렇지 않은 경우 앱이 출시 된 후 얼마 후 사용자로부터 개선 요청이 접수됩니다. 그 이유는 분명해야합니다. 중지되거나 완료하는 데 너무 오래 걸리는 작업은 사용자 또는 시간 초과로 취소 할 수 있어야합니다. 괜찮은 약속 라이브러리는 취소를 지원해야합니다.


1
이것은 응답이없는 가장 긴 상을받습니다. 그러나 노력에 대한 :-)
Traveling Man

3

나는 이것이 오래된 질문이라는 것을 알고 있지만 내 대답이 다른 질문과 다르기 때문에 대답해야합니다.

개인적인 취향의 문제라고 말하는 사람들에게는 동의하지 않습니다. 하나를 선호하는 논리적이고 합리적 인 이유가 있습니다 ...

완료 사례에서 블록은 두 개의 객체를 전달합니다. 하나는 성공을 나타내고 다른 하나는 실패를 나타냅니다 ... 둘 다없는 경우 어떻게해야합니까? 둘 다 가치가 있다면 어떻게해야합니까? 이들은 컴파일 타임에 피할 수있는 질문이며 그렇게해야합니다. 두 개의 별도 블록을 가짐으로써 이러한 질문을 피할 수 있습니다.

별도의 성공 및 실패 블록이 있으면 코드를 정적으로 확인할 수 있습니다.


Swift에 따라 상황이 변경됩니다. 그것에, 우리는의 개념을 구현할 수 있습니다Either 에서 단일 완성 블록에 객체 또는 오류가 있는지 확인하고 정확히 하나만 포함 열거 있습니다. 따라서 Swift의 경우 단일 블록이 더 좋습니다.


1

나는 그것이 개인적 취향이 될 것이라고 생각합니다 ...

그러나 나는 별도의 성공 / 실패 블록을 선호합니다. 나는 성공 / 실패 논리를 분리하는 것을 좋아합니다. 중첩 된 성공 / 실패를 겪었다면 더 읽기 쉬운 것 (최소한 의견으로는)이 될 것입니다.

이러한 중첩의 상대적으로 극단적 인 예로, 이 패턴을 보여주는 Ruby 가 있습니다.


1
둘 다 중첩 된 체인을 보았습니다. 둘 다 끔찍해 보인다고 생각하지만 그건 내 개인적인 의견입니다.
Jeffery Thomas

1
그러나 어떻게 비동기 호출을 연결할 수 있습니까?
Frank Shearar

나는 사람을 모른다… 나는 모른다. 내가 묻는 이유 중 하나는 비동기 코드의 모양이 마음에 들지 않기 때문입니다.
Jeffery Thomas

확실한. 연속 전달 스타일로 코드를 작성하는 것은 놀라운 일이 아닙니다. (Haskell은 정확히 이런 이유로 표기법을 가지고 있습니다 : 표면적으로 직접 스타일을 쓰도록하세요.)
Frank Shearar

이 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
    }];
}];

나는 잠시 동안 mope 갈 것이다 생각합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.