Saya kadang-kadang akhirnya harus menulis metode atau properti untuk perpustakaan kelas yang tidak biasa tidak memiliki jawaban nyata, tetapi gagal. Sesuatu tidak dapat ditentukan, tidak tersedia, tidak ditemukan, saat ini tidak mungkin atau tidak ada lagi data yang tersedia.
Saya pikir ada tiga solusi yang mungkin untuk situasi yang relatif tidak biasa untuk menunjukkan kegagalan dalam C # 4:
- mengembalikan nilai ajaib yang tidak memiliki arti sebaliknya (seperti
null
dan-1
); - melempar pengecualian (misalnya
KeyNotFoundException
); - kembali
false
dan berikan nilai pengembalian aktual dalam suatuout
parameter, (sepertiDictionary<,>.TryGetValue
).
Jadi pertanyaannya adalah: di mana situasi luar biasa yang harus saya berikan pengecualian? Dan jika saya tidak harus melempar: kapan mengembalikan nilai ajaib yang ditentukan di atas menerapkan Try*
metode dengan out
parameter ? (Bagi saya out
parameternya tampak kotor, dan lebih sulit untuk menggunakannya dengan benar.)
Saya mencari jawaban faktual, seperti jawaban yang melibatkan pedoman desain (saya tidak tahu tentang Try*
metode), kegunaan (karena saya meminta perpustakaan kelas), konsistensi dengan BCL, dan keterbacaan.
Di perpustakaan .NET Framework Base Class Library, ketiga metode digunakan:
- mengembalikan nilai ajaib yang tidak memiliki arti sebaliknya:
Collection<T>.IndexOf
mengembalikan -1,StreamReader.Read
mengembalikan -1,Math.Sqrt
mengembalikan NaN,Hashtable.Item
mengembalikan nol;
- lempar pengecualian:
Dictionary<,>.Item
melempar KeyNotFoundException,Double.Parse
melempar FormatException; atau
- kembali
false
dan berikan nilai pengembalian aktual dalam suatuout
parameter:
Perhatikan bahwa seperti Hashtable
yang dibuat pada saat tidak ada obat generik di C #, ia menggunakan object
dan karenanya dapat kembali null
sebagai nilai ajaib. Tetapi dengan obat generik, pengecualian digunakan Dictionary<,>
, dan pada awalnya tidak ada TryGetValue
. Rupanya wawasan berubah.
Jelas, Item
- TryGetValue
dan Parse
- TryParse
dualitas ada karena suatu alasan, jadi saya berasumsi bahwa melemparkan pengecualian untuk kegagalan non-strategis di C # 4 tidak dilakukan . Namun, Try*
metode itu tidak selalu ada, bahkan ketika Dictionary<,>.Item
ada.
sumber
Jawaban:
Saya tidak berpikir bahwa contoh Anda benar-benar setara. Ada tiga kelompok berbeda, masing-masing dengan alasannya sendiri untuk perilakunya.
StreamReader.Read
atau ketika ada nilai sederhana untuk digunakan yang tidak akan pernah menjadi jawaban yang valid (-1 untukIndexOf
).Contoh yang Anda berikan sangat jelas untuk kasus 2 dan 3. Untuk nilai ajaib, dapat diperdebatkan apakah ini adalah keputusan desain yang baik atau tidak dalam semua kasus.
Yang
NaN
dikembalikan olehMath.Sqrt
adalah kasus khusus - mengikuti standar floating point.sumber
Either<TLeft, TRight>
monad?Anda mencoba untuk berkomunikasi dengan pengguna API tentang apa yang harus mereka lakukan. Jika Anda melemparkan pengecualian, tidak ada yang memaksa mereka untuk menangkapnya, dan hanya membaca dokumentasi yang akan membuat mereka tahu apa semua kemungkinannya. Secara pribadi saya merasa lambat dan membosankan untuk menggali ke dalam dokumentasi untuk menemukan semua pengecualian bahwa metode tertentu mungkin melempar (bahkan jika itu dalam intellisense, saya masih harus menyalinnya secara manual).
Nilai ajaib masih mengharuskan Anda untuk membaca dokumentasi, dan mungkin referensi beberapa
const
tabel untuk memecahkan kode nilainya. Setidaknya itu tidak memiliki overhead pengecualian untuk apa yang Anda sebut kejadian yang tidak biasa.Itulah sebabnya meskipun
out
kadang-kadang parameter disukai, saya lebih suka metode itu, denganTry...
sintaks. Ini kan .ical dan C # sintaks. Anda sedang berkomunikasi dengan pengguna API bahwa mereka harus memeriksa nilai kembali sebelum menggunakan hasilnya. Anda juga dapat memasukkanout
parameter kedua dengan pesan kesalahan yang bermanfaat, yang lagi-lagi membantu dengan debugging. Itu sebabnya saya memilih solusiTry...
denganout
parameter.Pilihan lain adalah mengembalikan objek "hasil" khusus, meskipun menurut saya ini lebih membosankan:
Kemudian fungsi Anda terlihat benar karena hanya memiliki parameter input dan hanya mengembalikan satu hal. Hanya saja satu hal yang dikembalikan adalah jenis tuple.
Bahkan, jika Anda menyukai hal-hal seperti itu, Anda dapat menggunakan
Tuple<>
kelas - kelas baru yang diperkenalkan di. NET 4. Secara pribadi saya tidak suka fakta bahwa makna setiap bidang kurang eksplisit karena saya tidak bisa memberikanItem1
danItem2
nama yang bermanfaat.sumber
IMyResult
penyebab Anda memungkinkan untuk mengkomunikasikan hasil yang lebih kompleks daripada hanyatrue
ataufalse
atau nilai hasil.Try*()
hanya berguna untuk hal-hal sederhana seperti string ke konversi int.Seperti yang ditunjukkan oleh contoh Anda, setiap kasing harus dievaluasi secara terpisah dan terdapat spektrum abu-abu yang cukup besar antara "keadaan luar biasa" dan "kontrol aliran", terutama jika metode Anda dimaksudkan untuk dapat digunakan kembali, dan dapat digunakan dalam pola yang sangat berbeda. daripada yang awalnya dirancang untuk. Jangan berharap kita semua di sini setuju tentang arti "tidak luar biasa", terutama jika Anda segera mendiskusikan kemungkinan menggunakan "pengecualian" untuk mengimplementasikannya.
Kami mungkin juga tidak menyetujui desain apa yang membuat kode paling mudah dibaca dan dipelihara, tetapi saya akan berasumsi bahwa perancang perpustakaan memiliki visi pribadi yang jelas tentang itu dan hanya perlu menyeimbangkannya dengan pertimbangan lain yang terlibat.
Jawaban singkat
Ikuti perasaan Anda kecuali ketika Anda merancang metode yang sangat cepat dan mengharapkan kemungkinan penggunaan kembali yang tidak terduga.
Jawaban panjang
Setiap penelepon masa depan dapat dengan bebas menerjemahkan antara kode kesalahan dan pengecualian sesuai keinginan di kedua arah; ini membuat dua pendekatan desain hampir setara kecuali untuk kinerja, keramahan debugger dan beberapa konteks interoperabilitas terbatas. Ini biasanya bermuara pada kinerja, jadi mari kita fokus pada hal itu.
Sebagai aturan praktis, berharap bahwa melempar pengecualian adalah 200x lebih lambat dari pengembalian reguler (pada kenyataannya, ada perbedaan yang signifikan dalam hal itu).
Sebagai aturan praktis lainnya, melempar pengecualian mungkin sering memungkinkan kode yang lebih bersih dibandingkan dengan nilai sihir yang paling kasar, karena Anda tidak bergantung pada pemrogram yang menerjemahkan kode kesalahan ke dalam kode kesalahan lain, karena ia berjalan melalui beberapa lapis kode klien menuju suatu titik di mana ada konteks yang cukup untuk menanganinya dengan cara yang konsisten dan memadai. (Kasus khusus:
null
cenderung lebih baik di sini daripada nilai sihir lainnya karena kecenderungannya untuk secara otomatis menerjemahkan dirinya keNullReferenceException
dalam kasus beberapa, tetapi tidak semua jenis cacat; biasanya, tetapi tidak selalu, cukup dekat dengan sumber cacat. )Jadi apa pelajarannya?
Untuk fungsi yang dipanggil hanya beberapa kali selama masa aplikasi (seperti inisialisasi aplikasi), gunakan apa pun yang memberi Anda kode yang lebih bersih dan mudah dipahami. Kinerja tidak bisa menjadi perhatian.
Untuk fungsi membuang, gunakan apa pun yang memberi Anda kode bersih. Kemudian lakukan beberapa profil (jika diperlukan sama sekali) dan ubah pengecualian untuk mengembalikan kode jika mereka termasuk di antara kemacetan teratas yang dicurigai berdasarkan pada pengukuran atau keseluruhan struktur program.
Untuk fungsi yang dapat digunakan kembali yang mahal, gunakan apa pun yang memberi Anda kode bersih. Jika pada dasarnya Anda selalu harus menjalani pulang-pergi jaringan atau mem-parsing file XML pada disk, biaya overhead untuk melempar pengecualian mungkin dapat diabaikan. Lebih penting untuk tidak kehilangan detail kegagalan apa pun, bahkan tidak sengaja, daripada kembali dari "kegagalan luar biasa" ekstra cepat.
Fungsi lean yang dapat digunakan kembali membutuhkan lebih banyak pemikiran. Dengan menggunakan pengecualian, Anda memaksa sesuatu seperti 100 kali melambat pada penelepon yang akan melihat pengecualian pada setengah dari (banyak) panggilan mereka, jika tubuh fungsi dijalankan dengan sangat cepat. Pengecualian masih merupakan opsi desain, tetapi Anda harus memberikan alternatif overhead rendah untuk penelepon yang tidak mampu membayar ini. Mari kita lihat sebuah contoh.
Anda mencantumkan contoh yang bagus
Dictionary<,>.Item
, yang, secara longgar, berubah dari mengembalikannull
nilai menjadi melemparKeyNotFoundException
antara. NET 1.1 dan .NET 2.0 (hanya jika Anda mau dianggapHashtable.Item
sebagai pelopor non-generik yang praktis). Alasan "perubahan" ini bukan tanpa minat di sini. Optimalisasi kinerja tipe nilai (tidak ada lagi tinju) menjadikan nilai sulap asli (null
) bukan pilihan;out
parameter hanya akan membawa sebagian kecil dari biaya kinerja kembali. Pertimbangan kinerja yang terakhir ini benar-benar dapat diabaikan dibandingkan dengan overhead dari melempar aKeyNotFoundException
, tetapi desain pengecualian masih lebih unggul di sini. Mengapa?Contains
sebelum panggilan apa pun ke pengindeks, dan pola ini dibaca secara alami. Jika pengembang ingin tetapi lupa meneleponContains
, tidak ada masalah kinerja yang dapat merambah;KeyNotFoundException
cukup keras untuk diperhatikan dan diperbaiki.sumber
Contains
sebelum panggilan ke pengindeks mungkin menyebabkan kondisi balapan yang tidak harus hadirTryGetValue
.ConcurrentDictionary
untuk akses multi-utas danDictionary
untuk akses utas tunggal untuk kinerja optimal. Artinya, tidak menggunakan `` Berisi` TIDAK membuat thread kode aman denganDictionary
kelas khusus ini .Anda seharusnya tidak membiarkan kegagalan.
Saya tahu, tangan-bergelombang dan idealis, tetapi dengarkan saya. Saat melakukan desain, ada beberapa kasus di mana Anda memiliki kesempatan untuk memilih versi yang tidak memiliki mode kegagalan. Alih-alih memiliki 'FindAll' yang gagal, LINQ menggunakan klausa mana yang hanya mengembalikan enumerable kosong. Alih-alih memiliki objek yang perlu diinisialisasi sebelum digunakan, biarkan konstruktor menginisialisasi objek (atau menginisialisasi ketika yang tidak diinisialisasi terdeteksi). Kuncinya adalah menghapus cabang kegagalan dalam kode konsumen. Ini masalahnya, jadi fokuslah.
Strategi lain untuk ini adalah
KeyNotFound
skenario. Di hampir setiap basis kode yang saya kerjakan sejak 3.0, sesuatu seperti metode ekstensi ini ada:Tidak ada mode kegagalan nyata untuk ini.
ConcurrentDictionary
memilikiGetOrAdd
built in yang serupa .Semua yang dikatakan, akan selalu ada saat di mana itu tidak dapat dihindari. Ketiganya memiliki tempat masing-masing, tetapi saya akan memilih opsi pertama. Terlepas dari semua yang dibuat dari bahaya nol, itu terkenal dan cocok dengan banyak 'item tidak ditemukan' atau 'hasil tidak berlaku' skenario yang membentuk set "tidak luar biasa kegagalan". Terutama ketika Anda membuat tipe nilai yang dapat dibatalkan, arti penting dari 'ini mungkin gagal' sangat eksplisit dalam kode dan sulit untuk dilupakan / dikacaukan.
Opsi kedua cukup baik ketika pengguna Anda melakukan sesuatu yang bodoh. Memberi Anda string dengan format yang salah, mencoba mengatur tanggal ke 42 Desember ... sesuatu yang Anda ingin hal-hal meledak dengan cepat dan spektakuler selama pengujian sehingga kode buruk diidentifikasi dan diperbaiki.
Pilihan terakhir adalah yang saya semakin tidak suka. Parameter keluar canggung, dan cenderung melanggar beberapa praktik terbaik saat membuat metode, seperti berfokus pada satu hal dan tidak memiliki efek samping. Plus, param luar biasanya hanya bermakna selama kesuksesan. Yang mengatakan, mereka sangat penting untuk operasi tertentu yang biasanya dibatasi oleh masalah konkurensi, atau pertimbangan kinerja (di mana Anda tidak ingin melakukan perjalanan kedua ke DB misalnya).
Jika nilai kembali dan param tidak non-sepele, maka saran Scott Whitlock tentang objek hasil lebih disukai (seperti
Match
kelas Regex ).sumber
out
parameter benar-benar ortogonal terhadap masalah efek samping. Mengubahref
parameter adalah efek samping, dan memodifikasi keadaan objek yang Anda lewati melalui parameter input adalah efek samping, tetapiout
parameter hanya cara canggung untuk membuat fungsi mengembalikan lebih dari satu nilai. Tidak ada efek samping, hanya beberapa nilai balik.Selalu lebih suka melempar pengecualian. Itu punya antarmuka yang seragam di antara semua fungsi yang bisa gagal, dan ini menunjukkan kegagalan yang berisik mungkin - properti yang sangat diinginkan.
Perhatikan itu
Parse
danTryParse
tidak benar-benar hal yang sama selain dari mode kegagalan. Fakta yangTryParse
juga bisa mengembalikan nilainya agak ortogonal, sungguh. Pertimbangkan situasi di mana, misalnya, Anda memvalidasi beberapa input. Anda sebenarnya tidak peduli berapa nilainya, asalkan nilainya. Dan tidak ada yang salah dengan menawarkan semacamIsValidFor(arguments)
fungsi. Tapi itu tidak pernah bisa menjadi utama modus operasi.sumber
Seperti yang telah dicatat orang lain, nilai ajaib (termasuk nilai pengembalian boolean) bukanlah solusi yang hebat, kecuali sebagai penanda "end-of-range". Alasan: Semantik tidak eksplisit, bahkan jika Anda memeriksa metode objek. Anda harus benar-benar membaca dokumentasi lengkap untuk seluruh objek hingga "oh yeah, jika mengembalikan -42 itu berarti bla bla bla".
Solusi ini dapat digunakan untuk alasan historis atau untuk alasan kinerja, tetapi sebaliknya harus dihindari.
Ini meninggalkan dua kasus umum: Probing atau pengecualian.
Di sini aturan praktisnya adalah bahwa program tidak boleh bereaksi terhadap pengecualian kecuali untuk menangani ketika program / tidak sengaja / melanggar beberapa kondisi. Probing harus digunakan untuk memastikan bahwa ini tidak terjadi. Oleh karena itu, pengecualian berarti bahwa penyelidikan yang relevan tidak dilakukan sebelumnya, atau bahwa sesuatu yang sama sekali tidak terduga telah terjadi.
Contoh:
Anda ingin membuat file dari jalur yang diberikan.
Anda harus menggunakan objek File untuk mengevaluasi terlebih dahulu apakah jalur ini sah untuk pembuatan atau penulisan file.
Jika program Anda entah bagaimana masih berakhir dengan mencoba menulis ke jalur yang ilegal atau tidak dapat ditulis, Anda harus mendapatkan kutipan. Ini bisa terjadi karena kondisi balapan (beberapa pengguna lain menghapus direktori, atau membuatnya hanya-baca, setelah Anda bermasalah)
Tugas menangani kegagalan yang tidak terduga (ditandai dengan pengecualian) dan memeriksa apakah kondisinya tepat untuk operasi sebelumnya (menyelidiki) biasanya akan disusun secara berbeda, dan karenanya harus menggunakan mekanisme yang berbeda.
sumber
Saya pikir
Try
polanya adalah pilihan terbaik, ketika kode hanya menunjukkan apa yang terjadi. Saya benci param dan suka objek nullable. Saya telah membuat kelas berikutjadi saya bisa menulis kode berikut
Sepertinya dapat dibatalkan tetapi tidak hanya untuk tipe nilai
Contoh lain
sumber
try
catch
akan mendatangkan malapetaka pada kinerja Anda jika Anda meneleponGetXElement()
dan gagal berkali-kali.public struct Nullable<T> where T : struct
perbedaan utama dalam kendala. btw versi terbaru ada di sini github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Jika Anda tertarik dengan rute "nilai ajaib", cara lain untuk menyelesaikannya adalah dengan membebani tujuan kelas Lazy. Meskipun Malas dimaksudkan untuk menunda instantiasi, tidak ada yang benar-benar mencegah Anda menggunakan seperti Maybe atau Option. Misalnya:
sumber