Checked vs Unchecked vs No Exception ... Praktik terbaik dari keyakinan yang bertentangan

10

Ada banyak persyaratan yang dibutuhkan suatu sistem untuk menyampaikan dan menangani pengecualian dengan benar. Ada juga banyak pilihan untuk dipilih suatu bahasa untuk mengimplementasikan konsep tersebut.

Persyaratan untuk pengecualian (tanpa urutan tertentu):

  1. Dokumentasi : Suatu bahasa harus memiliki maksud untuk mendokumentasikan pengecualian yang dapat dilemparkan API. Idealnya media dokumentasi ini dapat digunakan mesin untuk memungkinkan kompiler dan IDE untuk memberikan dukungan kepada programmer.

  2. Mentransmisikan Situasi Luar Biasa : Yang ini jelas, untuk memungkinkan suatu fungsi menyampaikan situasi yang mencegah fungsi yang disebut melakukan tindakan yang diharapkan. Menurut pendapat saya ada tiga kategori besar dari situasi seperti ini:

    2.1 Bug dalam kode yang menyebabkan beberapa data menjadi tidak valid.

    2.2 Masalah dalam konfigurasi atau sumber daya eksternal lainnya.

    2.3 Sumber daya yang secara inheren tidak dapat diandalkan (jaringan, sistem file, database, pengguna akhir dll). Ini adalah sedikit kasus sudut karena sifat tidak dapat diandalkan kita harus mengharapkan kegagalan sporadis mereka. Dalam hal ini apakah situasi ini dianggap luar biasa?

  3. Berikan informasi yang cukup untuk kode untuk menanganinya : Pengecualian harus memberikan informasi yang cukup kepada callee sehingga dapat bereaksi dan mungkin menangani situasi. informasi juga harus memadai sehingga ketika dicatat pengecualian ini akan memberikan konteks yang cukup bagi seorang programmer untuk mengidentifikasi dan mengisolasi pernyataan yang menyinggung dan memberikan solusi.

  4. Berikan kepercayaan kepada programmer tentang status saat ini dari status eksekusi kode-nya : Pengecualian kemampuan penanganan sistem perangkat lunak harus ada cukup untuk memberikan perlindungan yang diperlukan sambil tetap berada di luar jalan programmer sehingga ia bisa tetap fokus pada tugas di tangan.

Untuk mengatasinya, metode berikut diimplementasikan dalam berbagai bahasa:

  1. Pengecualian yang Diperiksa Menyediakan cara yang bagus untuk mendokumentasikan pengecualian, dan secara teoritis bila diterapkan dengan benar harus memberikan jaminan yang cukup bahwa semuanya baik. Namun biayanya sedemikian rupa sehingga banyak yang merasa lebih produktif untuk mem-bypass baik dengan menelan pengecualian atau melemparkannya kembali sebagai pengecualian yang tidak diperiksa. Ketika digunakan pengecualian diperiksa tidak tepat cukup banyak kehilangan semua manfaatnya. Selain itu, pengecualian yang diperiksa membuat sulit untuk membuat API yang stabil dalam waktu. Implementasi sistem generik dalam domain tertentu akan membawa beban situasi luar biasa yang akan sulit dipertahankan dengan menggunakan pengecualian yang hanya diperiksa.

  2. Pengecualian yang Tidak Dicentang - jauh lebih fleksibel daripada pengecualian yang diperiksa, mereka gagal mendokumentasikan situasi luar biasa yang mungkin terjadi dari implementasi yang diberikan. Mereka mengandalkan dokumentasi ad-hoc jika sama sekali. Ini menciptakan situasi di mana sifat media yang tidak dapat diandalkan ditutupi oleh API yang memberikan tampilan keandalan. Juga ketika dilontarkan pengecualian ini kehilangan artinya ketika bergerak kembali melalui lapisan abstraksi. Karena mereka didokumentasikan dengan buruk, seorang programmer tidak dapat menargetkan mereka secara khusus dan seringkali perlu menggunakan jaringan yang lebih luas daripada yang diperlukan untuk memastikan bahwa sistem sekunder, jika mereka gagal, jangan menjatuhkan seluruh sistem. Yang membawa kita kembali ke masalah menelan pengecualian yang disediakan.

  3. Multistate return types Ini dia bergantung pada set disjoint, tuple, atau konsep serupa lainnya untuk mengembalikan hasil yang diharapkan atau objek yang mewakili pengecualian. Di sini tidak ada tumpukan yang dibuka, tidak ada pemotongan kode, semuanya dijalankan secara normal tetapi nilai kembali harus divalidasi untuk kesalahan sebelum melanjutkan. Saya belum benar-benar bekerja dengan ini, jadi saya tidak bisa berkomentar dari pengalaman. Saya mengakuinya menyelesaikan beberapa masalah pengecualian dengan melewati aliran normal tapi tetap saja akan menderita banyak masalah yang sama seperti pengecualian diperiksa sebagai melelahkan dan terus-menerus "di wajah Anda".

