Apakah ada alasan yang sah untuk mengembalikan objek pengecualian daripada melemparkannya?

45

Pertanyaan ini dimaksudkan untuk diterapkan pada bahasa pemrograman OO yang mendukung penanganan pengecualian; Saya menggunakan C # untuk tujuan ilustrasi saja.

Pengecualian biasanya dimaksudkan untuk dimunculkan ketika masalah muncul bahwa kode tidak dapat segera ditangani, dan kemudian ditangkap dalam catchklausa di lokasi yang berbeda (biasanya bingkai tumpukan luar).

T: Apakah ada situasi yang sah di mana pengecualian tidak dilemparkan dan ditangkap, tetapi hanya dikembalikan dari metode dan kemudian diedarkan sebagai objek kesalahan?

Pertanyaan ini muncul untuk saya karena System.IObserver<T>.OnErrormetode .NET 4 menyarankan hanya itu: pengecualian dilewatkan sebagai objek kesalahan.

Mari kita lihat skenario lain, validasi. Katakanlah saya mengikuti kebijakan konvensional, dan karena itu saya membedakan antara tipe objek kesalahan IValidationErrordan tipe pengecualian terpisah ValidationExceptionyang digunakan untuk melaporkan kesalahan tak terduga:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

( System.Component.DataAnnotationsNamespace melakukan sesuatu yang sangat mirip.)

Jenis-jenis ini dapat digunakan sebagai berikut:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Sekarang saya bertanya-tanya, dapatkah saya menyederhanakan hal di atas menjadi ini:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

T: Apa kelebihan dan kekurangan dari dua pendekatan yang berbeda ini?

stakx
sumber
Jika Anda mencari dua jawaban terpisah, Anda harus mengajukan dua pertanyaan terpisah. Menggabungkan mereka hanya membuat itu lebih sulit untuk dijawab.
Bobson
2
@ Bobson: Saya melihat dua pertanyaan ini sebagai pelengkap: Pertanyaan sebelumnya seharusnya mendefinisikan konteks umum masalah ini secara luas, sementara yang terakhir meminta informasi spesifik yang harus memungkinkan pembaca untuk menarik kesimpulan mereka sendiri. Sebuah jawaban tidak harus memberikan jawaban yang terpisah dan eksplisit untuk kedua pertanyaan; jawaban "di antara" sama-sama disambut.
stakx
5
Saya tidak jelas tentang sesuatu: Apa alasan untuk mendapatkan ValidationError dari Exception, jika Anda tidak akan membuangnya?
pdr
@ pdr: Apakah maksud Anda dalam hal melempar AggregateException? Poin bagus.
stakx
3
Tidak juga. Maksud saya, jika Anda akan melemparnya, gunakan pengecualian. Jika Anda akan mengembalikannya, gunakan objek kesalahan. Jika Anda mengharapkan pengecualian yang benar-benar kesalahan, tangkap mereka dan masukkan ke dalam objek kesalahan Anda. Apakah salah satu memungkinkan untuk beberapa kesalahan sama sekali tidak relevan.
pdr

Jawaban:

31

Apakah ada situasi yang sah di mana pengecualian tidak dilemparkan dan ditangkap, tetapi hanya dikembalikan dari metode dan kemudian diedarkan sebagai objek kesalahan?

Jika tidak pernah dibuang maka itu bukan pengecualian. Ini adalah objek deriveddari kelas Exception, meskipun tidak mengikuti perilaku. Menyebutnya Pengecualian adalah semantik murni pada titik ini, tetapi saya tidak melihat ada yang salah dengan tidak melemparkannya. Dari sudut pandang saya, tidak melempar pengecualian adalah hal yang persis sama dengan fungsi dalam menangkapnya dan mencegahnya menyebar.

Apakah valid untuk fungsi untuk mengembalikan objek pengecualian?

Benar. Berikut adalah daftar contoh singkat di mana ini mungkin sesuai:

  • Pabrik pengecualian.
  • Objek konteks yang melaporkan jika ada kesalahan sebelumnya sebagai pengecualian siap pakai.
  • Fungsi yang menyimpan pengecualian yang sebelumnya tertangkap.
  • API pihak ketiga yang membuat pengecualian tipe dalam.

