Bagaimana saya bisa membuat dan menegakkan kontrak untuk pengecualian?

33

Saya mencoba meyakinkan pimpinan tim saya untuk mengizinkan menggunakan pengecualian dalam C ++ alih-alih mengembalikan bool isSuccessfulatau enum dengan kode kesalahan. Namun, saya tidak bisa melawan kritiknya ini.

Pertimbangkan perpustakaan ini:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Pertimbangkan pengembang memanggil B()fungsi. Dia memeriksa dokumentasinya dan melihat bahwa itu tidak menghasilkan pengecualian, jadi dia tidak mencoba menangkap apa pun. Kode ini dapat menyebabkan crash program dalam produksi.

  2. Pertimbangkan pengembang memanggil C()fungsi. Dia tidak memeriksa dokumentasinya jadi tidak menangkap pengecualian. Panggilan tidak aman dan dapat menyebabkan crash program dalam produksi.

Tetapi jika kita memeriksa kesalahan dengan cara ini:

void old_C(myenum &return_code);

Pengembang yang menggunakan fungsi itu akan diperingatkan oleh kompiler jika dia tidak memberikan argumen itu, dan dia akan berkata "Aha, ini mengembalikan kode kesalahan yang harus saya periksa."

Bagaimana saya bisa menggunakan pengecualian dengan aman, sehingga ada semacam kontrak?

DBedrenko
sumber
4
@ Sojerd Keuntungan utama adalah bahwa pengembang dipaksa oleh kompiler untuk memberikan variabel ke fungsi untuk menyimpan kode kembali; dia dibuat sadar bahwa mungkin ada kesalahan dan dia harus menanganinya. Itu jauh lebih baik daripada pengecualian, yang tidak memiliki waktu kompilasi memeriksa sama sekali.
DBedrenko
4
"Dia harus menanganinya" - tidak ada pemeriksaan apakah ini terjadi juga.
Sjoerd
4
@ Soerd Itu tidak perlu. Cukup baik untuk membuat pengembang sadar bahwa kesalahan dapat terjadi dan mereka harus memeriksanya. Ada di depan mata pengulas, apakah mereka memeriksa kesalahan atau tidak.
DBedrenko
5
Anda mungkin memiliki kesempatan yang lebih baik menggunakan Either/ Resultmonad untuk mengembalikan kesalahan dengan cara yang dapat dikomposisikan dengan tipe aman
Daenyth
5
@gardenhead Karena terlalu banyak pengembang yang tidak menggunakannya dengan benar, berakhir dengan kode jelek, dan terus menyalahkan fitur alih-alih sendiri. Fitur pengecualian diperiksa di Jawa adalah hal yang indah, tetapi mendapat rap buruk karena beberapa orang tidak mendapatkannya.
AxiomaticNexus

Jawaban:

47

Ini adalah kritik yang sah terhadap pengecualian. Mereka sering kurang terlihat daripada penanganan kesalahan sederhana seperti mengembalikan kode. Dan tidak ada cara mudah untuk menegakkan "kontrak". Bagian dari intinya adalah untuk memungkinkan Anda membiarkan pengecualian ditangkap pada level yang lebih tinggi (jika Anda harus menangkap setiap pengecualian di setiap level, seberapa berbedanya dengan mengembalikan kode kesalahan, toh?). Dan ini berarti bahwa kode Anda dapat dipanggil oleh beberapa kode lain yang tidak menanganinya dengan tepat.

Pengecualian memang memiliki kelemahan; Anda harus membuat kasing berdasarkan biaya-manfaat.
Saya menemukan dua artikel ini bermanfaat: Perlunya pengecualian dan Semuanya salah dengan pengecualian. Juga, posting blog ini menawarkan pendapat banyak pakar tentang pengecualian, dengan fokus pada C ++ . Sementara pendapat para ahli tampaknya lebih condong pada pengecualian, itu jauh dari konsensus yang jelas.

Sedangkan untuk meyakinkan pimpinan tim Anda, ini mungkin bukan pertempuran yang tepat untuk dipilih. Terutama tidak dengan kode lama. Seperti disebutkan dalam tautan kedua di atas:

