Kesalahan dalam menangani pertimbangan

31

Masalah:

Sejak lama, saya khawatir dengan exceptionsmekanismenya, karena saya merasa tidak benar-benar menyelesaikan apa yang seharusnya.

KLAIM: Ada perdebatan panjang di luar tentang topik ini, dan kebanyakan dari mereka kesulitan membandingkan dan exceptionsmengembalikan kode kesalahan. Ini jelas bukan topik di sini.

Mencoba mendefinisikan kesalahan, saya setuju dengan CppCoreGuidelines, dari Bjarne Stroustrup & Herb Sutter

Kesalahan berarti bahwa fungsi tidak dapat mencapai tujuan yang diiklankan

KLAIM: exceptionMekanisme ini adalah semantik bahasa untuk menangani kesalahan.

KLAIM: Bagi saya, ada "tidak ada alasan" untuk suatu fungsi untuk tidak mencapai tugas: Entah kita salah mendefinisikan kondisi sebelum / sesudah sehingga fungsi tidak dapat memastikan hasil, atau beberapa kasus luar biasa tertentu tidak dianggap cukup penting untuk menghabiskan waktu dalam mengembangkan sebuah solusi. Menimbang bahwa, IMO, perbedaan antara kode normal dan penanganan kode kesalahan adalah (sebelum implementasi) garis yang sangat subyektif.

KLAIM: Menggunakan pengecualian untuk menunjukkan ketika kondisi pra atau pasca tidak disimpan adalah tujuan lain dari exceptionmekanisme, terutama untuk tujuan debugging. Saya tidak menargetkan penggunaan ini di exceptionssini.

Dalam banyak buku, tutorial, dan sumber lain, mereka cenderung menunjukkan penanganan kesalahan sebagai ilmu yang cukup objektif, yang diselesaikan dengan exceptionsdan Anda hanya perlu catchmereka untuk memiliki perangkat lunak yang kuat, dapat pulih dari situasi apa pun. Tetapi beberapa tahun saya sebagai pengembang membuat saya melihat masalah dari pendekatan yang berbeda:

  • Pemrogram cenderung untuk menyederhanakan tugas mereka dengan melemparkan pengecualian ketika kasus spesifik tampaknya terlalu jarang untuk diimplementasikan dengan hati-hati. Kasus khas ini adalah: kehabisan masalah memori, masalah disk penuh, masalah file rusak, dll. Ini mungkin cukup, tetapi tidak selalu diputuskan dari tingkat arsitektur.
  • Pemrogram cenderung tidak membaca dokumentasi dengan hati-hati tentang pengecualian di perpustakaan, dan biasanya tidak mengetahui yang mana dan kapan suatu fungsi dilemparkan. Lebih jauh lagi, bahkan ketika mereka tahu, mereka tidak benar-benar mengelolanya.
  • Programmer cenderung tidak menangkap pengecualian cukup awal, dan ketika mereka melakukannya, sebagian besar untuk login dan melempar lebih jauh. (lihat poin pertama).

Ini memiliki dua konsekuensi:

  1. Kesalahan yang terjadi sering terdeteksi pada awal pengembangan dan debugged (yang bagus).
  2. Pengecualian langka tidak dikelola dan membuat sistem macet (dengan pesan log yang bagus) di rumah pengguna. Beberapa kali kesalahan dilaporkan, atau bahkan tidak.

Mempertimbangkan itu, IMO tujuan utama dari mekanisme kesalahan harus:

  1. Buat terlihat dalam kode di mana beberapa kasus tertentu tidak dikelola.
  2. Komunikasikan masalah runtime ke kode terkait (setidaknya penelepon) ketika situasi ini terjadi.
  3. Menyediakan mekanisme pemulihan

Kelemahan utama dari exceptionsemantik sebagai mekanisme penanganan kesalahan adalah IMO: mudah untuk melihat di mana a throwberada dalam kode sumber, tetapi sama sekali tidak jelas untuk mengetahui apakah fungsi tertentu dapat melempar dengan melihat pada deklarasi. Ini membawa semua masalah yang saya perkenalkan di atas.

Bahasa tidak menegakkan dan memeriksa kode kesalahan seketat yang dibuat untuk aspek lain dari bahasa (misalnya jenis variabel yang kuat)

Mencoba solusi

