Pengecualian, kode kesalahan dan serikat yang didiskriminasi

80

Saya baru saja memulai pekerjaan pemrograman C #, tapi saya punya sedikit latar belakang di Haskell.

Tapi saya mengerti C # adalah bahasa berorientasi objek, saya tidak ingin memaksa pasak bundar ke dalam lubang persegi.

Saya membaca artikel Melempar Pengecualian dari Microsoft yang menyatakan:

JANGAN mengembalikan kode kesalahan.

Tetapi karena sudah terbiasa dengan Haskell, saya telah menggunakan tipe data C # OneOf, mengembalikan hasilnya sebagai nilai "benar" atau kesalahan (paling sering Pencacahan) sebagai nilai "kiri".

Ini seperti konvensi Eitherdi Haskell.

Bagi saya, ini tampaknya lebih aman daripada pengecualian. Di C #, mengabaikan pengecualian tidak menghasilkan kesalahan kompilasi, dan jika tidak ketahuan, mereka hanya menggembung dan merusak program Anda. Ini mungkin lebih baik daripada mengabaikan kode kesalahan dan menghasilkan perilaku yang tidak terdefinisi, tetapi menabrak perangkat lunak klien Anda masih bukan hal yang baik, terutama ketika melakukan banyak tugas bisnis penting lainnya di latar belakang.

Dengan OneOf, kita harus cukup eksplisit tentang membongkar dan menangani nilai kembali dan kode kesalahan. Dan jika seseorang tidak tahu bagaimana menanganinya pada tahap itu di tumpukan panggilan, itu harus dimasukkan ke dalam nilai balik dari fungsi saat ini, sehingga penelepon tahu kesalahan bisa terjadi.

Tapi ini sepertinya bukan pendekatan yang disarankan Microsoft.

Apakah menggunakan OneOfbukannya pengecualian untuk menangani pengecualian "biasa" (seperti File Tidak Ditemukan dll) merupakan pendekatan yang masuk akal atau apakah itu praktik yang mengerikan?


Perlu dicatat bahwa saya pernah mendengar bahwa pengecualian sebagai aliran kontrol dianggap sebagai antipattern yang serius , jadi jika "pengecualian" adalah sesuatu yang biasanya Anda tangani tanpa mengakhiri program, bukankah "aliran kendali" itu ada hubungannya? Saya mengerti ada sedikit area abu-abu di sini.

Catatan Saya tidak menggunakan OneOfuntuk hal-hal seperti "Kehabisan Memori", kondisi yang saya tidak harapkan untuk pulih masih akan melempar pengecualian. Tapi saya merasa masalah yang cukup masuk akal, seperti input pengguna yang tidak menguraikan pada dasarnya adalah "aliran kontrol" dan mungkin tidak boleh melempar pengecualian.


Pikiran selanjutnya:

Dari diskusi ini apa yang saya ambil saat ini adalah sebagai berikut:

  1. Jika Anda mengharapkan penelepon langsung ke catchdan menangani pengecualian sebagian besar waktu dan melanjutkan pekerjaannya, mungkin melalui jalur lain, itu mungkin harus menjadi bagian dari tipe pengembalian. Optionalatau OneOfbisa bermanfaat disini.
  2. Jika Anda mengharapkan penelepon segera tidak menangkap pengecualian sebagian besar waktu, melemparkan pengecualian, untuk menyimpan kekonyolan melewatkannya secara manual ke tumpukan.
  3. Jika Anda tidak yakin apa yang akan dilakukan penelepon langsung, mungkin berikan keduanya, seperti Parsedan TryParse.
Clinton
sumber
13
Gunakan F # Result;-)
Astrinus
8
Agak di luar topik, tetapi perhatikan bahwa pendekatan yang Anda lihat memiliki kerentanan umum bahwa tidak ada cara untuk memastikan pengembang menangani kesalahan dengan benar. Tentu saja, sistem pengetikan dapat bertindak sebagai pengingat, tetapi Anda kehilangan default untuk meledakkan program karena gagal menangani kesalahan, yang membuatnya lebih mungkin Anda berakhir dalam keadaan yang tidak terduga. Apakah pengingat itu sepadan dengan hilangnya standar itu adalah debat terbuka. Saya cenderung berpikir bahwa lebih baik menulis semua kode Anda dengan asumsi pengecualian akan menghentikannya di beberapa titik; maka pengingat memiliki nilai lebih kecil.
jpmc26
9
Artikel terkait: Eric Lippert pada empat "ember" pengecualian . Contoh yang dia berikan pada eksepsi eksogen menunjukkan ada skenario di mana Anda hanya perlu berurusan dengan pengecualian, yaitu FileNotFoundException.
Søren D. Ptæus
7
Saya mungkin sendirian dalam hal ini, tetapi saya pikir menabrak itu baik . Tidak ada bukti yang lebih baik bahwa ada masalah pada perangkat lunak Anda selain dari crash log dengan penelusuran yang tepat. Jika Anda memiliki tim yang dapat memperbaiki dan menyebarkan tambalan dalam waktu 30 menit atau kurang, membiarkan teman-teman jelek itu muncul mungkin bukan hal yang buruk.
T. Sar - Reinstate Monica
9
Kenapa tidak ada jawaban menjelaskan bahwa yang Eithermonad bukan kode kesalahan , dan juga bukan OneOf? Mereka pada dasarnya berbeda, dan pertanyaannya tampaknya didasarkan pada kesalahpahaman. (Meskipun dalam bentuk yang dimodifikasi itu masih pertanyaan yang valid.)
Konrad Rudolph