Pengecualian tidak dapat diperbanyak melalui kode apa pun yang tidak terkecuali aman. Penggunaan pengecualian dengan demikian menyiratkan bahwa semua kode dalam proyek harus pengecualian aman.

Menambahkan sedikit kode yang menggunakan pengecualian untuk proyek yang sebagian besar tidak mungkin tidak akan menjadi perbaikan. Tidak menggunakan pengecualian dalam kode yang ditulis dengan baik adalah jauh dari masalah bencana; itu mungkin tidak menjadi masalah sama sekali, tergantung pada aplikasi dan ahli yang Anda tanyakan. Anda harus memilih pertempuran Anda.

Ini mungkin bukan argumen yang akan saya habiskan - setidaknya tidak sampai proyek baru dimulai. Dan bahkan jika Anda memiliki proyek baru, apakah itu akan digunakan atau digunakan oleh kode warisan apa pun?


sumber
1
Terima kasih atas saran dan tautannya. Ini adalah proyek baru, bukan proyek warisan. Saya bertanya-tanya mengapa pengecualian begitu luas ketika mereka memiliki kelemahan besar yang membuat penggunaannya tidak aman. Jika Anda menangkap pengecualian n-level lebih tinggi, maka Anda kemudian memodifikasi kode dan memindahkannya, tidak ada pemeriksaan statis untuk memastikan pengecualian masih tertangkap dan ditangani (dan terlalu banyak meminta pengulas untuk memeriksa semua fungsi n-level mendalam ).
DBedrenko
3
@Dee lihat tautan di atas untuk beberapa argumen yang mendukung pengecualian (Saya telah menambahkan tautan tambahan dengan pendapat lebih ahli).
6
Jawaban ini tepat!
Doc Brown
1
Saya dapat menyatakan dari pengalaman bahwa mencoba menambahkan pengecualian ke basis kode yang sudah ada sebelumnya adalah ide yang BENAR-BENAR buruk. Saya telah bekerja dengan basis kode seperti itu dan itu BENAR-BENAR sulit untuk bekerja dengan karena Anda tidak pernah tahu apa yang akan meledak di wajah Anda. Pengecualian HANYA harus digunakan dalam proyek di mana Anda berencana untuk itu di muka.
Michael Kohne
26

Secara harfiah ada banyak buku yang ditulis tentang hal ini, jadi jawaban apa pun akan menjadi ringkasan terbaik. Berikut adalah beberapa poin penting yang menurut saya layak untuk dibuat, berdasarkan pertanyaan Anda. Ini bukan daftar lengkap.


Pengecualian dimaksudkan untuk TIDAK ditangkap di semua tempat.

Selama ada penangan pengecualian umum di loop utama - tergantung pada jenis aplikasi (server web, layanan lokal, utilitas baris perintah ...) - Anda biasanya memiliki semua penangan pengecualian yang Anda butuhkan.

Dalam kode saya, hanya ada beberapa pernyataan tangkap - jika ada - di luar loop utama. Dan itu tampaknya menjadi pendekatan umum dalam C ++ modern.


Pengecualian dan kode pengembalian tidak saling eksklusif.

Anda seharusnya tidak menjadikan ini pendekatan semua-atau-tidak sama sekali. Pengecualian harus digunakan untuk situasi luar biasa. Hal-hal seperti "File konfigurasi tidak ditemukan," "Disk Full" atau apa pun yang tidak dapat ditangani secara lokal.

Kegagalan umum, seperti memeriksa apakah nama file yang diberikan oleh pengguna valid, bukan merupakan use case untuk pengecualian; Sebagai gantinya gunakan nilai balik.

Seperti yang Anda lihat dari contoh di atas, "file tidak ditemukan" dapat berupa pengecualian atau kode pengembalian, tergantung pada kasus penggunaan: "adalah bagian dari instalasi" versus "pengguna dapat membuat kesalahan ketik."

Jadi tidak ada aturan absolut. Pedoman kasarnya adalah: jika bisa ditangani secara lokal, jadikan itu nilai balik; jika Anda tidak bisa mengatasinya secara lokal, berikan pengecualian.


Pemeriksaan pengecualian secara statis tidak berguna.