Bukankah membuangnya buruk?

Melempar pengecualian adalah seperti: "Pergi ke penjara. Jangan lulus Pergi!" di papan permainan Monopoli. Ini memberitahu kompiler untuk melompati semua kode sumber hingga tangkapan tanpa mengeksekusi salah satu kode sumber itu. Itu tidak ada hubungannya dengan kesalahan, pelaporan atau menghentikan bug. Saya suka berpikir untuk melempar pengecualian sebagai pernyataan "super return" untuk suatu fungsi. Ini mengembalikan eksekusi kode ke suatu tempat yang jauh lebih tinggi dari pemanggil asli.

Yang penting di sini adalah untuk memahami bahwa nilai sebenarnya dari pengecualian ada dalam try/catchpola, dan bukan instantiasi objek pengecualian. Objek pengecualian hanyalah pesan status.

Dalam pertanyaan Anda, Anda tampaknya membingungkan penggunaan dua hal ini: melompat ke pengendali pengecualian, dan status kesalahan yang diwakili oleh pengecualian. Hanya karena Anda mengambil status kesalahan dan membungkusnya dalam pengecualian tidak berarti Anda mengikuti try/catchpola atau manfaatnya.

Reactgular
sumber
4
"Jika tidak pernah dilempar maka itu bukan pengecualian. Ini adalah objek yang berasal dari Exceptionkelas tetapi tidak mengikuti perilaku." - Dan itulah masalahnya: Apakah sah untuk menggunakan Exceptionobjek yang diarsipkan dalam situasi di mana ia tidak akan terlempar sama sekali, baik oleh produsen objek, maupun dengan metode panggilan apa pun; di mana itu hanya berfungsi sebagai "pesan negara" yang diedarkan, tanpa aliran kontrol "super return"? Atau apakah saya melanggar kontrak try/ tidak terucapkan catch(Exception)begitu buruk sehingga saya tidak boleh melakukan ini, dan bukannya menggunakan, non- Exceptiontipe yang terpisah ?
stakx
10
Pemrogram @stakx telah dan akan terus melakukan hal-hal yang membingungkan bagi pemrogram lain, tetapi secara teknis tidak salah. Itu sebabnya saya bilang itu semantik. Jawaban hukumnya adalah NoAnda tidak melanggar kontrak apa pun. The popular opinionakan bahwa Anda tidak harus melakukan itu. Pemrograman penuh dengan contoh-contoh di mana kode sumber Anda tidak melakukan kesalahan tetapi semua orang tidak menyukainya. Kode sumber harus selalu ditulis agar jelas maksudnya. Apakah Anda benar-benar membantu programmer lain dengan menggunakan pengecualian seperti itu? Jika ya, maka lakukanlah. Kalau tidak, jangan terkejut jika itu tidak disukai.
Reactgular
@cgTag Penggunaan blok kode inline untuk huruf miring membuat otak saya sakit ..
Frayt
51

Mengembalikan pengecualian alih-alih melemparkannya dapat masuk akal secara semantik ketika Anda memiliki metode penolong untuk menganalisis situasi dan mengembalikan pengecualian yang sesuai yang kemudian dilontarkan oleh penelepon (Anda dapat menyebutnya sebagai "pabrik pengecualian"). Melempar pengecualian dalam fungsi analisa kesalahan ini akan berarti bahwa ada kesalahan selama analisis itu sendiri, sementara mengembalikan pengecualian berarti jenis kesalahan dianalisis dengan sukses.

Salah satu use case yang mungkin adalah fungsi yang mengubah kode respons HTTP menjadi pengecualian:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Perhatikan bahwa melempar pengecualian berarti bahwa metode yang digunakan salah atau memiliki kesalahan internal, sementara mengembalikan pengecualian berarti bahwa kode kesalahan diidentifikasi berhasil.