Jawaban:

131

tetapi menabrak perangkat lunak klien Anda masih bukan hal yang baik

Ini pastinya adalah hal yang baik.

Anda ingin apa pun yang membuat sistem dalam keadaan tidak terdefinisi untuk menghentikan sistem karena sistem yang tidak terdefinisi dapat melakukan hal-hal buruk seperti data yang korup, memformat hard drive, dan mengirim presiden mengancam email. Jika Anda tidak dapat memulihkan dan mengembalikan sistem ke kondisi yang ditentukan, maka menabrak adalah hal yang harus dilakukan. Itulah tepatnya mengapa kita membangun sistem yang crash daripada diam-diam memisahkan diri. Sekarang tentu saja, kita semua menginginkan sistem stabil yang tidak pernah crash tetapi kami hanya benar-benar menginginkannya ketika sistem tetap dalam kondisi aman yang dapat diprediksi yang telah ditentukan.

Saya pernah mendengar bahwa pengecualian sebagai aliran kontrol dianggap sebagai antipattern yang serius

Itu benar sekali tetapi sering disalahpahami. Ketika mereka menemukan sistem pengecualian, mereka takut melanggar pemrograman terstruktur. Pemrograman terstruktur adalah mengapa kita memiliki for, while, until, break, dan continueketika semua yang kita butuhkan, untuk melakukan semua itu, adalah goto.

Dijkstra mengajarkan kepada kita bahwa menggunakan goto secara informal (yaitu, melompat-lompat di mana pun Anda suka) membuat membaca kode menjadi mimpi buruk. Ketika mereka memberi kami sistem pengecualian, mereka takut menciptakan kembali kebagian. Jadi mereka mengatakan kepada kami untuk tidak "menggunakannya untuk kontrol aliran" berharap kami akan mengerti. Sayangnya, banyak dari kita tidak.

Anehnya, kita tidak sering menyalahgunakan pengecualian untuk membuat kode spageti seperti yang biasa kita lakukan dengan goto. Saran itu sendiri tampaknya telah menyebabkan lebih banyak masalah.

Pengecualian pada dasarnya adalah tentang menolak asumsi. Ketika Anda meminta agar suatu file disimpan, Anda menganggap bahwa file tersebut dapat dan akan disimpan. Pengecualian yang Anda dapatkan ketika itu tidak mungkin tentang nama yang ilegal, HD yang penuh, atau karena tikus telah menggerogoti kabel data Anda. Anda dapat menangani semua kesalahan itu secara berbeda, Anda dapat menangani semuanya dengan cara yang sama, atau Anda dapat membiarkannya menghentikan sistem. Ada jalur bahagia di kode Anda di mana asumsi Anda harus benar. Salah satu cara atau pengecualian lain membawa Anda keluar dari jalan bahagia itu. Sebenarnya, ya itu semacam "kontrol aliran" tapi bukan itu yang mereka peringatkan. Mereka berbicara tentang omong kosong seperti ini :

masukkan deskripsi gambar di sini

"Pengecualian harus luar biasa". Tautologi kecil ini lahir karena perancang sistem pengecualian membutuhkan waktu untuk membangun jejak tumpukan. Dibandingkan dengan melompat-lompat, ini lambat. Itu memakan waktu CPU. Tetapi jika Anda akan log dan menghentikan sistem atau setidaknya menghentikan pemrosesan intensif waktu saat ini sebelum memulai yang berikutnya maka Anda memiliki waktu untuk mematikan. Jika orang mulai menggunakan pengecualian "untuk kontrol aliran" asumsi-asumsi tentang waktu semua keluar jendela. Jadi "Pengecualian harus luar biasa" benar-benar diberikan kepada kami sebagai pertimbangan kinerja.

Jauh lebih penting daripada itu tidak membingungkan kita. Berapa lama waktu yang Anda butuhkan untuk menemukan infinite loop pada kode di atas?

JANGAN mengembalikan kode kesalahan.

... adalah saran yang bagus ketika Anda berada di basis kode yang biasanya tidak menggunakan kode kesalahan. Mengapa? Karena tidak ada yang akan mengingat untuk menyimpan nilai pengembalian dan memeriksa kode kesalahan Anda. Ini masih merupakan konvensi yang bagus ketika Anda berada di C.

OneOf

Anda menggunakan konvensi lain lagi. Tidak masalah selama Anda mengatur konvensi dan tidak hanya melawan yang lain. Sangat membingungkan memiliki dua konvensi kesalahan dalam basis kode yang sama. Jika entah bagaimana Anda sudah menyingkirkan semua kode yang menggunakan konvensi lain, silakan.

Saya suka konvensi itu sendiri. Salah satu penjelasan terbaiknya saya temukan di sini * :

masukkan deskripsi gambar di sini

Tetapi sebanyak yang saya suka saya masih tidak akan mencampurnya dengan konvensi lain. Pilih satu dan pertahankan. 1