Untuk memperbaiki hal ini, saya mengembangkan sistem penanganan kesalahan yang sangat sederhana, yang mencoba menempatkan penanganan kesalahan pada tingkat kepentingan yang sama dengan kode normal.

Idenya adalah:

  • Setiap fungsi (yang relevan) menerima referensi ke objek yang successsangat ringan, dan dapat mengaturnya ke status kesalahan jika terjadi. Objek sangat ringan sampai kesalahan dengan teks disimpan.
  • Suatu fungsi didorong untuk melewati tugasnya jika objek yang disediakan sudah mengandung kesalahan.
  • Kesalahan tidak boleh ditimpa.

Desain lengkap jelas mempertimbangkan dengan seksama setiap aspek (sekitar 10 halaman), juga bagaimana menerapkannya pada OOP.

Contoh Successkelas:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Pemakaian:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

Saya menggunakan itu dalam banyak kode saya (sendiri) dan memaksa programmer (saya) untuk berpikir lebih jauh tentang kemungkinan kasus luar biasa dan bagaimana menyelesaikannya (baik). Namun, ia memiliki kurva belajar dan tidak terintegrasi dengan baik dengan kode yang sekarang menggunakannya.

Pertanyaan

Saya ingin lebih memahami implikasi penggunaan paradigma semacam itu dalam proyek:

  • Apakah premis untuk masalah itu benar? atau Apakah saya melewatkan sesuatu yang relevan?
  • Apakah solusinya ide arsitektur yang bagus? atau harganya terlalu tinggi?

EDIT:

Perbandingan antara metode:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
Adrian Maire
sumber
25
Terpilih untuk "Pertanyaan ini menunjukkan upaya penelitian; ini berguna dan jelas", bukan karena saya setuju: Saya pikir beberapa pemikiran salah arah. (Perincian dapat mengikuti dalam jawaban.)
Martin Ba
2
Tentu saja, saya mengerti dan menyetujui hal itu! Adalah tujuan dari pertanyaan ini untuk dikritik. Dan skor pertanyaan menunjukkan pertanyaan baik / buruk, bukan berarti OP benar.
Adrian Maire
2
Jika saya mengerti dengan benar, keluhan utama Anda tentang pengecualian adalah bahwa orang dapat mengabaikannya (dalam c ++) alih-alih menanganinya. Namun, konstruk Keberhasilan Anda memiliki cacat yang sama dengan desain. Seperti pengecualian, mereka hanya akan mengabaikannya. Lebih buruk lagi: lebih bertele-tele, mengarah pada pengembalian yang mengalir, dan Anda bahkan tidak bisa "menangkapnya" di hulu.
dagnelies
3
Mengapa tidak menggunakan sesuatu seperti Monad saja? Mereka membuat kesalahan Anda tersirat tetapi mereka tidak akan diam selama menjalankan. Sebenarnya, hal pertama yang saya pikirkan ketika melihat kode Anda adalah "monad, bagus". Lihatlah mereka.
bash0r
2
Alasan utama saya menyukai pengecualian adalah mereka memungkinkan Anda menangkap semua kesalahan tak terduga dari blok kode tertentu dan menanganinya secara konsisten. Ya, tidak ada alasan bagus mengapa kode tidak melakukan tugasnya - "ada bug" adalah alasan yang buruk tetapi masih terjadi , dan ketika itu terjadi, Anda ingin mencatat penyebabnya dan menampilkan pesan, atau coba lagi. (Saya memiliki beberapa kode yang melakukan interaksi yang kompleks dan dapat dimulai kembali dengan sistem jarak jauh; jika sistem jarak jauh turun, saya ingin mencatatnya dan mencoba lagi dari awal)
user253751

Jawaban:

32

Penanganan kesalahan mungkin merupakan bagian tersulit dari suatu program.

Secara umum, menyadari bahwa ada kondisi kesalahan itu mudah; namun mengisinya dengan cara yang tidak dapat dielakkan dan menanganinya dengan tepat (lihat tingkat Keamanan Pengecualian Abrahams ) sangat sulit.

Dalam C, kesalahan pensinyalan dilakukan oleh kode pengembalian, yang isomorfis untuk solusi Anda.

C ++ memperkenalkan pengecualian karena kekurangan pendekatan semacam itu; yaitu, itu hanya berfungsi jika penelepon ingat untuk memeriksa apakah kesalahan terjadi atau tidak dan gagal berpisah sebaliknya. Setiap kali Anda menemukan diri Anda berkata "Tidak apa-apa selama setiap kali ..." Anda memiliki masalah; manusia tidak begitu teliti, bahkan ketika mereka peduli.