Karena pengecualian tidak untuk ditangani secara lokal, biasanya tidak penting pengecualian mana yang dapat dilemparkan. Satu-satunya informasi yang berguna adalah apakah ada pengecualian yang dapat dilemparkan.

Java memiliki pemeriksaan statis, tetapi biasanya dianggap percobaan yang gagal, dan sebagian besar bahasa sejak - terutama C # - tidak memiliki jenis pemeriksaan statis. Ini adalah bacaan yang bagus tentang alasan mengapa C # tidak memilikinya.

Untuk itu, C ++ telah usang yang throw(exceptionA, exceptionB)mendukung noexcept(true). Standarnya adalah bahwa suatu fungsi dapat melempar, sehingga pemrogram harus berharap bahwa kecuali dokumentasi secara eksplisit menjanjikan sebaliknya.


Menulis pengecualian kode aman tidak ada hubungannya dengan menulis penangan pengecualian.

Saya lebih suka mengatakan bahwa menulis kode aman pengecualian adalah semua tentang bagaimana menghindari menulis penangan pengecualian!

Sebagian besar praktik terbaik bermaksud mengurangi jumlah penangan pengecualian. Menulis kode sekali dan menjalankannya secara otomatis - misalnya melalui RAII - menghasilkan lebih sedikit bug daripada menyalin-menempelkan kode yang sama di semua tempat.

Sjoerd
sumber
3
" Selama ada penangan pengecualian umum ... Anda biasanya sudah baik-baik saja. " Ini bertentangan dengan sebagian besar sumber, yang menekankan bahwa sangat sulit untuk menulis pengecualian kode aman.
12
@ dan1111 "Menulis kode aman pengecualian" tidak ada hubungannya dengan menulis penangan pengecualian. Menulis pengecualian kode aman adalah tentang menggunakan RAII untuk merangkum sumber daya, menggunakan "lakukan semua pekerjaan secara lokal terlebih dahulu kemudian gunakan swap / pindah," dan praktik terbaik lainnya. Itu menantang ketika Anda tidak terbiasa dengan mereka. Tetapi "menulis penangan pengecualian" bukan bagian dari praktik terbaik itu.
Sjoerd
4
@AxiomaticNexus Pada tingkat tertentu, Anda dapat menjelaskan setiap penggunaan fitur yang gagal sebagai "pengembang buruk." Ide-ide terbesar adalah yang mengurangi penggunaan yang buruk karena mereka membuat melakukan hal yang benar menjadi sederhana. Pengecualian yang dicek secara statis tidak termasuk dalam kategori itu. Mereka menciptakan pekerjaan yang tidak proporsional dengan nilai mereka, seringkali di tempat-tempat di mana kode yang ditulis hanya tidak perlu peduli tentang mereka. Pengguna tergoda untuk membungkam mereka dengan cara yang salah untuk membuat kompiler berhenti mengganggu mereka. Meski dilakukan dengan benar, mereka menyebabkan banyak kekacauan.
jpmc26
4
@AxiomaticNexus Alasannya tidak proporsional adalah karena ini dapat dengan mudah menyebar ke hampir setiap fungsi di seluruh basis kode Anda . Ketika semua fungsi dalam setengah kelas saya mengakses database, tidak setiap dari mereka perlu menyatakan secara eksplisit bahwa ia dapat melempar SQLException; tidak juga setiap metode pengontrol yang memanggil mereka. Kebisingan sebanyak itu sebenarnya merupakan nilai negatif, terlepas dari seberapa kecil jumlah pekerjaannya. Sjoerd memukul paku di kepala: sebagian besar kode Anda tidak perlu khawatir dengan pengecualian karena toh tidak bisa berbuat apa-apa terhadap mereka .
jpmc26
3
@AxiomaticNexus Ya, karena pengembang terbaik begitu sempurna kodenya akan selalu menangani setiap input pengguna yang mungkin dengan sempurna, dan Anda tidak akan pernah melihat pengecualian tersebut di prod. Dan jika Anda melakukannya, itu berarti Anda gagal dalam hidup. Serius, jangan beri aku sampah ini. Tidak ada yang sesempurna itu, bahkan bukan yang terbaik. Dan aplikasi Anda harus berperilaku waras bahkan dalam menghadapi kesalahan itu, bahkan jika "sanely" adalah "berhenti dan kembalikan setengah halaman kesalahan / pesan yang layak". Dan coba tebak: itu adalah hal yang sama yang Anda lakukan dengan 99% dari IOExceptions juga . Divisi yang diperiksa adalah arbitrer dan tidak berguna.
jpmc26
8