1: Maksud saya jangan membuat saya memikirkan lebih dari satu kebaktian pada saat yang bersamaan.


Pikiran selanjutnya:

Dari diskusi ini apa yang saya ambil saat ini adalah sebagai berikut:

  1. Jika Anda mengharapkan penelepon segera menangkap dan menangani pengecualian sebagian besar waktu dan melanjutkan pekerjaannya, mungkin melalui jalur lain, itu mungkin harus menjadi bagian dari tipe pengembalian. Opsional atau OneOf dapat bermanfaat di sini.
  2. Jika Anda mengharapkan penelepon segera tidak menangkap pengecualian sebagian besar waktu, melemparkan pengecualian, untuk menyimpan kekonyolan melewatkannya secara manual ke tumpukan.
  3. Jika Anda tidak yakin apa yang akan dilakukan pemanggil langsung, mungkin berikan keduanya, seperti Parse dan TryParse.

Ini benar-benar tidak sesederhana ini. Salah satu hal mendasar yang perlu Anda pahami adalah apa itu nol.

Berapa hari tersisa di bulan Mei? 0 (karena ini bukan bulan Mei. Sudah bulan Juni).

Pengecualian adalah cara untuk menolak asumsi tetapi bukan satu-satunya cara. Jika Anda menggunakan pengecualian untuk menolak asumsi Anda meninggalkan jalan bahagia. Tetapi jika Anda memilih nilai untuk mengirim jalan bahagia yang menandakan bahwa hal-hal tidak sesederhana seperti yang diasumsikan maka Anda bisa tetap berada di jalur itu selama itu bisa berurusan dengan nilai-nilai itu. Terkadang 0 sudah digunakan untuk mengartikan sesuatu sehingga Anda harus menemukan nilai lain untuk memetakan asumsi penolakan Anda. Anda mungkin mengenali ide ini dari penggunaannya dalam aljabar lama yang bagus . Monads dapat membantu dengan itu tetapi tidak harus selalu menjadi monad.

Misalnya 2 :

IList<int> ParseAllTheInts(String s) { ... }

Bisakah Anda memikirkan alasan bagus mengapa ini harus dirancang sedemikian rupa sehingga dengan sengaja melemparkan apa pun? Tebak apa yang Anda dapatkan ketika int tidak bisa diuraikan? Aku bahkan tidak perlu memberitahumu.

Itu pertanda nama baik. Maaf tapi TryParse bukan ide saya tentang nama baik.

Kita sering menghindari melempar perkecualian untuk tidak mendapatkan apa-apa ketika jawabannya bisa lebih dari satu hal pada saat yang sama tetapi untuk beberapa alasan jika jawabannya adalah satu hal atau tidak sama sekali, kita terobsesi untuk bersikeras bahwa itu memberi kita satu hal atau lempar:

IList<Point> Intersection(Line a, Line b) { ... }

Apakah garis paralel benar-benar perlu menyebabkan pengecualian di sini? Benarkah seburuk itu jika daftar ini tidak akan mengandung lebih dari satu poin?

Mungkin secara semantik Anda tidak bisa menerimanya. Jika demikian, sangat disayangkan. Tapi Mungkin Monads, yang tidak memiliki ukuran sewenang-wenang seperti Listitu, akan membuat Anda merasa lebih baik tentang hal itu.

Maybe<Point> Intersection(Line a, Line b) { ... }

The Monads adalah koleksi tujuan khusus kecil yang dimaksudkan untuk digunakan dengan cara tertentu yang menghindari perlu mengujinya. Kita seharusnya menemukan cara untuk berurusan dengan mereka terlepas dari apa yang dikandungnya. Dengan begitu jalan yang bahagia tetap sederhana. Jika Anda membuka dan menguji setiap Monad yang Anda sentuh, Anda salah menggunakannya.

Saya tahu, ini aneh. Tetapi ini adalah alat baru (bagi kami). Jadi berikan waktu. Palu lebih masuk akal ketika Anda berhenti menggunakannya pada sekrup.


Jika Anda akan memanjakan saya, saya ingin membahas komentar ini:

Kenapa tidak ada jawaban yang menjelaskan bahwa monad bukan kode kesalahan, dan OneOf juga tidak? Mereka pada dasarnya berbeda, dan pertanyaannya tampaknya didasarkan pada kesalahpahaman. (Meskipun dalam bentuk yang dimodifikasi itu masih pertanyaan yang valid.) - Konrad Rudolph 4 Jun 18 pada 14:08

Ini mutlak benar. Monads jauh lebih dekat dengan koleksi daripada pengecualian, bendera, atau kode kesalahan. Mereka membuat wadah yang bagus untuk hal-hal seperti itu ketika digunakan dengan bijak.