Masalahnya, bagaimanapun, adalah bahwa pengecualian memiliki masalah mereka sendiri. Yaitu, aliran kontrol tak terlihat / tersembunyi. Ini dimaksudkan: menyembunyikan kasus kesalahan sehingga logika kode tidak dikaburkan oleh kesalahan penanganan boilerplate. Itu membuat "jalan bahagia" jauh lebih jelas (dan cepat!), Dengan biaya membuat jalur kesalahan hampir tidak bisa dipahami.


Saya merasa menarik untuk melihat bagaimana bahasa lain mendekati masalah ini:

  • Java telah memeriksa pengecualian (dan yang tidak dicentang),
  • Go menggunakan kode kesalahan / panik,
  • Karat menggunakan jumlah tipe / panik).
  • Bahasa FP secara umum.

C ++ dulu memiliki beberapa bentuk pengecualian diperiksa, Anda mungkin telah memperhatikan itu telah ditinggalkan dan disederhanakan menjadi dasar noexcept(<bool>)sebagai gantinya: salah satu fungsi dinyatakan mungkin melempar, atau itu dinyatakan tidak pernah. Pengecualian yang diperiksa agak bermasalah karena tidak dapat diperpanjang, yang dapat menyebabkan pemetaan canggung / bersarang. Dan hierarki pengecualian yang berbelit-belit (salah satu kasus penggunaan utama dari warisan virtual adalah pengecualian ...).

Sebaliknya, Go dan Rust mengambil pendekatan bahwa:

  • kesalahan harus ditandai di band,
  • pengecualian harus digunakan untuk situasi yang sangat luar biasa.

Yang terakhir ini agak jelas dalam hal (1) mereka menyebutkan kepanikan pengecualian mereka dan (2) tidak ada tipe hierarki / klausa rumit di sini. Bahasa tidak menawarkan fasilitas untuk memeriksa konten "panik": tidak ada hierarki tipe, tidak ada konten yang ditentukan pengguna, hanya "oops, ada yang salah, tidak ada pemulihan yang mungkin".

Ini secara efektif mendorong pengguna untuk menggunakan penanganan kesalahan yang tepat, sementara masih menyisakan cara mudah untuk menyelamatkan dalam situasi luar biasa (seperti: "tunggu, saya belum menerapkan itu!").

Tentu saja, pendekatan Go sangat mirip dengan Anda karena Anda dapat dengan mudah lupa memeriksa kesalahan ...

... Namun pendekatan Rust sebagian besar berpusat di sekitar dua jenis:

  • Option, yang mirip dengan std::optional,
  • Result, yang merupakan varian dua kemungkinan: Ok dan Err.

ini jauh lebih rapi karena tidak ada kesempatan untuk secara tidak sengaja menggunakan hasil tanpa memeriksa keberhasilan: jika Anda melakukannya, program akan panik.


Bahasa FP membentuk penanganan kesalahan mereka dalam konstruksi yang dapat dibagi menjadi tiga lapisan: - Functor - Aplikatif / Alternatif - Monad / Alternatif

Mari kita lihat Functortypeclass Haskell :

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Pertama-tama, typeclasses agak mirip tetapi tidak sama dengan antarmuka. Fungsi tanda tangan Haskell terlihat sedikit menakutkan pada tampilan pertama. Tapi mari kita menguraikannya. Fungsi ini fmapmengambil fungsi sebagai parameter pertama yang agak mirip std::function<a,b>. Hal selanjutnya adalah m a. Anda dapat membayangkan msebagai sesuatu seperti std::vectordan m asebagai sesuatu seperti std::vector<a>. Tetapi perbedaannya adalah, itu m atidak mengatakan itu harus secara eksplisit std:vector. Jadi bisa jadi std::optionjuga. Dengan memberi tahu bahasa bahwa kita memiliki instance untuk typeclass Functoruntuk tipe tertentu seperti std::vectoratau std::option, kita dapat menggunakan fungsi fmapuntuk tipe itu. Hal yang sama harus dilakukan untuk kacamata ketik Applicative, AlternativedanMonadyang memungkinkan Anda untuk melakukan perhitungan stateful, kemungkinan gagal. The Alternativeabstraksi pemulihan alat typeclass kesalahan. Dengan itu Anda bisa mengatakan sesuatu seperti atau istilah . Jika tak satu pun dari kedua perhitungan berhasil, itu masih kesalahan.a <|> b artinya adalah istilah yang baik ab

