berhasil: / gagal: blok vs selesai: blok

23

Saya melihat dua pola umum untuk blok di Objective-C. Salah satunya adalah sepasang keberhasilan: / kegagalan: blok, yang lain adalah penyelesaian tunggal: blok.

Misalnya, katakanlah saya memiliki tugas yang akan mengembalikan objek secara tidak sinkron dan tugas itu mungkin gagal. Pola pertama adalah -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Pola kedua adalah -taskWithCompletion:(void (^)(id object, NSError *error))completion.

berhasil: / gagal:

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

penyelesaian:

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

Pola mana yang disukai? Apa kelebihan dan kekurangannya? Kapan Anda akan menggunakan satu di atas yang lain?

Jeffery Thomas
sumber
Saya cukup yakin Objective-C memiliki penanganan pengecualian dengan melempar / menangkap, apakah ada alasan Anda tidak dapat menggunakannya?
FrustratedWithFormsDesigner
Salah satu dari ini mengizinkan chaining panggilan async, yang pengecualiannya tidak memberi Anda.
Frank Shearar
5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - idiomatic objc tidak menggunakan try / catch untuk kontrol aliran.
Semut
1
Harap pertimbangkan untuk memindahkan Jawaban Anda dari pertanyaan ke jawaban ... setelah semua, itu adalah jawaban (dan Anda dapat menjawab pertanyaan Anda sendiri).
1
Saya akhirnya menyerah pada tekanan teman sebaya dan memindahkan jawaban saya ke jawaban yang sebenarnya.
Jeffery Thomas

Jawaban:

8

Callback penyelesaian (berlawanan dengan pasangan sukses / gagal) lebih umum. Jika Anda perlu menyiapkan beberapa konteks sebelum berurusan dengan status pengembalian, Anda dapat melakukannya tepat sebelum klausa "jika (objek)". Dalam kasus sukses / gagal Anda harus menduplikasi kode ini. Ini tergantung pada semantik panggilan balik, tentu saja.


sumber
Tidak dapat mengomentari pertanyaan asli ... Pengecualian tidak berlaku untuk kontrol aliran dalam objektif-c (yah, kakao) dan tidak boleh digunakan seperti itu. Pengecualian yang dilemparkan harus ditangkap hanya untuk mengakhiri dengan anggun.
Ya, saya bisa melihatnya. Jika -task…bisa mengembalikan objek, tetapi objek tidak dalam kondisi yang benar, maka Anda masih perlu penanganan kesalahan dalam kondisi sukses.
Jeffery Thomas
Ya, dan jika blok tidak di tempat, tetapi dilewatkan sebagai argumen ke controller Anda, Anda harus melemparkan dua blok di sekitar. Ini mungkin membosankan ketika panggilan balik harus dilewati banyak lapisan. Anda selalu dapat membelah / menyusun kembali.
Saya tidak mengerti bagaimana penyelesaian handler lebih umum. Penyelesaian pada dasarnya mengubah beberapa metode params menjadi satu - dalam bentuk params blok. Juga, apakah generik berarti lebih baik? Dalam MVC Anda sering kali memiliki kode duplikat dalam pengontrol tampilan juga, itu adalah kejahatan yang perlu karena pemisahan keprihatinan. Saya tidak berpikir itu alasan untuk menjauh dari MVC.
Boon
@Boon Salah satu alasan saya melihat handler tunggal lebih umum adalah untuk kasus-kasus di mana Anda lebih suka callee / handler / block itu sendiri menentukan apakah operasi berhasil atau gagal. Pertimbangkan kasus keberhasilan sebagian di mana Anda mungkin memiliki objek dengan sebagian data dan objek kesalahan Anda adalah kesalahan yang menunjukkan bahwa tidak semua data dikembalikan. Blok dapat memeriksa data itu sendiri dan memeriksa untuk melihat apakah itu cukup. Ini tidak mungkin dengan skenario keberhasilan / kegagalan panggilan balik biner.
Travis
8

Saya akan mengatakan, apakah API menyediakan satu handler penyelesaian atau sepasang blok sukses / gagal, pada dasarnya adalah masalah preferensi pribadi.