candied_orange
sumber
9
Bukankah lebih tepat jika jalur merah berada di bawah kotak, sehingga tidak melewatinya?
Flater
4
Secara logis, Anda benar. Namun, setiap kotak dalam diagram khusus ini merepresentasikan operasi ikatan monadik. bind termasuk (mungkin menciptakan!) jalur merah. Saya sarankan menonton presentasi lengkap. Itu menarik dan Scott adalah pembicara yang hebat.
Gusdor
7
Saya memikirkan pengecualian lebih seperti instruksi COMEFROM daripada GOTO, karena throwtidak benar-benar tahu / mengatakan di mana kita akan melompat;)
Warbo
3
Tidak ada yang salah dengan konvensi pencampuran jika Anda dapat menetapkan batas-batas, yang merupakan inti dari enkapsulasi.
Robert Harvey
4
Seluruh bagian pertama dari jawaban ini dibaca dengan kuat seolah-olah Anda berhenti membaca pertanyaan setelah kalimat yang dikutip. Pertanyaannya mengakui bahwa crash program lebih unggul daripada beralih ke perilaku yang tidak terdefinisi: mereka bertentangan crash run-time tidak dengan eksekusi berkelanjutan, tidak terdefinisi, melainkan dengan kesalahan kompilasi waktu yang mencegah Anda dari bahkan membangun program tanpa mempertimbangkan potensi kesalahan dan menanganinya (yang mungkin berakhir menjadi crash jika tidak ada lagi yang bisa dilakukan, tetapi itu akan menjadi crash karena Anda menginginkannya, bukan karena Anda lupa).
KRyan
35

C # bukan Haskell, dan Anda harus mengikuti konsensus ahli dari komunitas C #. Jika Anda mencoba mengikuti praktik Haskell dalam proyek C #, Anda akan mengasingkan orang lain di tim Anda, dan pada akhirnya Anda mungkin akan menemukan alasan mengapa komunitas C # melakukan sesuatu secara berbeda. Satu alasan besar adalah bahwa C # tidak dengan mudah mendukung serikat yang didiskriminasi.

Saya pernah mendengar bahwa pengecualian sebagai aliran kontrol dianggap sebagai antipattern yang serius,

Ini bukan kebenaran yang diterima secara universal. Pilihan dalam setiap bahasa yang mendukung pengecualian adalah melempar pengecualian (yang tidak dapat ditangani oleh penelepon) atau mengembalikan nilai majemuk (yang harus ditangani oleh penelepon).

Menyebarkan kondisi kesalahan ke atas melalui tumpukan panggilan membutuhkan kondisi pada setiap tingkat, menggandakan kompleksitas siklomatik dari metode tersebut dan akibatnya menggandakan jumlah kasus uji unit. Dalam aplikasi bisnis yang umum, banyak pengecualian yang tidak dapat dipulihkan, dan dapat dibiarkan menyebar ke level tertinggi (misalnya, titik masuk layanan aplikasi web).

kevin cline
sumber
9
Menyebarkan monad tidak membutuhkan apa-apa.
Basilevs
23
@ Basilevs Ini membutuhkan dukungan untuk menyebarkan monad sambil menjaga agar kode dapat dibaca, yang tidak dimiliki oleh C #. Anda bisa mendapatkan instruksi if-return atau rantai lambdas.
Sebastian Redl
4
@SebastianRedl lambdas terlihat OK di C #.
Basilev
@ Basilevs Dari tampilan sintaks ya, tapi saya curiga dari sudut pandang kinerja mereka mungkin kurang menarik.
Pharap
5
Di C # modern Anda mungkin dapat menggunakan sebagian fungsi lokal untuk mendapatkan kecepatan kembali. Bagaimanapun, sebagian besar perangkat lunak bisnis melambat jauh lebih signifikan oleh IO daripada yang lain.
Turksarama
26

Anda datang ke C # pada waktu yang menarik. Sampai relatif baru-baru ini, bahasa tersebut berada di ruang pemrograman imperatif. Menggunakan pengecualian untuk mengkomunikasikan kesalahan jelas merupakan norma. Menggunakan kode kembali menderita karena kurangnya dukungan bahasa (misalnya di sekitar serikat terdiskriminasi dan pencocokan pola). Cukup wajar, panduan resmi dari Microsoft adalah untuk menghindari kode pengembalian dan menggunakan pengecualian.

Selalu ada pengecualian untuk ini, dalam bentuk TryXXXmetode, yang akan mengembalikan booleanhasil keberhasilan dan menyediakan hasil nilai kedua melalui outparameter. Ini sangat mirip dengan pola "coba" dalam pemrograman fungsional, kecuali bahwa hasilnya datang melalui parameter keluar, bukan melalui Maybe<T>nilai balik.

Tapi segalanya berubah. Pemrograman fungsional menjadi lebih populer dan bahasa seperti C # merespons. C # 7.0 memperkenalkan beberapa fitur dasar pencocokan pola ke bahasa; C # 8 akan memperkenalkan lebih banyak lagi, termasuk ekspresi switch, pola rekursif dll. Seiring dengan ini, telah ada pertumbuhan "perpustakaan fungsional" untuk C #, seperti perpustakaan Succinc <T> saya sendiri , yang menyediakan dukungan untuk serikat yang didiskriminasi juga .

Gabungan dua hal ini berarti bahwa kode seperti berikut ini perlahan-lahan semakin populer.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

Itu masih cukup ceruk saat ini, meskipun bahkan dalam dua tahun terakhir saya telah melihat perubahan yang ditandai dari permusuhan umum antara pengembang C # untuk kode seperti itu menjadi minat yang tumbuh dalam menggunakan teknik tersebut.