Pemrogram C ++ tidak mencari spesifikasi pengecualian. Mereka mencari jaminan pengecualian.

Misalkan sepotong kode tidak membuang pengecualian. Asumsi apa yang dapat dibuat oleh programmer yang masih valid? Dari cara kode ditulis, apa yang dijamin oleh kode tersebut setelah pengecualian?

Atau mungkinkah sepotong kode tertentu dapat menjamin tidak pernah membuang (yaitu tidak ada proses OS yang dihentikan)?

Kata "roll back" sering muncul dalam diskusi tentang pengecualian. Mampu memutar kembali ke status yang valid (yang didokumentasikan secara eksplisit) adalah contoh jaminan pengecualian. Jika tidak ada jaminan pengecualian, program harus berakhir di tempat karena bahkan tidak dijamin bahwa kode apa pun yang dieksekusi setelah itu akan berfungsi sebagaimana dimaksud - katakanlah, jika memori rusak, setiap operasi lebih lanjut adalah perilaku yang secara teknis tidak ditentukan.

Berbagai teknik pemrograman C ++ mempromosikan jaminan pengecualian. RAII (manajemen sumber daya berbasis lingkup) menyediakan mekanisme untuk mengeksekusi kode pembersihan dan memastikan bahwa sumber daya dirilis dalam kasus normal dan luar biasa. Membuat salinan data sebelum melakukan modifikasi pada objek memungkinkan seseorang untuk mengembalikan keadaan objek itu jika operasi gagal. Dan seterusnya.

Jawaban untuk pertanyaan StackOverflow ini memberikan pandangan sekilas pada programmer C ++ untuk memahami semua mode kegagalan yang mungkin terjadi pada kode mereka dan mencoba menjaga validitas status program meskipun gagal. Analisis baris-demi-baris dari kode C ++ menjadi kebiasaan.

Ketika berkembang di C ++ (untuk penggunaan produksi), orang tidak mampu mengabaikan detail. Juga, biner gumpalan (non-open-source) adalah kutukan dari programmer C ++. Jika saya harus memanggil beberapa biner gumpalan, dan gumpalan gagal, maka rekayasa balik adalah apa yang akan dilakukan programmer C ++ selanjutnya.

Referensi: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - lihat di bawah Exception Safety.

C ++ gagal dalam mengimplementasikan spesifikasi pengecualian. Analisis selanjutnya dalam bahasa lain mengatakan bahwa spesifikasi pengecualian tidak praktis.

Mengapa ini merupakan upaya yang gagal: untuk menegakkannya secara ketat, itu harus menjadi bagian dari sistem tipe. Tapi ternyata tidak. Kompiler tidak memeriksa spesifikasi pengecualian.

Mengapa C ++ memilih itu, dan mengapa pengalaman dari bahasa lain (Java) membuktikan bahwa spesifikasi pengecualian diperdebatkan: Ketika seseorang memodifikasi implementasi suatu fungsi (misalnya, perlu membuat panggilan ke fungsi yang berbeda yang mungkin melempar jenis baru dari pengecualian), penegakan spesifikasi pengecualian yang ketat berarti Anda harus memperbarui spesifikasi itu juga. Ini menyebar - Anda mungkin akhirnya harus memperbarui spesifikasi pengecualian untuk puluhan atau ratusan fungsi untuk apa itu perubahan sederhana. Hal-hal menjadi lebih buruk untuk kelas dasar abstrak (antarmuka C ++ yang setara). Jika spesifikasi pengecualian diberlakukan pada antarmuka, implementasi antarmuka tidak akan diizinkan untuk memanggil fungsi yang membuang berbagai jenis pengecualian.

Referensi: http://www.gotw.ca/publications/mill22.htm

Dimulai dengan C ++ 17, [[nodiscard]]atribut dapat digunakan pada nilai-nilai fungsi kembali (lihat: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -tidak dapat diabaikan ).