Philipp
sumber
Ini juga membantu kompiler memahami aliran kode: jika suatu metode tidak pernah kembali dengan benar (karena ia melempar pengecualian "yang diinginkan") dan kompiler tidak tahu tentang itu, maka Anda kadang-kadang mungkin perlu returnpernyataan berlebihan untuk membuat kompiler berhenti mengeluh.
Joachim Sauer
@ JoachimSauer Ya. Lebih dari sekali saya berharap mereka akan menambahkan jenis kembali "tidak pernah" ke C # - itu akan menunjukkan fungsi harus keluar dengan melempar, menempatkan pengembalian di dalamnya adalah kesalahan. Terkadang Anda memiliki kode yang pekerjaannya hanya membuat pengecualian.
Loren Pechtel
2
@LorenPechtel Fungsi yang tidak pernah kembali bukanlah fungsi. Itu terminator. Memanggil fungsi seperti itu akan menghapus tumpukan panggilan. Ini mirip dengan menelepon System.Exit(). Kode setelah panggilan fungsi tidak dieksekusi. Itu tidak terdengar seperti pola desain yang bagus untuk suatu fungsi.
Reactgular
5
@MathewFoscarini Pertimbangkan kelas yang memiliki banyak metode yang dapat kesalahan dengan cara yang sama. Anda ingin membuang beberapa informasi status sebagai bagian dari pengecualian untuk menunjukkan apa yang dikunyahnya saat booming. Anda ingin satu salinan kode pengubahan karena KERING. Yang meninggalkan baik memanggil fungsi yang melakukan pemotongan dan menggunakan hasil dalam lemparan Anda atau itu berarti fungsi yang membangun teks yang telah disiapkan dan kemudian melemparkannya. Saya biasanya menemukan yang terakhir superior.
Loren Pechtel
1
@LorenPechtel ah, ya saya sudah melakukannya juga. Oke saya mengerti sekarang.
Reactgular
5

Secara konseptual, jika objek pengecualian adalah hasil yang diharapkan dari operasi, maka ya. Kasus-kasus yang dapat saya pikirkan, bagaimanapun, selalu melibatkan lemparan-tangkapan di beberapa titik:

  • Variasi pada pola "Coba" (di mana metode merangkum metode melempar pengecualian kedua, tetapi menangkap pengecualian dan sebaliknya mengembalikan boolean yang menunjukkan keberhasilan). Anda bisa, alih-alih mengembalikan Boolean, mengembalikan Pengecualian apa pun yang dilemparkan (nol jika berhasil), memungkinkan pengguna akhir lebih banyak informasi sambil mempertahankan indikasi keberhasilan atau kegagalan hasil tangkapan.

  • Pemrosesan kesalahan alur kerja. Mirip dalam praktiknya dengan enkapsulasi metode "coba pola", Anda mungkin memiliki langkah alur kerja yang diabstraksi dalam pola rantai-perintah. Jika tautan dalam rantai melempar pengecualian, sering kali lebih bersih untuk menangkap pengecualian itu dalam objek abstrak, lalu kembalikan sebagai hasil dari operasi tersebut sehingga mesin alur kerja dan kode aktual dijalankan sebagai langkah alur kerja tidak perlu dicoba secara ekstensif Logika -catch mereka sendiri.

KeithS
sumber
Saya tidak berpikir melihat orang-orang terkemuka menganjurkan variasi seperti pada pola "Coba", tetapi pendekatan "yang direkomendasikan" untuk mengembalikan Boolean tampaknya cukup anemia. Saya akan berpikir mungkin lebih baik untuk memiliki OperationResultkelas abstrak , dengan boolproperti "Sukses", GetExceptionIfAnymetode, dan AssertSuccessfulmetode (yang akan membuang pengecualian dari GetExceptionIfAnyjika tidak nol). Memiliki GetExceptionIfAnymetode daripada properti akan memungkinkan penggunaan objek kesalahan statis yang tidak dapat diubah untuk kasus-kasus di mana tidak banyak yang berguna untuk dikatakan.
supercat
5

