Lempar pengecualian atau biarkan kode gagal

52

Saya bertanya-tanya apakah ada pro dan kontra terhadap gaya ini:

private void LoadMaterial(string name)
{
    if (_Materials.ContainsKey(name))
    {
        throw new ArgumentException("The material named " + name + " has already been loaded.");
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );
}

Metode itu, untuk masing-masing name, harus dijalankan hanya sekali. _Materials.Add()akan mengeluarkan pengecualian jika akan dipanggil beberapa kali untuk hal yang sama name. Apakah penjaga saya, sebagai akibatnya, sepenuhnya berlebihan, atau ada beberapa manfaat yang kurang jelas?

Itu C #, Unity, kalau ada yang tertarik.

async
sumber
3
Apa yang terjadi jika Anda memuat bahan dua kali tanpa pelindung? Apakah _Materials.Addmelempar pengecualian?
user253751
2
Sebenarnya saya telah menyebutkan dalam melempar pengecualian: P
async
9
Catatan tambahan: 1) Pertimbangkan untuk menggunakan string.Formatrangkaian string lebih untuk membangun pesan pengecualian. 2) hanya gunakan asjika Anda berharap para pemain gagal dan periksa hasilnya null. Jika Anda berharap untuk selalu mendapatkan Material, gunakan gips seperti (Material)Resources.Load(...). Pengecualian yang jelas lebih mudah untuk di-debug daripada pengecualian referensi nol yang terjadi kemudian.
CodesInChaos
3
Dalam kasus khusus ini, penelepon Anda juga dapat menemukan pendamping LoadMaterialIfNotLoadedatau ReloadMaterial(nama ini dapat menggunakan perbaikan) berguna.
jpmc26
1
Pada catatan lain: Pola ini ok untuk terkadang menambahkan item ke daftar. Namun jangan gunakan pola ini untuk mengisi seluruh daftar karena Anda akan mengalami situasi O (n ^ 2) di mana dengan daftar besar Anda akan mengalami masalah kinerja. Anda tidak akan melihatnya pada 100 entri tetapi pada 1000 entri Anda tentu dan pada 10.000 entri itu akan menjadi hambatan utama.
Pieter B

Jawaban:

109

Manfaatnya adalah pengecualian "khusus" Anda memiliki pesan kesalahan yang bermakna bagi siapa saja yang memanggil fungsi ini tanpa mengetahui bagaimana penerapannya (yang di masa depan mungkin Anda!).

Memang, dalam hal ini mereka mungkin bisa menebak apa arti "standar", tetapi Anda masih memperjelas bahwa mereka melanggar kontrak Anda, daripada menemukan bug aneh dalam kode Anda.

Ixrec
sumber
3
Setuju. Hampir selalu lebih baik untuk melemparkan pengecualian untuk pelanggaran prasyarat.
Frank Hileman
22
"Manfaatnya adalah pengecualian" kebiasaan "Anda memiliki pesan kesalahan yang berarti bagi siapa saja yang memanggil fungsi ini tanpa mengetahui bagaimana itu diterapkan (yang di masa depan mungkin Anda!)." Saran atas.
async
2
"Eksplisit lebih baik daripada implisit." - Zen of Python
jpmc26
4
Bahkan jika kesalahan yang dilemparkan _Materials.Addbukan kesalahan yang ingin Anda kirimkan ke pemanggil, saya pikir metode relabelling kesalahan ini tidak efisien. Untuk setiap sukses panggilan LoadMaterial(operasi normal) Anda akan melakukan tes kewarasan yang sama dua kali , di LoadMaterialdan kemudian lagi pada _Materials.Add. Jika lebih banyak lapisan dibungkus, masing-masing menggunakan gaya yang sama, Anda bahkan dapat memiliki lebih banyak kali tes yang sama.
Marc van Leeuwen
15
... Alih-alih mempertimbangkan untuk terjun _Materials.Addtanpa syarat, kemudian menangkap kesalahan potensial dan di pawang melemparkan yang berbeda. Pekerjaan tambahan sekarang hanya dilakukan dalam kasus-kasus yang keliru, jalur eksekusi yang luar biasa di mana Anda tidak benar-benar khawatir tentang efisiensi sama sekali.
Marc van Leeuwen
100

