Masalah:
Sejak lama, saya khawatir dengan exceptions
mekanismenya, karena saya merasa tidak benar-benar menyelesaikan apa yang seharusnya.
KLAIM: Ada perdebatan panjang di luar tentang topik ini, dan kebanyakan dari mereka kesulitan membandingkan dan exceptions
mengembalikan kode kesalahan. Ini jelas bukan topik di sini.
Mencoba mendefinisikan kesalahan, saya setuju dengan CppCoreGuidelines, dari Bjarne Stroustrup & Herb Sutter
Kesalahan berarti bahwa fungsi tidak dapat mencapai tujuan yang diiklankan
KLAIM: exception
Mekanisme ini adalah semantik bahasa untuk menangani kesalahan.
KLAIM: Bagi saya, ada "tidak ada alasan" untuk suatu fungsi untuk tidak mencapai tugas: Entah kita salah mendefinisikan kondisi sebelum / sesudah sehingga fungsi tidak dapat memastikan hasil, atau beberapa kasus luar biasa tertentu tidak dianggap cukup penting untuk menghabiskan waktu dalam mengembangkan sebuah solusi. Menimbang bahwa, IMO, perbedaan antara kode normal dan penanganan kode kesalahan adalah (sebelum implementasi) garis yang sangat subyektif.
KLAIM: Menggunakan pengecualian untuk menunjukkan ketika kondisi pra atau pasca tidak disimpan adalah tujuan lain dari exception
mekanisme, terutama untuk tujuan debugging. Saya tidak menargetkan penggunaan ini di exceptions
sini.
Dalam banyak buku, tutorial, dan sumber lain, mereka cenderung menunjukkan penanganan kesalahan sebagai ilmu yang cukup objektif, yang diselesaikan dengan exceptions
dan Anda hanya perlu catch
mereka untuk memiliki perangkat lunak yang kuat, dapat pulih dari situasi apa pun. Tetapi beberapa tahun saya sebagai pengembang membuat saya melihat masalah dari pendekatan yang berbeda:
- Pemrogram cenderung untuk menyederhanakan tugas mereka dengan melemparkan pengecualian ketika kasus spesifik tampaknya terlalu jarang untuk diimplementasikan dengan hati-hati. Kasus khas ini adalah: kehabisan masalah memori, masalah disk penuh, masalah file rusak, dll. Ini mungkin cukup, tetapi tidak selalu diputuskan dari tingkat arsitektur.
- Pemrogram cenderung tidak membaca dokumentasi dengan hati-hati tentang pengecualian di perpustakaan, dan biasanya tidak mengetahui yang mana dan kapan suatu fungsi dilemparkan. Lebih jauh lagi, bahkan ketika mereka tahu, mereka tidak benar-benar mengelolanya.
- Programmer cenderung tidak menangkap pengecualian cukup awal, dan ketika mereka melakukannya, sebagian besar untuk login dan melempar lebih jauh. (lihat poin pertama).
Ini memiliki dua konsekuensi:
- Kesalahan yang terjadi sering terdeteksi pada awal pengembangan dan debugged (yang bagus).
- Pengecualian langka tidak dikelola dan membuat sistem macet (dengan pesan log yang bagus) di rumah pengguna. Beberapa kali kesalahan dilaporkan, atau bahkan tidak.
Mempertimbangkan itu, IMO tujuan utama dari mekanisme kesalahan harus:
- Buat terlihat dalam kode di mana beberapa kasus tertentu tidak dikelola.
- Komunikasikan masalah runtime ke kode terkait (setidaknya penelepon) ketika situasi ini terjadi.
- Menyediakan mekanisme pemulihan
Kelemahan utama dari exception
semantik sebagai mekanisme penanganan kesalahan adalah IMO: mudah untuk melihat di mana a throw
berada dalam kode sumber, tetapi sama sekali tidak jelas untuk mengetahui apakah fungsi tertentu dapat melempar dengan melihat pada deklarasi. Ini membawa semua masalah yang saya perkenalkan di atas.
Bahasa tidak menegakkan dan memeriksa kode kesalahan seketat yang dibuat untuk aspek lain dari bahasa (misalnya jenis variabel yang kuat)
Mencoba solusi
Untuk memperbaiki hal ini, saya mengembangkan sistem penanganan kesalahan yang sangat sederhana, yang mencoba menempatkan penanganan kesalahan pada tingkat kepentingan yang sama dengan kode normal.
Idenya adalah:
- Setiap fungsi (yang relevan) menerima referensi ke objek yang
success
sangat ringan, dan dapat mengaturnya ke status kesalahan jika terjadi. Objek sangat ringan sampai kesalahan dengan teks disimpan. - Suatu fungsi didorong untuk melewati tugasnya jika objek yang disediakan sudah mengandung kesalahan.
- Kesalahan tidak boleh ditimpa.
Desain lengkap jelas mempertimbangkan dengan seksama setiap aspek (sekitar 10 halaman), juga bagaimana menerapkannya pada OOP.
Contoh Success
kelas:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Pemakaian:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Saya menggunakan itu dalam banyak kode saya (sendiri) dan memaksa programmer (saya) untuk berpikir lebih jauh tentang kemungkinan kasus luar biasa dan bagaimana menyelesaikannya (baik). Namun, ia memiliki kurva belajar dan tidak terintegrasi dengan baik dengan kode yang sekarang menggunakannya.
Pertanyaan
Saya ingin lebih memahami implikasi penggunaan paradigma semacam itu dalam proyek:
- Apakah premis untuk masalah itu benar? atau Apakah saya melewatkan sesuatu yang relevan?
- Apakah solusinya ide arsitektur yang bagus? atau harganya terlalu tinggi?
EDIT:
Perbandingan antara metode:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.
sumber
Jawaban:
Penanganan kesalahan mungkin merupakan bagian tersulit dari suatu program.
Secara umum, menyadari bahwa ada kondisi kesalahan itu mudah; namun mengisinya dengan cara yang tidak dapat dielakkan dan menanganinya dengan tepat (lihat tingkat Keamanan Pengecualian Abrahams ) sangat sulit.
Dalam C, kesalahan pensinyalan dilakukan oleh kode pengembalian, yang isomorfis untuk solusi Anda.
C ++ memperkenalkan pengecualian karena kekurangan pendekatan semacam itu; yaitu, itu hanya berfungsi jika penelepon ingat untuk memeriksa apakah kesalahan terjadi atau tidak dan gagal berpisah sebaliknya. Setiap kali Anda menemukan diri Anda berkata "Tidak apa-apa selama setiap kali ..." Anda memiliki masalah; manusia tidak begitu teliti, bahkan ketika mereka peduli.
Masalahnya, bagaimanapun, adalah bahwa pengecualian memiliki masalah mereka sendiri. Yaitu, aliran kontrol tak terlihat / tersembunyi. Ini dimaksudkan: menyembunyikan kasus kesalahan sehingga logika kode tidak dikaburkan oleh kesalahan penanganan boilerplate. Itu membuat "jalan bahagia" jauh lebih jelas (dan cepat!), Dengan biaya membuat jalur kesalahan hampir tidak bisa dipahami.
Saya merasa menarik untuk melihat bagaimana bahasa lain mendekati masalah ini:
C ++ dulu memiliki beberapa bentuk pengecualian diperiksa, Anda mungkin telah memperhatikan itu telah ditinggalkan dan disederhanakan menjadi dasar
noexcept(<bool>)
sebagai gantinya: salah satu fungsi dinyatakan mungkin melempar, atau itu dinyatakan tidak pernah. Pengecualian yang diperiksa agak bermasalah karena tidak dapat diperpanjang, yang dapat menyebabkan pemetaan canggung / bersarang. Dan hierarki pengecualian yang berbelit-belit (salah satu kasus penggunaan utama dari warisan virtual adalah pengecualian ...).Sebaliknya, Go dan Rust mengambil pendekatan bahwa:
Yang terakhir ini agak jelas dalam hal (1) mereka menyebutkan kepanikan pengecualian mereka dan (2) tidak ada tipe hierarki / klausa rumit di sini. Bahasa tidak menawarkan fasilitas untuk memeriksa konten "panik": tidak ada hierarki tipe, tidak ada konten yang ditentukan pengguna, hanya "oops, ada yang salah, tidak ada pemulihan yang mungkin".
Ini secara efektif mendorong pengguna untuk menggunakan penanganan kesalahan yang tepat, sementara masih menyisakan cara mudah untuk menyelamatkan dalam situasi luar biasa (seperti: "tunggu, saya belum menerapkan itu!").
Tentu saja, pendekatan Go sangat mirip dengan Anda karena Anda dapat dengan mudah lupa memeriksa kesalahan ...
... Namun pendekatan Rust sebagian besar berpusat di sekitar dua jenis:
Option
, yang mirip denganstd::optional
,Result
, yang merupakan varian dua kemungkinan: Ok dan Err.ini jauh lebih rapi karena tidak ada kesempatan untuk secara tidak sengaja menggunakan hasil tanpa memeriksa keberhasilan: jika Anda melakukannya, program akan panik.
Bahasa FP membentuk penanganan kesalahan mereka dalam konstruksi yang dapat dibagi menjadi tiga lapisan: - Functor - Aplikatif / Alternatif - Monad / Alternatif
Mari kita lihat
Functor
typeclass Haskell :Pertama-tama, typeclasses agak mirip tetapi tidak sama dengan antarmuka. Fungsi tanda tangan Haskell terlihat sedikit menakutkan pada tampilan pertama. Tapi mari kita menguraikannya. Fungsi ini
fmap
mengambil fungsi sebagai parameter pertama yang agak miripstd::function<a,b>
. Hal selanjutnya adalahm a
. Anda dapat membayangkanm
sebagai sesuatu sepertistd::vector
danm a
sebagai sesuatu sepertistd::vector<a>
. Tetapi perbedaannya adalah, itum a
tidak mengatakan itu harus secara eksplisitstd:vector
. Jadi bisa jadistd::option
juga. Dengan memberi tahu bahasa bahwa kita memiliki instance untuk typeclassFunctor
untuk tipe tertentu sepertistd::vector
ataustd::option
, kita dapat menggunakan fungsifmap
untuk tipe itu. Hal yang sama harus dilakukan untuk kacamata ketikApplicative
,Alternative
danMonad
yang memungkinkan Anda untuk melakukan perhitungan stateful, kemungkinan gagal. TheAlternative
abstraksi pemulihan alat typeclass kesalahan. Dengan itu Anda bisa mengatakan sesuatu seperti atau istilah . Jika tak satu pun dari kedua perhitungan berhasil, itu masih kesalahan.a <|> b
artinya adalah istilah yang baika
b
Mari kita lihat Haskell's
Maybe
tipe .Ini berarti, bahwa di mana Anda mengharapkan
Maybe a
, Anda mendapatkan salah satuNothing
atauJust a
. Saat melihatfmap
dari atas, sebuah implementasi bisa terlihat sepertiThe
case ... of
ekspresi disebut pencocokan pola dan menyerupai apa yang dikenal dalam dunia OOP sebagaivisitor pattern
. Bayangkan gariscase m of
sebagaim.apply(...)
dan titik-titik adalah instantiation dari kelas yang mengimplementasikan fungsi pengiriman. Garis-garis di bawahcase ... of
ekspresi adalah fungsi masing-masing pengiriman yang membawa bidang kelas langsung dalam lingkup dengan nama. DiNothing
cabang kami membuatNothing
dan diJust a
cabang kami beri nama nilai kami satu-satunyaa
dan membuat yang lainJust ...
dengan fungsi transformasif
diterapkana
. Membacanya sebagai:new Just(f(a))
.Ini sekarang dapat menangani perhitungan yang salah sambil mengabstraksi pemeriksaan kesalahan yang sebenarnya. Ada implementasi untuk antarmuka lain yang membuat jenis komputasi ini sangat kuat. Sebenarnya,
Maybe
adalah inspirasi untuk RustOption
-Type.Saya akan mendorong Anda untuk memperbaiki
Success
kelas Anda sebagaiResult
gantinya. Alexandrescu sebenarnya mengusulkan sesuatu yang sangat dekat, disebutexpected<T>
, yang merupakan standar proposal dibuat .Saya akan tetap menggunakan penamaan Rust dan API hanya karena ... ini didokumentasikan dan berfungsi. Tentu saja, Rust memiliki
?
operator akhiran yang bagus yang akan membuat kode lebih manis; di C ++, kami akan menggunakan ekspresi pernyataanTRY
makro dan GCC untuk menirunya.Catatan: ini
Result
adalah placeholder. Implementasi yang tepat akan menggunakan enkapsulasi dan aunion
. Ini cukup untuk menyampaikan maksudnya.Yang memungkinkan saya untuk menulis ( lihat beraksi ):
yang menurut saya sangat rapi:
Success
kelas Anda ), lupa memeriksa kesalahan akan menghasilkan kesalahan runtime 1 daripada beberapa perilaku acak,concepts
dalam standar. Ini akan membuat pemrograman jenis ini jauh lebih menyenangkan karena kita bisa meninggalkan pilihan daripada jenis kesalahan. Misalnya dengan implementasistd::vector
sebagai hasilnya, kita dapat menghitung semua solusi yang mungkin sekaligus. Atau kami dapat memilih untuk meningkatkan penanganan kesalahan, seperti yang Anda usulkan.1 Dengan
Result
implementasi yang dienkapsulasi dengan benar ;)Catatan: tidak seperti pengecualian, ringan
Result
ini tidak memiliki backtraces, yang membuat penebangan kurang efisien; Anda mungkin merasa berguna untuk setidaknya mencatat nomor file / baris di mana pesan kesalahan dihasilkan, dan untuk umumnya menulis pesan kesalahan yang kaya. Ini dapat diperparah dengan menangkap file / baris setiap kaliTRY
makro digunakan, pada dasarnya membuat backtrace secara manual, atau menggunakan kode dan pustaka platform-spesifik sepertilibbacktrace
untuk membuat daftar simbol di callstack.Ada satu peringatan besar: perpustakaan C ++ yang ada, dan bahkan
std
, didasarkan pada pengecualian. Akan sulit untuk menggunakan gaya ini, karena API perpustakaan pihak ketiga mana pun harus dibungkus dengan adaptor ...sumber
({...})
adalah beberapa ekstensi gcc, tetapi meskipun demikian, bukankah seharusnya begituif (!result.ok) return result;
? Kondisi Anda muncul mundur dan Anda membuat salinan kesalahan yang tidak perlu.({...})
adalah pernyataan pernyataan gcc .std::variant
untuk mengimplementasikanResult
jika Anda menggunakan C ++ 17. Juga, untuk mendapatkan peringatan jika Anda mengabaikan kesalahan, gunakan[[nodiscard]]
std::variant
atau tidak adalah masalah selera, mengingat timbal balik seputar penanganan pengecualian.[[nodiscard]]
memang merupakan kemenangan murni.pengecualian adalah mekanisme aliran kontrol. Motivasi untuk mekanisme kontrol-aliran ini, secara khusus memisahkan penanganan kesalahan dari kode penanganan non-kesalahan, dalam kasus umum bahwa penanganan kesalahan sangat berulang dan memiliki sedikit relevansi dengan bagian utama dari logika.
Pertimbangkan: Saya mencoba membuat file. Perangkat penyimpanan penuh.
Sekarang, ini bukan kegagalan untuk mendefinisikan prasyarat saya: Anda tidak dapat menggunakan "harus ada penyimpanan yang cukup" sebagai prasyarat secara umum, karena penyimpanan bersama tunduk pada kondisi ras yang membuat ini tidak mungkin dipenuhi.
Jadi, haruskah program saya entah bagaimana mengosongkan beberapa ruang dan kemudian melanjutkan dengan sukses, kalau tidak saya terlalu malas untuk "mengembangkan solusi"? Ini sepertinya tidak masuk akal. "Solusi" untuk mengelola penyimpanan bersama adalah di luar ruang lingkup program saya , dan memungkinkan program saya gagal dengan anggun, dan dijalankan kembali begitu pengguna telah merilis beberapa ruang, atau menambahkan beberapa penyimpanan lagi, baik - baik saja .
Apa yang dilakukan kelas kesuksesan Anda adalah interleave penanganan kesalahan yang sangat eksplisit dengan logika program Anda. Setiap fungsi tunggal perlu memeriksa, sebelum menjalankan, apakah telah terjadi kesalahan yang berarti ia tidak boleh melakukan apa-apa. Setiap fungsi perpustakaan perlu dibungkus dengan fungsi lain, dengan satu argumen lagi (dan semoga penerusan sempurna), yang melakukan hal yang persis sama.
Perhatikan juga bahwa
mySqrt
fungsi Anda perlu mengembalikan nilai meskipun gagal (atau fungsi sebelumnya gagal). Jadi, Anda akan mengembalikan nilai ajaib (sepertiNaN
), atau menyuntikkan nilai tak tentu ke dalam program Anda dan berharap tidak ada yang menggunakan itu tanpa memeriksa status keberhasilan yang telah Anda utus melalui eksekusi Anda.Untuk kebenaran - dan kinerja - jauh lebih baik untuk melepaskan kendali kembali dari ruang lingkup begitu Anda tidak dapat membuat kemajuan. Pengecualian dan pengecekan galat eksplisit gaya-C dengan pengembalian awal keduanya menghasilkan hal ini.
Sebagai perbandingan, contoh ide Anda yang benar-benar berfungsi adalah Error monad di Haskell. Keuntungan dari sistem Anda adalah bahwa Anda menulis sebagian besar logika Anda secara normal, dan kemudian membungkusnya dalam monad yang menangani penghentian evaluasi ketika satu langkah gagal. Dengan cara ini satu-satunya kode yang menyentuh sistem penanganan kesalahan secara langsung adalah kode yang mungkin gagal (melempar kesalahan) dan kode yang perlu mengatasi kegagalan (menangkap pengecualian).
Saya tidak yakin bahwa gaya monad dan evaluasi malas diterjemahkan dengan baik ke C ++.
sumber
and allowing my program to fail gracefully, and be re-run
ketika ia baru saja kehilangan pekerjaan 2 jam:std::exception
di tingkat yang lebih tinggi dari operasi logis, beri tahu pengguna "X gagal karena ex.what ()" , dan menawarkan untuk mencoba kembali seluruh operasi ketika dan jika mereka siap.showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try
. Itu adalah penanganan masalah yang anggun yang biasanya tidak dapat dilakukan dari kode yang mendeteksi bahwa lokasi penyimpanan pertama penuh.Pendekatan Anda membawa beberapa masalah besar ke dalam kode sumber Anda:
itu bergantung pada kode klien selalu ingat untuk memeriksa nilai
s
. Ini biasa terjadi pada penggunaan kode pengembalian untuk pendekatan penanganan kesalahan , dan salah satu alasan bahwa pengecualian diperkenalkan ke dalam bahasa: dengan pengecualian, jika Anda gagal, Anda tidak gagal secara diam-diam.semakin banyak kode yang Anda tulis dengan pendekatan ini, semakin banyak kode kesalahan yang harus Anda tambahkan juga, untuk penanganan kesalahan (kode Anda tidak lagi minimalis) dan upaya pemeliharaan Anda meningkat.
Solusi untuk masalah ini harus didekati di tingkat pemimpin teknis atau tingkat tim:
Jika Anda menemukan diri Anda menangani setiap jenis pengecualian yang dapat dilempar, sepanjang waktu, maka desainnya tidak bagus; Kesalahan apa yang ditangani, harus diputuskan sesuai dengan spesifikasi untuk proyek, bukan menurut apa yang ingin diimplementasikan oleh pengembang.
Alamat dengan menyiapkan pengujian otomatis, memisahkan spesifikasi pengujian unit dan implementasi (mintalah dua orang yang berbeda melakukan ini).
Anda tidak akan membahas ini dengan menulis lebih banyak kode. Saya pikir taruhan terbaik Anda adalah ulasan kode yang diterapkan dengan cermat.
Penanganan kesalahan yang tepat sulit, tetapi kurang membosankan dengan pengecualian dibandingkan dengan nilai kembali (apakah mereka benar-benar dikembalikan atau diteruskan sebagai argumen i / o).
Bagian yang paling sulit dari penanganan kesalahan bukanlah bagaimana Anda menerima kesalahan, tetapi bagaimana memastikan aplikasi Anda tetap dalam keadaan konsisten di hadapan kesalahan.
Untuk mengatasinya, lebih banyak perhatian perlu dialokasikan untuk mengidentifikasi dan menjalankan dalam kondisi kesalahan (lebih banyak pengujian, lebih banyak unit / tes integrasi, dll).
sumber