Jadi jika saya membuat perubahan kode dan itu memperkenalkan kondisi kegagalan jenis baru (yaitu jenis pengecualian baru), apakah ini merupakan perubahan besar? Haruskah itu memaksa penelepon untuk memperbarui kode, atau setidaknya diperingatkan tentang perubahan?

Jika Anda menerima argumen bahwa pemrogram C ++ mencari jaminan pengecualian alih-alih spesifikasi pengecualian, maka jawabannya adalah bahwa jika jenis kegagalan yang baru tidak merusak salah satu pengecualian yang menjamin kode yang sebelumnya dijanjikan, itu bukan perubahan yang berarti.

rwong
sumber
"Ketika seseorang memodifikasi implementasi suatu fungsi (misalnya, ia perlu melakukan panggilan ke fungsi yang berbeda yang dapat menimbulkan jenis pengecualian baru), penegakan spesifikasi pengecualian yang ketat berarti Anda harus memperbarui spesifikasi itu juga" - Bagus , seperti yang seharusnya. Apa alternatifnya? Mengabaikan pengecualian yang baru diperkenalkan?
AxiomaticNexus
@AxiomaticNexus Itu juga dijawab dengan jaminan pengecualian. Sebenarnya saya berpendapat bahwa spesifikasi tidak akan pernah lengkap; jaminan adalah apa yang kita cari. Seringkali, kami membandingkan jenis pengecualian dalam upaya untuk mencari tahu jaminan apa yang rusak - untuk menebak jaminan apa yang masih valid. Spesifikasi pengecualian akan mencantumkan beberapa jenis yang perlu Anda periksa, tetapi ingat bahwa dalam sistem praktis apa pun, daftar tersebut tidak mungkin lengkap. Sebagian besar waktu, memaksa orang untuk mengambil jalan pintas dari "makan" jenis pengecualian, membungkusnya dalam pengecualian lain, yang membuat pemeriksaan jenis pengecualian sulit
rwong
@AxiomaticNexus Saya tidak berdebat untuk atau menentang penggunaan pengecualian. Penggunaan pengecualian umum mendorong memiliki kelas pengecualian dasar dan beberapa kelas pengecualian turunan. Sementara itu, kita harus ingat bahwa jenis pengecualian lain dimungkinkan, misalnya yang dilempar dari C ++ STL atau perpustakaan lain. Dapat diperdebatkan bahwa spesifikasi pengecualian dapat dibuat berfungsi dalam kasus ini: tentukan bahwa pengecualian standar ( std::exception) dan pengecualian dasar Anda sendiri akan dibuang. Tetapi ketika diterapkan pada keseluruhan proyek, itu berarti setiap fungsi tunggal akan memiliki spesifikasi yang sama: noise.
rwong
Saya akan menunjukkan bahwa ketika datang ke pengecualian, C ++ dan Java / C # adalah bahasa yang berbeda. Pertama C ++ bukan tipe-aman dalam arti memungkinkan eksekusi kode arbitrer atau menimpa data, kedua C ++ tidak mencetak jejak stack yang bagus. Perbedaan-perbedaan ini memaksa pemrogram C ++ untuk membuat pilihan yang berbeda dalam hal penanganan kesalahan dan pengecualian. Ini juga berarti bahwa proyek tertentu dapat secara sah mengklaim bahwa C ++ bukan bahasa yang cocok untuk mereka. (Misalnya, saya pribadi menganggap bahwa penguraian gambar TIFF tidak diizinkan untuk diimplementasikan dalam C atau C ++.)
rwong
1
@ rwong: Masalah besar dengan penanganan pengecualian dalam bahasa yang mengikuti model C ++ adalah yang catchdidasarkan pada jenis objek pengecualian, ketika dalam banyak kasus yang penting bukanlah penyebab langsung dari pengecualian, melainkan apa yang tersirat tentang status sistem. Sayangnya, tipe pengecualian umumnya tidak mengatakan apa-apa tentang apakah itu menyebabkan kode secara tidak sengaja keluar dari sepotong kode dengan cara yang meninggalkan objek dalam keadaan sebagian diperbarui (dan dengan demikian tidak valid).
supercat
4