Mari kita lihat Haskell's Maybe tipe .

data Maybe a
  = Nothing
  | Just a

Ini berarti, bahwa di mana Anda mengharapkan Maybe a, Anda mendapatkan salah satu Nothingatau Just a. Saat melihatfmap dari atas, sebuah implementasi bisa terlihat seperti

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

The case ... ofekspresi disebut pencocokan pola dan menyerupai apa yang dikenal dalam dunia OOP sebagai visitor pattern. Bayangkan garis case m ofsebagai m.apply(...)dan titik-titik adalah instantiation dari kelas yang mengimplementasikan fungsi pengiriman. Garis-garis di bawah case ... ofekspresi adalah fungsi masing-masing pengiriman yang membawa bidang kelas langsung dalam lingkup dengan nama. Di Nothingcabang kami membuat Nothingdan di Just acabang kami beri nama nilai kami satu-satunya adan membuat yang lain Just ...dengan fungsi transformasif diterapkan a. Membacanya sebagai: new Just(f(a)).

Ini sekarang dapat menangani perhitungan yang salah sambil mengabstraksi pemeriksaan kesalahan yang sebenarnya. Ada implementasi untuk antarmuka lain yang membuat jenis komputasi ini sangat kuat. Sebenarnya, Maybeadalah inspirasi untuk RustOption -Type.


Saya akan mendorong Anda untuk memperbaiki Successkelas Anda sebagai Resultgantinya. Alexandrescu sebenarnya mengusulkan sesuatu yang sangat dekat, disebut expected<T>, yang merupakan standar proposal dibuat .

Saya akan tetap menggunakan penamaan Rust dan API hanya karena ... ini didokumentasikan dan berfungsi. Tentu saja, Rust memiliki ?operator akhiran yang bagus yang akan membuat kode lebih manis; di C ++, kami akan menggunakan ekspresi pernyataanTRY makro dan GCC untuk menirunya.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Catatan: ini Resultadalah placeholder. Implementasi yang tepat akan menggunakan enkapsulasi dan a union. Ini cukup untuk menyampaikan maksudnya.

Yang memungkinkan saya untuk menulis ( lihat beraksi ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

yang menurut saya sangat rapi:

  • tidak seperti penggunaan kode kesalahan (atau Successkelas Anda ), lupa memeriksa kesalahan akan menghasilkan kesalahan runtime 1 daripada beberapa perilaku acak,
  • tidak seperti penggunaan pengecualian, terlihat jelas di situs panggilan yang fungsinya bisa gagal sehingga tidak mengejutkan.
  • dengan C ++ - 2X standar, kita dapat masuk conceptsdalam standar. Ini akan membuat pemrograman jenis ini jauh lebih menyenangkan karena kita bisa meninggalkan pilihan daripada jenis kesalahan. Misalnya dengan implementasi std::vectorsebagai hasilnya, kita dapat menghitung semua solusi yang mungkin sekaligus. Atau kami dapat memilih untuk meningkatkan penanganan kesalahan, seperti yang Anda usulkan.

1 Dengan Resultimplementasi yang dienkapsulasi dengan benar ;)


Catatan: tidak seperti pengecualian, ringan Resultini tidak memiliki backtraces, yang membuat penebangan kurang efisien; Anda mungkin merasa berguna untuk setidaknya mencatat nomor file / baris di mana pesan kesalahan dihasilkan, dan untuk umumnya menulis pesan kesalahan yang kaya. Ini dapat diperparah dengan menangkap file / baris setiap kali TRYmakro digunakan, pada dasarnya membuat backtrace secara manual, atau menggunakan kode dan pustaka platform-spesifik seperti libbacktraceuntuk membuat daftar simbol di callstack.


Ada satu peringatan besar: perpustakaan C ++ yang ada, dan bahkan std, didasarkan pada pengecualian. Akan sulit untuk menggunakan gaya ini, karena API perpustakaan pihak ketiga mana pun harus dibungkus dengan adaptor ...