Saya setuju dengan jawaban Ixrec . Namun, Anda mungkin ingin mempertimbangkan alternatif ketiga: menjadikan fungsi idempoten. Dengan kata lain, kembali lebih awal daripada melempar ArgumentException. Ini sering lebih disukai jika Anda akan dipaksa untuk memeriksa apakah sudah dimuat sebelum menelepon LoadMaterialsetiap waktu. Semakin sedikit prasyarat yang Anda miliki, semakin sedikit beban kognitif pada programmer.

Melempar pengecualian akan lebih baik jika itu benar-benar kesalahan programmer, di mana itu harus jelas dan diketahui pada waktu kompilasi jika materi sudah dimuat, tanpa harus memeriksa saat runtime sebelum memanggil.

Karl Bielefeldt
sumber
2
Di sisi lain, kegagalan fungsi secara diam-diam dapat menyebabkan perilaku yang tidak terduga sehingga lebih sulit untuk menemukan bug yang terjadi kemudian.
Philipp
30
@ Pilip fungsi tidak akan gagal diam-diam. Menjadi idempoten berarti menjalankannya berkali-kali memiliki efek yang sama dengan menjalankannya sekali. Dengan kata lain, jika material sudah dimuat, maka spec kami ("material harus dimuat") sudah puas, jadi kami tidak perlu melakukan apa pun. Kami hanya bisa kembali.
Warbo
8
Saya lebih suka ini, sebenarnya. Tentu saja, tergantung pada kendala yang diberikan oleh persyaratan aplikasi, ini mungkin salah. Kemudian lagi, saya benar-benar penggemar metode idempoten.
FP
6
@ Pilip jika kita berpikir tentang itu LoadMaterialmemiliki kendala sebagai berikut: "Setelah memanggilnya materi selalu dimuat dan jumlah materi yang dimuat tidak berkurang". Melontarkan pengecualian untuk kendala ketiga "Materi tidak dapat ditambahkan dua kali" tampaknya konyol dan berlawanan dengan intuisi saya. Membuat fungsi idempoten mengurangi ketergantungan kode pada urutan eksekusi dan status aplikasi. Sepertinya ini kemenangan ganda bagi saya.
Benjamin Gruenbaum
7
Saya suka ide ini, tetapi menyarankan satu perubahan kecil: mengembalikan boolean yang mencerminkan apakah sesuatu dimuat. Jika penelepon benar-benar peduli tentang logika aplikasi, mereka dapat memeriksanya dan melemparkan pengecualian.
user949300
8

Pertanyaan mendasar yang harus Anda tanyakan adalah: Apa yang Anda inginkan dari antarmuka untuk fungsi Anda? Secara khusus, apakah kondisi tersebut _Materials.ContainsKey(name)merupakan prasyarat dari fungsi tersebut?

Jika ini bukan prasyarat, fungsi harus memberikan hasil yang terdefinisi dengan baik untuk semua kemungkinan katup name. Dalam hal ini, pengecualian yang dilemparkan jika namebukan merupakan _Materialsbagian dari antarmuka fungsi. Itu berarti perlu menjadi bagian dari dokumentasi antarmuka dan jika Anda pernah memutuskan untuk mengubah pengecualian itu di masa depan, itu akan menjadi perubahan antarmuka yang melanggar .

Pertanyaan yang lebih menarik adalah apa yang terjadi jika itu merupakan prasyarat. Dalam hal ini prasyarat itu sendiri menjadi bagian dari antarmuka fungsi, tetapi cara suatu fungsi berperilaku ketika kondisi ini dilanggar belum tentu merupakan bagian dari antarmuka.

Dalam hal ini, pendekatan yang Anda posting, memeriksa pelanggaran prekondisi dan melaporkan kesalahan, adalah apa yang dikenal sebagai pemrograman defensif . Pemrograman defensif adalah baik karena memberi tahu pengguna lebih awal ketika mereka melakukan kesalahan dan memanggil fungsi dengan argumen palsu. Itu buruk karena secara signifikan meningkatkan beban pemeliharaan, karena kode pengguna mungkin bergantung bahwa fungsi menangani pelanggaran prasyarat dengan cara tertentu. Secara khusus, jika Anda menemukan bahwa pemeriksaan runtime pernah menjadi hambatan kinerja di masa mendatang (tidak mungkin untuk kasus sederhana seperti itu, tetapi sangat umum untuk prasyarat yang lebih kompleks), Anda mungkin tidak lagi dapat menghapusnya.

