Cara klasik untuk memprogram adalah dengan try ... catch
. Kapan pantas digunakan try
tanpa catch
?
Dalam Python berikut ini tampak legal dan dapat masuk akal:
try:
#do work
finally:
#do something unconditional
Namun, kodenya tidak catch
apa - apa. Demikian pula orang dapat berpikir di Jawa itu adalah sebagai berikut:
try {
//for example try to get a database connection
}
finally {
//closeConnection(connection)
}
Kelihatannya bagus dan tiba-tiba saya tidak perlu khawatir tentang jenis pengecualian, dll. Jika ini praktik yang baik, kapan itu praktik yang baik? Atau, apa alasan mengapa ini bukan praktik yang baik atau tidak sah? (Saya tidak mengkompilasi sumbernya. Saya bertanya tentang hal itu karena itu bisa menjadi kesalahan sintaksis untuk Java. Saya memeriksa bahwa Python pasti mengkompilasi.)
Masalah terkait yang saya temui adalah ini: Saya terus menulis fungsi / metode, pada akhirnya harus mengembalikan sesuatu. Namun, itu mungkin berada di tempat yang tidak boleh dijangkau dan harus menjadi titik kembali. Jadi, bahkan jika saya menangani pengecualian di atas, saya masih kembali NULL
atau string kosong di beberapa titik dalam kode yang tidak boleh dijangkau, sering kali merupakan akhir dari metode / fungsi. Saya selalu berhasil merestrukturisasi kodenya sehingga tidak harus return NULL
, karena itu kelihatannya seperti kurang dari praktik yang baik.
sumber
try/catch
bukan "cara klasik untuk memprogram." Ini adalah cara klasik C ++ untuk memprogram, karena C ++ tidak memiliki coba / akhirnya membangun yang tepat, yang berarti Anda harus menerapkan perubahan negara yang dijamin reversibel menggunakan peretasan jelek yang melibatkan RAII. Tetapi bahasa OO yang layak tidak memiliki masalah itu, karena mereka memberikan coba / akhirnya. Ini digunakan untuk tujuan yang sangat berbeda dari coba / tangkap.Jawaban:
Itu tergantung pada apakah Anda dapat menangani pengecualian yang dapat diajukan pada saat ini atau tidak.
Jika Anda dapat menangani pengecualian secara lokal, Anda harus melakukannya, dan lebih baik menangani kesalahan sedekat mungkin dengan kesalahan tersebut.
Jika Anda tidak dapat mengatasinya secara lokal, maka hanya memiliki
try / finally
blok sangat masuk akal - dengan asumsi ada beberapa kode yang perlu Anda jalankan terlepas dari apakah metode tersebut berhasil atau tidak. Sebagai contoh (dari komentar Neil ), membuka aliran dan kemudian meneruskan aliran itu ke metode batin untuk dimuat adalah contoh yang sangat baik ketika Anda akan membutuhkantry { } finally { }
, menggunakan klausa terakhir untuk memastikan bahwa aliran ditutup terlepas dari keberhasilan atau kegagalan membaca.Namun, Anda masih memerlukan penangan pengecualian di suatu tempat dalam kode Anda - kecuali jika Anda ingin aplikasi Anda rusak sepenuhnya. Tergantung pada arsitektur aplikasi Anda persis di mana pawang itu.
sumber
try { } finally { }
, mengambil keuntungan dari akhirnya klausa untuk memastikan aliran pada akhirnya ditutup terlepas dari keberhasilan / kegagalan.The
finally
blok digunakan untuk kode yang harus selalu dijalankan, apakah kondisi kesalahan (pengecualian) terjadi atau tidak.Kode dalam
finally
blok dijalankan setelahtry
blok selesai dan, jika pengecualian yang tertangkap terjadi, setelahcatch
blok yang sesuai selesai. Itu selalu dijalankan, bahkan jika pengecualian tanpa tertangkap terjadi di bloktry
ataucatch
.The
finally
blok biasanya digunakan untuk file penutupan, koneksi jaringan, dll yang dibuka ditry
blok. Alasannya adalah bahwa file atau koneksi jaringan harus ditutup, apakah operasi yang menggunakan file atau koneksi jaringan itu berhasil atau gagal.Perawatan harus diambil di
finally
blok untuk memastikan bahwa itu tidak dengan sendirinya melemparkan pengecualian. Misalnya, pastikan dua kali lipat untuk memeriksa semua variabelnull
, dll.sumber
try-finally
dapat diganti denganwith
pernyataan.try/finally
konstruksi. C # hasusing
, Python haswith
, etc.using
dantry-finally
, karenaDispose
metode ini tidak akan dipanggil olehusing
blok jika pengecualian terjadi diIDisposable
konstruktor objek.try-finally
memungkinkan Anda untuk mengeksekusi kode meskipun konstruktor objek melempar pengecualian.Contoh di mana coba ... akhirnya tanpa klausa tangkapan sesuai (dan bahkan lebih, idiomatik ) di Jawa adalah penggunaan Lock dalam paket kunci utilitas bersamaan.
sumber
l.lock()
mencoba mencoba?try{ l.lock(); }finally{l.unlock();}
try
dalam cuplikan ini dimaksudkan untuk membungkus akses sumber daya, mengapa mencemarinya dengan sesuatu yang tidak terkait dengan itul.lock()
gagal,finally
blok masih akan berjalan jikal.lock()
ada di dalamtry
blok. Jika Anda melakukannya seperti yang disarankan nyamuk,finally
blok hanya akan berjalan ketika kita tahu bahwa kunci telah diperoleh.Pada tingkat dasar
catch
danfinally
selesaikan dua masalah terkait tetapi berbeda:catch
digunakan untuk menangani masalah yang dilaporkan oleh kode yang Anda panggilfinally
digunakan untuk membersihkan data / sumber daya yang dibuat / dimodifikasi oleh kode saat ini, tidak peduli apakah ada masalah atau tidakJadi keduanya terkait entah bagaimana dengan masalah (pengecualian), tapi itu cukup banyak kesamaan mereka.
Perbedaan penting adalah bahwa
finally
blok harus dalam metode yang sama di mana sumber daya dibuat (untuk menghindari kebocoran sumber daya) dan tidak dapat diletakkan pada tingkat yang berbeda di tumpukan panggilan.The
catch
namun adalah hal yang berbeda: tempat yang tepat untuk itu tergantung pada di mana Anda benar-benar dapat menangani pengecualian. Tidak ada gunanya menangkap pengecualian di tempat di mana Anda tidak bisa berbuat apa-apa, karena itu terkadang lebih baik membiarkannya saja.sumber
finally
pernyataan coba terlampir (secara statis atau dinamis) ... dan masih 100% anti bocor.@yfeldblum memiliki jawaban yang benar: coba-akhirnya tanpa pernyataan tangkap biasanya harus diganti dengan konstruksi bahasa yang sesuai.
Di C ++, ia menggunakan RAII dan konstruktor / destruktor; dalam Python itu adalah
with
pernyataan; dan di C #, itu adalahusing
pernyataan.Ini hampir selalu lebih elegan karena kode inisialisasi dan finalisasi berada di satu tempat (objek abstrak) daripada di dua tempat.
sumber
Dalam banyak bahasa
finally
pernyataan juga berjalan setelah pernyataan kembali. Ini berarti Anda dapat melakukan sesuatu seperti:Yang melepaskan sumber daya terlepas dari bagaimana metode itu berakhir dengan pengecualian atau pernyataan pengembalian reguler.
Apakah ini baik atau buruk masih bisa diperdebatkan, tetapi
try {} finally {}
tidak selalu terbatas pada penanganan pengecualian.sumber
Saya mungkin memohon kemarahan Pythonistas (tidak tahu karena saya tidak banyak menggunakan Python) atau programmer dari bahasa lain dengan jawaban ini, tetapi menurut saya sebagian besar fungsi seharusnya tidak memiliki
catch
blok, idealnya berbicara. Untuk menunjukkan alasannya, izinkan saya membandingkan ini dengan propagasi kode kesalahan manual seperti yang harus saya lakukan ketika bekerja dengan Turbo C di akhir tahun 80an dan awal tahun 90an.Jadi katakanlah kita memiliki fungsi untuk memuat gambar atau sesuatu seperti itu sebagai respons terhadap pengguna yang memilih file gambar untuk dimuat, dan ini ditulis dalam C dan assembly:
Saya menghilangkan beberapa fungsi tingkat rendah tetapi kita dapat melihat bahwa saya telah mengidentifikasi berbagai kategori fungsi, kode warna, berdasarkan pada tanggung jawab apa yang mereka miliki sehubungan dengan penanganan kesalahan.
Titik Kegagalan dan Pemulihan
Sekarang tidak pernah sulit untuk menulis kategori fungsi yang saya sebut "kemungkinan titik kegagalan" (fungsi yang
throw
, yaitu) dan fungsi "pemulihan kesalahan dan melaporkan" (fungsi yangcatch
, yaitu).Fungsi-fungsi yang selalu sepele untuk menulis dengan benar sebelum penanganan pengecualian adalah tersedia sejak fungsi yang bisa lari ke kegagalan eksternal, seperti gagal untuk mengalokasikan memori, hanya bisa mengembalikan
NULL
atau0
atau-1
atau menetapkan kode kesalahan global atau sesuatu untuk efek ini. Dan pemulihan / pelaporan kesalahan selalu mudah karena begitu Anda menelusuri tumpukan panggilan ke titik di mana masuk akal untuk memulihkan dan melaporkan kegagalan, Anda cukup mengambil kode kesalahan dan / atau pesan dan melaporkannya kepada pengguna. Dan tentu saja fungsi di daun hirarki ini yang tidak pernah bisa gagal tidak peduli bagaimana itu berubah di masa depan (Convert Pixel
) sangat sederhana untuk menulis dengan benar (setidaknya sehubungan dengan penanganan kesalahan).Propagasi Kesalahan
Namun, fungsi-fungsi membosankan yang rentan terhadap kesalahan manusia adalah penyebar kesalahan , yang tidak secara langsung mengalami kegagalan tetapi disebut fungsi yang bisa gagal di suatu tempat yang lebih dalam di hierarki. Pada titik itu,
Allocate Scanline
mungkin harus menangani kegagalan darimalloc
dan kemudian mengembalikan kesalahan keConvert Scanlines
, kemudianConvert Scanlines
harus memeriksa kesalahan itu dan meneruskannya keDecompress Image
, laluDecompress Image->Parse Image
, danParse Image->Load Image
, danLoad Image
ke perintah pengguna-akhir tempat kesalahan akhirnya dilaporkan .Di sinilah banyak manusia membuat kesalahan karena hanya membutuhkan satu penyebar kesalahan untuk gagal memeriksa dan meneruskan kesalahan untuk seluruh hierarki fungsi yang akan terguling ketika datang untuk menangani kesalahan dengan benar.
Lebih lanjut, jika kode kesalahan dikembalikan oleh fungsi, kami kehilangan banyak kemampuan dalam, katakanlah, 90% dari basis kode kami, untuk mengembalikan nilai minat pada kesuksesan karena begitu banyak fungsi harus menyimpan nilai pengembaliannya untuk mengembalikan kode kesalahan pada kegagalan .
Mengurangi Kesalahan Manusia: Kode Kesalahan Global
Jadi bagaimana kita bisa mengurangi kemungkinan kesalahan manusia? Di sini saya bahkan mungkin mengundang kemarahan beberapa programmer C, tetapi perbaikan langsung dalam pendapat saya adalah dengan menggunakan kode kesalahan global , seperti OpenGL dengan
glGetError
. Ini setidaknya membebaskan fungsi untuk mengembalikan nilai-nilai yang menarik dari kesuksesan. Ada cara untuk membuat utas ini aman dan efisien di mana kode kesalahan dilokalkan ke utas.Ada juga beberapa kasus di mana suatu fungsi mungkin mengalami kesalahan tetapi itu relatif tidak berbahaya untuk itu terus sedikit lebih lama sebelum kembali sebelum waktunya sebagai akibat dari menemukan kesalahan sebelumnya. Hal ini memungkinkan hal seperti itu terjadi tanpa harus memeriksa kesalahan terhadap 90% panggilan fungsi yang dilakukan di setiap fungsi tunggal, sehingga masih dapat memungkinkan penanganan kesalahan yang tepat tanpa begitu teliti.
Mengurangi Kesalahan Manusia: Penanganan Eksepsi
Namun, solusi di atas masih membutuhkan begitu banyak fungsi untuk menangani aspek aliran kontrol propagasi kesalahan manual, bahkan jika itu mungkin telah mengurangi jumlah baris
if error happened, return error
tipe kode manual . Itu tidak akan menghilangkannya sepenuhnya karena masih sering harus ada setidaknya satu tempat memeriksa kesalahan dan kembali untuk hampir setiap fungsi propagasi kesalahan tunggal. Jadi ini adalah saat penanganan pengecualian muncul untuk menyelamatkan hari (agak).Tetapi nilai penanganan pengecualian di sini adalah untuk membebaskan kebutuhan untuk berurusan dengan aspek aliran kontrol dari propagasi kesalahan manual. Itu artinya nilainya terikat pada kemampuan untuk menghindari keharusan menulis muatan kapal
catch
blok sepanjang basis kode Anda. Dalam diagram di atas, satu-satunya tempat yang harus memilikicatch
blok adalahLoad Image User Command
tempat kesalahan dilaporkan. Tidak ada hal lain yang idealnya haruscatch
apa - apa karena selain itu mulai membosankan dan rawan kesalahan seperti penanganan kode kesalahan.Jadi jika Anda bertanya kepada saya, jika Anda memiliki basis kode yang benar-benar mendapat manfaat dari penanganan pengecualian dengan cara yang elegan, itu harus memiliki jumlah minimum
catch
blok (minimum saya tidak berarti nol, tetapi lebih seperti satu untuk setiap tinggi yang unik) operasi pengguna akhir yang bisa gagal, dan mungkin bahkan lebih sedikit jika semua operasi pengguna kelas atas dipanggil melalui sistem perintah pusat).Pembersihan Sumber Daya
Namun, penanganan pengecualian hanya memecahkan kebutuhan untuk menghindari berurusan secara manual dengan aspek aliran kontrol propagasi kesalahan dalam jalur luar biasa terpisah dari aliran eksekusi normal. Seringkali fungsi yang berfungsi sebagai penyebar kesalahan, bahkan jika ia melakukan ini secara otomatis sekarang dengan EH, mungkin masih memperoleh beberapa sumber daya yang perlu dihancurkan. Misalnya, fungsi semacam itu mungkin membuka file sementara yang harus ditutup sebelum kembali dari fungsi apa pun yang terjadi, atau mengunci mutex yang diperlukan untuk membuka kunci apa pun yang terjadi.
Untuk ini, saya mungkin mengundang kemarahan banyak programmer dari semua jenis bahasa, tapi saya pikir pendekatan C ++ untuk ini sangat ideal. Bahasa ini memperkenalkan destruktor yang dapat dipanggil secara deterministik saat instan objek keluar dari ruang lingkup. Karena itu, kode C ++ yang, misalnya, mengunci mutex melalui objek mutex scoped dengan destruktor tidak perlu membukanya secara manual, karena itu akan secara otomatis dibuka setelah objek keluar dari ruang lingkup tidak peduli apa yang terjadi (bahkan jika pengecualian adalah ditemui). Jadi benar-benar tidak perlu kode C ++ yang ditulis dengan baik untuk berurusan dengan pembersihan sumber daya lokal.
Dalam bahasa yang tidak memiliki destruktor, mereka mungkin perlu menggunakan
finally
blok untuk membersihkan sumber daya lokal secara manual. Yang mengatakan, itu masih mengalahkan harus membuang sampah sembarangan kode Anda dengan propagasi kesalahan manual asalkan Anda tidak perlucatch
pengecualian di seluruh tempat yang menakutkan.Membalikkan Efek Samping Eksternal
Ini adalah yang masalah konseptual yang paling sulit untuk memecahkan. Jika ada fungsi, apakah itu penyebar kesalahan atau titik kegagalan menyebabkan efek samping eksternal, maka perlu memutar kembali atau "membatalkan" efek samping tersebut untuk mengembalikan sistem kembali ke keadaan seolah-olah operasi tidak pernah terjadi, alih-alih " "setengah valid" ketika operasi separuh berhasil. Saya tahu tidak ada bahasa yang membuat masalah konseptual ini jauh lebih mudah kecuali bahasa yang hanya mengurangi kebutuhan sebagian besar fungsi untuk menyebabkan efek samping eksternal, seperti bahasa fungsional yang berkisar seputar ketidakberdayaan dan struktur data yang persisten.
Berikut
finally
ini adalah solusi paling elegan di luar sana untuk masalah dalam bahasa yang berputar di sekitar mutabilitas dan efek samping, karena seringkali jenis logika ini sangat spesifik untuk fungsi tertentu dan tidak memetakan dengan baik konsep "pembersihan sumber daya" ". Dan saya sarankan menggunakanfinally
secara bebas dalam kasus ini untuk memastikan fungsi Anda membalikkan efek samping dalam bahasa yang mendukungnya, terlepas dari apakah Anda memerlukancatch
blok atau tidak (dan lagi, jika Anda bertanya kepada saya, kode yang ditulis dengan baik harus memiliki jumlah minimumcatch
blok, dan semuacatch
blok harus berada di tempat yang paling masuk akal dengan diagram di atasLoad Image User Command
).Bahasa Impian
Namun, IMO
finally
sangat ideal untuk pembalikan efek samping tetapi tidak cukup. Kita perlu memperkenalkan satuboolean
variabel untuk secara efektif memutar kembali efek samping dalam kasus keluar prematur (dari pengecualian yang dilemparkan atau sebaliknya), seperti:Jika saya bisa mendesain bahasa, cara impian saya untuk memecahkan masalah ini adalah seperti ini untuk mengotomatiskan kode di atas:
... dengan destruktor untuk mengotomatiskan pembersihan sumber daya lokal, membuatnya jadi kita hanya perlu
transaction
,,rollback
dancatch
(meskipun saya mungkin masih ingin menambahkanfinally
, katakanlah, bekerja dengan sumber daya C yang tidak membersihkan diri sendiri). Namun,finally
denganboolean
variabel adalah hal yang paling dekat untuk membuat ini secara langsung yang saya temukan sejauh ini kurang bahasa mimpi saya. Solusi paling mudah kedua yang saya temukan untuk ini adalah ruang lingkup penjaga dalam bahasa seperti C ++ dan D, tapi saya selalu menemukan ruang lingkup penjaga sedikit canggung secara konseptual karena mengaburkan gagasan "pembersihan sumber daya" dan "pembalikan efek samping". Menurut pendapat saya itu adalah ide yang sangat berbeda untuk ditangani dengan cara yang berbeda.Mimpi pipa kecil saya tentang bahasa juga akan banyak berputar di sekitar ketidakmampuan dan struktur data yang persisten untuk membuatnya lebih mudah, meskipun tidak diperlukan, untuk menulis fungsi yang efisien yang tidak harus menyalin struktur data besar secara keseluruhan meskipun fungsi menyebabkan tidak ada efek samping.
Kesimpulan
Jadi, dengan mengesampingkan ocehan saya, saya pikir
try/finally
kode Anda untuk menutup soket baik-baik saja dan bagus mengingat bahwa Python tidak memiliki C ++ yang setara dengan destruktor, dan saya pribadi berpikir Anda harus menggunakannya secara bebas untuk tempat-tempat yang perlu membalikkan efek samping dan meminimalkan jumlah tempat di mana Anda haruscatch
ke tempat-tempat yang paling masuk akal.sumber
Menangkap kesalahan / pengecualian dan menanganinya dengan cara yang rapi sangat dianjurkan bahkan jika tidak wajib.
Alasan saya mengatakan ini adalah karena saya percaya setiap pengembang harus tahu dan menangani perilaku aplikasinya jika tidak dia tidak menyelesaikan pekerjaannya dengan cara yang sepatutnya. Tidak ada situasi di mana blok coba-akhirnya menggantikan blok coba-tangkap-akhirnya.
Saya akan memberikan Anda contoh sederhana: Asumsikan bahwa Anda telah menulis kode untuk mengunggah file di server tanpa menangkap pengecualian. Sekarang, jika karena suatu alasan unggahan gagal, klien tidak akan pernah tahu apa yang salah. Tetapi, jika Anda telah menangkap pengecualian, Anda dapat menampilkan pesan kesalahan yang menjelaskan apa yang salah dan bagaimana pengguna dapat memperbaikinya.
Aturan emas: Selalu menangkap pengecualian, karena menebak membutuhkan waktu
sumber
catch
klausa harus selalu ada setiap kali adatry
. Kontributor yang lebih berpengalaman, termasuk saya, percaya ini adalah saran yang buruk. Alasan Anda cacat. Metode yang mengandungtry
bukan satu-satunya tempat yang mungkin pengecualian dapat ditangkap. Sering kali paling sederhana dan terbaik untuk memungkinkan penangkapan dan melaporkan pengecualian dari tingkat paling atas, daripada menduplikasi klausa tangkapan di seluruh kode.