Kami belum ada di sana mengatakan " JANGAN mengembalikan kode kesalahan." kuno dan perlu pensiun. Tetapi pawai di jalan menuju tujuan itu sedang berlangsung. Jadi pilihlah: tetap dengan cara-cara lama melemparkan pengecualian dengan meninggalkan gay jika itu cara Anda suka melakukan sesuatu; atau mulai menjelajahi dunia baru di mana konvensi dari bahasa fungsional menjadi lebih populer di C #.

David Arno
sumber
22
Inilah sebabnya saya benci C # :) Mereka terus menambahkan cara untuk menguliti kucing, dan tentu saja cara lama tidak pernah hilang. Pada akhirnya tidak ada konvensi untuk apa pun, seorang programmer perfeksionis dapat duduk berjam-jam memutuskan metode mana yang "lebih baik," dan seorang programmer baru harus mempelajari segalanya sebelum dapat membaca kode orang lain.
Aleksandr Dubinsky
24
@AlexandrDubinsky, Anda menemukan masalah inti dengan bahasa pemrograman secara umum, bukan hanya C #. Bahasa baru diciptakan, ramping, segar dan penuh dengan ide-ide modern. Dan sangat sedikit orang yang menggunakannya. Seiring waktu, mereka mendapatkan pengguna dan fitur baru, melesat di atas fitur lama yang tidak dapat dihapus karena akan merusak kode yang ada. Gembung itu tumbuh. Jadi rakyat menciptakan bahasa baru yang ramping, segar dan penuh dengan ide-ide modern ...
David Arno
7
@AleksandrDubinsky jangan Anda benci ketika mereka menambahkan fitur sekarang dari tahun 1960-an.
ctrl-alt-delor
6
@AlexandrDubinsky Ya. Karena Java mencoba menambahkan sesuatu sekali (generik), mereka telah gagal, kita sekarang harus hidup dengan kekacauan mengerikan yang dihasilkan sehingga mereka telah mempelajari pelajaran dan berhenti menambahkan apa pun sama sekali
:)
3
@ Agg_L: Anda tahu Java ditambahkan java.util.Stream(mirip IEnumerable-setengah) dan lambda sekarang, kan? :)
cHao
5

Saya pikir itu sangat masuk akal untuk menggunakan DU untuk mengembalikan kesalahan, tapi kemudian saya akan mengatakan itu ketika saya menulis OneOf :) Tetapi saya tetap akan mengatakan itu, karena ini adalah praktik umum di F # dll (misalnya jenis Hasil, seperti yang disebutkan oleh orang lain ).

Saya tidak memikirkan Kesalahan yang biasanya saya kembalikan sebagai situasi luar biasa, tetapi sebagai normal, tetapi semoga jarang menghadapi situasi yang mungkin, atau mungkin tidak perlu ditangani, tetapi harus dimodelkan.

Inilah contoh dari halaman proyek.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Melontarkan pengecualian untuk kesalahan ini sepertinya berlebihan - mereka memiliki overhead kinerja, selain dari kerugian tidak eksplisit dalam tanda tangan metode, atau menyediakan pencocokan lengkap.

Sejauh mana OneOf idiomatis dalam c #, itu pertanyaan lain. Sintaksnya valid dan cukup intuitif. DU dan pemrograman berorientasi rel adalah konsep yang sudah dikenal luas.

Saya akan menyimpan Pengecualian untuk hal-hal yang mengindikasikan ada sesuatu yang rusak jika memungkinkan.

mcintyre321
sumber
Apa yang Anda katakan adalah OneOftentang membuat nilai pengembalian lebih kaya, seperti mengembalikan Nullable. Ini menggantikan pengecualian untuk situasi yang tidak biasa untuk memulai.
Aleksandr Dubinsky
Ya - dan menyediakan cara mudah melakukan pencocokan lengkap di pemanggil, yang tidak mungkin menggunakan hierarki Hasil berdasarkan warisan.
mcintyre321
4