Katakanlah Anda memerlukan beberapa tugas untuk dieksekusi di beberapa kumpulan utas. Jika tugas ini melempar pengecualian, ini utas berbeda sehingga Anda tidak menangkapnya. Thread di mana itu dieksekusi mati.

Sekarang pertimbangkan bahwa sesuatu (baik kode dalam tugas itu, atau implementasi thread pool) menangkap pengecualian yang menyimpannya bersama dengan tugas dan mempertimbangkan tugas selesai (tidak berhasil). Sekarang Anda dapat bertanya apakah tugas sudah selesai (dan apakah bertanya apakah itu melempar, atau dapat dilemparkan lagi di utas Anda (atau lebih baik, pengecualian baru dengan sumber asli sebagai penyebab)).

Jika Anda melakukannya secara manual, Anda dapat melihat bahwa Anda sedang membuat pengecualian baru, melempar dan menangkapnya, lalu menyimpannya, di berbagai utas yang mengambil, melempar, menangkap dan bereaksi padanya. Jadi masuk akal untuk melewatkan lempar dan tangkap dan simpan saja dan selesaikan tugas lalu tanyakan apakah ada pengecualian dan bereaksi padanya. Tapi ini bisa mengarah pada kode yang lebih rumit jika di tempat yang sama bisa ada pengecualian.