Ternyata kerugian itu bisa sangat signifikan, yang memberikan jenis pemrograman reputasi buruk di kalangan tertentu. Namun, tujuan awal masih valid: Kami ingin pengguna fungsi kami memperhatikan sejak awal ketika mereka melakukan kesalahan.

Oleh karena itu banyak pengembang saat ini mengusulkan pendekatan yang sedikit berbeda untuk masalah seperti ini. Alih-alih melempar pengecualian, mereka menggunakan mekanisme seperti-tegas untuk memeriksa prasyarat. Artinya, prasyarat dapat diperiksa dalam debug build untuk membantu pengguna dalam menangkap kesalahan sejak awal, tetapi mereka bukan bagian dari antarmuka fungsi. Perbedaannya mungkin tampak halus pada pandangan pertama, tetapi bisa membuat perbedaan besar dalam praktik.

Secara teknis, memanggil fungsi dengan prasyarat yang dilanggar adalah perilaku yang tidak terdefinisi. Tetapi implementasinya mungkin memutuskan untuk mendeteksi kasus-kasus itu dan segera memberi tahu pengguna jika itu terjadi. Sayangnya, pengecualian bukan alat yang baik untuk mengimplementasikan ini, karena kode pengguna dapat bereaksi terhadapnya dan dengan demikian mulai bergantung pada keberadaan mereka.

Untuk penjelasan terperinci tentang masalah dengan pendekatan defensif klasik, serta kemungkinan implementasi untuk pemeriksaan prasyarat gaya tegas, lihat ceramah John Lakos ' Programming Defensive Done Right from CppCon 2014 ( Slides , Video ).

KomikSMS
sumber
4

Sudah ada beberapa jawaban di sini, tetapi inilah jawaban yang memperhitungkan Unity3D (jawabannya sangat spesifik untuk Unity3D, saya akan melakukan sebagian besar ini sangat berbeda dalam sebagian besar konteks):

Secara umum, Unity3D tidak menggunakan pengecualian secara tradisional. Jika Anda melemparkan pengecualian di Unity3D, itu tidak seperti di aplikasi .NET rata-rata Anda, yaitu tidak akan menghentikan program, t sebagian besar Anda dapat mengkonfigurasi editor untuk berhenti. Itu hanya akan login. Ini dapat dengan mudah membuat game dalam keadaan tidak valid dan membuat efek kaskade yang membuat kesalahan sulit dilacak. Jadi saya akan mengatakan dalam kasus Unity, membiarkan Addmelemparkan pengecualian adalah opsi yang sangat tidak diinginkan.

Tetapi pada pemeriksaan kecepatan pengecualian bukanlah kasus optimasi prematur dalam beberapa kasus karena cara kerja Mono di Unity pada beberapa platform. Faktanya, Unity3D di iOS mendukung beberapa optimasi skrip tingkat lanjut, dan pengecualian yang dinonaktifkan * adalah efek samping dari salah satunya. Ini benar-benar sesuatu yang perlu dipertimbangkan karena optimasi itu terbukti sangat berharga bagi banyak pengguna, menunjukkan kasus yang realistis untuk mempertimbangkan membatasi penggunaan pengecualian di Unity3D. (* pengecualian terkelola dari mesin, bukan kode Anda)

Saya akan mengatakan bahwa di Unity Anda mungkin ingin mengambil pendekatan yang lebih khusus. Ironisnya, jawaban yang sangat rendah pada saat penulisan ini , menunjukkan satu cara saya dapat menerapkan sesuatu seperti ini secara khusus dalam konteks Unity3D (di tempat lain sesuatu seperti ini benar-benar tidak dapat diterima, dan bahkan dalam Unity itu cukup tidak elegan).