Kedua pendekatan memiliki pro dan kontra, meskipun hanya ada sedikit perbedaan.

Pertimbangkan bahwa ada juga varian lebih lanjut, misalnya di mana satu selesai handler mungkin hanya satu parameter menggabungkan hasil akhirnya atau kesalahan yang potensial:

typedef void (^completion_t)(id result);

- (void) taskWithCompletion:(completion_t)completionHandler;

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

Tujuan dari tanda tangan ini adalah bahwa handler penyelesaian dapat digunakan secara umum di API lain.

Sebagai contoh dalam Kategori untuk NSArray ada metode forEachApplyTask:completion:yang secara berurutan memanggil tugas untuk setiap objek dan memecah loop IFF ada kesalahan. Karena metode ini sendiri juga tidak sinkron, ia memiliki penangan penyelesaian juga:

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

Faktanya, completion_tsebagaimana didefinisikan di atas cukup umum dan cukup untuk menangani semua skenario.

Namun, ada cara lain untuk tugas asinkron untuk mengirimkan notifikasi penyelesaiannya ke situs panggilan:

Janji

Janji, juga disebut "Berjangka", "Ditangguhkan" atau "Tertunda" merupakan hasil akhirnya dari tugas asinkron (lihat juga: wiki Futures dan janji ).

Awalnya, sebuah janji berada dalam kondisi "tertunda". Artinya, "nilainya" belum dievaluasi dan belum tersedia.

Di Objective-C, sebuah Janji akan menjadi objek biasa yang akan dikembalikan dari metode asinkron seperti yang ditunjukkan di bawah ini:

- (Promise*) doSomethingAsync;

! Keadaan awal suatu Janji adalah "tertunda".

Sementara itu, tugas asinkron mulai mengevaluasi hasilnya.

Perhatikan juga, bahwa tidak ada handler penyelesaian. Sebaliknya, Janji akan memberikan cara yang lebih kuat di mana situs panggilan dapat memperoleh hasil akhirnya dari tugas asinkron, yang akan segera kita lihat.

Tugas asinkron, yang menciptakan objek janji, HARUS akhirnya "menyelesaikan" janjinya. Itu berarti, karena suatu tugas dapat berhasil atau gagal, itu HARUS "memenuhi" janji lewat itu hasil yang dievaluasi, atau HARUS "menolak" janji lewat itu kesalahan yang menunjukkan alasan kegagalan.

! Suatu tugas pada akhirnya harus menyelesaikan janjinya.

Ketika Janji telah diselesaikan, itu tidak dapat mengubah statusnya lagi, termasuk nilainya.

! Janji hanya bisa diselesaikan satu kali .

Setelah janji telah diselesaikan, situs panggilan dapat memperoleh hasilnya (apakah gagal atau berhasil). Bagaimana ini dilakukan tergantung pada apakah janji diimplementasikan menggunakan gaya sinkron atau asinkron.

Sebuah Janji dapat diimplementasikan dalam sinkron atau gaya asynchronous yang mengarah ke salah blocking masing-masing non-blocking semantik.

Dalam gaya sinkron untuk mengambil nilai janji, situs panggilan akan menggunakan metode yang akan memblokir utas saat ini sampai setelah janji telah diselesaikan oleh tugas asinkron dan hasil akhirnya tersedia.

Dalam gaya yang tidak sinkron, situs panggilan akan mendaftarkan panggilan balik atau blok penangan yang dipanggil segera setelah janji telah diselesaikan.

Ternyata gaya sinkron memiliki sejumlah kelemahan signifikan yang secara efektif mengalahkan kelebihan tugas asinkron. Artikel menarik tentang implementasi "futures" yang saat ini cacat dalam standar C ++ 11 lib dapat dibaca di sini: Patah janji – C ++ 0x futures .

Bagaimana, di Objective-C, sebuah situs panggilan akan mendapatkan hasilnya?

Yah, mungkin yang terbaik untuk menunjukkan beberapa contoh. Ada beberapa perpustakaan yang menerapkan Janji (lihat tautan di bawah).