Matthieu M.
sumber
3
Makro itu terlihat ... sangat salah. Saya akan berasumsi ({...})adalah beberapa ekstensi gcc, tetapi meskipun demikian, bukankah seharusnya begitu if (!result.ok) return result;? Kondisi Anda muncul mundur dan Anda membuat salinan kesalahan yang tidak perlu.
Mooing Duck
@ MoingDuck Jawabannya menjelaskan bahwa itu ({...})adalah pernyataan pernyataan gcc .
jamesdlin
1
Saya akan merekomendasikan menggunakan std::variantuntuk mengimplementasikan Resultjika Anda menggunakan C ++ 17. Juga, untuk mendapatkan peringatan jika Anda mengabaikan kesalahan, gunakan[[nodiscard]]
Justin
2
@Justin: Apakah akan digunakan std::variantatau tidak adalah masalah selera, mengingat timbal balik seputar penanganan pengecualian. [[nodiscard]]memang merupakan kemenangan murni.
Matthieu M.
46

KLAIM: Mekanisme pengecualian adalah semantik bahasa untuk menangani kesalahan

pengecualian adalah mekanisme aliran kontrol. Motivasi untuk mekanisme kontrol-aliran ini, secara khusus memisahkan penanganan kesalahan dari kode penanganan non-kesalahan, dalam kasus umum bahwa penanganan kesalahan sangat berulang dan memiliki sedikit relevansi dengan bagian utama dari logika.

KLAIM: Bagi saya, ada "tidak ada alasan" untuk suatu fungsi untuk tidak mencapai tugas: Entah kita salah mendefinisikan kondisi sebelum / sesudah sehingga fungsi tidak dapat memastikan hasil, atau beberapa kasus luar biasa tertentu tidak dianggap cukup penting untuk menghabiskan waktu dalam mengembangkan sebuah solusi

Pertimbangkan: Saya mencoba membuat file. Perangkat penyimpanan penuh.

Sekarang, ini bukan kegagalan untuk mendefinisikan prasyarat saya: Anda tidak dapat menggunakan "harus ada penyimpanan yang cukup" sebagai prasyarat secara umum, karena penyimpanan bersama tunduk pada kondisi ras yang membuat ini tidak mungkin dipenuhi.

Jadi, haruskah program saya entah bagaimana mengosongkan beberapa ruang dan kemudian melanjutkan dengan sukses, kalau tidak saya terlalu malas untuk "mengembangkan solusi"? Ini sepertinya tidak masuk akal. "Solusi" untuk mengelola penyimpanan bersama adalah di luar ruang lingkup program saya , dan memungkinkan program saya gagal dengan anggun, dan dijalankan kembali begitu pengguna telah merilis beberapa ruang, atau menambahkan beberapa penyimpanan lagi, baik - baik saja .


Apa yang dilakukan kelas kesuksesan Anda adalah interleave penanganan kesalahan yang sangat eksplisit dengan logika program Anda. Setiap fungsi tunggal perlu memeriksa, sebelum menjalankan, apakah telah terjadi kesalahan yang berarti ia tidak boleh melakukan apa-apa. Setiap fungsi perpustakaan perlu dibungkus dengan fungsi lain, dengan satu argumen lagi (dan semoga penerusan sempurna), yang melakukan hal yang persis sama.

Perhatikan juga bahwa mySqrtfungsi Anda perlu mengembalikan nilai meskipun gagal (atau fungsi sebelumnya gagal). Jadi, Anda akan mengembalikan nilai ajaib (seperti NaN), atau menyuntikkan nilai tak tentu ke dalam program Anda dan berharap tidak ada yang menggunakan itu tanpa memeriksa status keberhasilan yang telah Anda utus melalui eksekusi Anda.

Untuk kebenaran - dan kinerja - jauh lebih baik untuk melepaskan kendali kembali dari ruang lingkup begitu Anda tidak dapat membuat kemajuan. Pengecualian dan pengecekan galat eksplisit gaya-C dengan pengembalian awal keduanya menghasilkan hal ini.


Sebagai perbandingan, contoh ide Anda yang benar-benar berfungsi adalah Error monad di Haskell. Keuntungan dari sistem Anda adalah bahwa Anda menulis sebagian besar logika Anda secara normal, dan kemudian membungkusnya dalam monad yang menangani penghentian evaluasi ketika satu langkah gagal. Dengan cara ini satu-satunya kode yang menyentuh sistem penanganan kesalahan secara langsung adalah kode yang mungkin gagal (melempar kesalahan) dan kode yang perlu mengatasi kegagalan (menangkap pengecualian).