OneOfkira-kira setara dengan pengecualian yang diperiksa di Jawa. Ada beberapa perbedaan:

  • OneOftidak bekerja dengan metode yang menghasilkan metode yang disebut efek sampingnya (karena dapat disebut secara bermakna dan hasilnya diabaikan). Jelas, kita semua harus mencoba menggunakan fungsi murni, tetapi ini tidak selalu mungkin.

  • OneOftidak memberikan informasi tentang di mana masalah terjadi. Ini bagus karena menghemat kinerja (pengecualian yang dilemparkan di Jawa mahal karena mengisi jejak tumpukan, dan dalam C # itu akan sama) dan memaksa Anda untuk memberikan informasi yang cukup di anggota kesalahan OneOfitu sendiri. Ini juga buruk karena informasi ini mungkin tidak mencukupi dan Anda dapat kesulitan menemukan asal masalahnya.

OneOf dan pengecualian yang diperiksa memiliki satu "fitur" penting yang sama:

  • keduanya memaksa Anda untuk menanganinya di mana saja di tumpukan panggilan

Ini mencegah rasa takut Anda

Di C #, mengabaikan pengecualian tidak menghasilkan kesalahan kompilasi, dan jika tidak ketahuan, mereka hanya menggembung dan merusak program Anda.

tetapi seperti yang sudah dikatakan, ketakutan ini tidak rasional. Pada dasarnya, biasanya ada satu tempat di program Anda, di mana Anda perlu menangkap semuanya (kemungkinan Anda sudah akan menggunakan kerangka kerja melakukannya). Karena Anda dan tim Anda biasanya menghabiskan waktu berminggu-minggu atau bertahun-tahun untuk mengerjakan program ini, Anda tidak akan melupakannya, bukan?

Keuntungan besar dari pengecualian yang dapat diabaikan adalah bahwa mereka dapat ditangani di mana pun Anda ingin menanganinya, daripada di mana-mana di tumpukan jejak. Ini menghilangkan banyak boilerplate (lihat saja beberapa kode Java yang menyatakan "melempar ...." atau membungkus pengecualian) dan itu juga membuat kode Anda kurang buggy: Karena metode apa pun mungkin melempar, Anda harus menyadarinya. Untungnya, tindakan yang benar biasanya tidak melakukan apa-apa , yaitu membiarkannya menggelembung ke tempat yang bisa ditangani secara wajar.

maaartinus
sumber
+1 tetapi mengapa OneOf tidak bekerja dengan efek samping? (Saya kira itu karena akan melewati cek jika Anda tidak menetapkan nilai kembali, tetapi karena saya tidak tahu OneOf, juga tidak banyak C # Saya tidak yakin - klarifikasi akan meningkatkan jawaban Anda.)
dcorking
1
Anda bisa mengembalikan info kesalahan dengan OneOf dengan membuat objek kesalahan yang dikembalikan berisi informasi yang berguna misalnya di OneOf<SomeSuccess, SomeErrorInfo>mana SomeErrorInfomemberikan rincian kesalahan.
mcintyre321
@dcorking Diedit. Tentu, jika Anda mengabaikan nilai pengembalian, maka Anda tidak tahu apa-apa tentang masalah apa yang terjadi. Jika Anda melakukan ini dengan fungsi murni, tidak apa-apa (Anda hanya membuang waktu).
maaartinus
2
@ mcintyre321 Tentu, Anda dapat menambahkan informasi, tetapi Anda harus melakukannya secara manual dan itu tunduk pada kemalasan dan lupa. Saya kira, dalam kasus yang lebih rumit, jejak tumpukan lebih bermanfaat.
maaartinus
4

Apakah menggunakan OneOf alih-alih pengecualian untuk menangani pengecualian "biasa" (seperti File Tidak Ditemukan dll) merupakan pendekatan yang masuk akal atau apakah itu praktik yang mengerikan?

Perlu dicatat bahwa saya pernah mendengar bahwa pengecualian sebagai aliran kontrol dianggap sebagai antipattern yang serius, jadi jika "pengecualian" adalah sesuatu yang biasanya Anda tangani tanpa mengakhiri program, bukankah itu "aliran kontrol" dengan cara apa pun?

Kesalahan memiliki sejumlah dimensi. Saya mengidentifikasi tiga dimensi: dapatkah mereka dicegah, seberapa sering mereka terjadi, dan apakah mereka dapat dipulihkan. Sementara itu, pensinyalan kesalahan terutama memiliki satu dimensi: memutuskan sejauh mana mengalahkan pemanggil di atas kepala untuk memaksa mereka menangani kesalahan, atau membiarkan kesalahan "diam-diam" meluap sebagai pengecualian.

Kesalahan yang tidak dapat dicegah, sering, dan dapat dipulihkan adalah kesalahan yang benar-benar perlu ditangani di situs panggilan. Mereka membenarkan memaksa penelepon untuk menghadapi mereka. Di Jawa, saya membuat mereka diperiksa pengecualian, dan efek yang sama dicapai dengan membuat mereka Try*metode atau kembali serikat diskriminasi. Serikat yang didiskriminasi sangat baik ketika suatu fungsi memiliki nilai balik. Mereka adalah versi pengembalian yang jauh lebih halus null. Sintaks try/catchblok tidak terlalu bagus (terlalu banyak tanda kurung), membuat alternatif terlihat lebih baik. Juga, pengecualian sedikit lambat karena mereka merekam jejak tumpukan.

Kesalahan yang dapat dicegah (yang tidak dicegah karena kesalahan / kelalaian programmer) dan kesalahan yang tidak dapat dipulihkan berfungsi dengan sangat baik sebagai pengecualian biasa. Kesalahan yang jarang terjadi juga berfungsi lebih baik sebagai pengecualian biasa karena programmer sering kali menimbangnya tidak sepadan dengan upaya untuk mengantisipasi (itu tergantung pada tujuan program).

Poin penting adalah seringnya situs yang menentukan bagaimana mode kesalahan suatu metode cocok dengan dimensi ini, yang harus diingat ketika mempertimbangkan hal-hal berikut.

Dalam pengalaman saya, memaksa penelepon untuk menghadapi kondisi kesalahan baik-baik saja ketika kesalahan tidak dapat dicegah, sering, dan dapat dipulihkan, tetapi menjadi sangat menjengkelkan ketika penelepon dipaksa untuk melompat melalui lingkaran ketika mereka tidak perlu atau ingin . Ini mungkin karena penelepon tahu kesalahan tidak akan terjadi, atau karena mereka (belum) ingin membuat kode tangguh. Di Jawa, ini menjelaskan kekhawatiran terhadap seringnya menggunakan pengecualian yang diperiksa dan mengembalikan Opsional (yang mirip dengan Mungkin dan baru-baru ini diperkenalkan di tengah banyak kontroversi). Jika ragu, lempar pengecualian dan biarkan penelepon memutuskan bagaimana mereka ingin menanganinya.

Terakhir, perlu diingat bahwa hal yang paling penting tentang kondisi kesalahan, jauh di atas pertimbangan lain seperti bagaimana mereka diberi sinyal, adalah untuk mendokumentasikannya secara menyeluruh .

Aleksandr Dubinsky
sumber
2

Jawaban lain telah membahas pengecualian vs kode kesalahan secara cukup rinci, jadi saya ingin menambahkan perspektif lain yang spesifik untuk pertanyaan:

OneOf bukan kode kesalahan, ini lebih seperti sebuah monad

Cara itu digunakan, OneOf<Value, Error1, Error2>itu adalah wadah yang mewakili hasil aktual atau beberapa kesalahan. Itu seperti sebuah Optional, kecuali bahwa ketika tidak ada nilai yang hadir, itu dapat memberikan rincian lebih lanjut mengapa itu ada.

Masalah utama dengan kode kesalahan adalah Anda lupa memeriksanya. Tapi di sini, Anda benar-benar tidak dapat mengakses hasilnya tanpa memeriksa kesalahan.

Satu-satunya masalah adalah ketika Anda tidak peduli dengan hasilnya. Contoh dari dokumentasi OneOf memberikan:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... dan kemudian mereka membuat respons HTTP berbeda untuk setiap hasil, yang jauh lebih baik daripada menggunakan pengecualian. Tetapi jika Anda memanggil metode seperti ini.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

maka Anda tidak memiliki masalah dan pengecualian akan menjadi pilihan yang lebih baik. Bandingkan Rail savevs save!.

Cephalopod
sumber
1

Satu hal yang perlu Anda pertimbangkan adalah bahwa kode dalam C # umumnya tidak murni. Dalam basis kode yang penuh dengan efek samping, dan dengan kompiler yang tidak peduli dengan apa yang Anda lakukan dengan nilai pengembalian beberapa fungsi, pendekatan Anda dapat menyebabkan Anda sangat sedih. Misalnya, jika Anda memiliki metode yang menghapus file, Anda ingin memastikan pemberitahuan aplikasi ketika metode gagal, meskipun tidak ada alasan untuk memeriksa kode kesalahan "di jalan bahagia". Ini adalah perbedaan besar dari bahasa fungsional yang mengharuskan Anda untuk secara eksplisit mengabaikan nilai pengembalian yang tidak Anda gunakan. Dalam C #, nilai kembali selalu diabaikan secara implisit (satu-satunya pengecualian adalah properti getter IIRC).

Alasan mengapa MSDN memberitahu Anda "untuk tidak mengembalikan kode kesalahan" adalah persis ini - tidak ada yang memaksa Anda untuk bahkan membaca kode kesalahan. Kompiler sama sekali tidak membantu Anda. Namun, jika fungsi Anda bebas efek samping, Anda dapat dengan aman menggunakan sesuatu seperti Either- intinya adalah bahwa bahkan jika Anda mengabaikan hasil kesalahan (jika ada), Anda hanya dapat melakukannya jika Anda tidak menggunakan "hasil yang tepat" antara. Ada beberapa cara bagaimana Anda bisa mencapai ini - misalnya, Anda mungkin hanya mengizinkan "membaca" nilai dengan melewati delegasi untuk menangani kasus-kasus keberhasilan dan kesalahan, dan Anda dapat dengan mudah menggabungkannya dengan pendekatan seperti Pemrograman Berorientasi Kereta Api (itu tidak bekerja dengan baik di C #).

int.TryParseada di suatu tempat di tengah. Itu murni. Ini mendefinisikan apa nilai dari dua hasil setiap saat - sehingga Anda tahu bahwa jika nilai kembali false, parameter output akan diatur ke 0. Itu masih tidak mencegah Anda menggunakan parameter output tanpa memeriksa nilai kembali, tetapi setidaknya Anda dijamin apa hasilnya meskipun fungsi gagal.

Tetapi satu hal yang sangat penting di sini adalah konsistensi . Kompiler tidak akan menyelamatkan Anda jika seseorang mengubah fungsi itu untuk memiliki efek samping. Atau untuk melempar pengecualian. Jadi, meskipun pendekatan ini baik-baik saja dalam C # (dan saya menggunakannya), Anda perlu memastikan bahwa semua orang di tim Anda memahami dan menggunakannya . Banyak invarian yang diperlukan untuk pemrograman fungsional agar berfungsi dengan baik tidak dipaksakan oleh kompiler C #, dan Anda perlu memastikan bahwa mereka diikuti sendiri. Anda harus tetap sadar akan hal-hal yang melanggar jika Anda tidak mengikuti aturan yang diterapkan Haskell secara default - dan ini adalah sesuatu yang Anda ingat untuk paradigma fungsional apa pun yang Anda perkenalkan ke kode Anda.

Luaan
sumber
0

Pernyataan yang benar adalah Jangan menggunakan kode kesalahan untuk pengecualian! Dan jangan gunakan pengecualian untuk non-pengecualian.

Jika sesuatu terjadi sehingga kode Anda tidak dapat dipulihkan (yaitu membuatnya berfungsi), itu merupakan pengecualian. Tidak ada kode langsung Anda akan lakukan dengan kode kesalahan kecuali menyerahkannya ke atas untuk login atau mungkin memberikan kode kepada pengguna dengan cara tertentu. Namun inilah tepatnya pengecualian - mereka berurusan dengan semua penyerahan dan membantu untuk menganalisis apa yang sebenarnya terjadi.

Namun, jika sesuatu terjadi, seperti input tidak valid yang dapat Anda tangani, baik dengan memformat ulang secara otomatis atau dengan meminta pengguna untuk memperbaiki data (dan Anda memikirkan hal itu), itu sebenarnya bukan pengecualian di tempat pertama - karena Anda harapkan itu. Anda dapat merasa bebas untuk menangani kasus-kasus tersebut dengan kode kesalahan atau arsitektur lainnya. Hanya saja tidak ada pengecualian.

(Perhatikan bahwa saya tidak terlalu berpengalaman dalam C #, tetapi jawaban saya harus berlaku dalam kasus umum berdasarkan tingkat konseptual dari apa pengecualian itu.)

Frank Hopkins
sumber
7
Saya pada dasarnya tidak setuju dengan premis jawaban ini. Jika perancang bahasa tidak bermaksud pengecualian untuk digunakan untuk kesalahan yang dapat dipulihkan, catchkata kunci tidak akan ada. Dan karena kita berbicara dalam konteks C #, perlu dicatat bahwa filosofi yang dianut di sini tidak diikuti oleh .NET framework; mungkin contoh paling sederhana yang bisa dibayangkan dari "input tidak valid yang dapat Anda tangani" adalah pengguna mengetikkan non-angka dalam bidang di mana angka diharapkan ... dan int.Parsemelempar pengecualian.
Mark Amery
@MarkAmery Untuk int.Parse bukan apa-apa yang bisa dipulihkan atau apa pun yang diharapkan. Klausa tangkapan biasanya digunakan untuk menghindari kegagalan total dari tampilan luar, itu tidak akan meminta pengguna untuk kredensial Database baru atau sejenisnya, itu akan menghindari program macet. Ini kegagalan teknis bukan alur kontrol aplikasi.
Frank Hopkins
3
Masalah apakah lebih baik bagi suatu fungsi untuk melemparkan pengecualian ketika suatu kondisi terjadi tergantung pada apakah pemanggil langsung fungsi dapat berguna menangani kondisi itu. Dalam kasus di mana ia bisa, melemparkan pengecualian yang harus ditangkap oleh penelepon langsung merupakan kerja ekstra untuk pembuat kode panggilan, dan kerja ekstra untuk mesin yang memprosesnya. Jika seorang penelepon tidak akan siap untuk menangani suatu kondisi, namun, pengecualian meringankan kebutuhan untuk memiliki penelepon secara eksplisit kode untuk itu.
supercat
@Darkwing "Anda dapat dengan bebas menangani kasus-kasus tersebut dengan kode kesalahan atau arsitektur lainnya". Ya, Anda bebas melakukan itu. Namun Anda tidak mendapatkan dukungan apa pun dari .NET untuk melakukannya, karena .NET CL sendiri menggunakan pengecualian alih-alih kode kesalahan. Jadi tidak hanya Anda dalam banyak kasus dipaksa untuk menangkap .NET pengecualian dan mengembalikan kode kesalahan yang sesuai alih-alih membiarkan pengecualian muncul, Anda juga memaksa orang lain di tim Anda ke konsep penanganan kesalahan lain yang harus mereka gunakan paralel dengan pengecualian, konsep penanganan kesalahan standar.
Aleksander
-2

Ini adalah 'Ide Mengerikan'.

Ini mengerikan karena, setidaknya di .net, Anda tidak memiliki daftar lengkap tentang kemungkinan pengecualian. Kode apa pun mungkin dapat membuang pengecualian apa pun. Terutama jika Anda melakukan OOP dan bisa memanggil metode yang diganti pada tipe sub daripada tipe yang dideklarasikan

Jadi Anda harus mengubah semua metode dan fungsi OneOf<Exception, ReturnType>, maka jika Anda ingin menangani berbagai jenis pengecualian Anda harus memeriksa jenisnya If(exception is FileNotFoundException)dll

try catch adalah setara dengan OneOf<Exception, ReturnType>

Sunting ----

Saya sadar Anda mengusulkan untuk kembali, OneOf<ErrorEnum, ReturnType>tetapi saya merasa ini adalah perbedaan tanpa perbedaan.

Anda menggabungkan kode pengembalian, pengecualian yang diharapkan, dan bercabang dengan pengecualian. Tetapi efek keseluruhannya sama.

Ewan
sumber
2
Pengecualian 'normal' juga merupakan ide yang buruk. petunjuknya adalah nama
Ewan
4
Saya tidak mengusulkan pengembalian Exception.
Clinton
apakah Anda mengusulkan bahwa alternatif untuk salah satunya adalah aliran kontrol melalui pengecualian
Ewan