Namun, untuk cuplikan kode berikutnya, saya akan menggunakan implementasi tertentu dari perpustakaan Promise, tersedia di GitHub RXPromise . Saya penulis RXPromise.

Implementasi lain mungkin memiliki API yang serupa, tetapi mungkin ada perbedaan kecil dan mungkin perbedaan dalam sintaksis. RXPromise adalah versi Objective-C dari spesifikasi Promise / A + yang mendefinisikan standar terbuka untuk implementasi yang kuat dan interoperable dari janji dalam JavaScript.

Semua perpustakaan janji yang tercantum di bawah ini menerapkan gaya asinkron.

Ada perbedaan yang cukup signifikan di antara implementasi yang berbeda. RXPromise secara internal menggunakan lib pengiriman, sepenuhnya aman dari benang, sangat ringan, dan juga menyediakan sejumlah fitur bermanfaat lainnya, seperti pembatalan.

Situs panggilan mendapatkan hasil akhirnya dari tugas asinkron melalui penangan “pendaftaran”. "Janji / spesifikasi A +" mendefinisikan metode then.

Metode then

Dengan RXPromise tampilannya sebagai berikut:

promise.then(successHandler, errorHandler);

di mana successHandler adalah blok yang dipanggil ketika janji telah "dipenuhi" dan errorHandler adalah blok yang dipanggil ketika janji telah "ditolak".

! thendigunakan untuk mendapatkan hasil akhirnya dan untuk menentukan sukses atau penangan kesalahan.

Dalam RXPromise, blok pawang memiliki tanda tangan berikut:

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

Success_handler memiliki hasil parameter yang jelas merupakan hasil akhirnya dari tugas asinkron. Demikian juga, error_handler memiliki kesalahan parameter yang merupakan kesalahan yang dilaporkan oleh tugas asinkron ketika gagal.

Kedua blok memiliki nilai pengembalian. Tentang nilai pengembalian ini, akan menjadi jelas segera.

Di RXPromise, thenadalah properti yang mengembalikan blok. Blok ini memiliki dua parameter, blok penangan sukses dan blok penangan kesalahan. Penangan harus ditentukan oleh situs panggilan.

! Penangan harus ditentukan oleh situs panggilan.

Jadi, ungkapan itu promise.then(success_handler, error_handler);adalah bentuk singkat dari

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

Kami bahkan dapat menulis kode yang lebih ringkas:

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

Kode tersebut berbunyi: "Jalankan doSomethingAsync, ketika berhasil, kemudian jalankan penangan sukses".

Di sini, penangan kesalahan adalah nilyang berarti, jika terjadi kesalahan, itu tidak akan ditangani dalam janji ini.

Fakta penting lainnya adalah bahwa memanggil blok yang dikembalikan dari properti thenakan mengembalikan Janji:

! then(...)mengembalikan Janji

Saat memanggil blok yang dikembalikan dari properti then, "penerima" mengembalikan Janji baru , janji anak . Penerima menjadi janji orang tua .

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

Apa artinya?

Nah, karena ini kita dapat "rantai" tugas-tugas tidak sinkron yang secara efektif dieksekusi secara berurutan.

Selain itu, nilai pengembalian dari salah satu pawang akan menjadi "nilai" dari janji yang dikembalikan. Jadi, jika tugas berhasil dengan hasil akhirnya @ "OK", janji yang dikembalikan akan "diselesaikan" (yaitu "terpenuhi") dengan nilai @ "OK":

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

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

Demikian juga, ketika tugas asinkron gagal, janji yang dikembalikan akan diselesaikan (yaitu "ditolak") dengan kesalahan.

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

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

Pawang juga dapat mengembalikan janji lain. Misalnya ketika pawang itu menjalankan tugas asinkron lainnya. Dengan mekanisme ini, kita dapat "rantai" tugas tidak sinkron:

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

! Nilai kembali blok pawang menjadi nilai janji anak.

Jika tidak ada janji anak, nilai kembali tidak berpengaruh.

Contoh yang lebih kompleks:

Di sini, kita mengeksekusi asyncTaskA, asyncTaskB, asyncTaskCdan asyncTaskD berurutan - dan masing-masing tugas berikutnya mengambil hasil dari tugas sebelumnya sebagai masukan:

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);

"Rantai" seperti itu juga disebut "kelanjutan".

Menangani kesalahan

Janji membuatnya sangat mudah untuk menangani kesalahan. Kesalahan akan "diteruskan" dari orangtua ke anak jika tidak ada penangan kesalahan yang ditentukan dalam janji orangtua. Kesalahan akan diteruskan ke rantai sampai seorang anak menanganinya. Dengan demikian, dengan memiliki rantai di atas, kita dapat menerapkan penanganan kesalahan hanya dengan menambahkan "kelanjutan" lain yang berhubungan dengan kesalahan potensial yang mungkin terjadi di mana saja di atas :

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;
});

Ini mirip dengan gaya sinkron yang mungkin lebih akrab dengan penanganan pengecualian:

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

Janji secara umum memiliki fitur berguna lainnya:

Misalnya, memiliki referensi ke janji, melalui thenseseorang dapat "mendaftar" sebanyak penangan yang diinginkan. Di RXPromise, pendaftaran penangan dapat terjadi kapan saja dan dari utas apa pun karena sepenuhnya aman dari benang.

RXPromise memiliki beberapa fitur fungsional yang lebih bermanfaat, tidak diharuskan oleh spesifikasi Promise / A +. Salah satunya adalah "pembatalan".

Ternyata "pembatalan" adalah fitur yang tak ternilai dan penting. Misalnya situs panggilan yang memegang referensi janji dapat mengirimkannya cancelpesan untuk menunjukkan bahwa itu tidak lagi tertarik pada hasil akhirnya.

Bayangkan saja tugas asinkron yang memuat gambar dari web dan yang akan ditampilkan di view controller. Jika pengguna menjauh dari pengontrol tampilan saat ini, pengembang dapat menerapkan kode yang mengirim pesan pembatalan ke imagePromise , yang pada gilirannya memicu penangan kesalahan yang ditentukan oleh Operasi Permintaan HTTP di mana permintaan akan dibatalkan.

Di RXPromise, pesan pembatalan hanya akan diteruskan dari orangtua ke anak-anaknya, tetapi tidak sebaliknya. Artinya, janji "root" akan membatalkan semua janji anak-anak. Tetapi janji anak hanya akan membatalkan "cabang" di mana itu adalah orang tua. Pesan yang dibatalkan juga akan diteruskan ke anak-anak jika janji telah diselesaikan.

Tugas asinkron itu sendiri dapat mendaftarkan handler untuk janjinya sendiri, dan dengan demikian dapat mendeteksi ketika orang lain membatalkannya. Mungkin kemudian secara prematur berhenti melakukan tugas yang mungkin panjang dan mahal.

Berikut adalah beberapa implementasi lain dari Janji dalam Objective-C yang ditemukan di 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

dan implementasi saya sendiri: RXPromise .

Daftar ini kemungkinan tidak lengkap!

Saat memilih perpustakaan ketiga untuk proyek Anda, harap periksa dengan cermat apakah implementasi perpustakaan mengikuti prasyarat yang tercantum di bawah ini:

  • Sebuah perpustakaan janji yang dapat diandalkan HARUS aman!

    Ini semua tentang pemrosesan yang tidak sinkron, dan kami ingin menggunakan banyak CPU dan mengeksekusi di utas yang berbeda secara bersamaan bila memungkinkan. Hati-hati, sebagian besar implementasi tidak aman utas!

  • Penangan AKAN disebut asinkron yang berkenaan dengan situs panggilan! Selalu, dan apa pun yang terjadi!

    Setiap implementasi yang layak juga harus mengikuti pola yang sangat ketat ketika menjalankan fungsi asinkron. Banyak pelaksana yang cenderung "mengoptimalkan" kasing, di mana pawang akan dipanggil secara serempak ketika janji sudah diselesaikan ketika pawang akan terdaftar. Ini dapat menyebabkan segala macam masalah. Lihat Jangan lepaskan Zalgo! .

  • Seharusnya juga ada mekanisme untuk membatalkan janji.

    Kemungkinan untuk membatalkan tugas asinkron sering menjadi persyaratan dengan prioritas tinggi dalam analisis kebutuhan. Jika tidak, pasti akan diajukan permintaan tambahan dari pengguna beberapa waktu kemudian setelah aplikasi dirilis. Alasannya harus jelas: tugas apa pun yang mungkin terhenti atau terlalu lama untuk diselesaikan, harus dibatalkan oleh pengguna atau dengan batas waktu. Perpustakaan janji yang layak harus mendukung pembatalan.