Saya tidak yakin bahwa gaya monad dan evaluasi malas diterjemahkan dengan baik ke C ++.

Tak berguna
sumber
1
Berkat jawaban Anda, itu menambah cahaya ke topik. Saya kira pengguna akan tidak setuju dengan and allowing my program to fail gracefully, and be re-runketika ia baru saja kehilangan pekerjaan 2 jam:
Adrian Maire
14
Solusi Anda berarti bahwa setiap tempat Anda dapat membuat file, Anda harus meminta pengguna untuk memperbaiki situasi dan coba lagi. Maka setiap hal lain yang mungkin salah, Anda juga perlu memperbaiki secara lokal. Dengan pengecualian, Anda hanya menangkap std::exceptiondi tingkat yang lebih tinggi dari operasi logis, beri tahu pengguna "X gagal karena ex.what ()" , dan menawarkan untuk mencoba kembali seluruh operasi ketika dan jika mereka siap.
berguna
13
@AdrianMaire: "Memungkinkan untuk gagal dengan anggun dan dijalankan kembali" juga dapat diimplementasikan sebagai showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Itu adalah penanganan masalah yang anggun yang biasanya tidak dapat dilakukan dari kode yang mendeteksi bahwa lokasi penyimpanan pertama penuh.
Bart van Ingen Schenau
3
@Useless Malas evaluasi tidak ada hubungannya dengan penggunaan monad Kesalahan, sebagaimana dibuktikan oleh bahasa evaluasi ketat seperti Rust, OCaml, dan F # yang semuanya memanfaatkannya.
8bittree
1
@Useless IMO untuk perangkat lunak kualitas, itu tidak masuk akal bahwa “setiap tempat Anda dapat membuat file, Anda perlu meminta pengguna untuk memperbaiki situasi dan coba lagi”. Pemrogram awal sering berusaha keras untuk pemulihan kesalahan, setidaknya program TeX Knuth penuh dengan mereka. Dan dengan kerangka kerja "pemrograman melek" dia menemukan cara untuk menjaga penanganan kesalahan di bagian lain, sehingga kode tetap dapat dibaca dan pemulihan kesalahan ditulis dengan lebih hati-hati (karena ketika Anda sedang menulis bagian pemulihan kesalahan, itu intinya dan programmer cenderung melakukan pekerjaan yang lebih baik).
ShreevatsaR
15

Saya ingin lebih memahami implikasi penggunaan paradigma semacam itu dalam proyek:

  • Apakah premis untuk masalah itu benar? atau Apakah saya melewatkan sesuatu yang relevan?
  • Apakah solusinya ide arsitektur yang bagus? atau harganya terlalu tinggi?

Pendekatan Anda membawa beberapa masalah besar ke dalam kode sumber Anda:

  • itu bergantung pada kode klien selalu ingat untuk memeriksa nilai s. Ini biasa terjadi pada penggunaan kode pengembalian untuk pendekatan penanganan kesalahan , dan salah satu alasan bahwa pengecualian diperkenalkan ke dalam bahasa: dengan pengecualian, jika Anda gagal, Anda tidak gagal secara diam-diam.

  • semakin banyak kode yang Anda tulis dengan pendekatan ini, semakin banyak kode kesalahan yang harus Anda tambahkan juga, untuk penanganan kesalahan (kode Anda tidak lagi minimalis) dan upaya pemeliharaan Anda meningkat.

Tetapi beberapa tahun saya sebagai pengembang membuat saya melihat masalah dari pendekatan yang berbeda:

Solusi untuk masalah ini harus didekati di tingkat pemimpin teknis atau tingkat tim:

Programmer cenderung untuk menyederhanakan tugas mereka dengan melemparkan pengecualian ketika kasus spesifik tampaknya terlalu jarang untuk diimplementasikan dengan hati-hati. Kasus khas ini adalah: kehabisan masalah memori, masalah disk penuh, masalah file rusak, dll. Ini mungkin cukup, tetapi tidak selalu diputuskan dari tingkat arsitektur.

Jika Anda menemukan diri Anda menangani setiap jenis pengecualian yang dapat dilempar, sepanjang waktu, maka desainnya tidak bagus; Kesalahan apa yang ditangani, harus diputuskan sesuai dengan spesifikasi untuk proyek, bukan menurut apa yang ingin diimplementasikan oleh pengembang.

