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 yang catch
, 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
atau 0
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 dari malloc
dan kemudian mengembalikan kesalahan ke Convert Scanlines
, kemudian Convert Scanlines
harus memeriksa kesalahan itu dan meneruskannya ke Decompress Image
, lalu Decompress Image->Parse Image
, dan Parse Image->Load Image
, dan Load 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 memiliki catch
blok adalah Load Image User Command
tempat kesalahan dilaporkan. Tidak ada hal lain yang idealnya harus catch
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 minimumcatch
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 perlu catch
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 menggunakan finally
secara bebas dalam kasus ini untuk memastikan fungsi Anda membalikkan efek samping dalam bahasa yang mendukungnya, terlepas dari apakah Anda memerlukan catch
blok atau tidak (dan lagi, jika Anda bertanya kepada saya, kode yang ditulis dengan baik harus memiliki jumlah minimum catch
blok, dan semua catch
blok harus berada di tempat yang paling masuk akal dengan diagram di atas Load Image User Command
).
Bahasa Impian
Namun, IMO finally
sangat ideal untuk pembalikan efek samping tetapi tidak cukup. Kita perlu memperkenalkan satu boolean
variabel untuk secara efektif memutar kembali efek samping dalam kasus keluar prematur (dari pengecualian yang dilemparkan atau sebaliknya), seperti:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
Jika saya bisa mendesain bahasa, cara impian saya untuk memecahkan masalah ini adalah seperti ini untuk mengotomatiskan kode di atas:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
... dengan destruktor untuk mengotomatiskan pembersihan sumber daya lokal, membuatnya jadi kita hanya perlu transaction
,, rollback
dan catch
(meskipun saya mungkin masih ingin menambahkan finally
, katakanlah, bekerja dengan sumber daya C yang tidak membersihkan diri sendiri). Namun, finally
dengan boolean
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 harus catch
ke tempat-tempat yang paling masuk akal.