PS: ini ditulis dengan pengalaman dengan Java, di mana informasi jejak stack dibuat ketika pengecualian dibuat, (tidak seperti C # di mana ia dibuat saat melempar). Jadi pendekatan pengecualian tidak dilempar di Jawa akan lebih lambat dari pada di C # (kecuali yang telah diolah dan digunakan kembali), tetapi akan memiliki info jejak stack yang tersedia.

Secara umum, saya akan tinggal jauh dari membuat pengecualian dan tidak pernah membuangnya (kecuali jika optimalisasi kinerja setelah profiling menunjukkannya sebagai bottleneck). Paling tidak di java, di mana pembuatan pengecualian mahal (tumpukan jejak). Dalam C # itu adalah kemungkinan, tetapi IMO itu mengejutkan dan karenanya harus dihindari.

pengguna470365
sumber
Ini sering terjadi dengan objek pendengar kesalahan juga. Antrian akan menjalankan tugas, menangkap pengecualian apa pun, lalu meneruskan pengecualian itu ke pendengar galat yang bertanggung jawab. Biasanya tidak masuk akal untuk mematikan utas tugas yang tidak tahu banyak tentang pekerjaan dalam kasus ini. Gagasan semacam ini juga dibangun di Java APIs di mana satu utas dapat melewati pengecualian dari yang lain: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek
1

Tergantung pada desain Anda, biasanya saya tidak akan mengembalikan pengecualian ke penelepon, melainkan saya akan melempar dan menangkapnya dan membiarkannya begitu saja. Biasanya kode ditulis untuk gagal lebih awal dan cepat. Sebagai contoh, pertimbangkan kasus membuka file dan memprosesnya (Ini adalah C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

Dalam hal ini, kami akan melakukan kesalahan dan berhenti memproses file segera setelah menemukan catatan buruk.

Tetapi katakan persyaratannya adalah kami ingin terus memproses file bahkan jika satu atau lebih baris memiliki kesalahan. Kode mungkin mulai terlihat seperti ini:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Jadi dalam hal ini kita mengembalikan pengecualian kembali ke penelepon, meskipun saya mungkin tidak akan mengembalikan pengecualian tetapi sebaliknya semacam objek kesalahan karena pengecualian telah ditangkap dan sudah ditangani. Ini tidak ada salahnya mengembalikan kembali pengecualian, tetapi penelepon lain dapat melemparkan kembali pengecualian yang mungkin tidak diinginkan karena objek pengecualian memiliki perilaku itu. Jika Anda ingin penelepon memiliki kemampuan untuk melemparkan kembali maka kembalikan pengecualian, jika tidak, keluarkan informasi dari objek pengecualian dan buat objek yang lebih ringan dan kembalikan. Gagal cepat biasanya kode bersih.

Sebagai contoh validasi Anda, saya mungkin tidak akan mewarisi dari kelas pengecualian atau melempar pengecualian karena kesalahan validasi bisa sangat umum. Akan lebih murah untuk mengembalikan objek daripada melemparkan pengecualian jika 50% pengguna saya gagal mengisi formulir dengan benar pada upaya pertama.

Jon Raynor
sumber
1

T: Apakah ada situasi yang sah di mana pengecualian tidak dilemparkan dan ditangkap, tetapi hanya dikembalikan dari metode dan kemudian diedarkan sebagai objek kesalahan?

Iya. Sebagai contoh, saya baru-baru ini menghadapi situasi situasi di mana saya menemukan bahwa melemparkan Pengecualian dalam ASMX Web Service tidak menyertakan elemen dalam pesan SOAP yang dihasilkan, jadi saya harus membuatnya.

Kode ilustrasi:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
Tom Tom
sumber
1

Dalam kebanyakan bahasa (afaik), pengecualian datang dengan beberapa barang tambahan. Sebagian besar, snapshot dari jejak stack saat ini disimpan di objek. Ini bagus untuk debugging, tetapi juga dapat memiliki implikasi memori.

Seperti yang sudah dikatakan @ThinkingMedia, Anda benar-benar menggunakan pengecualian sebagai objek kesalahan.

Dalam contoh kode Anda, sepertinya Anda melakukan ini terutama untuk penggunaan kembali kode dan untuk menghindari komposisi. Saya pribadi tidak berpikir ini adalah alasan yang bagus untuk melakukan ini.

Alasan lain yang mungkin adalah untuk menipu bahasa agar memberi Anda objek kesalahan dengan jejak tumpukan. Ini memberikan informasi kontekstual tambahan untuk kode penanganan kesalahan Anda.

Di sisi lain, kita dapat mengasumsikan bahwa menjaga jejak stack di sekitar memiliki biaya memori. Misalnya jika Anda mulai mengumpulkan objek-objek ini di suatu tempat, Anda mungkin melihat dampak memori yang tidak menyenangkan. Tentu saja ini sangat tergantung pada bagaimana jejak stack pengecualian diimplementasikan dalam bahasa / mesin masing-masing.

Jadi, apakah itu ide yang bagus?

Sejauh ini saya belum melihatnya digunakan atau direkomendasikan. Tetapi ini tidak berarti banyak.

Pertanyaannya adalah: Apakah jejak tumpukan benar-benar berguna untuk penanganan kesalahan Anda? Atau lebih umum, informasi apa yang ingin Anda berikan kepada pengembang atau administrator?

Pola lemparan / coba / tangkap yang khas membuatnya perlu untuk memiliki jejak tumpukan, karena jika tidak Anda tidak tahu dari mana asalnya. Untuk objek yang dikembalikan, selalu berasal dari fungsi yang disebut. Jejak tumpukan mungkin berisi semua informasi yang dibutuhkan pengembang, tetapi mungkin sesuatu yang kurang berat dan lebih spesifik sudah cukup.

donquixote
sumber
-4

Jawabannya iya. Lihat http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions dan buka panah untuk panduan gaya C ++ Google tentang pengecualian. Anda dapat melihat di sana argumen yang mereka gunakan untuk memutuskan, tetapi katakan bahwa jika mereka harus melakukannya lagi, mereka kemungkinan akan memutuskan untuk melakukannya.

Namun perlu dicatat bahwa bahasa Go juga tidak secara idiomatis menggunakan pengecualian, untuk alasan yang sama.

btilly
sumber
1
Maaf, tapi saya tidak mengerti bagaimana panduan ini membantu menjawab pertanyaan: Mereka hanya merekomendasikan bahwa penanganan pengecualian harus dihindari sepenuhnya. Ini bukan yang saya tanyakan; pertanyaan saya adalah apakah sah menggunakan jenis pengecualian untuk menggambarkan kondisi kesalahan dalam situasi di mana objek pengecualian yang dihasilkan tidak akan pernah dilempar. Tampaknya tidak ada yang seperti itu dalam pedoman ini.
stakx