CouchDeveloper
sumber
1
Ini mendapatkan hadiah tanpa jawaban terlama yang pernah ada. Tapi A untuk usaha :-)
Traveling Man
3

Saya menyadari ini adalah pertanyaan lama tetapi saya harus menjawabnya karena jawaban saya berbeda dari yang lain.

Bagi mereka yang mengatakan itu masalah preferensi pribadi, saya harus tidak setuju. Ada alasan yang baik, logis, untuk memilih satu daripada yang lain ...

Dalam kasus penyelesaian, blok Anda diserahkan dua objek, satu mewakili kesuksesan sementara yang lain mewakili kegagalan ... Jadi apa yang Anda lakukan jika keduanya nihil? Apa yang Anda lakukan jika keduanya memiliki nilai? Ini adalah pertanyaan yang dapat dihindari pada waktu kompilasi dan karena itu seharusnya. Anda menghindari pertanyaan-pertanyaan ini dengan memiliki dua blok terpisah.

Memiliki blok keberhasilan dan kegagalan yang terpisah membuat kode Anda diverifikasi secara statis.


Perhatikan bahwa segalanya berubah dengan Swift. Di dalamnya, kita dapat menerapkan gagasan Eitherenum sehingga blok penyelesaian tunggal dijamin memiliki objek atau kesalahan, dan harus memiliki salah satunya. Jadi untuk Swift, satu blok lebih baik.

Daniel T.
sumber
1

Saya menduga itu akan berakhir menjadi preferensi pribadi ...

Tapi saya lebih suka blok kesuksesan / kegagalan yang terpisah. Saya suka memisahkan logika keberhasilan / kegagalan. Jika Anda telah menyarangkan kesuksesan / kegagalan, Anda akan berakhir dengan sesuatu yang akan lebih mudah dibaca (menurut saya setidaknya).

Sebagai contoh yang relatif ekstrim dari sarang seperti ini, berikut adalah beberapa Ruby yang menunjukkan pola ini.

Frank Shearar
sumber
1
Saya telah melihat rantai bersarang dari keduanya. Saya pikir mereka berdua terlihat mengerikan, tapi itu pendapat pribadi saya.
Jeffery Thomas
1
Tapi bagaimana lagi Anda bisa menelepon async?
Frank Shearar
Saya tidak kenal pria ... Saya tidak tahu. Bagian dari alasan saya bertanya adalah karena saya tidak suka bagaimana kode async saya terlihat.
Jeffery Thomas
Yakin. Anda akhirnya menulis kode Anda dengan gaya kelanjutan-kelanjutan, yang tidak terlalu mengejutkan. (Haskell memiliki notasi untuk alasan ini: membiarkan Anda menulis dengan gaya langsung.)
Frank Shearar
Anda mungkin tertarik dengan implementasi ObjC Promises ini: github.com/couchdeveloper/RXPromise
e1985
0

Ini terasa seperti solusi lengkap, tapi saya rasa tidak ada jawaban yang tepat di sini. Saya pergi dengan blok penyelesaian hanya karena penanganan kesalahan mungkin masih perlu dilakukan dalam kondisi sukses ketika menggunakan blok sukses / gagal.

Saya pikir kode akhir akan terlihat seperti

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

atau sederhana

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

Bukan potongan kode terbaik dan bersarang akan semakin buruk

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

Saya pikir saya akan pergi mope untuk sementara waktu.

Jeffery Thomas
sumber