Alamat dengan menyiapkan pengujian otomatis, memisahkan spesifikasi pengujian unit dan implementasi (mintalah dua orang yang berbeda melakukan ini).

Pemrogram cenderung tidak membaca dokumentasi dengan seksama [...] Selain itu, bahkan ketika mereka tahu, mereka tidak benar-benar mengelolanya.

Anda tidak akan membahas ini dengan menulis lebih banyak kode. Saya pikir taruhan terbaik Anda adalah ulasan kode yang diterapkan dengan cermat.

Programmer cenderung tidak menangkap pengecualian cukup awal, dan ketika mereka melakukannya, sebagian besar untuk login dan melempar lebih jauh. (lihat poin pertama).

Penanganan kesalahan yang tepat sulit, tetapi kurang membosankan dengan pengecualian dibandingkan dengan nilai kembali (apakah mereka benar-benar dikembalikan atau diteruskan sebagai argumen i / o).

Bagian yang paling sulit dari penanganan kesalahan bukanlah bagaimana Anda menerima kesalahan, tetapi bagaimana memastikan aplikasi Anda tetap dalam keadaan konsisten di hadapan kesalahan.

Untuk mengatasinya, lebih banyak perhatian perlu dialokasikan untuk mengidentifikasi dan menjalankan dalam kondisi kesalahan (lebih banyak pengujian, lebih banyak unit / tes integrasi, dll).

utnapistim
sumber
12
Semua kode setelah kesalahan dilewati, jika Anda ingat untuk memeriksa masing-masing dan setiap kali Anda menerima contoh sebagai argumen . Inilah yang saya maksud dengan "semakin banyak kode yang Anda tulis dengan pendekatan ini, semakin banyak kode kesalahan yang harus Anda tambahkan juga". Anda harus memecahkan teka-teki kode Anda dengan contoh sukses, dan setiap kali Anda lupa, itu adalah bug. Masalah kedua yang disebabkan oleh lupa untuk memeriksa: kode yang dijalankan sampai Anda memeriksa lagi, seharusnya tidak dieksekusi sama sekali (melanjutkan jika Anda lupa memeriksa, merusak data Anda).
utnapistim
11
Tidak, menangani pengecualian (atau mengembalikan kode kesalahan) bukan merupakan gangguan - kecuali kesalahan / pengecualian secara logis fatal atau Anda memilih untuk tidak menanganinya. Anda masih memiliki kesempatan untuk menangani kasus kesalahan, tanpa harus secara eksplisit memeriksa di setiap langkah apakah kesalahan terjadi sebelumnya
berguna
11
@AdrianMaire Di hampir setiap aplikasi tempat saya bekerja, saya lebih suka crash daripada melanjutkan. Saya bekerja pada perangkat lunak bisnis-kritis di mana mengambil beberapa output buruk dan terus beroperasi dapat mengakibatkan banyak uang hilang. Jika kebenaran itu penting dan menerobos dapat diterima, maka pengecualian memiliki keuntungan yang sangat besar di sini.
Chris Hayes
1
@AdrianMaire - Saya pikir jauh lebih sulit untuk melupakan untuk menangani pengecualian bahwa metode Anda melupakan pernyataan if ... Selain itu - manfaat utama pengecualian adalah lapisan mana yang menanganinya. Anda mungkin ingin membiarkan pengecualian sistem naik lebih jauh untuk menampilkan pesan kesalahan tingkat aplikasi tetapi menangani situasi yang Anda ketahui di tingkat yang lebih rendah. Jika Anda menggunakan perpustakaan pihak ketiga atau kode pengembang lain, ini benar-benar satu-satunya pilihan ...
Milney
5
@Adrian Tidak salah, Anda sepertinya salah membaca apa yang saya tulis atau melewatkan bagian kedua dari itu. Seluruh poin saya bukanlah bahwa pengecualian akan dilemparkan selama pengujian / pengembangan dan bahwa pengembang akan menyadari bahwa mereka perlu menanganinya. Intinya adalah bahwa konsekuensi dari pengecualian yang sama sekali tidak tertangani dalam produksi lebih disukai daripada konsekuensi dari kode kesalahan yang tidak dicentang. jika Anda melewatkan kode kesalahan, Anda mendapatkan dan terus menggunakan hasil yang salah. Jika Anda melewatkan pengecualian, aplikasi macet dan tidak terus berjalan, Anda mendapatkan hasil tidak salah hasil . (lanjutan)
Mr.Mindor