Apakah saya boleh menggunakan pengecualian sebagai alat untuk "menangkap" kesalahan lebih awal?

29

Saya menggunakan pengecualian untuk menangkap masalah lebih awal. Sebagai contoh:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Program saya seharusnya tidak pernah lulus nulldalam fungsi ini. Saya tidak pernah menginginkannya. Namun seperti yang kita semua tahu, hal-hal yang tidak diinginkan terjadi dalam pemrograman.

Melontarkan pengecualian jika masalah ini terjadi, memungkinkan saya mengenali dan memperbaikinya, sebelum menyebabkan lebih banyak masalah di tempat lain dalam program. Pengecualian menghentikan program dan memberi tahu saya "hal-hal buruk terjadi di sini, perbaiki". Alih-alih ini nullbergerak di sekitar program yang menyebabkan masalah di tempat lain.

Sekarang, Anda benar, dalam hal ini nullhanya akan menyebabkan NullPointerExceptionlangsung, jadi itu mungkin bukan contoh terbaik.

Tetapi pertimbangkan metode seperti ini misalnya:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Dalam hal ini, a nullsebagai parameter akan dilewatkan di sekitar program, dan mungkin menyebabkan kesalahan jauh di kemudian hari, yang akan sulit dilacak kembali ke asalnya.

Mengubah fungsi seperti ini:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Memungkinkan saya untuk menemukan masalah jauh sebelum menyebabkan kesalahan aneh di tempat lain.

Juga, nullreferensi sebagai parameter hanyalah sebuah contoh. Bisa jadi ada banyak jenis masalah, dari argumen yang tidak valid hingga yang lainnya. Itu selalu lebih baik untuk menemukan mereka lebih awal.


Jadi pertanyaan saya sederhana: apakah ini praktik yang baik? Apakah saya menggunakan pengecualian sebagai alat pencegah masalah baik? Apakah ini aplikasi pengecualian yang sah atau bermasalah?

Aviv Cohn
sumber
2
Bisakah sang downvoter menjelaskan apa yang salah dengan pertanyaan ini?
Giorgio
2
"crash awal, crash sering"
Bryan Chen
16
Itulah pengecualian yang ada.
raptortech97
Perhatikan bahwa kontrak pengkodean memungkinkan Anda mendeteksi banyak kesalahan seperti itu secara statis. Ini umumnya lebih baik daripada melempar pengecualian pada saat runtime. Dukungan dan efektivitas kontrak pengkodean bervariasi menurut bahasa.
Brian
3
Karena Anda melempar IllegalArgumentException, berlebihan untuk mengatakan " Input person".
kevin cline

Jawaban:

29

Ya, "gagal sejak awal" adalah prinsip yang sangat baik, dan ini hanyalah salah satu cara yang mungkin untuk menerapkannya. Dan dalam metode yang harus mengembalikan nilai tertentu, tidak ada banyak hal lain yang dapat Anda lakukan untuk gagal dengan sengaja - itu bisa melempar pengecualian, atau memicu pernyataan. Pengecualian seharusnya menandakan kondisi 'luar biasa', dan mendeteksi kesalahan pemrograman tentu saja luar biasa.

Kilian Foth
sumber
Gagal lebih awal adalah cara yang harus ditempuh jika tidak ada cara untuk pulih dari kesalahan atau kondisi. Sebagai contoh jika Anda perlu menyalin file dan sumber atau jalur tujuan kosong maka Anda harus segera melemparkan pengecualian.
Michael Shopsin
Ia juga dikenal sebagai "gagal cepat"
keuleJ
8

Ya, melempar pengecualian adalah ide yang bagus. Lempar mereka lebih awal, lempar mereka sering, lempar mereka dengan penuh semangat.

Saya tahu ada debat "pengecualian vs pernyataan", dengan beberapa jenis perilaku luar biasa (khususnya, yang dianggap mencerminkan kesalahan pemrograman) yang ditangani oleh pernyataan yang dapat "dikompilasi" untuk runtime daripada debugging / pengujian build. Tetapi jumlah kinerja yang dikonsumsi dalam beberapa pemeriksaan ketelitian tambahan minimal pada perangkat keras modern, dan biaya tambahan apa pun jauh melebihi nilai memiliki hasil yang benar dan tidak rusak. Saya tidak pernah benar-benar bertemu basis kode aplikasi yang saya ingin (sebagian besar) pemeriksaan dihapus saat runtime.

Saya tergoda untuk mengatakan saya tidak ingin banyak pemeriksaan tambahan dan persyaratan dalam loop ketat kode numerik intensif ... tapi di situlah banyak kesalahan numerik dihasilkan, dan jika tidak ada, akan menyebar ke luar ke efek semua hasil. Bahkan di sana, cek layak dilakukan. Bahkan, beberapa algoritma numerik terbaik dan paling efisien didasarkan pada evaluasi kesalahan.