Pendekatan lain yang saya pertimbangkan sebenarnya tidak menunjukkan kesalahan sejauh pemanggil yang bersangkutan, melainkan menggunakan Debug.LogXXfungsi. Dengan begitu Anda mendapatkan perilaku yang sama seperti melempar pengecualian yang tidak ditangani (karena cara Unity3D menangani mereka) tanpa risiko menempatkan sesuatu dalam keadaan aneh di telepon. Juga pertimbangkan apakah ini benar-benar kesalahan (Sedang mencoba memuat materi yang sama dua kali berarti kesalahan dalam kasus Anda? Atau mungkin ini kasus Debug.LogWarningyang lebih dapat diterapkan).

Dan sehubungan dengan penggunaan hal-hal seperti Debug.LogXXfungsi alih-alih pengecualian, Anda masih harus mempertimbangkan apa yang terjadi ketika pengecualian akan dilemparkan dari sesuatu yang mengembalikan nilai (seperti GetMaterial). Saya cenderung untuk mendekati ini dengan melewatkan nol bersamaan dengan mencatat kesalahan ( sekali lagi, hanya di Unity ). Saya kemudian menggunakan cek nol di MonoBehaviors saya untuk memastikan setiap dependensi seperti materi bukan nilai nol, dan menonaktifkan MonoBehavior jika itu. Contoh untuk perilaku sederhana yang memerlukan beberapa dependensi adalah sesuatu seperti ini:

    public void Awake()
    {
        _inputParameters = GetComponent<VehicleInputParameters>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbodyTransform = _rigidbody.transform;
        _raycastStrategySelector = GetComponent<RaycastStrategySelectionBehavior>();

        _trackParameters =
            SceneManager.InstanceOf.CurrentSceneData.GetValue<TrackParameters>();

        this.DisableIfNull(() => _rigidbody);
        this.DisableIfNull(() => _raycastStrategySelector);
        this.DisableIfNull(() => _inputParameters);
        this.DisableIfNull(() => _trackParameters);
    }

SceneData.GetValue<>mirip dengan contoh Anda dalam hal ini memanggil fungsi pada kamus yang melempar pengecualian. Tapi alih-alih melempar pengecualian yang digunakannya Debug.LogErroryang memberikan jejak stack seperti pengecualian normal dan mengembalikan null. Pemeriksaan yang mengikuti * akan menonaktifkan perilaku alih-alih membiarkannya terus ada dalam keadaan tidak valid.

* cek terlihat seperti itu karena pembantu kecil yang saya gunakan yang mencetak pesan yang diformat ketika menonaktifkan objek game **. Cek nol sederhana dengan ifkerja di sini (** cek pembantu hanya dikompilasi di build Debug (seperti yang ditegaskan). Menggunakan lambdas dan ekspresi seperti itu di Unity dapat mengurangi kinerja)

Selali Adobor
sumber
1
+1 untuk menambahkan beberapa informasi yang benar-benar baru ke tumpukan. Saya tidak mengharapkan itu.
Ixrec
3

Saya suka dua jawaban utama, tetapi ingin menyarankan agar nama fungsi Anda dapat ditingkatkan. Saya sudah terbiasa dengan Java, jadi YMMV, tetapi metode "add" seharusnya, IMO, tidak membuang Pengecualian jika item sudah ada di sana. Seharusnya menambahkan item lagi , atau, jika tujuannya adalah Set, jangan lakukan apa pun. Karena itu bukan perilaku Materials.Add, yang harus diganti nama TryPut atau AddOnce atau AddOrThrow atau serupa.