Jadi pertanyaannya adalah:

Apa pengalaman Anda dalam hal ini dan apa, menurut Anda adalah kandidat terbaik untuk membuat sistem penanganan perkecualian yang baik untuk sebuah bahasa?


EDIT: Beberapa menit setelah menulis pertanyaan ini saya menemukan posting ini , seram!

Newtopian
sumber
2
"itu akan mengalami banyak masalah yang sama dengan pengecualian yang diperiksa sebagai melelahkan dan terus-menerus di wajah Anda": Tidak juga: dengan dukungan bahasa yang tepat Anda hanya perlu memprogram "jalur keberhasilan", dengan mesin bahasa yang mendasari mengurus penyebaran kesalahan.
Giorgio
"Bahasa seharusnya memiliki maksud untuk mendokumentasikan pengecualian yang dapat dilemparkan oleh API." - weeeel. Dalam C ++ "kami" belajar bahwa ini tidak benar-benar berfungsi. Semua Anda dapat benar-benar berguna lakukan adalah untuk negara apakah API dapat membuang setiap pengecualian. (Itu benar-benar memotong cerita panjang, tapi saya pikir melihat noexceptcerita di C ++ dapat menghasilkan wawasan yang sangat baik untuk EH di C # dan Java juga.)
Martin Ba

Jawaban:

10

Pada hari-hari awal C ++ kami menemukan bahwa tanpa semacam pemrograman generik, bahasa yang diketik sangat sulit. Kami juga menemukan bahwa pengecualian yang diperiksa dan pemrograman generik tidak bekerja bersama dengan baik, dan pengecualian yang diperiksa pada dasarnya ditinggalkan.

Jenis pengembalian multiset bagus, tetapi tidak ada pengganti untuk pengecualian. Tanpa pengecualian, kode ini penuh dengan noise pengecekan error.

Masalah lain dengan pengecualian yang diperiksa adalah bahwa perubahan dalam pengecualian yang dilemparkan oleh fungsi tingkat rendah memaksa sejumlah perubahan di semua penelepon, dan penelepon mereka, dan seterusnya. Satu-satunya cara untuk mencegah hal ini adalah agar setiap level kode menangkap setiap pengecualian yang dilemparkan oleh level yang lebih rendah dan membungkusnya dengan pengecualian baru. Sekali lagi, Anda berakhir dengan kode yang sangat bising.

kevin cline
sumber
2
Generik membantu menyelesaikan seluruh kelas kesalahan yang sebagian besar disebabkan oleh keterbatasan dukungan bahasa terhadap paradigma OO. meskipun demikian, alternatifnya tampaknya memiliki kode yang sebagian besar melakukan pengecekan kesalahan atau yang berjalan berharap tidak ada yang salah. Entah Anda memiliki situasi luar biasa terus-menerus di wajah Anda atau hidup di tanah impian kelinci putih berbulu yang berubah sangat buruk ketika Anda menjatuhkan serigala jahat besar di tengah!
Newtopian
3
+1 untuk masalah cascading. Setiap sistem / arsitektur yang membuat perubahan sulit hanya mengarah pada tambalan monyet dan sistem yang berantakan, tidak peduli seberapa baik yang dirancang penulis pikir mereka.
Matthieu M.
2
@Newtopian: Templat melakukan hal-hal yang tidak dapat dilakukan dalam orientasi objek yang ketat, seperti memberikan keamanan tipe statis untuk wadah umum.
David Thornley
2
Saya ingin melihat sistem pengecualian dengan konsep "pengecualian yang diperiksa", tetapi yang sangat berbeda dari Java. Checked-ness tidak boleh menjadi atribut dari tipe pengecualian , melainkan membuang situs, menangkap situs, dan contoh pengecualian; jika suatu metode diiklankan sebagai melempar pengecualian yang diperiksa, yang seharusnya memiliki dua efek: (1) fungsi tersebut harus menangani "lemparan" dari pengecualian yang dicentang dengan melakukan sesuatu yang istimewa sebagai balasannya (misalnya mengatur bendera jinjing, dll. tergantung pada platform yang tepat) kode panggilan mana yang harus dipersiapkan.
supercat
7
"Tanpa pengecualian, kode ini penuh dengan noise pengecekan error.": Saya tidak yakin tentang ini: di Haskell Anda dapat menggunakan monads untuk ini dan semua noise pengecekan error hilang. Kebisingan yang diperkenalkan oleh "tipe pengembalian multistate" lebih merupakan batasan bahasa pemrograman daripada solusi itu sendiri.
Giorgio
9

Untuk bahasa OO yang lama, penggunaan pengecualian telah menjadi standar de-facto untuk mengkomunikasikan kesalahan. Tetapi bahasa pemrograman fungsional memberikan kemungkinan pendekatan yang berbeda, misalnya menggunakan monad (yang belum pernah saya gunakan), atau "Pemrograman Berorientasi Kereta Api" yang lebih ringan, seperti dijelaskan oleh Scott Wlaschin.

Ini benar-benar varian dari tipe hasil multistate.

  • Fungsi mengembalikan keberhasilan, atau kesalahan. Tidak dapat mengembalikan keduanya (seperti halnya tupel).
  • Semua kesalahan yang mungkin telah didokumentasikan secara ringkas (Setidaknya dalam F # dengan tipe hasil sebagai serikat terdiskriminasi).
  • Penelepon tidak dapat menggunakan hasilnya tanpa mempertimbangkan apakah hasilnya berhasil atau gagal.

Jenis hasil dapat dinyatakan seperti ini

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

Jadi hasil dari fungsi yang mengembalikan tipe ini akan berupa a Successatau Failtipe. Tidak mungkin keduanya.

Dalam bahasa pemrograman yang lebih berorientasi imperatif, gaya semacam ini bisa memerlukan sejumlah besar kode di situs pemanggil. Tetapi pemrograman fungsional memungkinkan Anda untuk membangun fungsi yang mengikat atau operator untuk menyatukan beberapa fungsi sehingga pengecekan kesalahan tidak memakan setengah kode. Sebagai contoh:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

The updateUserfungsi panggilan masing-masing fungsi berturut-turut, dan masing-masing dari mereka bisa gagal. Jika mereka semua berhasil, hasil dari fungsi terakhir yang dipanggil dikembalikan. Jika salah satu fungsi gagal, maka hasil dari fungsi itu akan menjadi hasil dari keseluruhan updateUserfungsi. Ini semua ditangani oleh operator kustom >> =.

Dalam contoh di atas, tipe kesalahan bisa jadi

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

Jika penelepon updateUsertidak secara eksplisit menangani semua kemungkinan kesalahan dari fungsi, kompiler akan mengeluarkan peringatan. Jadi, Anda telah mendokumentasikan semuanya.

Di Haskell, ada donotasi yang dapat membuat kode lebih bersih.

Pete
sumber
2
Jawaban dan referensi yang sangat bagus (pemrograman berorientasi kereta api), +1. Anda mungkin ingin menyebutkan donotasi Haskell , yang membuat kode yang dihasilkan lebih bersih.
Giorgio
1
@Iorgio - Saya lakukan sekarang, tapi saya belum bekerja dengan Haskell, hanya F #, jadi saya tidak bisa menulis banyak tentang itu. Tapi Anda bisa menambahkan jawabannya jika mau.
Pete
Terima kasih, saya menulis contoh kecil tetapi karena itu tidak cukup kecil untuk ditambahkan ke jawaban Anda, saya menulis jawaban lengkap (dengan beberapa informasi latar belakang tambahan).
Giorgio
2
The Railway Oriented Programmingadalah perilaku persis monadik.
Daenyth
5

Saya menemukan jawaban Pete sangat bagus dan saya ingin menambahkan beberapa pertimbangan dan satu contoh. Diskusi yang sangat menarik tentang penggunaan pengecualian versus mengembalikan nilai kesalahan khusus dapat ditemukan di Pemrograman dalam ML Standar, oleh Robert Harper , di akhir Bagian 29.3, halaman 243, 244.

Masalahnya adalah untuk mengimplementasikan fungsi parsial fmengembalikan nilai dari beberapa tipe t. Salah satu solusinya adalah memiliki fungsi berjenis

f : ... -> t

dan melemparkan pengecualian ketika tidak ada hasil yang mungkin. Solusi kedua adalah mengimplementasikan fungsi dengan tipe

f : ... -> t option

dan kembali SOME vpada kesuksesan, dan NONEpada kegagalan.

Ini adalah teks dari buku, dengan adaptasi kecil yang dibuat oleh saya sendiri untuk menjadikan teks lebih umum (buku merujuk pada contoh tertentu). Teks yang dimodifikasi ditulis dalam huruf miring .

Apa trade-off antara dua solusi?

  1. Solusi berdasarkan jenis opsi membuat eksplisit dalam jenis fungsi fkemungkinan kegagalan. Ini memaksa programmer untuk secara eksplisit menguji kegagalan menggunakan analisis kasus pada hasil panggilan. Pemeriksa tipe akan memastikan bahwa seseorang tidak dapat menggunakan t optiontempat yangt diharapkan. Solusi berdasarkan pengecualian tidak secara eksplisit menunjukkan kegagalan pada tipenya. Namun, programmer tetap dipaksa untuk menangani kegagalan tersebut, karena jika tidak kesalahan pengecualian akan dinaikkan pada saat run-time, daripada waktu kompilasi.
  2. Solusi berdasarkan jenis opsi memerlukan analisis kasus eksplisit pada hasil setiap panggilan. Jika "sebagian besar" hasil berhasil, cek itu berlebihan dan karena itu mahal. Solusi berdasarkan pengecualian bebas dari overhead ini: itu bias terhadap kasus "normal" mengembalikan t, daripada kasus "kegagalan" tidak mengembalikan hasil sama sekali. Implementasi pengecualian memastikan bahwa penggunaan handler lebih efisien daripada analisis kasus eksplisit dalam kasus kegagalan jarang terjadi dibandingkan dengan keberhasilan.

[potong] Secara umum, jika efisiensi adalah yang terpenting, kami cenderung memilih pengecualian jika kegagalan jarang terjadi, dan lebih memilih opsi jika kegagalan relatif umum. Jika, di sisi lain, pemeriksaan statis adalah yang terpenting, maka menguntungkan untuk menggunakan opsi karena pemeriksa tipe akan menegakkan persyaratan bahwa programmer memeriksa kegagalan, daripada kesalahan muncul hanya pada saat run-time.

Sejauh menyangkut pilihan antara pengecualian dan jenis pengembalian opsi.

Mengenai gagasan bahwa merepresentasikan kesalahan dalam tipe pengembalian menyebabkan pemeriksaan kesalahan tersebar di seluruh kode: ini tidak perlu menjadi masalah. Berikut adalah contoh kecil dalam Haskell yang menggambarkan ini.

Misalkan kita ingin menguraikan dua angka dan kemudian membagi yang pertama dengan yang kedua. Jadi mungkin ada kesalahan saat menguraikan setiap angka, atau saat membagi (pembagian dengan nol). Jadi kita harus memeriksa kesalahan setelah setiap langkah.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

Penguraian dan pembagian dilakukan di let ...blok. Perhatikan bahwa dengan menggunakan Maybemonad dan donotasi, hanya jalur keberhasilan yang ditentukan: semantik Maybemonad secara implisit menyebarkan nilai kesalahan ( Nothing). Tidak ada overhead untuk programmer.

Giorgio
sumber
2
Saya pikir dalam kasus seperti ini di mana Anda ingin mencetak beberapa jenis pesan kesalahan yang berguna, Eitherjenisnya akan lebih cocok. Apa yang kamu lakukan jika sampai di Nothingsini? Anda baru saja mendapatkan pesan "kesalahan". Tidak sangat membantu untuk debugging.
sara
1

Saya telah menjadi penggemar berat Pengecualian Tercatat dan saya ingin membagikan aturan umum saya tentang kapan harus menggunakannya.

Saya sampai pada kesimpulan bahwa pada dasarnya ada 2 jenis kesalahan yang harus ditangani kode saya. Ada kesalahan yang dapat diuji sebelum kode dijalankan dan ada kesalahan yang tidak dapat diuji sebelum kode dijalankan. Contoh sederhana untuk kesalahan yang dapat diuji sebelum kode dieksekusi di NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

Tes sederhana bisa menghindari kesalahan seperti ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

Ada saat-saat dalam komputasi di mana Anda dapat menjalankan 1 tes atau lebih sebelum mengeksekusi kode untuk memastikan Anda aman DAN ANDA AKAN MENDAPATKAN PENGECUALIAN. Misalnya, Anda dapat menguji sistem file untuk memastikan ada cukup ruang disk pada hard drive sebelum Anda menulis data ke drive. Dalam sistem operasi multiprosesing, seperti yang digunakan saat ini, proses Anda dapat menguji ruang disk dan sistem file akan mengembalikan nilai dengan mengatakan ada ruang yang cukup, maka pergantian konteks ke proses lain dapat menulis byte tersisa yang tersedia untuk operasi tersebut sistem. Ketika konteks sistem operasi beralih kembali ke proses Anda berjalan di mana Anda menulis konten Anda ke disk, Pengecualian akan terjadi hanya karena tidak ada cukup ruang disk pada sistem file.

Saya menganggap skenario di atas sebagai kasus yang sempurna untuk Pengecualian yang Diperiksa. Ini merupakan pengecualian dalam kode yang memaksa Anda untuk berurusan dengan sesuatu yang buruk meskipun kode Anda dapat ditulis dengan sempurna. Jika Anda memilih untuk melakukan hal-hal buruk seperti 'menelan pengecualian', Anda adalah programmer yang buruk. Ngomong-ngomong, saya telah menemukan kasus-kasus di mana masuk akal untuk menelan pengecualian, tetapi tolong tinggalkan komentar dalam kode mengapa pengecualian itu tertelan. Mekanisme penanganan pengecualian tidak bisa disalahkan. Saya biasanya bercanda bahwa saya lebih suka alat pacu jantung saya ditulis dengan bahasa yang memiliki Pengecualian.

Ada kalanya menjadi sulit untuk memutuskan apakah kode tersebut dapat diuji atau tidak. Misalnya, jika Anda menulis penerjemah dan SyntaxException dilemparkan ketika kode gagal dieksekusi karena beberapa alasan sintaksis, haruskah SyntaxException menjadi Pengecualian yang Diperiksa atau (dalam Java) RuntimeException? Saya akan menjawab jika penerjemah memeriksa sintaks kode sebelum kode dieksekusi maka Pengecualian haruslah RuntimeException. Jika penerjemah hanya menjalankan kode 'panas' dan hanya menemukan kesalahan Sintaks, saya akan mengatakan pengecualian harus menjadi Pengecualian yang Diperiksa.

Saya akan mengakui bahwa saya tidak selalu senang harus menangkap atau melempar Pengecualian Tercatat karena ada waktu di mana saya tidak yakin apa yang harus dilakukan. Pengecualian yang Diperiksa adalah cara untuk memaksa programmer untuk memperhatikan potensi masalah yang dapat terjadi. Salah satu alasan mengapa saya memprogram di Jawa adalah karena ia telah Memeriksa Pengecualian.

James Moliere
sumber
1
Saya lebih suka alat pacu jantung saya ditulis dalam bahasa yang tidak memiliki pengecualian sama sekali, dan semua baris kode menangani kesalahan melalui kode kembali. Saat Anda melempar pengecualian, Anda mengatakan "semuanya salah" dan satu-satunya cara aman untuk melanjutkan pemrosesan adalah berhenti dan mulai ulang. Sebuah program yang dengan mudah berakhir dalam keadaan tidak valid bukanlah sesuatu yang Anda inginkan untuk perangkat lunak penting (dan Java secara eksplisit melarang penggunaannya untuk perangkat lunak kritis di EULA)
gbjbaanb
Menggunakan pengecualian dan tidak memeriksa mereka vs menggunakan kode kembali dan tidak memeriksa mereka pada akhirnya itu semua menghasilkan serangan jantung yang sama.
Newtopian
-1

Saya saat ini di tengah-tengah proyek / API berbasis OOP yang agak besar dan saya telah menggunakan tata letak pengecualian ini. Tapi itu semua benar-benar tergantung dari seberapa dalam Anda ingin pergi dengan penanganan pengecualian dan sejenisnya.

ExpectedException
- AuthorisedException
- EmptySetException
- NoRemainingException
- NoRowsException
- NotFoundException
- ValidationException

UnexpectedException
- ConnectivityException
- EnvironmentException
- ProgrammerException
- SQLException

CONTOH

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}
MattyD
sumber
11
Jika pengecualian diharapkan, itu bukan pengecualian. "NoRowsException"? Kedengarannya seperti kontrol mengalir ke saya, dan karena itu penggunaan pengecualian yang buruk.
quentin-starin
1
@ qes: Masuk akal untuk memunculkan pengecualian setiap kali suatu fungsi tidak dapat menghitung nilai, misalnya, double Math.sqrt (double v) atau User findUser (long id). Ini memberi si penelepon kebebasan untuk menangkap dan menangani kesalahan di tempat yang nyaman, alih-alih memeriksa setelah setiap panggilan.
kevin cline
1
Diharapkan = aliran kontrol = anti-pola pengecualian. Pengecualian tidak boleh digunakan untuk aliran kontrol. Jika diharapkan menghasilkan kesalahan untuk input tertentu, maka itu hanya akan diteruskan sebagai bagian dari nilai pengembalian. Jadi kita punya NANatau NULL.
Eonil
1
@Eonil ... atau Opsi <T>
Maarten Bodewes