Satu tempat terakhir untuk menjadi sangat sadar akan kode tambahan adalah kode yang sangat sensitif terhadap latensi, di mana persyaratan tambahan dapat menyebabkan saluran pipa berhenti. Jadi, di tengah sistem operasi, DBMS, dan kernel middleware lainnya, dan komunikasi / protokol penanganan tingkat rendah. Tetapi sekali lagi, itu adalah beberapa kesalahan tempat yang paling mungkin diamati, dan pengaruhnya (keamanan, kebenaran, dan integritas data) paling merusak.

Satu perbaikan yang saya temukan adalah tidak hanya melemparkan pengecualian tingkat dasar. IllegalArgumentExceptionitu bagus, tapi itu bisa datang dari mana saja. Tidak perlu banyak dalam kebanyakan bahasa untuk menambahkan pengecualian khusus. Untuk modul penanganan orang Anda, katakan:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Lalu ketika seseorang melihat a PersonArgumentExceptionJelas dari mana asalnya. Ada tindakan penyeimbangan tentang berapa banyak pengecualian khusus yang ingin Anda tambahkan, karena Anda tidak ingin melipatgandakan entitas yang tidak perlu (Occam's Razor). Seringkali hanya beberapa pengecualian khusus yang cukup untuk memberi sinyal "modul ini tidak mendapatkan data yang tepat!" atau "modul ini tidak dapat melakukan apa yang seharusnya dilakukan!" dengan cara yang spesifik dan khusus, tetapi tidak terlalu tepat sehingga Anda harus menerapkan kembali seluruh hierarki pengecualian. Saya sering datang ke sekelompok kecil pengecualian khusus dengan memulai dengan pengecualian stok, kemudian memindai kode dan menyadari bahwa "N tempat ini meningkatkan pengecualian stok, tetapi mereka bermuara pada gagasan tingkat yang lebih tinggi bahwa mereka tidak mendapatkan data mereka membutuhkan;

Jonathan Eunice
sumber
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Maka Anda belum melakukan kode yang kritis terhadap kinerja. Saya sedang mengerjakan sesuatu saat ini yang tidak 37M ops per detik dengan pernyataan yang disusun dan 42M tanpa mereka. Pernyataan tidak ada di sana memvalidasi input luar, mereka ada di sana untuk memastikan kode itu benar. Pelanggan saya lebih dari senang menerima kenaikan 13% begitu saya puas bahwa barang saya tidak rusak.
Blrfl
Membumbui basis kode dengan pengecualian khusus harus dihindari karena meskipun dapat membantu Anda, itu tidak membantu orang lain yang harus mengenalnya. Biasanya jika Anda menemukan diri Anda memperluas pengecualian umum dan tidak menambahkan anggota atau fungsi apa pun, Anda sebenarnya tidak membutuhkannya. Bahkan dalam contoh Anda, PersonArgumentExceptiontidak sejelas IllegalArgumentException. Yang terakhir secara universal diketahui dilemparkan ketika argumen ilegal disahkan. Saya benar-benar berharap yang pertama dilempar jika Personada yang tidak valid untuk panggilan (Mirip dengan InvalidOperationExceptiondi C #).
Selali Adobor
Memang benar bahwa jika Anda tidak berhati-hati, Anda dapat memicu pengecualian yang sama dari tempat lain dalam fungsi, tetapi itu adalah cacat kode, bukan dari sifat pengecualian umum. Dan di situlah jejak debugging dan stack masuk. Jika Anda mencoba untuk "memberi label" pada pengecualian Anda, maka gunakan fasilitas yang ada yang disediakan oleh pengecualian yang paling umum, seperti menggunakan konstruktor pesan (itulah yang ada untuknya). Beberapa pengecualian bahkan memberikan konstruktor dengan parameter tambahan untuk mendapatkan nama argumen yang bermasalah, tetapi Anda dapat memberikan fasilitas yang sama dengan pesan yang terbentuk dengan baik.
Selali Adobor
Saya akui bahwa hanya mengubah ulang pengecualian umum ke label pribadi pada dasarnya pengecualian yang sama adalah contoh yang lemah, dan jarang sepadan dengan usaha. Dalam praktiknya saya mencoba memancarkan pengecualian khusus pada tingkat semantik yang lebih tinggi, lebih erat terkait dengan maksud modul.
Jonathan Eunice
Saya telah melakukan pekerjaan yang peka terhadap kinerja di bidang numerik / HPC dan OS / middleware. Benjolan kinerja 13% bukan hal kecil. Saya mungkin mematikan beberapa cek untuk mendapatkannya, dengan cara yang sama seorang komandan militer mungkin meminta 105% dari output reaktor nominal. Tetapi saya telah melihat "ini akan berjalan lebih cepat dengan cara ini" sering diberikan sebagai alasan untuk mematikan cek, cadangan, dan perlindungan lainnya. Ini pada dasarnya menjual ketahanan dan keamanan (dalam banyak kasus, termasuk integritas data) untuk kinerja ekstra. Apakah itu berharga adalah panggilan penghakiman.
Jonathan Eunice
3

Gagal sesegera mungkin hebat ketika men-debug aplikasi. Saya ingat kesalahan segmentasi tertentu dalam program C ++ lama: tempat di mana bug terdeteksi tidak ada hubungannya dengan tempat di mana ia diperkenalkan (null pointer dengan senang hati dipindahkan dari satu tempat ke tempat lain dalam memori sebelum akhirnya menyebabkan masalah ). Jejak tumpukan tidak dapat membantu Anda dalam kasus tersebut.

Jadi ya, pemrograman defensif adalah pendekatan yang sangat efektif untuk mendeteksi dan memperbaiki bug dengan cepat. Di sisi lain, bisa berlebihan, terutama dengan referensi nol.

Dalam kasus spesifik Anda, misalnya: jika salah satu referensi adalah nol, maka NullReferenceExceptionakan dilemparkan pada pernyataan berikutnya, ketika mencoba untuk mendapatkan usia satu orang. Anda tidak benar-benar perlu memeriksa hal-hal sendiri di sini: biarkan sistem yang mendasarinya menangkap kesalahan itu dan melemparkan pengecualian, itu sebabnya mereka ada .

Untuk contoh yang lebih realistis, Anda dapat menggunakan assertpernyataan, yang:

  1. Lebih pendek untuk menulis dan membaca:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Dirancang khusus untuk pendekatan Anda. Di dunia di mana Anda memiliki kedua pernyataan dan pengecualian, Anda dapat membedakannya sebagai berikut:

    • Pernyataan untuk kesalahan pemrograman ("/ * ini seharusnya tidak pernah terjadi * /"),
    • Pengecualian untuk kasus sudut (situasi luar biasa tapi kemungkinan)

Jadi, memaparkan asumsi Anda tentang input dan / atau status aplikasi Anda dengan pernyataan, biarkan pengembang berikutnya sedikit lebih memahami tujuan kode Anda.

Penganalisa statis (misalnya kompiler) mungkin lebih bahagia juga.

Akhirnya, pernyataan dapat dihapus dari aplikasi yang digunakan menggunakan satu sakelar. Tapi secara umum, jangan berharap untuk meningkatkan efisiensi dengan itu: pemeriksaan pernyataan saat runtime diabaikan.

coredump
sumber
1

Sejauh yang saya tahu programmer berbeda lebih suka satu solusi atau yang lain.

Solusi pertama biasanya lebih disukai karena lebih ringkas, khususnya, Anda tidak perlu memeriksa kondisi yang sama berulang-ulang dalam fungsi yang berbeda.

Saya menemukan solusi kedua, misalnya

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

lebih solid, karena

  1. Ini menangkap kesalahan sesegera mungkin, yaitu ketika registerPerson()dipanggil, bukan ketika pengecualian pointer nol dilemparkan ke suatu tempat di tumpukan panggilan. Debugging menjadi jauh lebih mudah: kita semua tahu sejauh mana nilai yang tidak valid dapat berjalan melalui kode sebelum memanifestasikan dirinya sebagai bug.
  2. Ini mengurangi penggabungan antar fungsi: registerPerson()tidak membuat asumsi tentang fungsi mana yang akan berakhir menggunakan personargumen dan bagaimana mereka akan menggunakannya: keputusan yang nullmerupakan kesalahan diambil dan diimplementasikan secara lokal.

Jadi, terutama jika kodenya agak rumit, saya cenderung memilih pendekatan kedua ini.

Giorgio
sumber
1
Memberikan alasan ("Input person adalah null.") Juga membantu untuk memahami masalahnya, tidak seperti ketika beberapa metode yang tidak jelas di perpustakaan gagal.
Florian F
1

Secara umum, ya, itu ide yang baik untuk "gagal lebih awal". Namun, dalam contoh spesifik Anda, eksplisit IllegalArgumentExceptiontidak memberikan peningkatan yang signifikan atas a NullReferenceException- karena kedua objek yang dioperasikan dikirimkan sebagai argumen untuk fungsi tersebut.

Tetapi mari kita lihat contoh yang sedikit berbeda.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Jika tidak ada argumen yang memeriksa konstruktor, Anda akan mendapatkan NullReferenceExceptionketika menelepon Calculate.

Tetapi bagian kode yang rusak bukanlah Calculatefungsi, atau konsumen Calculatefungsi. Sepotong kode yang rusak adalah kode yang mencoba untuk membangun PersonCalculatordengan nol Person- jadi di situlah kita ingin pengecualian terjadi.

Jika kami menghapus pemeriksaan argumen eksplisit, Anda harus mencari tahu mengapa a NullReferenceExceptionterjadi ketika Calculatedipanggil. Dan melacak mengapa objek itu dibangun dengannull seseorang bisa menjadi rumit, terutama jika kode membangun kalkulator tidak dekat dengan kode yang sebenarnya memanggil Calculatefungsi.

Pete
sumber
0

Tidak dalam contoh yang Anda berikan.

Seperti yang Anda katakan, melempar secara eksplisit tidak membuat Anda banyak ketika Anda akan mendapatkan pengecualian segera sesudahnya. Banyak yang akan berpendapat bahwa memiliki pengecualian eksplisit dengan pesan yang baik lebih baik, meskipun saya tidak setuju. Dalam skenario pra-dirilis, jejak tumpukan cukup baik. Dalam skenario pasca-rilis, situs panggilan sering dapat memberikan pesan yang lebih baik daripada di dalam fungsi.

Bentuk kedua adalah memberikan terlalu banyak info ke fungsi. Fungsi itu seharusnya tidak perlu tahu bahwa fungsi-fungsi lain akan menghasilkan input nol. Bahkan jika mereka melempar input nol sekarang , menjadi sangat merepotkan jika harus berhenti menjadi kasus karena pemeriksaan nol tersebar di seluruh kode.

Tetapi secara umum, Anda harus melempar lebih awal begitu Anda menentukan bahwa ada sesuatu yang salah (sambil menghormati KERING). Ini mungkin bukan contoh yang bagus untuk itu.

Telastyn
sumber
-3

Dalam fungsi contoh Anda, saya lebih suka Anda tidak melakukan pemeriksaan dan hanya mengizinkan NullReferenceException terjadi.

Untuk satu, tidak masuk akal untuk memberikan nol di sana, jadi saya akan mencari tahu masalah segera berdasarkan pada melempar a NullReferenceException.

Dua, adalah bahwa jika setiap fungsi melempar pengecualian yang sedikit berbeda berdasarkan pada jenis input yang jelas salah apa yang telah disediakan, maka segera Anda akan memiliki fungsi yang mungkin melempar 18 jenis pengecualian yang berbeda, dan segera Anda mendapati diri Anda mengatakan itu terlalu banyak pekerjaan yang harus dihadapi, dan hanya menekan semua pengecualian saja.

Tidak ada yang dapat Anda lakukan untuk membantu situasi kesalahan desain-waktu pada fungsi Anda, jadi biarkan kegagalan terjadi tanpa mengubahnya.

Apa namanya
sumber
Saya telah bekerja selama beberapa tahun berdasarkan kode yang didasarkan pada prinsip ini: pointer hanya dide-referensi ketika dibutuhkan dan hampir tidak pernah diperiksa terhadap NULL dan ditangani secara eksplisit. Men-debug kode ini selalu sangat memakan waktu dan menyusahkan karena pengecualian (atau dump inti) terjadi jauh lebih belakangan di titik kesalahan logis tersebut. Saya benar-benar membuang minggu mencari bug tertentu. Untuk alasan ini, saya lebih suka gaya gagal secepat mungkin, setidaknya untuk kode kompleks di mana tidak segera jelas dari mana pengecualian null-pointer berasal.
Giorgio
"Untuk satu, tidak masuk akal untuk memberikan nol di sana, jadi saya akan mencari tahu masalahnya segera berdasarkan melempar NullReferenceException.": Tidak harus, seperti contoh kedua Prog menggambarkan.
Giorgio
"Dua, adalah bahwa jika setiap fungsi melempar pengecualian yang sedikit berbeda berdasarkan jenis input yang jelas salah apa yang telah disediakan, maka segera Anda akan memiliki fungsi yang mungkin melempar 18 jenis pengecualian yang berbeda ...": Dalam hal ini Anda dapat menggunakan pengecualian yang sama (bahkan NullPointerException) di semua fungsi: yang penting adalah membuang lebih awal, bukan untuk membuang pengecualian yang berbeda dari setiap fungsi.
Giorgio
Itulah tujuan RuntimeExceptions. Kondisi apa pun yang "tidak boleh terjadi" tetapi mencegah kode Anda dari bekerja harus membuang satu. Anda tidak perlu mendeklarasikannya dalam tanda tangan metode. Tetapi Anda perlu menangkap mereka di beberapa titik dan melaporkan bahwa fungsi tindakan requestd gagal.
Florian F
1
Pertanyaannya tidak merinci suatu bahasa, jadi mengandalkan misalnya RuntimeExceptiontidak selalu merupakan asumsi yang valid.