Demikian juga, LoadMaterial Anda harus diganti nama menjadi LoadIfAbsent atau Put atau TryLoad atau LoadOrThrow (tergantung pada apakah Anda menggunakan jawaban # 1 atau # 2) atau semacamnya.

Ikuti konvensi penamaan C # Unity apa pun itu.

Ini akan sangat berguna jika Anda memiliki fungsi AddFoo dan LoadBar lain di mana diperbolehkan untuk memuat hal yang sama dua kali. Tanpa nama yang jelas pengembang akan frustrasi.

pengguna949300
sumber
Ada perbedaan antara kamus C # Kamus.Tambahkan () semantik (lihat msdn.microsoft.com/en-us/library/k7z0zy8k%28v=vs.110%29.aspx ) dan Java Collection.add () semantik (lihat dokumen. oracle.com/javase/8/docs/api/java/util/… ). C # mengembalikan batal dan melempar pengecualian. Sedangkan, Java mengembalikan bool dan menggantikan nilai yang sebelumnya disimpan dengan nilai baru atau melempar pengecualian ketika elemen baru tidak dapat ditambahkan.
Kasper van den Berg
Kasper - terima kasih, dan menarik. Bagaimana Anda mengganti nilai dalam Kamus C #? (Setara dengan Java put ()). Jangan melihat metode bawaan pada halaman itu.
user949300
pertanyaan yang bagus, orang bisa melakukan Dictionary.Remove () (lihat msdn.microsoft.com/en-us/library/bb356469%28v=vs.110%29.aspx ) diikuti oleh Dictionary.Add () (lihat msdn.microsoft .com / en-us / library / bb338565% 28v = vs.110% 29.aspx ), tetapi ini tampak agak canggung.
Kasper van den Berg
@ user949300 dictionary [key] = value menggantikan entri jika sudah ada
Rupe
@ Perbaiki dan mungkin melempar sesuatu jika tidak. Semua tampak sangat aneh bagi saya. Kebanyakan klien mendirikan dict tidak peduli wheteher entri itu ada, mereka hanya peduli bahwa, setelah kode setup mereka, itu adalah ada. Skor satu untuk Java Collections API (IMHO). PHP juga canggung dengan dikts, itu adalah kesalahan untuk C # untuk mengikuti preseden itu.
user949300
3

Semua jawaban menambah ide berharga, saya ingin menggabungkannya:

Putuskan semantik yang dimaksudkan dan yang diharapkan dari LoadMaterial()operasi. Setidaknya ada opsi berikut:

  • Prasyarat pada nameMemuat Bahan : →

    Ketika prasyarat dilanggar, efeknya LoadMaterial()tidak ditentukan (seperti dijawab oleh ComicSansMS ). Hal ini memungkinkan sebagian besar kebebasan dalam implementasi dan perubahan di masa depan LoadMaterial(). Atau,

  • efek pemanggilan LoadMaterial(name)dengan nameLoadedMaterial ditentukan; antara:

    • spesifikasi menyatakan bahwa pengecualian dilemparkan; atau
    • spesifikasi menyatakan bahwa hasilnya idempoten (seperti dalam jawaban oleh Karl Bielefeldt ),

Ketika telah memutuskan semantik, Anda harus memilih implementasi. Opsi dan pertimbangan berikut saat diusulkan:

  • Lempar pengecualian khusus (seperti yang disarankan oleh Ixrec ) →

    Manfaatnya adalah pengecualian "kustom" Anda memiliki pesan kesalahan yang bermakna bagi siapa saja yang memanggil fungsi ini - Ixrec dan User16547

    • Untuk menghindari biaya berulang kali memeriksa nameMaterial yang Dimuat, Anda dapat mengikuti saran Marc van Leeuwen :

      ... Alih-alih mempertimbangkan untuk terjun ke _Materials.Tambahkan tanpa syarat, kemudian tangkap kesalahan potensial dan di pawang lempar yang berbeda.

  • Biarkan Dictionay. Tambahkan pengecualian →

    Kode melempar pengecualian berlebihan. - Jon Raynor

    Meskipun, sebagian besar pemilih lebih setuju dengan Ixrec

    Alasan tambahan untuk memilih implementasi ini adalah:

    • bahwa penelepon sudah dapat menangani ArgumentExceptionpengecualian, dan
    • untuk menghindari kehilangan informasi tumpukan.

    Tetapi jika kedua alasan ini penting, Anda juga dapat menurunkan pengecualian khusus dari ArgumentExceptiondan menggunakan yang asli sebagai pengecualian berantai.

  • Jadikan LoadMaterial()idempotent sebagai anwser oleh Karl Bielefeldt dan terunggul paling sering (75 kali).

    Opsi implementasi untuk perilaku ini:

    • Periksa dengan Dictionary.ContainsKey ()

    • Selalu panggil Kamus.Tambahkan () tangkap yang ArgumentExceptiondilemparnya ketika kunci untuk memasukkan sudah ada dan abaikan pengecualian itu. Dokumentasikan bahwa mengabaikan pengecualian adalah apa yang ingin Anda lakukan dan mengapa.

      • Ketika LoadMaterials()(hampir) selalu dipanggil satu kali untuk masing-masing name, ini menghindari biaya berulang kali memeriksa nameLoadedMaterials cf. Marc van Leeuwen . Namun,
      • ketika LoadedMaterials()sering disebut berkali-kali untuk hal yang sama name, ini menimbulkan biaya (mahal) untuk melempar ArgumentExceptiondan menumpuk.
    • Saya berpikir bahwa ada TryAddanalog- metoda untuk TryGet () yang akan memungkinkan Anda untuk menghindari pengecualian mahal melempar dan menumpuk gulungan panggilan gagal ke Dictionary.Add.

      Tapi TryAddmetode ini tampaknya tidak ada.

Kasper van den Berg
sumber
1
+1 untuk menjadi jawaban paling komprehensif. Ini mungkin jauh melampaui apa yang awalnya OP, tapi ini ringkasan yang berguna dari semua opsi.
Ixrec
1

Kode melempar pengecualian berlebihan.

Jika Anda menelepon:

_Materials.Add (nama, Resources.Load (string.Format ("Material / {0}", name)) sebagai Material

dengan kunci yang sama ia akan melempar

System.ArgumentException

Pesannya adalah: "Item dengan kunci yang sama telah ditambahkan."

The ContainsKeycek berlebihan karena pada dasarnya perilaku yang sama dicapai tanpa itu. Satu-satunya item yang berbeda adalah pesan aktual dalam pengecualian. Jika benar ada manfaat dari memiliki pesan kesalahan khusus, maka kode penjaga memiliki beberapa kelebihan.

Kalau tidak, saya mungkin akan menolak penjaga dalam kasus ini.

Jon Raynor
sumber
0

Saya pikir ini lebih merupakan pertanyaan tentang desain API daripada pertanyaan konvensi pengkodean.

Apa hasil yang diharapkan (kontrak) dari panggilan:

LoadMaterial("wood");

Jika penelepon dapat mengharapkan / dijamin bahwa setelah memanggil metode ini, material "kayu" dimuat, maka saya tidak melihat alasan untuk melempar pengecualian ketika materi itu sudah dimuat.

Jika terjadi kesalahan saat memuat materi, misalnya, koneksi database tidak dapat dibuka, atau tidak ada material "kayu" di repositori, maka sudah benar untuk melemparkan pengecualian untuk memberi tahu penelepon masalah itu.

oɔɯǝɹ
sumber
-2

Mengapa tidak mengubah metode sehingga mengembalikan 'benar' jika materi ditambahkan dan 'salah' jika tidak?

private bool LoadMaterial(string name)
{
   if (_Materials.ContainsKey(name))

    {

        return false; //already present
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );

    return true;

}
MrIveck
sumber
15
Fungsi yang mengembalikan nilai kesalahan adalah persis anti-pola yang pengecualiannya dianggap usang.
Philipp
Apakah ini selalu masalahnya? Saya pikir itu hanya terjadi dengan semipredicate ( en.wikipedia.org/wiki/Semipredicate_problem ) Karena dalam sampel kode OP, gagal menambahkan bahan ke sistem tidak ada masalah nyata. Metode ini bertujuan untuk memastikan bahwa materi ada di sistem ketika Anda menjalankannya, dan itulah yang dilakukan metode ini. (Bahkan jika gagal) Boolean yang kembali hanya menunjukkan apakah metode ini sudah mencoba menambahkan materi ini atau tidak. ps: Saya tidak mempertimbangkan bahwa mungkin ada banyak materi dengan nama yang sama.
MrIveck
1
Saya rasa saya menemukan solusinya sendiri: en.wikipedia.org/wiki/... Ini menunjukkan bahwa jawaban Ixrec adalah yang paling benar
MrIveck
@ Pilip Dalam kasus ini koder / spec telah memutuskan bahwa tidak ada kesalahan ketika Anda mencoba untuk memuat dua kali. Nilai baliknya, dalam arti itu, bukan nilai kesalahan tetapi nilai informasional.
user949300