Pertimbangkan pengembang yang memanggil fungsi C (). Dia tidak memeriksa dokumentasinya jadi tidak menangkap pengecualian. Panggilan itu tidak aman

Benar-benar aman. Dia tidak perlu menangkap pengecualian di setiap tempat, dia hanya bisa melempar mencoba / menangkap di tempat di mana dia benar-benar dapat melakukan sesuatu yang bermanfaat tentang hal itu. Ini hanya tidak aman jika ia membiarkannya keluar dari utas, tetapi biasanya tidak sulit untuk mencegah hal itu terjadi.

DeadMG
sumber
Itu tidak aman karena dia tidak mengharapkan pengecualian keluar C(), dan akibatnya tidak melindungi terhadap kode setelah panggilan dilewati. Jika kode itu diperlukan untuk menjamin bahwa beberapa invarian tetap benar, jaminan itu tidak berlaku pada pengecualian pertama yang keluar C(). Tidak ada yang namanya perangkat lunak yang benar-benar pengecualian-aman, dan pastinya tidak di mana pengecualian tidak pernah diharapkan.
cmaster
1
@ cmaster Dia tidak mengharapkan pengecualian keluarC() adalah kesalahan besar. Default dalam C ++ adalah bahwa fungsi apa pun dapat melempar - itu membutuhkan eksplisit noexcept(true)untuk memberitahu kompiler sebaliknya. Dengan tidak adanya janji ini, seorang programmer harus berasumsi bahwa suatu fungsi dapat melempar.
Sjoerd
1
@ cmaster Itu adalah kesalahan bodohnya sendiri dan tidak ada hubungannya dengan penulis C (). Keamanan pengecualian adalah suatu hal, ia harus mempelajarinya, dan mengikutinya. Jika dia memanggil suatu fungsi yang dia tidak tahu bukan kecuali, dia harus menangani dengan baik apa yang terjadi jika itu melempar. Jika dia tidak memerlukan kecuali, dia harus menemukan implementasi yang.
DeadMG
@ Sojerd dan DeadMG: Sementara standar memungkinkan fungsi apa pun untuk melempar pengecualian, itu tidak berarti bahwa proyek harus mengizinkan pengecualian dalam kode mereka. Jika Anda melarang pengecualian dari kode Anda, seperti yang dilakukan pemimpin tim OP, Anda tidak perlu melakukan pemrograman pengecualian-aman. Dan jika Anda mencoba meyakinkan pimpinan tim untuk mengizinkan pengecualian ke basis kode yang sebelumnya bebas pengecualian, akan ada banyak C()fungsi yang pecah di hadapan pengecualian. Ini benar-benar hal yang sangat tidak bijaksana untuk dilakukan, itu pasti akan menyebabkan banyak masalah.
cmaster
@ cmaster Ini tentang proyek baru - lihat komentar pertama pada jawaban yang diterima. Jadi tidak ada fungsi yang ada yang akan rusak, karena tidak ada fungsi yang ada.
Sjoerd
3

Jika Anda sedang membangun sistem kritis, pertimbangkan untuk mengikuti saran ketua tim Anda dan jangan gunakan pengecualian. Ini adalah AV Rule 208 dalam Standar Coding C ++ dari Joint Strike Fighter Air Vehicle . Di sisi lain, pedoman MISRA C ++ memiliki aturan yang sangat spesifik mengenai kapan pengecualian dapat dan tidak dapat digunakan jika Anda sedang membangun sistem perangkat lunak yang sesuai dengan MISRA.

Jika Anda membangun sistem kritis, kemungkinan Anda juga menjalankan alat analisis statis. Banyak alat analisis statis akan memperingatkan jika Anda tidak memeriksa nilai pengembalian suatu metode, membuat kasus penanganan kesalahan yang hilang mudah terlihat. Sepengetahuan saya, dukungan alat serupa untuk mendeteksi penanganan pengecualian yang tepat tidak sekuat.

Pada akhirnya, saya berpendapat bahwa desain berdasarkan kontrak dan pemrograman defensif, ditambah dengan analisis statis lebih aman untuk sistem perangkat lunak penting daripada pengecualian.

Thomas Owens
sumber