BerandaComputers and TechnologyThe Error Model (2016)

The Error Model (2016)

Midori ditulis dalam kompilasi sebelumnya, aman jenis bahasa berdasarkan C # . Selain dari mikrokernel kami, keseluruhan sistem tertulis di dalamnya, termasuk driver, kernel domain, dan semua kode pengguna. Saya telah mengisyaratkan beberapa hal cara dan sekarang saatnya untuk mengatasinya secara langsung. Seluruh bahasa adalah ruang yang sangat besar untuk dibahas dan akan mengambil serangkaian dari postingan. Pertama? Model Kesalahan. Cara kesalahan dikomunikasikan dan ditangani sangat penting untuk bahasa apa pun, terutama yang digunakan untuk menulis sistem operasi yang andal. Seperti banyak hal lain yang kami lakukan di Midori, “seluruh sistem” pendekatan diperlukan untuk memperbaikinya, mengambil beberapa iterasi selama beberapa tahun. Saya sering mendengar kabar dari masa lalu Namun, rekan satu tim, bahwa ini adalah hal yang paling mereka rindukan tentang pemrograman di Midori. Itu juga ada di sana untuk saya. Jadi, tanpa basa-basi lagi, mari kita mulai.

Pertanyaan dasar yang ingin dijawab oleh Model Kesalahan adalah: bagaimana “kesalahan” dikomunikasikan kepada pemrogram dan pengguna sistem? Cukup sederhana, bukan? Jadi sepertinya.

Salah satu tantangan terbesar dalam menjawab pertanyaan ini ternyata adalah mendefinisikan apa sebenarnya kesalahan itu . Paling bahasa menggabungkan bug dan kesalahan yang dapat dipulihkan ke dalam kategori yang sama, dan menggunakan fasilitas yang sama untuk menanganinya. SEBUAH null dereferensi atau akses array di luar batas diperlakukan sebagai dengan cara yang sama seperti masalah konektivitas jaringan atau penguraian kesalahan. Konsistensi ini mungkin tampak bagus pada pandangan pertama, tetapi memiliki masalah yang mengakar. Secara khusus, ini menyesatkan dan sering kali mengarah ke kode yang tidak dapat diandalkan.

Solusi keseluruhan kami adalah menawarkan model kesalahan dua cabang. Di satu sisi, Anda gagal dengan cepat – kami menyebutnya pengabaian – untuk bug pemrograman. Dan di sisi lain, Anda telah memeriksa pengecualian untuk dipulihkan secara statis kesalahan. Keduanya sangat berbeda, baik dalam model pemrograman maupun mekanisme di belakangnya. Pengabaian tanpa menyesal menghancurkan seluruh proses dalam sekejap, menolak menjalankan kode pengguna apa pun saat melakukannya. (Ingat, program Midori yang khas memiliki banyak proses kecil dan ringan.) Pengecualian, tentu saja, memfasilitasi pemulihan, tetapi telah dukungan sistem tipe dalam untuk membantu pemeriksaan dan verifikasi.

Perjalanan ini panjang dan berliku. Untuk menceritakan kisahnya, saya telah membagi posting ini menjadi enam area utama:

Kalau dipikir-pikir, hasil tertentu tampak jelas. Terutama diberikan bahasa sistem modern seperti Go dan Rust. Tetapi beberapa hasilnya mengejutkan kami. Saya akan langsung mengejar apa pun yang saya bisa, tetapi saya akan memberikan banyak cerita latar di sepanjang jalan. Kami mencoba banyak hal yang tidak berhasil, dan saya rasa itu bahkan lebih menarik daripada tempat kita berakhir saat menjadi debu diselesaikan.

Mari kita mulai dengan memeriksa prinsip arsitektural, persyaratan, dan pembelajaran dari sistem yang ada.

Prinsip

Saat kami memulai perjalanan ini, kami menyebutkan beberapa persyaratan untuk Model Kesalahan yang baik:

  • Dapat digunakan. Pengembang harus dapat melakukan hal yang “benar” saat menghadapi kesalahan dengan mudah, seolah-olah kebetulan. SEBUAH teman dan kolega yang terkenal menyebut ini jatuh ke The Pit of Success . Para model hendaknya tidak memaksakan tata tertib upacara yang berlebihan untuk menulis kode idiomatik. Idealnya, ini secara kognitif akrab bagi audiens target kita.

  • Dapat diandalkan. Model Kesalahan adalah dasar dari keandalan seluruh sistem. Kami sedang membangun operasi sistem, bagaimanapun, jadi keandalan adalah yang terpenting. Anda bahkan mungkin menuduh kami sebagai pengejar ekstrem yang obsesif tingkat itu. Mantra kami yang memandu sebagian besar pengembangan model pemrograman adalah “ benar menurut konstruksi .”

  • Performant. Kasus umum harus sangat cepat. Itu berarti mendekati nol overhead mungkin jalur sukses. Setiap biaya tambahan untuk jalur kegagalan harus sepenuhnya “bayar untuk bermain”. Dan tidak seperti banyak sistem modern yang bersedia memberikan penalti secara berlebihan pada jalur kesalahan, kami memiliki beberapa komponen kinerja penting yang sebenarnya tidak demikian dapat diterima, jadi kesalahan juga harus cukup cepat.

  • Bersamaan. Seluruh sistem kami didistribusikan dan sangat serentak. Hal ini biasanya menimbulkan kekhawatiran renungan dalam Model Kesalahan lainnya. Mereka harus menjadi yang terdepan dalam diri kita.

  • Dapat didiagnosis. Kegagalan debugging, baik secara interaktif atau setelah kejadian, harus produktif dan mudah. ​​

  • Dapat disusun. Pada intinya, Model Kesalahan adalah fitur bahasa pemrograman, yang berada di tengah a ekspresi kode pengembang. Karena itu, ia harus memberikan ortogonalitas dan komposabilitas yang akrab dengan orang lain fitur sistem. Mengintegrasikan komponen yang dibuat secara terpisah harus alami, andal, dan dapat diprediksi.

Ini klaim yang berani, tapi saya pikir apa kami akhirnya berhasil di semua dimensi.

Pembelajaran

Model Kesalahan yang Ada tidak memenuhi persyaratan di atas untuk kami. Setidaknya tidak sepenuhnya. Jika seseorang melakukannya dengan baik pada suatu dimensi, itu akan buruk di tempat lain. Misalnya, kode kesalahan dapat memiliki keandalan yang baik, tetapi banyak pemrogram menganggapnya kesalahan rawan digunakan; lebih lanjut, mudah untuk melakukan hal yang salah – seperti lupa untuk memeriksa satu – yang jelas-jelas melanggar Persyaratan “lubang kesuksesan”.

Mengingat tingkat keandalan ekstrem yang kami cari, tidak mengherankan jika kami tidak puas dengan sebagian besar model.

Jika Anda mengoptimalkan kemudahan penggunaan daripada keandalan, seperti yang mungkin Anda lakukan dalam bahasa skrip, kesimpulan Anda akan berbeda secara signifikan. Bahasa seperti Java dan C # kesulitan karena mereka berada tepat di persimpangan skenario – terkadang digunakan untuk sistem, terkadang digunakan untuk aplikasi – tetapi secara keseluruhan Model Kesalahan mereka sangat tidak cocok untuk kebutuhan kita.

Akhirnya, ingat juga bahwa cerita ini dimulai pada pertengahan tahun 2000-an, sebelum Go, Rust, dan Swift tersedia untuk kami pertimbangan. Ketiga bahasa ini telah melakukan beberapa hal hebat dengan Model Kesalahan sejak saat itu.

Kode Kesalahan

Kode kesalahan bisa dibilang Model Kesalahan yang paling sederhana. Idenya sangat mendasar dan bahkan tidak membutuhkan bahasa atau dukungan runtime. Suatu fungsi hanya mengembalikan nilai, biasanya berupa bilangan bulat, untuk menunjukkan keberhasilan atau kegagalan:

  int foo () {     //      jika (gagal) {         kembali 1;     }     kembali 0; }  

Ini adalah pola tipikal, di mana pengembalian 0 berarti sukses dan bukan nol berarti kegagalan. Penelepon harus memeriksanya:

  int err=foo (); jika (err) {     // Error! Atasi itu. }  

Kebanyakan sistem menawarkan konstanta yang mewakili himpunan kode kesalahan, bukan angka ajaib. Mungkin ada atau mungkin tidak fungsi yang dapat Anda gunakan untuk mendapatkan informasi tambahan tentang kesalahan terbaru (seperti errno dalam standar C dan GetLastError di Win32). Kode pengembalian sebenarnya bukanlah sesuatu yang istimewa dalam bahasa – itu hanya nilai pengembalian.

C telah lama menggunakan kode kesalahan. Akibatnya, sebagian besar ekosistem berbasis C melakukannya. Lebih banyak kode sistem tingkat rendah telah ditulis menggunakan disiplin kode pengembalian daripada yang lain. Linux melakukannya , seperti yang tak terhitung jumlahnya sistem mission-critical dan realtime. Jadi adil untuk mengatakan bahwa mereka memiliki rekam jejak yang mengesankan untuk mereka!

Di Windows, HRESULT sama. Sebuah HRESULT hanyalah sebuah “pegangan” integer dan ada sekelompok konstanta dan makro di winerror.h seperti S_OK , E_FAULT , dan BERHASIL () , yang digunakan untuk membuat dan memeriksa nilai. Yang paling kode penting di Windows ditulis menggunakan disiplin kode kembali. Tidak ada pengecualian yang ditemukan di kernel. Di Setidaknya tidak sengaja.

Dalam lingkungan dengan manajemen memori manual, membatalkan alokasi memori saat kesalahan secara unik sulit dilakukan. Kode pengembalian bisa buat ini (lebih) bisa ditoleransi. C ++ memiliki cara yang lebih otomatis untuk melakukan ini menggunakan RAII , tetapi kecuali jika Anda membeli model C ++ seluruhnya – yang tidak dimiliki oleh banyak pemrogram sistem – maka tidak ada cara yang baik untuk menggunakan RAII secara bertahap di C Anda program.

Baru-baru ini, Go telah memilih kode kesalahan. Meskipun pendekatan Go mirip dengan C, ia telah banyak dimodernisasi sintaks dan pustaka yang lebih baik.

Banyak bahasa fungsional menggunakan kode balik yang disamarkan dalam monad dan menamai hal-hal seperti Opsi , Mungkin, atau Kesalahan, yang bila digabungkan dengan gaya pemrograman dataflow dan pencocokan pola, terasa jauh lebih alami. Ini Pendekatan ini menghilangkan beberapa kelemahan utama untuk menampilkan kode yang akan kita bahas, terutama dibandingkan dengan C. Rust sebagian besar telah mengadopsi model ini tetapi memiliki kubah beberapa hal menarik dengannya untuk pemrogram sistem.

Meskipun sederhana, kode pengembalian memang disertakan dengan beberapa bagasi; Singkatnya:

  • Performa bisa terganggu.
  • Kegunaan model pemrograman bisa buruk.
  • Masalah besar: Anda dapat secara tidak sengaja lupa untuk memeriksa kesalahan.

Mari kita bahas masing-masing, secara berurutan, dengan contoh dari bahasa yang dikutip di atas.

Kinerja

Kode kesalahan gagal dalam pengujian “overhead nol untuk kasus umum; bayar untuk bermain untuk kasus yang tidak biasa ”:

  1. Ada dampak konvensi pemanggilan. Anda sekarang memiliki dua nilai untuk dikembalikan (untuk non – void mengembalikan fungsi): nilai pengembalian aktual dan kemungkinan kesalahan. Ini membakar lebih banyak register dan / atau ruang stack, membuat panggilan lebih sedikit efisien. Sebaris tentu saja dapat membantu memulihkan ini untuk subset panggilan yang dapat disisipkan.

  2. Ada cabang yang disuntikkan ke dalam callites di mana callee bisa gagal. Saya menyebut biaya seperti ini “selai kacang”, karena pengecekan tercoreng di seluruh kode, sehingga menyulitkan untuk mengukur dampaknya secara langsung. Di Midori kami dapat bereksperimen dan mengukur, dan memastikan bahwa ya, memang, biaya di sini tidak sepele. Ada juga a efek sekunder, karena fungsi berisi lebih banyak cabang, ada lebih banyak risiko membingungkan pengoptimal.

  3. Ini mungkin mengejutkan bagi sebagian orang, karena tidak diragukan lagi semua orang telah mendengar bahwa “pengecualian itu lambat.” Ternyata bahwa mereka tidak harus seperti itu. Dan, jika dilakukan dengan benar, mereka mendapatkan kode penanganan kesalahan dan data dari jalur panas yang meningkat Kinerja I-cache dan TLB, dibandingkan dengan overhead di atas, yang jelas-jelas menurunkannya.

    Banyak sistem berkinerja tinggi telah dibuat dengan menggunakan kode balik, jadi Anda mungkin mengira saya rewel. Seperti banyak orang hal-hal yang kami lakukan, kritik yang mudah adalah bahwa kami mengambil pendekatan yang terlalu ekstrim. Tapi bagasinya semakin parah.

    Lupa untuk Memeriksa Mereka

    Sangat mudah untuk lupa memeriksa kode pengembalian barang. Misalnya, pertimbangkan sebuah fungsi:

    Sekarang di callsite, bagaimana jika kita diam-diam mengabaikan nilai yang dikembalikan sepenuhnya, dan terus berjalan?

      foo (); // Teruskan - tapi foo mungkin gagal!  

    Pada tahap ini, Anda telah menutupi potensi kesalahan kritis dalam program Anda. Ini dengan mudah adalah yang paling menjengkelkan dan masalah yang merusak dengan kode kesalahan. Seperti yang akan saya tunjukkan nanti, jenis opsi membantu menangani hal ini untuk bahasa fungsional. Tetapi dalam bahasa berbasis C, dan bahkan Go dengan sintaks modernnya, ini adalah masalah nyata.

    Masalah ini tidak bersifat teoretis. Saya menemukan banyak bug yang disebabkan oleh pengabaian kode pengembalian dan saya yakin Anda memilikinya terlalu. Memang, dalam pengembangan Model Kesalahan ini, tim saya menemukan beberapa yang menarik. Misalnya saat kami mem-porting Server Ucapan Microsoft ke Midori, kami menemukan bahwa 80% bahasa Mandarin Taiwan ( zh-tw ) permintaan gagal. Tidak gagal dengan cara yang langsung dilihat oleh para pengembang; sebaliknya, klien akan mendapatkan tanggapan yang tidak jelas. Pertama, kami mengira itu salah kami. Tapi kemudian kami menemukan HRESULT yang ditelan diam-diam dalam kode aslinya. Setelah kami mendapatkannya ke Midori, bug dilemparkan ke wajah kami, ditemukan, dan diperbaiki segera setelah porting. Pengalaman ini pasti menginformasikan pendapat kami tentang kode kesalahan.

    Saya terkejut bahwa Go membuat impor yang tidak terpakai adalah kesalahan, namun melewatkan yang jauh lebih kritis ini. Sangat dekat!

    Memang benar Anda dapat menambahkan pemeriksa analisis statis, atau mungkin peringatan “nilai kembali yang tidak terpakai” karena kebanyakan C ++ komersial kompiler lakukan. Namun begitu Anda melewatkan kesempatan untuk menambahkannya ke inti bahasa, sebagai persyaratan, tidak satu pun teknik tersebut akan mencapai massa kritis karena keluhan tentang analisis kebisingan.

    Untuk manfaatnya, lupa menggunakan nilai kembalian dalam bahasa kita adalah kesalahan waktu kompilasi. Anda harus secara eksplisit abaikan mereka; awal kami menggunakan API untuk ini, tetapi akhirnya membuat sintaks bahasa – setara dengan > / dev / null :

    Kami tidak menggunakan kode kesalahan, namun ketidakmampuan untuk mengabaikan nilai pengembalian secara tidak sengaja penting untuk keseluruhan keandalan sistem. Berapa kali Anda men-debug masalah hanya untuk menemukan bahwa akar masalahnya adalah kembalinya nilai yang Anda lupa gunakan? Bahkan ada eksploitasi keamanan di mana ini adalah akar masalahnya. Biarkan pengembang berkata abaikan tentu saja tidak anti peluru, karena masih bisa melakukan hal yang salah. Tapi setidaknya itu eksplisit dan bisa diaudit.

    Kegunaan Model Pemrograman

    Dalam bahasa berbasis C dengan kode kesalahan, Anda akhirnya menulis banyak tulisan tangan jika memeriksa di mana saja setelah berfungsi panggilan. Ini bisa sangat membosankan jika banyak fungsi Anda gagal, yang terjadi dalam program C di mana kegagalan alokasi terjadi juga dikomunikasikan dengan kode pengembalian, sering terjadi. Mengembalikan beberapa nilai juga akan terasa canggung.

    Peringatan: keluhan ini subjektif. Dalam banyak hal, kegunaan kode pengembalian sebenarnya elegan. Anda menggunakan kembali primitif yang sangat sederhana – bilangan bulat, pengembalian, dan jika cabang – itu digunakan dalam berbagai situasi lainnya. Dalam saya Pendapat yang rendah hati, kesalahan adalah aspek yang cukup penting dari pemrograman sehingga bahasa tersebut harus membantu Anda.

    Go memiliki pintasan sintaksis yang bagus untuk membuat pemeriksaan kode pengembalian standar sedikit lebih menyenangkan:

      jika err:=foo (); err!=nil {     // Atasi kesalahannya. }  

    Perhatikan bahwa kami telah memanggil foo dan memeriksa apakah kesalahannya bukan – nihil dalam satu baris. Cukup rapi.

    Masalah kegunaan tidak berhenti di situ.

    Banyak kesalahan dalam fungsi tertentu yang memiliki logika pemulihan atau remediasi yang sama. Banyak programmer C gunakan label dan goto untuk menyusun kode tersebut. Sebagai contoh:

    dalam teror;  // ...  error=langkah1 (); jika (kesalahan) {     goto Error; }  // ...  error=langkah2 (); jika (kesalahan) {     goto Error; }  // ...  // Keluar dari fungsi normal. kembali 0;  // ... Kesalahan: // Lakukan sesuatu tentang `error`. kesalahan pengembalian;  

    Tak perlu dikatakan, ini adalah jenis kode yang hanya bisa disukai oleh seorang ibu.

    Dalam bahasa seperti D, C #, dan Java, Anda akhirnya memiliki memblokir untuk menyandikan pola “sebelum cakupan keluar” ini secara lebih langsung. Demikian pula, ekstensi kepemilikan Microsoft untuk penawaran C ++ __ akhirnya , bahkan jika Anda tidak sepenuhnya membeli RAII dan pengecualian. Dan D menyediakan cakupan dan penawaran Go menunda. Semua bantuan ini untuk memberantas pola Goto Error .

    Selanjutnya, bayangkan fungsi saya ingin mengembalikan nilai nyata dan kemungkinan kesalahan? Kami telah membakar slot pengembalian sudah jadi ada dua kemungkinan yang jelas:

    1. Kita dapat menggunakan slot kembali untuk salah satu dari dua nilai (biasanya kesalahan), dan slot lainnya – seperti penunjuk parameter – untuk yang lain dari keduanya (biasanya nilai sebenarnya). Ini adalah pendekatan yang umum di C.

    2. Kita dapat mengembalikan struktur data yang membawa kemungkinan keduanya dalam strukturnya. Seperti yang akan kita lihat, inilah umum dalam bahasa fungsional. Tetapi dalam bahasa seperti C, atau bahkan Go, yang tidak memiliki polimorfisme parametrik, Anda kalah mengetik informasi tentang nilai yang dikembalikan, jadi ini kurang umum untuk dilihat. C ++ tentu saja menambahkan template, jadi masuk prinsipnya bisa melakukan ini, namun karena menambahkan pengecualian, ekosistem di sekitar kode pengembalian kurang.

    3. Untuk mendukung klaim kinerja di atas, bayangkan apa yang dilakukan keduanya pada kode rakitan yang dihasilkan program Anda.

      Mengembalikan Nilai “Di Samping”

      Contoh pendekatan pertama di C terlihat seperti ini:

        int foo (int out) {     //      jika (gagal) {         kembali 1;     }     keluar=42;     kembali 0; }  

      Nilai sebenarnya harus dikembalikan “di samping”, membuat situs panggilan menjadi kikuk:

        nilai int; int ret=foo (& nilai); if (ret) {     // Error! Atasi itu. } lain {     // Gunakan nilai ... }  

      Selain menjadi kikuk, pola ini mengganggu kompiler Anda analisis tugas yang pasti yang mengganggu kemampuan Anda untuk mendapatkan peringatan yang baik tentang hal-hal seperti menggunakan nilai yang tidak diinisialisasi.

      Go juga mengatasi masalah ini dengan sintaks yang lebih baik, berkat pengembalian multi-nilai:

        func foo () (int, kesalahan) {     jika gagal {         return 0, error.New ("Terjadi hal buruk")     }     return 42, nihil }  

      Dan sebagai hasilnya, situs panggilan jauh lebih bersih. Dikombinasikan dengan fitur garis tunggal jika memeriksa kesalahan – twist halus, karena pada pandangan pertama nilai kembali tidak akan tidak dalam ruang lingkup, tetapi ini – ini menjadi sentuhan yang lebih baik:

        jika nilai, err:=foo (); err!=nil {     // Error! Atasi itu. } lain {     // Gunakan nilai ... }  

      Perhatikan bahwa ini juga membantu mengingatkan Anda untuk memeriksa kesalahan. Namun, ini bukan antipeluru, karena fungsinya bisa mengembalikan kesalahan dan tidak ada yang lain, pada saat mana lupa untuk memeriksanya semudah di C.

      Seperti yang saya sebutkan di atas, beberapa orang akan membantah saya dalam hal kegunaan. Terutama desainer Go, saya kira. Besar mengajukan banding untuk Go menggunakan kode kesalahan adalah sebagai pemberontakan terhadap bahasa yang terlalu kompleks di lanskap saat ini. Kita punya kehilangan banyak hal yang membuat C begitu elegan – sehingga Anda biasanya dapat melihat baris kode mana pun dan menebak kode mesin apa itu diterjemahkan menjadi. Saya tidak akan membantah poin-poin ini. Nyatanya, saya sangat menyukai model Go daripada keduanya yang tidak dicentang pengecualian dan inkarnasi Java dari pengecualian yang dicentang. Bahkan saat saya menulis posting ini, setelah menulis banyak Go belakangan ini, Saya melihat kesederhanaan dan keajaiban Go, apakah kita bertindak terlalu jauh dengan semua percobaan dan membutuhkan dan seterusnya Lihat segera? Saya tidak yakin. Model kesalahan Go cenderung menjadi salah satu aspek bahasa yang paling memecah belah; itu mungkin sebagian besar karena Anda tidak bisa ceroboh dengan kesalahan seperti di kebanyakan bahasa, namun pemrogram sangat menikmati menulis kode di Midori’s. Pada akhirnya, sulit untuk membandingkannya. Saya yakin keduanya dapat digunakan untuk menulis kode yang andal.

      Mengembalikan Nilai dalam Struktur Data

      Bahasa fungsional mengatasi banyak tantangan kegunaan dengan mengemas kemungkinan baik sebuah nilai atau kesalahan, ke dalam satu struktur data. Karena Anda dipaksa untuk memisahkan kesalahan dari nilainya jika Anda mau untuk melakukan sesuatu yang berguna dengan nilai di situs panggilan – yang, berkat gaya pemrograman dataflow, mungkin Anda lakukan kemauan – mudah untuk menghindari masalah mematikan karena lupa memeriksa kesalahan.

      Untuk contoh pandangan modern tentang ini, lihat Opsi Scala Tipe. Yang malang berita adalah bahwa beberapa bahasa, seperti yang ada dalam keluarga ML dan bahkan Scala (berkat warisan JVM-nya), memadukan model dengan dunia pengecualian yang tidak dicentang. Hal ini mencemari keanggunan pendekatan struktur data monadik.

      Haskell melakukan sesuatu yang lebih keren dan memberikan ilusi penanganan pengecualian sambil tetap menggunakan nilai kesalahan dan aliran kontrol lokal :

      Ada perselisihan lama antara programmer C ++ tentang apakah pengecualian atau kode pengembalian kesalahan adalah cara yang benar. Niklas Wirth menganggap pengecualian sebagai reinkarnasi GOTO dan dengan demikian menghilangkannya dalam bahasanya. Haskell menyelesaikannya masalah ini dengan cara diplomatis: Fungsi mengembalikan kode kesalahan, tetapi penanganan kode kesalahan tidak memperburuk kode.

      Triknya di sini adalah untuk mendukung semua lemparan familiar dan pola tangkap , tetapi menggunakan monad daripada aliran kontrol.

      Meskipun Rust juga menggunakan kode kesalahan itu juga dalam gaya jenis kesalahan fungsional. Misalnya, bayangkan kita menulis fungsi bernama bar di Go : kami ingin memanggil foo , dan kemudian menyebarkan kesalahan ke pemanggil kami jika gagal:

        func bar () kesalahan {     jika nilai, err:=foo (); err!=nil {         kembali salah     } lain {         // Gunakan nilai ...     } }  

      Versi lama di Rust tidak lagi ringkas. Mungkin, bagaimanapun, mengirim programmer C terguncang dengan bahasa asingnya sintaks pencocokan pola (masalah nyata tetapi bukan pemecah kesepakatan). Siapapun yang nyaman dengan pemrograman fungsional, namun, mungkin tidak akan berkedip, dan pendekatan ini tentu saja berfungsi sebagai pengingat untuk menangani kesalahan Anda:

        bilah fn () -> Hasil  {     pertandingan foo () {         Ok (nilai)=> / Gunakan nilai ... /,         Err (err)=> mengembalikan Err (err)     } }  

      Tapi itu menjadi lebih baik. Karat memiliki mencoba! makro yang mengurangi boilerplate seperti contoh terbaru untuk satu ekspresi:

        bilah fn () -> Hasil 

      { biarkan nilai=coba! (foo); // Gunakan nilai ... }

      Ini membawa kita ke sweet spot yang indah. Itu memang mengalami masalah kinerja yang saya sebutkan sebelumnya, tetapi tidak sangat baik di semua dimensi lainnya. Itu sendiri adalah gambaran yang tidak lengkap – untuk itu, kita perlu membahas fail-fast (a.k.a. ditinggalkan) – tetapi seperti yang akan kita lihat, ini jauh lebih baik daripada model berbasis pengecualian lainnya yang digunakan secara luas saat ini.

      Pengecualian

      Sejarah pengecualian memang menarik. Selama perjalanan ini saya menghabiskan waktu berjam-jam menelusuri kembali langkah-langkah industri. Itu termasuk membaca beberapa makalah asli – seperti buku klasik Goodenough tahun 1975, Penanganan Pengecualian: Masalah dan Usulan Notasi – sebagai tambahan untuk melihat pendekatan dari beberapa bahasa: Ada, Eiffel, Modula-2 dan 3, ML, dan, yang paling inspiratif, CLU . Banyak makalah melakukan pekerjaan yang lebih baik daripada yang dapat saya rangkum perjalanan yang panjang dan sulit, jadi saya tidak akan melakukannya di sini. Sebaliknya, saya akan fokus pada apa yang berhasil dan yang tidak berhasil untuk membangun sistem yang andal.

      Keandalan adalah persyaratan terpenting kami di atas saat mengembangkan Model Kesalahan. Jika Anda tidak bisa bereaksi tepat untuk kegagalan, sistem Anda, menurut definisi, tidak akan terlalu andal. Sistem operasi secara umum membutuhkan untuk bisa diandalkan. Sayangnya, model yang paling umum – pengecualian yang tidak dicentang – adalah yang terburuk yang dapat Anda lakukan dalam dimensi ini.

      Karena alasan ini, sebagian besar sistem yang andal menggunakan kode pengembalian alih-alih pengecualian. Mereka memungkinkannya secara lokal alasan dan putuskan cara terbaik untuk bereaksi terhadap kondisi kesalahan. Tapi saya terlalu terburu-buru. Mari kita bahas.

      Pengecualian yang Tidak Dicentang

      Rekap singkat. Dalam model pengecualian yang tidak dicentang, Anda melempar dan menangkap pengecualian, tanpa itu menjadi bagian dari tipe sistem atau tanda tangan fungsi. Sebagai contoh:

        // Foo membuat pengecualian yang tidak tertangani: batal Foo () {     melempar Exception baru (...); }  // Bar memanggil Foo, dan menangani pengecualian itu: void Bar () {     coba {         Foo ();     }     catch (Exception e) {         // Tangani kesalahan tersebut.     } }  // Baz juga memanggil Foo, tetapi tidak menangani pengecualian itu: batal Baz () {     Foo (); // Biarkan kesalahan lolos ke penelepon kami. }  

      Dalam model ini, panggilan fungsi apa pun – dan terkadang pernyataan – dapat membuat pengecualian, mentransfer kontrol non-lokal di tempat lain. Dimana? Siapa tahu. Tidak ada anotasi atau jenis artefak sistem untuk memandu Anda analisis. Akibatnya, sulit bagi siapa pun untuk bernalar tentang status program pada saat pelemparan, status perubahan yang terjadi saat pengecualian itu disebarkan ke tumpukan panggilan – dan mungkin di seluruh utas secara bersamaan program – dan kondisi yang dihasilkan pada saat tertangkap atau tidak ditangani.

      Tentu saja mungkin untuk dicoba. Melakukannya membutuhkan membaca dokumentasi API, melakukan audit manual terhadap kode, bersandar banyak ulasan kode, dan dosis keberuntungan yang sehat. Bahasanya tidak membantu Anda sedikit pun di sini. Karena kegagalan jarang terjadi, hal ini cenderung tidak seburuk kedengarannya. Kesimpulan saya adalah mengapa banyak orang di industri ini menganggap pengecualian yang tidak dicentang adalah “cukup baik”. Mereka menjauhi jalan Anda untuk jalur kesuksesan umum dan, karena kebanyakan orang tidak menulis kode penanganan kesalahan yang kuat dalam program non-sistem, biasanya memberikan pengecualian membuat Anda keluar dari a acar cepat. Menangkap dan kemudian melanjutkan sering kali juga berhasil. Tidak ada salahnya, tidak busuk. Secara statistik, program “bekerja”.

      Mungkin kebenaran statistik baik-baik saja untuk bahasa skrip, tetapi untuk tingkat terendah dari sistem operasi, atau aplikasi atau layanan misi penting, ini bukan solusi yang tepat. Saya harap ini tidak kontroversial.

      . NET membuat situasi yang buruk menjadi lebih buruk karena pengecualian asinkron . C ++ memiliki apa yang disebut “pengecualian asinkron” juga: ini adalah kegagalan yang dipicu oleh kesalahan perangkat keras, seperti pelanggaran akses. Menjadi sangat buruk di .NET, namun. Untaian arbitrer dapat menyebabkan kegagalan di hampir semua titik dalam kode Anda. Bahkan antara RHS dan LHS dari file tugas! Akibatnya, hal-hal yang terlihat seperti atom dalam kode sumber tidak. Saya menulis tentang ini 10 tahun yang lalu dan tantangannya masih ada, meskipun risikonya telah berkurang karena .NET umumnya mengetahui bahwa utas dibatalkan bermasalah. Bahkan CoreCLR baru tidak memiliki AppDomains, dan tumpukan ASP.NET Core 1.0 baru tentu tidak menggunakan pembatalan thread seperti dulu. Tetapi API adalah masih di sana.

      Ada wawancara terkenal dengan Anders Hejlsberg, kepala desainer C #, yang disebut Masalah dengan Pengecualian yang Dicentang . Dari perspektif pemrogram sistem, banyak hal yang membuat Anda tergores kepalamu. Tidak ada pernyataan yang menegaskan bahwa pelanggan target untuk C # adalah pengembang aplikasi cepat lebih dari ini:

      Bill Venners : Tapi tidak bukankah Anda memecahkan kode mereka dalam kasus itu, bahkan dalam bahasa tanpa pengecualian yang dicentang? Jika versi baru foo akan mengeluarkan pengecualian baru yang harus dipikirkan klien tentang penanganannya, bukan milik mereka kode rusak hanya oleh fakta bahwa mereka tidak mengharapkan pengecualian itu ketika mereka menulis kode?

      Anders Hejlsberg : Tidak, karena dalam banyak kasus, orang tidak peduli. Mereka tidak akan menangani semua ini pengecualian. Ada pengendali pengecualian tingkat bawah di sekitar loop pesan mereka. Pawang itu baru saja akan membawa membuka dialog yang mengatakan apa yang salah dan lanjutkan. Para programmer melindungi kode mereka dengan menulis try akhirnya di mana-mana, jadi mereka akan mundur dengan benar jika pengecualian terjadi, tetapi mereka sebenarnya tidak tertarik untuk menangani pengecualian.

      Ini mengingatkan saya pada Saat Kesalahan Lanjutkan Berikutnya dalam Visual Basic, dan cara Windows Forms secara otomatis tertangkap dan tertelan kesalahan dilemparkan oleh aplikasi, dan berusaha untuk melanjutkan. Saya tidak menyalahkan Anders atas sudut pandangnya di sini; heck, untuk Popularitas C # yang liar, saya yakin itu keputusan yang tepat mengingat iklim pada saat itu. Tapi ini bukan caranya untuk menulis kode sistem operasi.

      C ++ setidaknya mencoba untuk ditawarkan sesuatu yang lebih baik daripada pengecualian yang tidak dicentang dengan melemparkan spesifikasi pengecualian . Sayangnya, fitur tersebut mengandalkan penegakan dinamis yang mana membunyikan lonceng kematiannya secara instan.

      Jika saya menulis fungsi void f () throw (SomeError) , badan f masih bebas menjalankan fungsi yang melempar sesuatu selain SomeError . Demikian pula, jika saya menyatakan bahwa f tidak memberikan pengecualian, menggunakan batal f () lempar () , masih mungkin untuk memanggil hal-hal yang melempar. Oleh karena itu, untuk mengimplementasikan kontrak yang dinyatakan, compiler dan runtime harus memastikan bahwa, jika ini terjadi, std :: tak terduga dipanggil merobek proses sebagai tanggapan.

      Saya bukan satu-satunya orang yang menyadari bahwa desain ini adalah kesalahan. Memang, lemparan sekarang sudah tidak digunakan lagi. WG21 terperinci kertas, Menghentikan Spesifikasi Pengecualian , menjelaskan bagaimana C ++ berakhir di sini, dan menawarkan ini dalam pernyataan pembukaannya:

      Spesifikasi pengecualian telah terbukti hampir tidak berguna dalam praktiknya, sambil menambahkan biaya tambahan yang dapat diukur ke program.

      Penulis mencantumkan tiga alasan penghentian lemparan . Dua dari tiga alasan tersebut adalah hasil dari pilihan dinamis: pemeriksaan waktu proses (dan mode kegagalan buram yang terkait) dan overhead kinerja waktu proses. Alasan ketiga, kekurangan komposisi dalam kode generik, dapat ditangani dengan menggunakan sistem tipe yang tepat (diakui dengan biaya).

      Tetapi bagian terburuknya adalah bahwa penyembuhannya bergantung pada konstruksi lain yang ditegakkan secara dinamis – noexcept penentu – yang menurut saya sama buruknya dengan penyakitnya.

      “Keamanan pengecualian” adalah praktik yang biasa dibahas di C ++ masyarakat. Pendekatan ini dengan rapi mengklasifikasikan bagaimana fungsi dimaksudkan untuk berperilaku dari perspektif pemanggil dengan sehubungan dengan kegagalan, transisi status, dan manajemen memori. Suatu fungsi terbagi dalam salah satu dari empat jenis: no-throw artinya kemajuan ke depan dijamin dan tidak ada pengecualian yang akan muncul; keamanan yang kuat berarti transisi status terjadi secara atomis dan kegagalan tidak akan meninggalkan status komitmen sebagian atau invarian yang rusak; arti keamanan dasar bahwa, meskipun suatu fungsi mungkin melakukan sebagian perubahan status, invarian tidak akan rusak dan kebocoran dicegah; dan akhirnya, tidak ada keamanan berarti segala sesuatu mungkin terjadi. Taksonomi ini cukup membantu dan saya mendorong siapa pun untuk berhati-hati dan teliti tentang perilaku kesalahan, baik menggunakan pendekatan ini atau yang serupa. Meskipun Anda menggunakan kode kesalahan. Masalahnya, pada dasarnya tidak mungkin mengikuti pedoman ini dalam sistem yang menggunakan pengecualian yang tidak dicentang, kecuali untuk struktur data node daun yang memanggil sekumpulan fungsi lain yang kecil dan mudah diaudit. Pikirkan saja: untuk menjamin keamanan yang kuat di mana-mana, Anda perlu mempertimbangkan kemungkinan semua panggilan fungsi yang dilempar , dan lindungi kode sekitarnya yang sesuai. Itu berarti pemrograman secara defensif, mempercayai fungsi lain prosa bahasa Inggris terdokumentasi (yang tidak diperiksa oleh komputer), beruntung dan hanya menelepon noexcept fungsi, atau hanya berharap yang terbaik. Berkat RAII, aspek kebebasan bocor dari keamanan dasar lebih mudah dicapai – dan cantik umum hari ini berkat petunjuk cerdas – tetapi bahkan invarian yang rusak pun sulit untuk dicegah. Artikel Penanganan Pengecualian: A False Sense of Security menyimpulkan hal ini dengan baik.

      Untuk C ++, solusi sebenarnya mudah diprediksi, dan cukup lugas: untuk program sistem yang tangguh, jangan gunakan pengecualian. Itulah pendekatannya Pengambilan C ++ yang disematkan, selain berbagai pedoman penting waktu nyata dan misi untuk C ++, termasuk Jet Propulsion Laboratory milik NASA. C ++ di Mars pasti tidak menggunakan pengecualian dalam waktu dekat .

      Jadi, jika Anda dapat dengan aman menghindari pengecualian dan tetap menggunakan kode pengembalian seperti C di C ++, apa dagingnya?

      Seluruh ekosistem C ++ menggunakan pengecualian. Untuk mematuhi panduan di atas, Anda harus menghindari bagian bahasa yang signifikan dan ternyata, bagian penting dari ekosistem perpustakaan. Ingin menggunakan Perpustakaan Template Standar? Sayang sekali menggunakan pengecualian. Ingin menggunakan Boost? Sayang sekali, ini menggunakan pengecualian. Pengalokasi Anda mungkin melempar bad_alloc . Sehingga di. Ini bahkan menyebabkan kegilaan seperti orang membuat garpu dari perpustakaan yang ada yang menghapus pengecualian. Jendela kernel, misalnya, memiliki cabang STL sendiri yang tidak menggunakan pengecualian. Ini adalah percabangan ekosistem tidak menyenangkan atau praktis untuk dipertahankan.

      Kekacauan ini menempatkan kita pada posisi yang buruk. Terutama karena banyak bahasa menggunakan pengecualian yang tidak dicentang. Jelas sekali tidak cocok untuk menulis kode sistem tingkat rendah dan andal. (Saya yakin saya akan membuat beberapa musuh C ++ dengan mengatakan demikian terus terang.) Setelah menulis kode di Midori selama bertahun-tahun, saya menangis untuk kembali dan menulis kode yang menggunakan tidak dicentang pengecualian; bahkan hanya meninjau kode adalah siksaan. Tapi “untungnya” kami telah memeriksa pengecualian dari Java untuk dipelajari dan meminjam dari… Benar?

      Pengecualian yang Diperiksa

      Ah, periksa pengecualian. Boneka kain yang hampir setiap programmer Java, dan setiap orang yang mengamati Java dari sebuah jarak lengan, suka mengalahkan. Jadi, secara tidak adil, menurut saya, jika Anda membandingkannya dengan pengecualian yang tidak dicentang kekacauan.

      Di Jawa, Anda tahu kebanyakan apa yang mungkin dilontarkan metode, karena metode harus mengatakan demikian:

        void foo () melempar FooException, BarException {     ... }  

      Sekarang penelepon tahu bahwa memanggil foo dapat menghasilkan FooException atau BarException dilempar. Di callites yang harus diputuskan oleh programmer sekarang: 1) menyebarkan pengecualian yang dilemparkan sebagaimana adanya, 2) menangkap dan menanganinya, atau 3) entah bagaimana mengubah jenis pengecualian yang dilempar (bahkan mungkin “melupakan” jenisnya sama sekali). Contohnya:

        // 1) Menyebarkan pengecualian sebagaimana adanya: void bar () memunculkan FooException, BarException {     foo (); }  // 2) Tangkap dan tangani mereka: bilah kosong () {     coba {         foo ();     }     catch (FooException e) {         // Menangani kondisi kesalahan FooException.     }     catch (BarException e) {         // Menangani kondisi error BarException.     } }  // 3) Ubah tipe pengecualian yang dilempar: void bar () melempar Exception {     foo (); }  

      Ini semakin mendekati sesuatu yang dapat kita gunakan. Tetapi gagal di beberapa akun:

      1. Pengecualian digunakan untuk mengkomunikasikan bug yang tidak dapat dipulihkan, seperti dereferensi nol, bagi-dengan-nol, dll.

      2. Anda sebenarnya tidak tahu segalanya yang mungkin terlempar, berkat teman kecil kita RuntimeException . Karena Java menggunakan pengecualian untuk semua kondisi kesalahan – bahkan bug, seperti yang disebutkan di atas – para desainer menyadari bahwa orang akan menjadi gila dengan semua spesifikasi pengecualian tersebut. Jadi mereka memperkenalkan semacam pengecualian yang tidak dicentang. Itu adalah metode dapat membuangnya tanpa mendeklarasikannya, sehingga pemanggil dapat memanggilnya dengan mulus.

      3. Meskipun tanda tangan menyatakan jenis pengecualian, tidak ada indikasi di situs panggilan apa yang mungkin dilontarkan oleh panggilan.

      4. Orang-orang membenci mereka.

      5. Yang terakhir itu menarik, dan saya akan kembali lagi nanti ketika menjelaskan pendekatan yang diambil Midori. Singkatnya, ketidaksukaan masyarakat terhadap pengecualian yang dicentang di Jawa sebagian besar berasal dari, atau setidaknya diperkuat secara signifikan oleh, tiga peluru lainnya di atas. Model yang dihasilkan tampaknya menjadi yang terburuk dari kedua dunia. Itu tidak membantu Anda untuk menulis kode antipeluru dan sulit digunakan. Anda akhirnya menuliskan banyak omong kosong dalam kode Anda karena sedikit dianggap manfaat. Dan membuat versi antarmuka Anda sangat menyebalkan. Seperti yang akan kita lihat nanti, kita bisa melakukan yang lebih baik.

        Poin pembuatan versi itu layak untuk direnungkan. Jika Anda tetap menggunakan satu jenis lemparan , maka pembuatan versi masalahnya tidak lebih buruk dari kode kesalahan. Salah satu fungsi gagal atau tidak. Memang benar jika Anda merancang API versi 1 untuk tidak memiliki mode kegagalan, dan kemudian ingin menambahkan kegagalan di versi 2, Anda kacau. Seperti yang seharusnya, menurut saya. Sebuah Mode kegagalan API adalah bagian penting dari desain dan kontraknya dengan penelepon. Sama seperti Anda tidak akan mengubah pengembaliannya jenis API secara diam-diam tanpa penelepon perlu mengetahuinya, Anda tidak boleh mengubah mode kegagalannya secara semantik cara yang berarti. Lebih lanjut tentang poin kontroversial ini nanti.

        CLU memiliki pendekatan yang menarik, seperti yang dijelaskan dalam pemindaian miring dan goyah dari makalah 1979 oleh Barbara Liskov, Penanganan Pengecualian di CLU . Perhatikan bahwa mereka banyak fokus tentang “linguistik”; dengan kata lain, mereka menginginkan bahasa yang disukai orang. Kebutuhan untuk memeriksa dan menyebarkan kembali semua kesalahan di situs panggilan terasa lebih seperti nilai yang dikembalikan, namun model pemrogramannya lebih kaya dan sedikit perasaan deklaratif dari apa yang sekarang kita kenal sebagai pengecualian. Dan yang terpenting, sinyal s (nama mereka untuk lemparan ) dulu diperiksa. Ada juga cara mudah untuk menghentikan program jika sinyal yang tidak terduga terjadi.

        Masalah Universal dengan Pengecualian

        Kebanyakan sistem pengecualian mendapatkan beberapa kesalahan besar, terlepas dari apakah mereka dicentang atau tidak.

        Pertama, memberikan pengecualian biasanya sangat mahal. Ini hampir selalu karena pengumpulan tumpukan jejak. Dalam sistem terkelola, mengumpulkan jejak tumpukan juga membutuhkan metadata groveling, untuk membuat string fungsi nama simbol. Namun, jika kesalahan diketahui dan ditangani, Anda bahkan tidak memerlukan informasi itu pada waktu proses! Diagnostik diimplementasikan dengan lebih baik di infrastruktur logging dan diagnostik, bukan sistem pengecualian itu sendiri. Itu perhatian bersifat ortogonal. Meskipun, untuk benar-benar memenuhi persyaratan diagnostik di atas, sesuatu harus dapat memulihkan jejak tumpukan; jangan pernah meremehkan kekuatan printf debugging dan betapa pentingnya jejak tumpukan untuk itu.

        Selanjutnya, pengecualian dapat mengganggu kualitas kode secara signifikan. Saya menyentuh topik ini di posting terakhir saya , dan ada Good-Bad-Ugly makalah bagus tentang topik dalam konteks C ++ . Tidak memiliki sistem tipe statis Informasi menyulitkan untuk memodelkan aliran kontrol di compiler, yang mengarah ke pengoptimalan yang terlalu konservatif.

        Hal lain yang membuat sebagian besar sistem pengecualian salah adalah mendorong granularitas penanganan kesalahan yang terlalu kasar. Pendukung kode kembali suka bahwa penanganan kesalahan dilokalkan ke panggilan fungsi tertentu. (Saya juga!) Dalam penanganan pengecualian sistem, terlalu mudah untuk menampar percobaan kasar / tangkap blok di sekitar beberapa kode besar, tanpa hati-hati bereaksi terhadap kegagalan individu. Itu menghasilkan kode rapuh yang hampir pasti salah; jika tidak hari ini, maka setelah refactoring tak terelakkan yang akan terjadi di jalan. Banyak dari ini berkaitan dengan memiliki sintaks yang benar.

        Terakhir, aliran kontrol untuk lemparan biasanya tidak terlihat. Bahkan dengan Java, di mana Anda menganotasi tanda tangan metode, sebenarnya tidak mungkin untuk mengaudit badan kode dan melihat dengan tepat dari mana pengecualian berasal. Aliran kendali diam sama buruknya dengan goto , atau setjmp / longjmp , dan membuat penulisan kode yang andal menjadi sangat sulit.

        Di mana kita?

        Sebelum melanjutkan, mari kita rekap posisi kita saat ini:

        Good-Bad-Ugly

        Bukankah lebih bagus jika kita bisa mengambil semua The Goods dan meninggalkan The Bads and The Uglies?

        Ini saja akan menjadi langkah maju yang bagus. Tapi itu tidak cukup. Ini membawa saya ke momen “ah-hah” besar pertama kita itu membentuk segala sesuatu yang akan datang. Untuk kelas kesalahan yang signifikan, tidak ada dari pendekatan ini yang sesuai!

        Perbedaan penting yang kami buat sejak awal adalah perbedaan antara kesalahan dan bug yang dapat dipulihkan:

  • A kesalahan yang dapat dipulihkan biasanya adalah hasil validasi data terprogram. Beberapa kode telah memeriksa negara bagian dunia dan menganggap situasi tidak dapat diterima untuk kemajuan. Mungkin beberapa teks markup sedang diurai, masukan pengguna dari situs web, atau kegagalan koneksi jaringan sementara. Dalam kasus ini, program diharapkan pulih. Pengembang yang menulis kode ini harus memikirkan tentang apa yang harus dilakukan jika terjadi kegagalan karena akan terjadi dalam konstruksi yang baik program apa pun yang Anda lakukan. Responsnya mungkin untuk mengkomunikasikan situasi kepada pengguna akhir, coba lagi, atau tinggalkan operasi sepenuhnya, namun dapat diprediksi dan, sering, situasi yang direncanakan , meskipun disebut “kesalahan.”

  • Bug adalah sejenis kesalahan yang tidak diharapkan programmer. Input tidak divalidasi dengan benar, logika ditulis salah, atau berbagai masalah yang muncul. Masalah seperti itu sering kali bahkan tidak terdeteksi dengan segera; dibutuhkan beberapa saat sampai “Efek sekunder” diamati secara tidak langsung, pada titik mana kerusakan signifikan pada status program mungkin terjadi terjadi. Karena pengembang tidak mengharapkan ini terjadi, semua taruhan dibatalkan. Semua struktur data dapat dijangkau oleh kode ini sekarang dicurigai. Dan karena masalah ini belum tentu terdeteksi dengan segera, pada kenyataannya, banyak sekali lebih banyak tersangka. Bergantung pada jaminan isolasi bahasa Anda, mungkin seluruh prosesnya tercemar.

Perbedaan ini sangat penting. Anehnya, kebanyakan sistem tidak membuatnya, setidaknya tidak dengan cara yang berprinsip! Seperti yang kita lihat di atas, Java, C #, dan bahasa dinamis hanya menggunakan pengecualian untuk semuanya; dan C dan Go menggunakan kode pengembalian. C ++ menggunakan file campuran tergantung pada audiens, tetapi cerita yang biasa adalah proyek memilih satu dan menggunakannya di mana-mana. Kamu biasanya tidak mendengar bahasa yang menyarankan dua teknik berbeda untuk penanganan kesalahan.

Mengingat bahwa bug secara inheren tidak dapat dipulihkan, kami tidak berusaha untuk mencoba. Semua bug yang terdeteksi saat runtime disebabkan sesuatu yang disebut pengabaian , yang merupakan istilah Midori untuk sesuatu yang dikenal sebagai “gagal-cepat” .

Masing-masing sistem di atas menawarkan mekanisme seperti pengabaian. C # memiliki Environment.FailFast ; C ++ memiliki std :: terminate ; Go memiliki panik ; Rust memiliki panik! ; dan seterusnya. Masing-masing merobek konteks sekitarnya secara tiba-tiba dan segera. Ruang lingkup konteks ini tergantung pada sistem – misalnya, C # dan C ++ menghentikan proses, Jalankan Goroutine saat ini, dan Karatkan utas saat ini, secara opsional dengan penangan panik terpasang untuk menyelamatkan proses.

Meskipun kami menggunakan pengabaian dengan cara yang lebih disiplin dan ada di mana-mana daripada yang umum, kami tentu bukan yang pertama untuk mengenali pola ini. Ini Esai Haskell , mengartikulasikan ini perbedaan yang cukup baik:

Saya terlibat dalam pengembangan perpustakaan yang ditulis dalam C ++. Salah satu pengembang memberi tahu saya bahwa pengembang dibagi menjadi orang-orang yang menyukai pengecualian dan yang lain yang lebih memilih kode pengembalian. Menurut saya, teman kode pengembalian menang. Namun, saya mendapat kesan bahwa mereka memperdebatkan poin yang salah: Pengecualian dan kode kembali sama ekspresifnya , namun kode tersebut tidak boleh digunakan untuk menjelaskan kesalahan. Sebenarnya kode kembali berisi definisi seperti ARRAY_INDEX_OUT_OF_RANGE . Tapi saya bertanya-tanya: Bagaimana fungsi saya bereaksi, ketika mendapatkan ini mengembalikan kode dari subrutin? Haruskah itu mengirim email ke pemrogramnya? Itu bisa mengembalikan kode ini ke pemanggilnya di berbalik, tetapi juga tidak akan tahu bagaimana mengatasinya. Lebih buruk lagi, karena saya tidak bisa membuat asumsi tentang implementasi fungsi, saya harus mengharapkan ARRAY_INDEX_OUT_OF_RANGE dari setiap subrutin. Kesimpulan saya adalah bahwa ARRAY_INDEX_OUT_OF_RANGE adalah kesalahan (pemrograman). Ini tidak dapat ditangani atau diperbaiki saat runtime, itu hanya bisa diperbaiki oleh pengembangnya. Jadi tidak boleh ada kode yang dikembalikan, tetapi harus ada pernyataan.

Mengabaikan cakupan memori bersama berbutir halus yang dapat berubah dicurigai – seperti Goroutine atau utas atau apa pun – kecuali Anda sistem memberikan jaminan tentang cakupan potensi kerusakan yang terjadi. Namun, mekanisme ini sangat bagus apakah ada untuk kita gunakan! Itu berarti menggunakan disiplin pengabaian dalam bahasa-bahasa ini memang memungkinkan.

Namun, ada elemen arsitektur yang diperlukan agar pendekatan ini berhasil dalam skala besar. Saya yakin Anda sedang berpikir “Jika saya membuang seluruh proses setiap kali saya memiliki dereferensi nol dalam program C # saya, saya akan sangat marah pelanggan ”; dan, demikian pula, “Itu tidak akan menjadi relia berdarah sama sekali! ” Keandalan, ternyata, mungkin tidak seperti yang Anda pikirkan.

Sebelum melangkah lebih jauh, kita perlu menyatakan keyakinan utama:

Shi Kegagalan Terjadi.

Untuk Membangun Sistem yang Andal

Kebijaksanaan umum adalah bahwa Anda membangun sistem yang dapat diandalkan dengan secara sistematis menjamin bahwa kegagalan tidak akan pernah terjadi. Secara intuitif, itu sangat masuk akal. Ada satu masalah: dalam batasnya, tidak mungkin. Jika Anda bisa menghabiskan jutaan dolar untuk properti ini saja – seperti yang dilakukan oleh banyak misi penting, sistem waktu nyata – maka Anda dapat menghasilkan keuntungan yang signifikan lekuk. Dan mungkin menggunakan bahasa seperti PERCIKAN (satu set ekstensi berbasis kontrak ke Ada) untuk secara resmi membuktikan kebenaran setiap baris yang ditulis. Namun, pengalaman menunjukkan bahwa pendekatan ini pun tidak selalu berhasil.

Daripada melawan fakta kehidupan ini, kami malah menerimanya. Jelas Anda mencoba menghilangkan kegagalan jika memungkinkan. Itu model kesalahan harus membuatnya transparan dan mudah ditangani. Tetapi yang lebih penting, Anda merancang sistem Anda sehingga keseluruhan tetap berfungsi bahkan ketika bagian individu gagal, dan kemudian mengajari sistem Anda untuk memulihkan yang gagal potongan dengan anggun. Ini terkenal dalam sistem terdistribusi. Jadi mengapa ini baru?

Di tengah semua itu, sistem operasi hanyalah jaringan terdistribusi dari proses yang bekerja sama, seperti cluster terdistribusi dari layanan mikro atau Internet itu sendiri. Perbedaan utama mencakup hal-hal seperti latensi; apa tingkat kepercayaan yang dapat Anda bangun dan seberapa mudah; dan berbagai asumsi tentang lokasi, identitas, dll. Tapi kegagalan di sangat asinkron, terdistribusi, dan I / O sistem intensif pasti akan terjadi. Kesan saya adalah, sebagian besar karena kesuksesan kernel monolitik yang berkelanjutan, dunia pada umumnya belum melakukan lompatan ke “sistem operasi sebagai wawasan sistem terdistribusi. Namun, begitu Anda melakukannya, banyak prinsip desain menjadi jelas.

Seperti kebanyakan sistem terdistribusi, arsitektur kami mengasumsikan kegagalan proses tidak dapat dihindari. Kami pergi dengan hebat panjang untuk mempertahankan terhadap kegagalan berjenjang, jurnal secara teratur, dan untuk mengaktifkan restart program dan layanan.

Anda membangun berbagai hal secara berbeda saat Anda mengasumsikan ini.

Secara khusus, isolasi sangat penting. Model proses Midori mendorong isolasi halus yang ringan. Sebagai Hasilnya, program dan apa yang biasanya menjadi “utas” dalam sistem operasi modern adalah entitas terisolasi independen. Melindungi dari kegagalan salah satu koneksi semacam itu jauh lebih mudah daripada saat berbagi status yang bisa berubah di ruang alamat.

Isolasi juga mendorong kesederhanaan. Klasik Butler Lampson Petunjuk tentang Desain Sistem Komputer membahas topik ini. Dan saya selalu menyukai kutipan dari Hoare ini:

Harga keandalan yang tidak dapat dihindari adalah kesederhanaan. (C. Hoare).

Dengan menjaga program dipecah menjadi bagian-bagian kecil, yang masing-masing dapat gagal atau berhasil dengan sendirinya, mesin negara di dalamnya mereka tetap lebih sederhana. Hasilnya, pulih dari kegagalan menjadi lebih mudah. Dalam bahasa kami, poin kemungkinan kegagalan eksplisit, yang selanjutnya membantu menjaga mesin status internal itu benar, dan menunjukkan koneksi tersebut dunia luar yang lebih berantakan. Di dunia ini, harga dari kegagalan individu tidak seburuk itu. Aku tidak bisa terlalu menekankan hal ini. Tak satu pun dari fitur bahasa yang saya jelaskan nanti akan bekerja dengan baik tanpa ini fondasi arsitektur isolasi yang murah dan selalu ada.

Erlang sangat berhasil dalam membangun properti ini ke dalam bahasa secara mendasar. Ini, seperti Midori, memanfaatkan proses ringan yang dihubungkan dengan penyampaian pesan, dan mendorong arsitektur yang toleran terhadap kesalahan. Biasa pola adalah “supervisor,” di mana beberapa proses bertanggung jawab untuk mengawasi dan, jika terjadi kegagalan, memulai kembali proses lainnya. Artikel ini melakukan pekerjaan yang hebat dalam mengartikulasikan filosofi ini – “biarkan crash” – dan teknik yang direkomendasikan untuk merancang program Erlang yang andal dalam praktiknya.

Maka, kuncinya bukanlah mencegah kegagalan itu sendiri, melainkan mengetahui bagaimana dan kapan harus menghadapinya.

Setelah Anda membangun arsitektur ini, Anda harus memastikannya berhasil. Bagi kami, ini artinya tekanan selama seminggu, di mana proses akan datang dan pergi, beberapa karena kegagalan, untuk memastikan sistem secara keseluruhan tetap terjaga membuat kemajuan yang baik ke depan. Ini mengingatkan saya pada sistem seperti Netflix Chaos Monkey yang secara acak membunuh seluruh mesin di cluster Anda memastikan layanan secara keseluruhan tetap sehat.

Saya berharap lebih banyak dunia yang mengadopsi filosofi ini saat pergeseran ke komputasi yang lebih terdistribusi terjadi. Dalam kelompok layanan mikro, misalnya, kegagalan satu container sering ditangani dengan mulus oleh cluster penutup perangkat lunak manajemen (Kubernetes, Amazon EC2 Container Service, Docker Swarm, dll). Alhasil, apa yang saya gambarkan di sini post mungkin berguna untuk menulis layanan Java, Node.js / JavaScript, Python, dan bahkan Ruby yang lebih andal. Itu Kabar yang tidak menguntungkan adalah Anda mungkin akan berjuang melawan bahasa Anda untuk sampai ke sana. Banyak kode dalam proses Anda akan bekerja sangat keras untuk tetap tertatih-tatih ketika ada sesuatu yang salah.

Pengabaian

Meskipun prosesnya murah dan terisolasi serta mudah dibuat ulang, masih masuk akal untuk berpikir bahwa meninggalkan seluruh proses dalam menghadapi bug adalah reaksi yang berlebihan. Biarkan saya mencoba meyakinkan Anda sebaliknya.

Melanjutkan saat menghadapi bug berbahaya saat Anda mencoba membangun sistem yang kuat. Jika seorang programmer tidak menyangka situasi tertentu yang muncul, siapa yang tahu apakah kode akan melakukan hal yang benar lagi. Struktur data penting mungkin telah tertinggal dalam keadaan yang salah. Sebagai contoh yang ekstrim (dan mungkin sedikit konyol), itulah rutinitas dimaksudkan untuk membulatkan nomor Anda ke bawah untuk tujuan perbankan mungkin mulai membulatkannya naik.

Dan Anda mungkin tergoda untuk mengurangi granularitas pengabaian menjadi sesuatu yang lebih kecil daripada proses. Tapi itu rumit. Untuk mengambil contoh, bayangkan utas dalam proses Anda menemukan bug, dan gagal. Bug ini mungkin saja dipicu oleh beberapa status yang disimpan dalam variabel statis. Meskipun beberapa utas lain mungkin tampak telah tidak terpengaruh oleh kondisi yang mengarah pada kegagalan, Anda tidak dapat membuat kesimpulan ini. Kecuali beberapa milik sistem Anda – isolasi dalam bahasa Anda, isolasi kumpulan akar objek yang diekspos ke utas independen, atau sesuatu yang lain – paling aman untuk mengasumsikan bahwa apa pun selain membuang seluruh ruang alamat ke luar jendela berisiko dan tidak dapat diandalkan.

Berkat sifat ringan dari proses Midori, meninggalkan proses lebih seperti meninggalkan satu utas dalam sistem klasik daripada keseluruhan proses. Tetapi model isolasi kami memungkinkan kami melakukan ini dengan andal.

Saya akui topik cakupannya adalah lereng licin. Mungkin semua data di dunia sudah rusak, jadi bagaimana menurut Anda ketahuilah bahwa membuang prosesnya saja sudah cukup ?! Ada perbedaan penting di sini. Status proses sementara oleh rancangan. Dalam sistem yang dirancang dengan baik, hal itu dapat dibuang dan dibuat ulang dengan cepat. Memang benar bahwa bug dapat merusak persisten, tetapi Anda memiliki masalah yang lebih besar di tangan Anda – masalah yang harus ditangani secara berbeda.

Untuk beberapa latar belakang, kita dapat melihat desain sistem yang toleran terhadap kesalahan. Pengabaian (gagal-cepat) sudah umum teknik di alam itu, dan kita dapat menerapkan banyak dari apa yang kita ketahui tentang sistem ini ke program dan proses biasa. Mungkin teknik yang paling penting adalah membuat jurnal secara teratur dan memeriksa status persisten yang berharga. Jim Gray Makalah 1985, Kenapa Komputer Berhenti dan Apa Yang Bisa Dilakukan Tentang Itu? , menjelaskan konsep ini dengan baik. Saat program terus berpindah ke cloud, dan secara agresif diuraikan menjadi layanan independen yang lebih kecil, ini pemisahan yang jelas antara status sementara dan persisten bahkan lebih penting. Akibat dari pergeseran cara software tersebut tertulis, pengabaian jauh lebih dapat dicapai dalam arsitektur modern daripada sebelumnya. Memang, pengabaian bisa membantu Anda menghindari kerusakan data, karena bug yang terdeteksi sebelum pos pemeriksaan berikutnya mencegah keadaan buruk melarikan diri.

Bug di kernel Midori ditangani secara berbeda. Bug di mikrokernel, misalnya, sangat berbeda lebih buruk dari bug dalam proses mode pengguna. Cakupan kemungkinan kerusakan lebih besar, dan respons teraman adalah meninggalkan seluruh “domain” (ruang alamat). Untungnya, sebagian besar dari apa yang Anda anggap sebagai “kernel” klasik fungsionalitas – penjadwal, manajer memori, sistem file, tumpukan jaringan, dan bahkan driver perangkat – dijalankan alih-alih dalam proses terisolasi dalam mode pengguna di mana kegagalan dapat diatasi dengan cara yang biasa dijelaskan di atas.

Sejumlah jenis bug di Midori dapat memicu pengabaian:

  • Pemeran yang salah.
  • Upaya untuk membatalkan referensi null penunjuk.
  • Upaya untuk mengakses array di luar batasnya.
  • Dibagi nol.
  • Aliran matematika yang tidak disengaja.
  • Memori habis.
  • Stack overflow.
  • Pengabaian eksplisit.
  • Kegagalan kontrak.
  • Kegagalan pernyataan.

Keyakinan mendasar kami adalah bahwa masing-masing adalah kondisi Program tidak dapat dipulihkan. Mari kita bahas masing-masing.

Bug Lama Biasa

Beberapa situasi ini tidak diragukan lagi merupakan indikasi bug program.

Pemeran yang salah, coba dereferensi null , akses di luar batas array, atau bagi-dengan-nol jelas merupakan masalah dengan logika program, yaitu mencoba melakukan operasi ilegal yang tak terbantahkan. Seperti yang akan kita lihat nanti, ada cara out (misalnya, mungkin Anda ingin propagasi gaya NaN untuk DbZ). Namun secara default kami menganggap ini bug.

Sebagian besar pemrogram bersedia menerima ini tanpa pertanyaan. Dan berurusan dengan mereka sebagai bug dengan cara ini pengabaian ke loop pengembangan dalam di mana bug selama pengembangan dapat ditemukan dan diperbaiki dengan cepat. Pengabaian benar-benar membantu membuat orang lebih produktif dalam menulis kode. Ini adalah kejutan bagi saya pada awalnya, tetapi masuk akal.

Beberapa situasi ini, di sisi lain, bersifat subjektif. Kami harus membuat keputusan tentang perilaku default, seringkali dengan kontroversi, dan terkadang menawarkan kontrol terprogram.

Aritmatika Over / Arus bawah

Mengatakan over / underflow aritmatika yang tidak disengaja mewakili bug tentu merupakan sikap yang kontroversial. Dalam sistem yang tidak aman, Namun, hal-hal seperti itu seringkali menyebabkan kerentanan keamanan. Saya mendorong Anda untuk meninjau Kerentanan Nasional Database untuk dilihat banyaknya ini .

Sebenarnya, parser Windows TrueType Font, yang kami porting ke Midori (dengan keuntungan dalam kinerja), telah mengalami a lusinan dari mereka dalam beberapa tahun terakhir saja. (Pengurai cenderung menjadi peternakan untuk lubang keamanan seperti ini.)

Ini telah memunculkan paket seperti SafeInt , yang pada dasarnya menjauhkan Anda dari operasi aritmatika bahasa ibu Anda, lebih memilih operasi pustaka yang diperiksa.

Sebagian besar eksploitasi ini tentu saja juga digabungkan dengan akses ke memori yang tidak aman. Oleh karena itu, Anda dapat membantah secara masuk akal bahwa overflow tidak berbahaya dalam bahasa yang aman dan oleh karena itu harus diizinkan. Namun, cukup jelas berdasarkan pengalaman keamanan, bahwa program sering melakukan hal yang salah dalam menghadapi over / underflow yang tidak diinginkan. Secara sederhana menempatkan, pengembang sering mengabaikan kemungkinan, dan program melanjutkan untuk melakukan hal-hal yang tidak direncanakan. Itu adalah definisi bug yang sebenarnya dimaksudkan untuk ditangkap oleh pengabaian. Paku terakhir di peti mati yang satu ini bahwa secara philisophically, ketika ada pertanyaan tentang kebenaran, kami cenderung keliru di sisi niat eksplisit.

Oleh karena itu, semua over / underflows yang tidak diberi tahu dianggap bug dan menyebabkan pengabaian. Ini mirip dengan kompilasi C # dengan

sakelar / dicentang , kecuali kompiler kita pengecekan redundan yang dioptimalkan secara agresif. (Karena sedikit orang yang pernah berpikir untuk membuang sakelar ini di C #, file pembuat kode tidak melakukan pekerjaan yang agresif dalam menghapus cek yang dimasukkan.) Berkat bahasa ini dan pengembangan bersama kompiler, hasilnya jauh lebih baik daripada apa yang kebanyakan kompiler C ++ akan hasilkan dalam menghadapi SafeInt hitung. Juga seperti C #, konstruksi pelingkupan yang tidak dicentang dapat digunakan di mana over / underflow dimaksudkan.

Meskipun reaksi awal dari sebagian besar pengembang C # dan C ++ yang saya ajak bicara tentang gagasan ini negatif, Pengalaman 9 kali dari 10, pendekatan ini membantu menghindari bug dalam program. Sisa 1 kali itu biasanya pengabaian kadang-kadang terlambat dalam salah satu dari 72 jam stres kami – di mana kami merusak seluruh sistem dengan browser dan pemutar multimedia dan apa pun yang bisa kami lakukan untuk menyiksa sistem – ketika beberapa counter tidak berbahaya meluap. Saya selalu merasa lucu bahwa kami menghabiskan waktu untuk memperbaikinya alih-alih cara klasik agar produk matang melalui program stres, yaitu kebuntuan dan kondisi balapan. Antara Anda dan saya, saya akan mengambil alih pengabaian!

Memori Habis dan Stack Overflow

Out-of-memory (OOM) rumit. Selalu begitu. Dan sikap kami di sini tentu saja kontroversial juga.

Dalam lingkungan di mana memori dikelola secara manual, pengecekan gaya kode kesalahan adalah pendekatan yang paling umum:

  X x=(X  malloc (...); jika (! x) {     // Menangani kegagalan alokasi. }  

Ini memiliki satu manfaat halus: alokasi itu menyakitkan, memerlukan pemikiran, dan karenanya program yang menggunakan teknik ini sering kali lebih hemat dan berhati-hati dengan cara mereka menggunakan memori. Namun, ada kerugian besar: rawan kesalahan dan mengarah ke sejumlah besar jalur kode yang sering tidak teruji. Dan jika jalur kode belum diuji, biasanya mereka tidak berfungsi.

Pengembang pada umumnya melakukan pekerjaan yang buruk sehingga perangkat lunak mereka berfungsi dengan baik tepat di ambang kehabisan sumber daya. Dalam pengalaman saya dengan Windows dan .NET Framework, di sinilah kesalahan besar terjadi. Dan itu mengarah ke model pemrograman yang sangat kompleks, seperti yang disebut .NET

Wilayah Eksekusi Terbatas

. Sebuah program yang tertatih-tatih, tidak dapat mengalokasikan memori dalam jumlah kecil, dapat dengan cepat menjadi musuh keandalan. Pos Keandalan menakjubkan Chris Brumme menjelaskan ini dan tantangan terkait dalam segala kemuliaan berdarahnya.

Bagian dari sistem kami tentu saja “dikeraskan” dalam artian, seperti tingkat terendah dari kernel, tempat pengabaian lingkup akan lebih luas dari satu proses. Tapi kami menyimpan kode ini sesedikit mungkin.

Untuk sisanya? Ya, Anda dapat menebaknya: pengabaian. Bagus dan sederhana.

Sungguh mengejutkan betapa banyak hal yang berhasil kami lakukan. Saya menghubungkan sebagian besar ini dengan model isolasi. Faktanya, kami bisa dengan sengaja membiarkan proses mengalami OOM, dan pengabaian berikutnya, sebagai akibat dari kebijakan manajemen, dan tetap yakin bahwa stabilitas dan pemulihan dibangun dalam keseluruhan arsitektur.

Dimungkinkan untuk ikut serta dalam kegagalan yang dapat dipulihkan untuk alokasi individu jika Anda benar-benar menginginkannya. Ini tidak umum Namun, setidaknya ada mekanisme untuk mendukungnya. Mungkin contoh motivasi terbaik adalah ini: bayangkan program Anda ingin mengalokasikan buffer berukuran 1MB. Situasi ini berbeda dari run-of-the-mill biasa Anda alokasi objek sub-1KB. Seorang pengembang mungkin sangat siap untuk berpikir dan secara eksplisit menangani fakta bahwa a blok bersebelahan berukuran 1MB mungkin tidak tersedia, dan menanganinya sesuai dengan itu. Sebagai contoh:

  var bb=coba byte baru [1024*1024] lain tangkap; jika (bb.Failed) {     // Menangani kegagalan alokasi. }  

Stack overflow adalah perpanjangan sederhana dari filosofi yang sama ini. Stack hanyalah sumber daya yang didukung memori. Terima kasih ke model tumpukan tertaut asinkron kami, kehabisan tumpukan secara fisik identik dengan kehabisan memori heap, jadi konsistensi cara penanganannya tidak mengejutkan pengembang. Banyak sistem memperlakukan stack overflow ini cara hari ini.

Pernyataan

Penegasan adalah pemeriksaan manual dalam kode bahwa beberapa kondisi dianggap benar, yang memicu pengabaian jika tidak benar. Sebagai dengan kebanyakan sistem, kami memiliki baik hanya debug dan pernyataan kode rilis, namun tidak seperti kebanyakan sistem lain, kami memiliki lebih banyak rilis daripada debug. Faktanya, kode kami dibumbui secara bebas dengan pernyataan. Kebanyakan metode memiliki banyak metode.

Ini sesuai dengan filosofi bahwa lebih baik menemukan bug saat runtime daripada melanjutkan di depan bug. Dan, dari Tentu saja, kompiler backend kami diajari cara mengoptimalkannya secara agresif seperti yang lainnya. Tingkat ini kepadatan pernyataan serupa dengan yang disarankan oleh pedoman untuk sistem yang sangat andal. Misalnya, dari makalah NASA, The Power of Ten -Rules for Developing Safety Critical Code :

Aturan: Kerapatan pernyataan kode harus rata-rata menjadi minimal dua pernyataan per fungsi. Pernyataan adalah digunakan untuk memeriksa kondisi anomali yang seharusnya tidak pernah terjadi dalam eksekusi kehidupan nyata. Pernyataan harus selalu seperti itu bebas efek samping dan harus didefinisikan sebagai tes Boolean.

Rasional: Statistik untuk upaya pengkodean industri menunjukkan bahwa pengujian unit sering menemukan setidaknya satu cacat per 10 hingga 100 baris kode tertulis. Kemungkinan cacat mencegat meningkat dengan kepadatan pernyataan. Penggunaan pernyataan adalah sering juga direkomendasikan sebagai bagian dari strategi pengkodean pertahanan yang kuat.

Untuk menunjukkan pernyataan, Anda cukup memanggil Debug.Assert atau Release.Assert :

  batal Foo () {     Debug.Assert (sesuatu); // Penegasan khusus debug.     Rilis.Assert (sesuatu); // Assert yang selalu dicentang. }  

Kami juga menerapkan fungsionalitas yang mirip dengan __ FILE __ dan __ LINE __ makro seperti di C ++, di selain __ EXPR __ untuk teks ekspresi predikat, sehingga pengabaian karena pernyataan gagal mengandung informasi yang berguna.

Pada masa-masa awal, kami menggunakan “tingkat” pernyataan yang berbeda dari ini. Kami memiliki tiga level, Contract.Strong.Assert , Kontrak. Assert , dan Contract.Weak.Assert . Level kuat berarti “selalu diperiksa”, yang di tengah berarti “sudah naik ke kompiler, “dan yang lemah berarti” hanya diperiksa dalam mode debug “. Saya membuat keputusan kontroversial untuk pindah dari model ini. Faktanya, saya cukup yakin 49,99% dari tim benar-benar membenci pilihan terminologi saya ( Debug. Assert

dan Release.Assert ), tetapi saya selalu menyukainya karena cukup jelas apa yang mereka lakukan. Masalah dengan yang lama taksonomi adalah bahwa tidak ada yang tahu persis kapan pernyataan itu akan diperiksa; kebingungan di bidang ini sama sekali tidak dapat diterima, menurut pendapat saya, mengingat betapa pentingnya disiplin pernyataan yang baik untuk keandalan program seseorang.

Saat kami memindahkan kontrak ke bahasa (lebih lanjut tentang itu segera), kami mencoba membuat menegaskan kata kunci juga. Namun, kami akhirnya beralih kembali menggunakan API. Alasan utamanya adalah bahwa pernyataan bukan bagian dari tanda tangan API seperti kontrak; dan mengingat bahwa pernyataan dapat dengan mudah diterapkan sebagai pustaka, tidak jelas apa yang kami peroleh dari memilikinya dalam bahasa. Selain itu, kebijakan seperti "check in debug" versus "check in release" cukup merasa tidak cocok dengan bahasa pemrograman. Saya akui, bertahun-tahun kemudian, saya masih ragu tentang ini.

Kontrak

Kontrak adalah mekanisme utama menangkap serangga di Midori. Meskipun kami memulai dengan Singularity , yang menggunakan Sing #, varian dari Spesifikasi # , kami segera pindah ke vanilla C # dan harus menemukan kembali apa kami mau. Kami akhirnya berakhir di tempat yang sangat berbeda setelah tinggal dengan model selama bertahun-tahun.

Semua kontrak dan pernyataan terbukti bebas efek samping berkat pemahaman bahasa kami tentang keabadian dan efek samping. Ini mungkin area inovasi bahasa terbesar, jadi saya pasti akan segera menulis postingan tentangnya.

Seperti halnya area lain, kami terinspirasi dan dipengaruhi oleh banyak sistem lain. Spec # adalah yang paling jelas. Eiffel dulu sangat berpengaruh terutama di sana banyak studi kasus yang dipublikasikan untuk dipelajari. Upaya penelitian seperti berbasis Ada PERCIKAN dan proposal untuk sistem waktu nyata dan tertanam juga. Pergi lebih dalam ke lubang kelinci teoritis, logika pemrograman seperti

Semantik aksiomatik Hoare

memberikan dasar untuk semua itu. Untuk saya, Namun, inspirasi paling filosofis datang dari CLU, dan kemudian Argus , pendekatan keseluruhan untuk penanganan kesalahan.

Prekondisi dan Postkondisi

Bentuk kontrak yang paling dasar adalah prasyarat metode. Ini menyatakan kondisi apa yang harus dipegang untuk metode tersebut dikirim. Ini paling sering digunakan untuk memvalidasi argumen. Terkadang digunakan untuk memvalidasi status target objek, namun hal ini umumnya tidak disukai, karena modalitas adalah hal yang sulit untuk dipikirkan oleh programmer. Prasyarat pada dasarnya adalah jaminan yang diberikan penelepon ke callee.

Dalam model terakhir kami, prasyarat dinyatakan menggunakan persyaratan kata kunci:

  batal Daftar (nama string)     membutuhkan! string.IsEmpty (nama) {     // Lanjutkan, mengetahui bahwa string tidak kosong. }  

Bentuk kontrak yang sedikit kurang umum adalah metode postcondition. Ini menyatakan kondisi apa yang menahan setelah metode sudah dikirim. Ini adalah jaminan yang diberikan oleh callee kepada pemanggil.

Dalam model terakhir kami, kondisi akhir dinyatakan menggunakan memastikan kata kunci:

  batal Hapus ()     memastikan Hitungan==0 {     // Memproses; penelepon dijamin Hitungannya 0 saat kita kembali. }  

Dimungkinkan juga untuk menyebutkan nilai pengembalian dalam kondisi pos, melalui nama khusus kembali . Nilai-nilai lama - seperti yang diperlukan untuk menyebutkan masukan dalam kondisi pasca - dapat ditangkap melalui lama (..) ; sebagai contoh:

  int AddOne (nilai int)     memastikan kembali==lama (nilai) +1 {     ... }  

Tentu saja, kondisi sebelum dan sesudah dapat dicampur. Misalnya, dari ou r ring buffer di kernel Midori:

  public bool PublishPosition ()     membutuhkan RemainingSize==0     memastikan UnpublishedSize==0 {     ... }  

Metode ini dapat dengan aman mengeksekusi tubuhnya dengan mengetahui bahwa RemainingSize adalah 0 dan penelepon bisa eksekusi dengan aman setelahnya kembali mengetahui bahwa UnpublishedSize juga 0 .

Jika salah satu kontrak ini ditemukan salah pada waktu proses, pengabaian terjadi.

Ini adalah area di mana kami berbeda dari upaya lainnya. Kontrak belakangan ini menjadi populer sebagai ekspresi program logika yang digunakan dalam teknik pembuktian lanjutan. Alat semacam itu membuktikan kebenaran atau kesalahan tentang kontrak yang dinyatakan, sering kali digunakan analisis global. Kami mengambil pendekatan yang lebih sederhana. Secara default, kontrak diperiksa pada waktu proses. Jika kompiler bisa membuktikan kebenaran atau kebohongan pada waktu kompilasi, itu bebas untuk elide pemeriksaan runtime atau mengeluarkan kesalahan waktu kompilasi, masing-masing.

Penyusun modern memiliki analisis berbasis kendala yang melakukan pekerjaan dengan baik dalam hal ini, seperti

analisis rentang Saya sebutkan di posting terakhir saya. Ini menyebarkan fakta dan menggunakannya untuk optimalkan kode. Ini termasuk menghilangkan pemeriksaan yang berlebihan: baik secara eksplisit dikodekan dalam kontrak, atau dalam logika program normal. Dan mereka dilatih untuk melakukan analisis ini dalam jumlah waktu yang wajar, jangan sampai programmer beralihlah ke kompiler lain yang lebih cepat. Teknik pembuktian teorema sama sekali tidak sesuai dengan kebutuhan kita; inti kami modul sistem membutuhkan waktu lebih dari satu hari untuk menganalisis menggunakan yang terbaik dalam kerangka kerja analisis pembuktian teorema berkembang biak!

Selanjutnya, kontrak yang dideklarasikan metode adalah bagian dari tanda tangannya. Ini berarti mereka akan muncul secara otomatis dokumentasi, keterangan alat IDE, dan banyak lagi. Kontrak sama pentingnya dengan pengembalian metode dan jenis argumen. Kontrak sebenarnya hanyalah perpanjangan dari sistem tipe, menggunakan logika arbitrer dalam bahasa untuk mengontrol bentuk pertukaran jenis. Akibatnya, semua persyaratan subtipe biasa diterapkan padanya. Dan, tentu saja, modular yang difasilitasi ini analisis lokal yang dapat dilakukan dalam hitungan detik menggunakan teknik penyusun pengoptimalan standar.

90% dari penggunaan umum pengecualian di .NET dan Java menjadi prasyarat. Semua dari ArgumentNullException , ArgumentOutOfRangeException , dan jenis terkait dan, yang lebih penting, pemeriksaan manual dan lemparan telah hilang. Metode sering dibumbui dengan pemeriksaan ini di C # hari ini; ada ribuan ini di .NET Repo CoreFX saja. Misalnya, inilah System.IO.TextReader Metode baca :

Ini rusak karena sejumlah alasan. Ini sangat melelahkan, tentu saja. Semua upacara itu! Tapi kita harus pergi jauh keluar dari cara kami untuk mendokumentasikan pengecualian ketika pengembang benar-benar tidak seharusnya menangkapnya. Sebaliknya, mereka harus melakukannya temukan bug selama pengembangan dan perbaiki. Semua pengecualian ini tidak masuk akal mendorong perilaku yang sangat buruk.

Jika kami menggunakan kontrak gaya Midori, di sisi lain, ini akan runtuh menjadi:

  ///  /// ... ///  public virtual int Read (char [] buffer, int index, int count)     membutuhkan buffer!=null     membutuhkan indeks>=0     membutuhkan hitungan>=0     membutuhkan buffer.Length - index>=count {     ... }  

Ada beberapa hal menarik tentang ini. Pertama, lebih ringkas. Lebih penting lagi, bagaimanapun, itu menggambarkan dirinya sendiri kontrak API dengan cara yang mendokumentasikan dirinya sendiri dan mudah dipahami oleh penelepon. Daripada membutuhkan programmer untuk mengekspresikan kondisi kesalahan dalam bahasa Inggris, ekspresi aktual tersedia untuk dibaca pemanggil, dan alat untuk memahami dan memanfaatkan. Dan itu menggunakan pengabaian untuk mengkomunikasikan kegagalan.

Saya juga harus menyebutkan bahwa kami memiliki banyak pembantu kontrak untuk membantu pengembang menulis prasyarat umum. Di atas pemeriksaan jangkauan eksplisit sangat berantakan dan mudah salah. Sebaliknya, kami bisa menulis:

  buffer baca int virtual publik (char [], indeks int, hitungan int )     membutuhkan buffer!=null     membutuhkan Range.IsValid (index, count, buffer.Length) {     ... }  

Dan, terlepas dari percakapan yang sedang berlangsung, ditambah dengan dua fitur canggih - array sebagai irisan dan bukan nol jenis - kami dapat mengurangi kode menjadi berikut ini, sambil mempertahankan jaminan yang sama:

  public int virtual Read (char [] buffer) {     ... }  

Tapi saya melompat jauh ke depan…

Awal yang Sederhana

Meskipun kami mendapatkan sintaks yang jelas yang sangat mirip Eiffel- dan Spec # - datang lingkaran penuh - seperti yang saya sebutkan sebelumnya, kami benar-benar tidak ingin mengubah bahasa sejak awal. Jadi kami sebenarnya mulai dengan pendekatan API sederhana:

  public bool PublishPosition () {     Contract.Requires (RemainingSize==0);     Contract.Ensures (UnpublishedSize==0);     ... }  

Ada sejumlah masalah dengan pendekatan ini, seperti

. Kontrak Kode NET upaya menemukan cara yang sulit.

Pertama, kontrak yang ditulis dengan cara ini adalah bagian dari implementasi API , sedangkan kami ingin mereka menjadi bagian dari tanda tangan. Ini mungkin tampak seperti perhatian teoritis tetapi jauh dari teori. Kami ingin hasilnya program untuk memuat metadata bawaan sehingga alat seperti IDE dan debugger dapat menampilkan kontrak di tempat panggilan. Dan kita ingin alat berada dalam posisi untuk menghasilkan dokumentasi secara otomatis dari kontrak. Mengubur mereka dalam implementasi tidak berfungsi kecuali Anda entah bagaimana membongkar metode untuk mengekstraknya nanti (yang merupakan retasan).

Ini juga mempersulit integrasi dengan kompiler backend yang menurut kami diperlukan untuk kinerja yang baik.

Kedua, Anda mungkin telah memperhatikan masalah dengan panggilan ke Kontrak. . Karena Pastikan dimaksudkan untuk menampung semua jalur keluar dari fungsi tersebut, bagaimana kita akan mengimplementasikan ini murni sebagai API? Jawabannya adalah, Anda tidak bisa. Salah satu pendekatannya adalah menulis ulang MSIL yang dihasilkan, setelah kompilator bahasa memancarkannya, tapi itu berantakan sekali. Pada titik ini, Anda mulai bertanya-tanya, mengapa tidak mengakui bahwa ini adalah masalah ekspresi dan semantik bahasa, dan menambahkan sintaks?

Bidang lain dari perjuangan terus-menerus bagi kami adalah apakah kontrak bersyarat atau tidak. Dalam banyak sistem klasik, Anda akan memeriksa kontrak dalam versi debug, tetapi tidak yang dioptimalkan sepenuhnya. Untuk waktu yang lama, kami memiliki tiga level yang sama untuk kontrak yang kami lakukan pernyataan yang disebutkan sebelumnya:

  • Lemah, ditunjukkan dengan Contract.Weak. , artinya hanya debug.
  • Normal, ditunjukkan hanya dengan Kontrak. , membiarkannya sebagai keputusan implementasi saat memeriksanya.
  • Kuat, ditunjukkan dengan Contract.Strong. , artinya selalu dicentang.

Saya akui, awalnya saya menemukan ini jadilah solusi yang elegan. Sayangnya, seiring waktu kami menemukan bahwa ada yang konstan kebingungan tentang apakah kontrak "normal" sedang dalam proses debug, rilis, atau semua hal di atas (sehingga orang menyalahgunakannya dengan lemah dan kuat sesuai). Bagaimanapun, ketika kami mulai mengintegrasikan skema ini ke dalam bahasa dan kompiler backend toolchain, kami mengalami masalah substansial dan harus mundur sedikit.

Pertama, jika Anda hanya menerjemahkan Contract.Weak.Requires hingga membutuhkan lemah dan Contract.Strong.Requires untuk kebutuhan yang kuat , menurut saya, Anda berakhir dengan cukup sintaks bahasa yang kikuk dan terspesialisasi, dengan lebih banyak kebijakan daripada membuatku nyaman. Ini segera memanggil parameterisasi dan substitusi dari lemah / kebijakan yang kuat.

Selanjutnya, pendekatan ini memperkenalkan semacam mode baru kompilasi bersyarat yang, bagi saya, terasa canggung. Di lain kata-kata, jika Anda menginginkan pemeriksaan khusus debug, Anda sudah dapat mengatakan sesuatu seperti:

  # jika DEBUG     membutuhkan X #berakhir jika  

Akhirnya - dan ini adalah paku di peti mati bagi saya - kontrak seharusnya menjadi bagian dari tanda tangan API. Apa apakah itu bahkan berarti memiliki kontrak bersyarat? Bagaimana alat bisa bernalar tentang itu? Hasilkan berbeda dokumentasi untuk debug build daripada rilis build? Selain itu, segera setelah Anda melakukan ini, Anda kehilangan jaminan kritis, yaitu kode tidak akan berjalan jika prasyaratnya tidak terpenuhi.

Akibatnya, kami memusnahkan seluruh skema kompilasi bersyarat.

Kami berakhir dengan satu jenis kontrak: kontrak yang merupakan bagian dari tanda tangan API dan diperiksa sepanjang waktu. Jika sebuah compiler dapat membuktikan kontrak tersebut terpenuhi pada waktu kompilasi - sesuatu yang kami habiskan cukup banyak energi untuk itu - itu benar bebas untuk membatalkan cek sama sekali. Tetapi kode dijamin tidak akan pernah dijalankan jika prasyaratnya tidak puas. Untuk kasus di mana Anda menginginkan pemeriksaan bersyarat, Anda selalu memiliki sistem pernyataan (dijelaskan di atas).

Saya merasa lebih baik tentang taruhan ini ketika kami menerapkan model baru dan menemukan bahwa banyak orang telah menyalahgunakan yang "lemah" dan gagasan "kuat" di atas karena kebingungan. Memaksa pengembang untuk membuat keputusan menghasilkan kode yang lebih sehat.

Arah masa depan

Sejumlah bidang pengembangan berada pada berbagai tahap kematangan saat proyek kami selesai.

Invarian

Kami banyak bereksperimen dengan invarian. Kapan pun kami berbicara dengan seseorang yang ahli dalam desain demi kontrak, mereka pasti paham borderline terkejut karena kami tidak memilikinya sejak hari pertama. Sejujurnya, desain kami memang menyertakannya sejak awal. Tapi kami tidak pernah bisa menyelesaikan implementasi dan menerapkannya. Ini sebagian hanya karena rekayasa bandwidth, tetapi juga karena beberapa pertanyaan sulit tetap ada. Dan sejujurnya tim hampir selalu puas kombinasi pra dan pasca kondisi plus pernyataan. Saya menduga bahwa dalam waktu yang penuh, kami telah menambahkan invarian untuk kelengkapan, tetapi sampai hari ini beberapa pertanyaan tetap ada untuk saya. Saya perlu melihatnya beraksi untuk sementara waktu.

Pendekatan yang kami rancang adalah di mana invarian menjadi anggota tipe penutupnya; sebagai contoh:

  Daftar kelas publik  {     private T [] array;     hitungan int pribadi;     private invariant index>=0 && index 

Perhatikan bahwa invarian ditandai pribadi . Pengubah aksesibilitas invarian mengontrol anggota yang mana invarian diminta untuk bertahan. Misalnya, invarian publik hanya harus ditahan di entri dan keluar dari fungsi dengan aksesibilitas publik ; ini memungkinkan pola umum fungsi privat sementara melanggar invarian, selama entrypoint publik mempertahankannya. Tentu saja, seperti pada contoh di atas, kelas bebas menyatakan invarian pribadi juga, yang diperlukan untuk menahan entri fungsi dan keluar.

Saya sebenarnya sangat menyukai desain ini, dan saya rasa ini akan berhasil. Perhatian utama kami semua adalah keheningan pengenalan cek di semua tempat. Sampai hari ini, bagian itu masih membuatku gugup. Misalnya, dalam Daftar Misalnya, Anda akan memiliki indeks >=0 && index periksa di awal dan akhir setiap fungsi dari jenisnya. Sekarang, compiler kami akhirnya menjadi sangat ahli dalam mengenali dan menggabungkan pemeriksaan kontrak yang berlebihan; dan ada banyak kasus di mana kehadiran kontrak benar-benar membuat kualitas kode lebih baik . Namun, secara ekstrim Contoh yang diberikan di atas, saya yakin akan ada hukuman kinerja. Itu akan memberi tekanan pada kami untuk berubah kebijakan ketika invarian diperiksa, yang mungkin akan mempersulit model kontrak secara keseluruhan.

Saya sangat berharap kami memiliki lebih banyak waktu untuk menjelajahi invarian lebih dalam. Saya tidak berpikir tim sangat kehilangan karena tidak memiliki mereka - tentu saja saya tidak mendengar banyak keluhan tentang ketidakhadiran mereka (mungkin karena tim sangat memperhatikan kinerja) - tapi saya pikir invarian akan menjadi lapisan gula yang bagus untuk digunakan pada kue kontrak.

Sistem Jenis Lanjutan

Saya selalu suka mengatakan bahwa kontrak dimulai ketika sistem tipe berhenti. Sistem tipe memungkinkan Anda untuk menyandikan atribut variabel menggunakan tipe. Jenis membatasi nilai rentang yang diharapkan yang mungkin dimiliki variabel. Kontrak juga memeriksa kisaran nilai yang dimiliki variabel. Perbedaan? Jenis dibuktikan pada saat kompilasi aturan induktif yang ketat dan dapat disusun yang cukup murah untuk memeriksa lokal ke suatu fungsi, biasanya, tetapi tidak selalu, dibantu oleh anotasi yang dibuat oleh pengembang. Kontrak dibuktikan pada waktu kompilasi jika memungkinkan dan pada waktu proses jika tidak, dan sebagai hasilnya, izinkan spesifikasi yang jauh lebih ketat menggunakan logika arbitrer yang dikodekan dalam bahasa itu sendiri.

Jenis lebih disukai, karena dijamin untuk diperiksa waktu kompilasi; dan dijamin akan cepat diperiksa. Jaminan yang diberikan kepada pengembang kuat dan keseluruhan produktivitas pengembang yang menggunakannya lebih baik.

Keterbatasan dalam sistem tipe tidak bisa dihindari; sistem tipe perlu meninggalkan beberapa ruang gerak, jika tidak dengan cepat tumbuh menjadi sulit dan tidak dapat digunakan dan, secara ekstrim, berubah menjadi bit dan byte nilai-bi. Di sisi lain, saya selalu kecewa dengan dua area ruang gerak tertentu yang membutuhkan penggunaan kontrak:

  1. Nullability.
  2. Rentang numerik.
  3. Sekitar 90% dari kontrak kami termasuk dalam dua kelompok ini. Alhasil, kami secara serius menjelajah lebih canggih ketik sistem untuk mengklasifikasikan nullability dan rentang variabel menggunakan sistem jenis alih-alih kontrak.

    Untuk membuatnya lebih konkret, inilah perbedaan antara kode ini yang menggunakan kontrak:

      buffer baca int virtual publik (char [], indeks int, hitungan int )     membutuhkan buffer!=null     membutuhkan indeks>=0     membutuhkan hitungan>=0     membutuhkan buffer.Length - index 

    Dan kode ini yang tidak perlu, namun membawa semua jaminan yang sama, diperiksa secara statis pada waktu kompilasi:

      public int virtual Read (char [] buffer) {     ... }  

    Menempatkan properti ini dalam sistem tipe secara signifikan mengurangi beban pemeriksaan kondisi kesalahan. Ayo Katakanlah untuk setiap 1 negara produsen ada 10 konsumen. Daripada memiliki masing-masing dari 10 pertahanan itu sendiri terhadap kondisi kesalahan, kami dapat mendorong tanggung jawab kembali ke 1 produsen tersebut, dan keduanya memerlukan a pernyataan tunggal yang memaksa tipe, atau bahkan lebih baik, bahwa nilai disimpan ke dalam tipe yang tepat sejak awal.

    Jenis Non-Null

    Yang pertama sangat sulit: menjamin secara statis bahwa variabel tidak mengambil null . Inilah apa Tony Hoare terkenal memanggilnya

    "kesalahan miliar dolar"

. Memperbaiki ini untuk selamanya adalah a tujuan yang benar untuk bahasa apa pun dan saya senang melihat desainer bahasa yang lebih baru menangani masalah ini secara langsung.

Banyak area bahasa yang melawan Anda dalam setiap langkah yang satu ini. Generik, inisialisasi nol, konstruktor, dan lainnya. Memperbaiki non-null menjadi bahasa yang sudah ada itu sulit!

Sistem Jenis

Singkatnya, non-nullability diringkas menjadi beberapa aturan sistem tipe sederhana:

  1. Semua tipe tanpa hiasan T bukan nol secara default.
  2. Jenis apa pun dapat dimodifikasi dengan ? , seperti di T? , untuk menandainya sebagai nullable.
  3. batal adalah nilai ilegal untuk variabel tipe bukan-null.
  4. T secara implisit mengkonversi ke T? . Dalam arti tertentu, T adalah subtipe dari T? (meskipun tidak sepenuhnya benar).
  5. Operator ada untuk mengubah T? ke T , dengan pemeriksaan waktu proses yang diabaikan pada null .
  6. Sebagian besar dari ini mungkin “jelas” dalam arti tidak banyak pilihan. Nama permainannya sistematis memastikan semua jalan null diketahui sistem tipe. Secara khusus, tidak ada null yang bisa "diam-diam" menjadi nilai dari tipe T non-null ; ini berarti menangani nol-inisialisasi, mungkin masalah tersulit dari semuanya.

    Sintaks

    Secara sintaksis, kami menawarkan beberapa cara untuk mencapai # 5, mengkonversi dari T? hingga T . Tentu saja, kami mengecilkan hati ini, dan memilih Anda untuk tetap berada di ruang "bukan nol" selama mungkin. Tapi terkadang itu tidak mungkin. Multi-langkah inisialisasi terjadi dari waktu ke waktu - terutama dengan struktur data koleksi - dan harus didukung.

    Bayangkan sejenak kita memiliki peta:

    Peta pelanggan=...;  

    Ini memberi tahu kita tiga hal berdasarkan konstruksi:

    1. Peta sendiri tidak null.
    2. The int kunci di dalamnya tidak akan null .
    3. Pelanggan nilai di dalamnya juga tidak akan null.
    4. Katakanlah sekarang pengindeks benar-benar mengembalikan null untuk menunjukkan bahwa kunci tersebut hilang:

        TValue publik? ini [TKey key] {     Dapatkan { ... } }  

      Sekarang kita perlu beberapa cara untuk memeriksa situs panggilan apakah pencarian berhasil. Kami memperdebatkan banyak sintaks.

      Yang termudah kami mendarat adalah cek yang dijaga:

      Pelanggan? pelanggan=pelanggan [id]; if (customer!=null) {     // Di sini, `customer` adalah jenis non-null` Customer`. }  

      Harus saya akui, saya selalu ragu tentang paksaan jenis "ajaib". Itu membuat saya kesal karena sulit untuk memahaminya apa yang salah saat gagal. Misalnya, ini tidak berhasil jika Anda membandingkan c dengan variabel yang menahan null nilai, hanya literal null . Tetapi sintaksnya mudah diingat dan biasanya melakukan hal yang benar.

      Pemeriksaan ini secara dinamis bercabang ke bagian logika yang berbeda jika nilainya memang null . Seringkali Anda ingin melakukannya dengan sederhana menegaskan bahwa nilainya bukan nol dan biarkan sebaliknya. Ada operator pernyataan tipe eksplisit untuk melakukan itu:

      Pelanggan? maybeCustomer=pelanggan [id]; Pelanggan pelanggan=notnull (maybeCustomer);  

      notnull operator mengubah ekspresi tipe apa pun T? menjadi ekspresi tipe T .

      Generik

      Generik sulit, karena ada beberapa tingkat nullability yang perlu dipertimbangkan. Pertimbangkan:

        kelas C {     T M publik  ();     publik T? N  (); }  var a=CM  (); var b=CM 
      (); var c=CN (); var d=CN
      ();

      Pertanyaan dasarnya adalah, apa jenis a , b , c , dan d ?

      Saya pikir kami membuat yang ini lebih sulit pada awalnya daripada yang kami butuhkan sebagian besar karena nullable C # yang ada adalah bebek yang cukup aneh dan kami teralihkan karena mencoba menirunya terlalu banyak. Kabar baiknya adalah kami akhirnya menemukan jalan kami, tetapi butuh beberapa saat.

      Untuk mengilustrasikan apa yang saya maksud, mari kita kembali ke contoh. Ada dua kubu:

      • Kamp .NET: a adalah obyek; b , c , dan d adalah objek? .
      • Kamp bahasa fungsional: a adalah objek ; pita c adalah objek? ; d adalah obyek??.

      Dengan kata lain, kamp .NET memikirkan Anda harus menciutkan urutan 1 atau lebih ? menjadi satu ? . Itu kamp bahasa fungsional - yang memahami keanggunan komposisi matematika - menghindari keajaiban dan membiarkan dunia menjadi apa adanya. Kami akhirnya menyadari bahwa rute .NET sangat kompleks, dan membutuhkan dukungan waktu proses.

      Rute bahasa fungsional memang sedikit membengkokkan pikiran Anda pada awalnya. Misalnya, contoh peta dari sebelumnya:

      Peta

      pelanggan=...; Pelanggan?? pelanggan=pelanggan [id]; if (customer!=null) { // Perhatikan, `pelanggan` masih` Pelanggan? `Di sini, dan masih bisa menjadi` null`! }

      Dalam model ini, Anda perlu mengupas satu lapisan ? pada suatu waktu. Tapi sejujurnya, ketika Anda berhenti untuk memikirkannya, itu masuk akal. Ini lebih transparan dan mencerminkan dengan tepat apa yang terjadi di bawah sini. Lebih baik tidak melawannya.

      Ada juga pertanyaan tentang implementasi. Implementasi termudah adalah dengan memperluas T? menjadi beberapa “tipe pembungkus , " suka Mungkin , lalu masukkan operasi bungkus dan buka bungkus yang sesuai. Memang, itu model mental yang masuk akal untuk mengetahui cara kerja implementasinya. Namun, ada dua alasan mengapa model sederhana ini tidak berfungsi.

      Pertama, untuk tipe referensi T , T? tidak boleh membawa barang ekstra yang boros sedikit; representasi runtime pointer dapat dijalankan null sebagai nilai, dan untuk bahasa sistem, we ingin memanfaatkan fakta ini dan menyimpan T? seefisien sebagai T . Hal ini dapat dilakukan dengan cukup mudah dengan mengkhususkan instantiation generik. Tapi ini berarti kaleng non-null tidak lagi hanya menjadi trik front-end. Ini membutuhkan dukungan kompiler back-end.

      (Perhatikan bahwa trik ini tidak begitu mudah untuk diperluas ke T ?? !)

      Kedua, Midori mendukung larik kovarian yang aman, berkat anotasi mutabilitas kami. Jika T dan T? punya perbedaan representasi fisik, bagaimanapun, kemudian mengubah T [] menjadi T? [] adalah operasi non-transformasi. Ini masih di bawah umur cacat, terutama karena array kovarian menjadi jauh kurang berguna setelah Anda memasang lubang pengaman yang sudah mereka miliki.

      Bagaimanapun, kami akhirnya membakar kapal di .NET Nullable dan menggunakan multi yang lebih mudah disusun - ? rancangan.

      Inisialisasi Nol

      Inisialisasi nol benar-benar menyebalkan. Untuk menjinakkannya berarti:

      • Semua bidang kelas yang bukan nol harus diinisialisasi pada waktu konstruksi.
      • Semua array elemen bukan-nol harus sepenuhnya diinisialisasi pada waktu konstruksi.

      Tapi semakin buruk. Dalam .NET, tipe nilai secara implisit diinisialisasi nol. Karena itu, aturan awalnya adalah:

      • Semua bidang struct harus nihil.

      Tapi itu omong kosong. Itu menginfeksi seluruh sistem dengan jenis nullable segera. Hipotesis saya adalah bahwa nullability saja benar-benar berfungsi jika nullable adalah kasus yang tidak umum (katakanlah 20%). Ini akan menghancurkannya dalam sekejap.

      Jadi kami menempuh jalur untuk menghilangkan semantik inisialisasi nol otomatis. Ini adalah perubahan yang cukup besar. (C # 6 pergi ke jalur yang memungkinkan struct untuk menyediakan konstruktor argumen nol mereka sendiri dan akhirnya harus mendukungnya keluar karena dampaknya terhadap ekosistem.) Itu bisa saja dibuat untuk bekerja tetapi membelok cukup jauh, dan mengangkat beberapa masalah lain yang mungkin kami dapatkan juga terganggu dengan. Jika saya bisa melakukannya lagi, saya hanya akan menghilangkan perbedaan nilai vs. jenis referensi semuanya di C #. Alasan untuk itu akan menjadi lebih jelas dalam posting mendatang tentang memerangi pemulung.

      Nasib Jenis Non-Null

      Kami memiliki desain yang solid, dan beberapa prototipe, tetapi tidak pernah menggunakan yang ini di seluruh sistem operasi. Itu alasan mengapa terikat pada tingkat kompatibilitas C # yang kami inginkan. Agar adil, saya mengacaukan yang ini sedikit, dan saya seandainya itu akhirnya keputusan saya. Pada masa-masa awal Midori, kami menginginkan "keakraban kognitif." Nanti hari proyek, kami benar-benar mempertimbangkan apakah semua fitur dapat dilakukan sebagai ekstensi "add on" ke C #. Itu adalah pola pikir kemudian yang mencegah kami melakukan tipe non-null dengan sungguh-sungguh. Keyakinan saya sampai hari ini adalah aditif itu anotasi tidak akan berfungsi; Spec # mencoba ini dengan ! dan polaritasnya selalu terasa terbalik. Non-null harus berupa default untuk ini memiliki dampak yang kami inginkan.

      Salah satu penyesalan terbesar saya adalah kami menunggu begitu lama pada tipe non-null. Kami hanya menjelajahinya dengan sungguh-sungguh sekali kontrak adalah kuantitas yang diketahui, dan kami perhatikan ribuan membutuhkan x!=null ada di semua tempat. Itu akan menjadi kompleks dan mahal, namun ini akan menjadi kombinasi yang sangat mematikan jika kita menukarkan jenis nilainya perbedaan pada saat yang sama. Hidup dan belajar!

      Jika kami mengirimkan bahasa kami sebagai bahasa mandiri, berbeda dari C #, saya yakin ini akan berhasil.

      Jenis Rentang

      Kami memiliki desain untuk menambahkan jenis jangkauan ke C #, tetapi selalu satu langkah di luar batas kompleksitas saya.

      Ide dasarnya adalah bahwa semua tipe numerik dapat diberi parameter tipe batas bawah dan atas. Misalnya, Anda pernah bilangan bulat yang hanya dapat menampung angka 0 hingga 1.000.000, secara eksklusif. Bisa dikatakan sebagai int . Tentu saja, ini menunjukkan bahwa Anda mungkin harus menggunakan uint sebagai gantinya dan kompilator akan memperingatkan Anda. Faktanya, kumpulan angka lengkap dapat direpresentasikan secara konseptual sebagai rentang dengan cara ini:

        nomor byte typedef ; nomor sbyte typedef ; nomor pendek typedef ; nomor ushort typedef ; nomor int typedef ; nomor uint typedef 

      ; // Dan seterusnya ...

      Bagian yang benar-benar “keren” - tetapi rumit yang menakutkan - kemudian digunakan jenis dependen untuk mengizinkan parameter rentang simbolik. Misalnya, saya memiliki array dan ingin meneruskan indeks yang jangkauannya dijamin berada di dalam batas. Biasanya saya akan menulis:

        T Dapatkan (T [] array, int indeks)         membutuhkan index>=0 && index 

      Atau mungkin saya akan menggunakan uint untuk menghilangkan paruh pertama cek:

        T Dapatkan (T [] array, indeks uint)         indeks 

      Dengan tipe rentang tertentu, saya dapat mengaitkan batas atas rentang angka dengan panjang array secara langsung:

        T Dapatkan (T [] array, angka  indeks) {     kembali array [index]; }  

      Tentu saja, tidak ada jaminan bahwa compiler akan menghilangkan pemeriksaan batas, jika Anda entah bagaimana membuat analisis aliasnya tersandung. Tapi kami berharap bahwa pekerjaan dengan jenis ini tidak lebih buruk daripada dengan pemeriksaan kontrak normal. Dan memang ini Pendekatan adalah pengkodean informasi yang lebih langsung dalam sistem tipe.

      Bagaimanapun, saya masih menganggap yang ini sebagai ide yang keren, tapi yang masih dalam ranah "menyenangkan untuk dimiliki tetapi tidak kritis."

      Aspek "tidak kritis" terutama benar berkat slice yang menjadi kelas satu dalam sistem tipe. Saya akan mengatakan 66% atau lebih dari situasi di mana pemeriksaan jarak digunakan akan lebih baik ditulis menggunakan irisan. Saya pikir kebanyakan orang begitu masih terbiasa memilikinya sehingga mereka akan menulis hal standar C # daripada hanya menggunakan sepotong. Saya akan membahasnya irisan di posting yang akan datang, tetapi mereka menghilangkan kebutuhan untuk pemeriksaan rentang penulisan sama sekali di sebagian besar kode.

      Pengabaian bukanlah satu-satunya cerita, tentu saja. Masih banyak situasi yang sah di mana kesalahan programmer dapat pulih dari kejadian. Contohnya termasuk:

      • I / O File.
      • Jaringan I / O.
      • Mengurai data (misalnya, pengurai kompiler).
      • Memvalidasi data pengguna (misalnya, pengiriman formulir web).

      Dalam setiap kasus ini, Anda biasanya tidak tidak ingin memicu pengabaian saat menghadapi masalah. Sebaliknya, Program mengharapkannya terjadi dari waktu ke waktu, dan perlu menghadapinya dengan melakukan sesuatu yang masuk akal. Seringkali oleh mengkomunikasikannya kepada seseorang: pengguna mengetik ke dalam halaman web, administrator sistem, pengembang menggunakan file alat, dll. Tentu saja, pengabaian adalah salah satu metode panggilan jika itu adalah tindakan yang paling tepat untuk dilakukan, tetapi sering kali terlalu drastis untuk situasi ini. Dan, terutama untuk IO, ini berisiko membuat sistem menjadi sangat rapuh. Membayangkan jika program yang Anda gunakan memutuskan untuk mengedipkan mata setiap kali koneksi jaringan Anda menjatuhkan paket!

      Masukkan Pengecualian

      Kami menggunakan pengecualian untuk kesalahan yang dapat dipulihkan. Bukan jenis yang tidak dicentang, dan juga bukan jenis yang dicentang di Java.

      Hal pertama yang pertama: meskipun Midori memiliki pengecualian, metode yang tidak dijelaskan sebagai lemparan tidak akan pernah bisa melempar. Tidak pernah. Tidak ada RuntimeException licik seperti di Java, misalnya. Kami toh tidak membutuhkannya, karena situasi yang sama yang digunakan Java untuk pengecualian waktu proses adalah menggunakan pengabaian di Midori.

      Hal ini menyebabkan sifat ajaib dari sistem hasil. 90-sesuatu% dari fungsi di sistem kami tidak bisa melempar pengecualian! Secara default, nyatanya mereka tidak bisa. Ini sangat kontras dengan sistem seperti C ++ di mana Anda harus keluar cara Anda untuk tidak melakukan pengecualian dan menyatakan fakta itu menggunakan noexcept . API masih bisa gagal karena pengabaian, tentu saja, tetapi hanya jika penelepon gagal memenuhi kontrak yang dinyatakan, mirip dengan menyampaikan argumen jenis yang salah.

      Pilihan pengecualian kami kontroversial pada awalnya. Kami memiliki campuran imperatif, prosedural, berorientasi objek, dan perspektif bahasa fungsional di tim. Pemrogram C ingin menggunakan kode kesalahan dan khawatir kami akan menggunakannya membuat ulang Java, atau lebih buruk lagi, desain C #. Perspektif fungsional akan menggunakan aliran data untuk semua kesalahan, tetapi pengecualian sangat berorientasi pada aliran kontrol. Pada akhirnya, saya pikir apa yang kami pilih adalah kompromi yang bagus antara semua tersedia model kesalahan yang dapat dipulihkan untuk kami. Seperti yang akan kita lihat nanti, kami memang menawarkan mekanisme untuk memperlakukan kesalahan sebagai nilai kelas satu untuk kasus yang jarang terjadi di mana gaya pemrograman aliran data yang lebih banyak adalah apa yang diinginkan pengembang.

      Namun yang paling penting, kami menulis banyak kode dalam model ini, dan itu bekerja dengan sangat baik bagi kami. Bahkan fungsional orang-orang bahasa akhirnya datang. Seperti yang dilakukan pemrogram C, berkat beberapa petunjuk yang kami ambil dari kode pengembalian.

      Bahasa dan Jenis Sistem

      Pada titik tertentu, saya membuat pengamatan dan keputusan yang kontroversial. Sama seperti Anda tidak akan mengubah jenis kembalian fungsi dengan ekspektasi dampak kompatibilitas nol, Anda tidak boleh mengubah jenis pengecualian fungsi dengan harapan. Dengan kata lain, pengecualian, seperti kode kesalahan, hanyalah jenis nilai pengembalian yang berbeda!

      Ini telah menjadi salah satu argumen parroted terhadap pengecualian yang dicentang. Jawaban saya mungkin terdengar basi, tetapi sederhana: sangat buruk. Anda menggunakan bahasa pemrograman yang diketik secara statis, dan sifat dinamis dari pengecualian adalah alasan mereka payah. Kami berusaha untuk mengatasi masalah ini, jadi oleh karena itu kami menerimanya, memperindah pengetikan yang kuat, dan tidak pernah melihat ke belakang. Ini saja membantu menjembatani kesenjangan antara kode kesalahan dan pengecualian.

      Pengecualian yang diberikan oleh suatu fungsi menjadi bagian dari tanda tangannya, sama seperti parameter dan nilai kembaliannya. Ingat, karena sifat pengecualian yang langka dibandingkan dengan pengabaian, ini tidak sesakit yang Anda kira. Dan banyak sifat intuitif mengalir secara alami dari keputusan ini.

      Hal pertama adalah

      Prinsip substitusi Liskov

      . Di untuk menghindari kekacauan yang ditemukan oleh C ++, semua "pemeriksaan" harus terjadi secara statis, pada waktu kompilasi. Sebagai Hasilnya, semua masalah kinerja tersebut disebutkan dalam

makalah WG21 tidak bermasalah kami. Sistem tipe ini harus antipeluru, bagaimanapun, tanpa pintu belakang untuk mengalahkannya. Karena kami perlu mengatasi tantangan kinerja tersebut dengan tergantung pada anotasi lemparan di kompiler pengoptimalan kami, keamanan jenis bergantung pada properti ini.

Kami mencoba banyak sintaks yang berbeda. Sebelum kami berkomitmen untuk mengubah bahasa, kami melakukan semuanya dengan C # atribut dan analisis statis. Pengalaman pengguna tidak terlalu baik dan sulit untuk melakukan sistem tipe nyata seperti itu. Selain itu, terasa terlalu menyatu. Kami bereksperimen dengan pendekatan dari proyek Redhawk – yang akhirnya menjadi .NET Native dan CoreRT – namun, pendekatan itu juga tidak memanfaatkan bahasanya dan mengandalkan analisis statis, meskipun memiliki banyak prinsip yang sama dengan solusi akhir kami.

Inti dasar dari sintaks terakhir adalah menyatakan metode lemparan sebagai bit tunggal:

  batal melempar Foo () {     ... }  

(Selama bertahun-tahun, kami benar-benar melakukan lemparan di awal metode, tetapi itu salah dibaca.)

Pada titik ini, masalah substitusi cukup sederhana. Fungsi lemparan tidak dapat menggantikan fungsi non- melempar fungsi (penguatan ilegal). Fungsi non – melempar , di sisi lain, dapat tempat lemparan fungsi (pelemahan hukum). Ini jelas berdampak pada penggantian virtual, implementasi antarmuka, dan lambda.

Tentu saja, kami melakukan lonceng dan peluit substitusi bersama dan kontravarian yang diharapkan. Misalnya, jika Foo adalah virtual dan Anda mengesampingkannya tetapi tidak memberikan pengecualian, Anda tidak perlu menyatakan lemparan kontrak. Siapa saja menjalankan fungsi seperti itu secara virtual, tentu saja, tidak dapat memanfaatkan ini tetapi panggilan langsung dapat.

Misalnya, ini legal:

  basis kelas {     public virtual void Foo () melempar {...} }  class Berasal: Base {     // Penerapan khusus saya tidak perlu membuang:     public override void Foo () {...} }  

dan penelepon Berasal dapat memanfaatkan kurangnya lemparan ; sedangkan ini sepenuhnya ilegal:

  basis kelas {     public virtual void Foo () {...} }  class Berasal: Base {     public override void void Foo () melempar {...} }  

Mendorong mode kegagalan tunggal cukup membebaskan. Sejumlah besar kerumitan yang datang dengan pemeriksaan Java pengecualian segera menguap. Jika Anda melihat sebagian besar API yang gagal, mereka tetap memiliki mode kegagalan tunggal (sekali semua mode kegagalan bug dilakukan dengan pengabaian): IO gagal, penguraian gagal, dll. Dan banyak tindakan pemulihan yang dilakukan pengembang cenderung menulis tidak benar-benar bergantung pada spesifikasi apa sebenarnya gagal ketika, katakanlah, melakukan IO. (Beberapa melakukannya, dan bagi mereka, pola penjaga seringkali merupakan jawaban yang lebih baik; lebih lanjut tentang topik ini segera.) Sebagian besar informasi di pengecualian modern tidak sebenarnya ada untuk penggunaan terprogram; sebagai gantinya, mereka untuk diagnostik.

Kami terjebak hanya dengan “mode kegagalan tunggal” ini selama 2-3 tahun. Akhirnya saya membuat keputusan kontroversial untuk mendukung beberapa mode kegagalan. Itu tidak umum, tetapi permintaan cukup sering muncul dari rekan satu tim, dan skenario tampaknya sah dan berguna. Itu memang datang dengan mengorbankan kompleksitas sistem tipe, tetapi hanya di semua yang biasa cara subtipe. Dan skenario yang lebih canggih – seperti dibatalkan (lebih lanjut tentang itu nanti) – mengharuskan kami melakukan ini.

Sintaksnya terlihat seperti ini:

  int Foo () menampilkan FooException, BarException {     ... }  

Jadi, dalam arti tertentu, single melempar adalah jalan pintas untuk melempar Pengecualian .

Sangat mudah untuk “melupakan” detail ekstra jika Anda tidak peduli. Misalnya, mungkin Anda ingin mengikat lambda ke di atas Foo API, tetapi tidak ingin penelepon peduli tentang FooException atau BarException . Lambda itu harus ditandai melempar , tentu saja, tetapi tidak diperlukan detail lebih lanjut. Ini ternyata merupakan pola yang sangat umum: Sistem internal akan menggunakan pengecualian yang diketik seperti ini untuk aliran kontrol internal dan penanganan kesalahan, tetapi menerjemahkan semuanya menjadi adil polos melempar di batas publik API, di mana ekstra detail tidak diperlukan.

Semua pengetikan tambahan ini menambahkan kekuatan besar untuk memulihkan kesalahan. Tetapi jika jumlah kontrak melebihi pengecualian sebesar 10: 1, kemudian metode yang luar biasa lemparan melebihi jumlah metode multi-kegagalan dengan 10: 1 lainnya.

Pada titik ini, Anda mungkin bertanya-tanya, apa yang membedakan ini dari pengecualian yang dicentang di Java?

  1. Fakta bahwa sebagian besar kesalahan diekspresikan menggunakan pengabaian berarti sebagian besar API tidak membuang.

  2. Fakta bahwa kami mendorong mode kegagalan tunggal sangat menyederhanakan seluruh sistem. Apalagi kami membuatnya mudah untuk beralih dari dunia berbagai mode, ke satu mode dan kembali lagi.

  3. Dukungan sistem tipe kaya seputar pelemahan dan penguatan juga membantu, begitu pula hal lain yang kami lakukan untuk membantu menjembatani kesenjangan antara kode pengembalian dan pengecualian, peningkatan pemeliharaan kode, dan banyak lagi…

    Callsites Mudah Diaudit

    Pada tahap cerita ini, kami masih belum mencapai sintaks kode kesalahan eksplisit penuh. Deklarasi fungsi mengatakan apakah mereka dapat gagal (baik), tetapi pemanggil fungsi tersebut masih mewarisi aliran kontrol diam (buruk).

    Hal ini menghasilkan sesuatu yang selalu saya sukai dari model pengecualian kami. Sebuah situs panggilan perlu mengatakan coba :

    Ini memanggil fungsi Foo , menyebarkan kesalahannya jika terjadi, dan menetapkan nilai kembali ke nilai jika tidak.

    Ini memiliki properti luar biasa: semua aliran kontrol tetap eksplisit dalam program. Anda dapat menganggap coba sebagai semacam pengembalian bersyarat (atau bersyarat lempar jika Anda mau). Saya sangat menyukai betapa lebih mudahnya membuat kode ini meninjau logika kesalahan! Misalnya, bayangkan fungsi panjang dengan beberapa coba di dalamnya dari itu; memiliki yang eksplisit penjelasan membuat titik-titik kegagalan, dan karena itu aliran kontrol, semudah memilih kembali pernyataan:

      batal melakukan doSomething () lemparan {     bla ();     var x=bla_blah (bla ());     var y=coba blah (); //  

    Jika Anda memiliki penyorotan sintaks di editor Anda, maka coba tebal dan biru, bahkan lebih baik.

    Ini memberikan banyak manfaat kuat dari kode pengembalian, tetapi tanpa semua bagasi.

    (Baik Rust dan Swift sekarang mendukung sintaks yang serupa. Saya harus mengakui, saya sedih kami tidak mengirimkan ini ke masyarakat umum tahun ag Hai. Implementasinya sangat berbeda, namun anggap ini sebagai mosi percaya yang besar dalam sintaksis mereka.)

    Tentu saja, jika Anda coba di fungsi yang melempar seperti ini, ada dua kemungkinan:

  • Pengecualian lolos dari fungsi panggilan.
  • Ada sekitar coba /menangkap blok yang menangani kesalahan.

Dalam kasus pertama, Anda diminta untuk menyatakan yang juga dilemparkan oleh fungsi Anda . Terserah Anda apakah akan menyebarkan atau tidak informasi pengetikan yang kuat jika callee menyatakannya, atau hanya memanfaatkan satu lemparan sedikit, tentu saja.

Dalam kasus kedua, kami tentu saja memahami semua informasi pengetikan. Akibatnya, jika Anda mencoba menangkap sesuatu yang tidak dinyatakan sebagai dibuang, kami dapat memberikan Anda kesalahan tentang kode mati. Ini adalah satu lagi kontroversial berangkat dari sistem pengecualian klasik. Itu selalu mengganggu saya bahwa catch (FooException) pada dasarnya menyembunyikan tes tipe dinamis. Apakah Anda akan mengizinkan seseorang untuk memanggil API yang mengembalikan hanya objek dan secara otomatis menetapkan yang mengembalikan nilai ke variabel yang diketik? Tidak! Jadi, kami juga tidak mengizinkan Anda melakukannya dengan pengecualian.

Di sini CLU juga memengaruhi kami. Liskov membicarakan hal ini di

Sejarah CLU :

Mekanisme CLU tidak biasa dalam perlakuannya terhadap pengecualian yang tidak tertangani. Sebagian besar mekanisme melewati ini: jika pemanggil tidak menangani pengecualian yang dimunculkan oleh prosedur yang dipanggil, pengecualian tersebut disebarkan ke pemanggilnya, dan seterusnya. Kita menolak pendekatan ini karena tidak sesuai dengan ide kami tentang konstruksi program modular. Kami ingin bisa memanggil prosedur yang hanya mengetahui spesifikasinya, bukan implementasinya. Namun, jika pengecualian disebarkan secara otomatis, suatu prosedur dapat memunculkan pengecualian yang tidak dijelaskan dalam spesifikasinya.

Meskipun kami tidak menyarankan lebar coba , ini secara konseptual adalah jalan pintas untuk menyebarkan kode kesalahan. Untuk melihat apa Maksud saya, pertimbangkan apa yang akan Anda lakukan dalam sistem dengan kode kesalahan. Di Go, Anda dapat mengucapkan yang berikut:

  jika err:=doSomething (); err!=nil {     kembali salah }  

Dalam sistem kami, Anda mengatakan:

Tapi kami menggunakan pengecualian, bisa dibilang! Benar-benar berbeda! Tentu, sistem runtime berbeda. Tapi dari a bahasa perspektif “semantik”, mereka isomorfik. Kami mendorong orang untuk berpikir dalam kerangka kode kesalahan dan bukan pengecualian yang mereka tahu dan cintai. Ini mungkin tampak lucu: Mengapa tidak menggunakan kode pengembalian saja, Anda mungkin bertanya-tanya? Dalam sebuah Bagian yang akan datang, saya akan menjelaskan isomorfisme sebenarnya dari situasi tersebut untuk mencoba meyakinkan Anda tentang pilihan kami.

Gula Sintaksis

Kami juga menawarkan beberapa gula sintaksis untuk menangani kesalahan. The mencoba / catch konstruksi pelingkupan blok agak sedikit bertele-tele, terutama jika Anda mengikuti praktik terbaik yang kami maksudkan dalam menangani kesalahan serelokal mungkin. Juga masih mempertahankan sedikit perasaan malang untuk beberapa orang, terutama jika Anda berpikir tentang kode pengembalian. Itu memberi cara untuk jenis yang kami sebut Hasil , yang sebelumnya baik a T nilai atau Pengecualian .

Ini pada dasarnya menjembatani dari dunia aliran kontrol ke dunia aliran data, untuk skenario di mana yang terakhir itu lebih alami. Keduanya tentu memiliki tempatnya, meskipun sebagian besar pengembang lebih menyukai sintaks aliran kontrol yang sudah dikenal.

Untuk mengilustrasikan penggunaan umum, bayangkan Anda ingin mencatat semua kesalahan yang terjadi, sebelum menyebarkan ulang pengecualian. Meskipun Ini adalah pola yang umum, menggunakan coba / blok tangkap terasa agak terlalu mengontrol aliran yang berat untuk selera saya:

di TV; coba {     v=coba Foo ();     // Mungkin beberapa hal lagi ... } catch (Exception e) {     Log (e);     melempar kembali; } // Gunakan nilai `v` ...  

Bagian “mungkin beberapa hal lagi” membujuk Anda untuk memeras lebih dari yang seharusnya ke dalam percobaan blokir. Bandingkan ini dengan menggunakan Hasil, yang mengarah ke nuansa kode pengembalian yang lebih banyak dan penanganan lokal yang lebih nyaman:

Percobaan ... lain konstruksi juga memungkinkan Anda untuk mengganti nilai Anda sendiri, atau bahkan memicu pengabaian, di respons terhadap kegagalan:

  int value1=coba Foo () lain 42; int value2=coba Foo () else Release.Fail ();  

Kami juga mendukung propagasi kesalahan aliran data gaya NaN dengan mengangkat akses ke T anggota dari Hasil . Misalnya, saya memiliki dua Hasil dan ingin menambahkannya bersama. Saya bisa melakukannya:

Perhatikan bahwa baris ketiga, di mana kita menambahkan dua Hasil bersama-sama, menghasilkan – benar – ketiga Hasil . Ini adalah propagasi aliran data gaya NaN, mirip dengan C # baru .? fitur.

Pendekatan ini memadukan apa yang menurut saya merupakan campuran elegan dari pengecualian, kode pengembalian, dan penyebaran kesalahan aliran data.

Penerapan

Model yang baru saja saya jelaskan tidak harus diterapkan dengan pengecualian. Ini cukup abstrak untuk menjadi masuk akal diimplementasikan menggunakan pengecualian atau kode pengembalian. Ini tidak teoretis. Kami benar-benar mencobanya. Dan inilah yang mengarahkan kami untuk memilih pengecualian daripada mengembalikan kode karena alasan kinerja.

Untuk mengilustrasikan bagaimana implementasi kode kembali dapat bekerja, bayangkan beberapa transformasi sederhana:

  int foo () melempar {     jika (... p ...) {         melempar Exception baru ();     }     kembali 42; }  

menjadi:

Dan kode seperti ini:

menjadi seperti ini:

Kompiler yang mengoptimalkan dapat merepresentasikan hal ini dengan lebih efisien, menghilangkan penyalinan yang berlebihan. Apalagi dengan inlining.

Jika Anda mencoba membuat model coba /menangkap/ akhirnya dengan cara yang sama, mungkin menggunakan goto , Anda akan segera mengetahui mengapa kompiler memilikinya kesulitan mengoptimalkan dengan adanya pengecualian yang tidak dicentang. Semua tepi aliran kontrol tersembunyi itu!

Bagaimanapun juga, latihan ini dengan sangat jelas menunjukkan kelemahan dari kode pengembalian. Semua kesalahan itu – yang dimaksudkan untuk jarang dibutuhkan (dengan asumsi, tentu saja, kegagalan itu jarang) – berada di jalur yang tepat, menyia-nyiakan jalur emas program Anda kinerja. Ini melanggar salah satu prinsip terpenting kami.

Saya menjelaskan hasil percobaan mode ganda kami di posting terakhir saya . Singkatnya, pendekatan pengecualian adalah 7% lebih kecil dan 4% lebih cepat sebagai geomean di seluruh tolok ukur utama kami, berkat beberapa hal:

  • Tidak ada dampak konvensi panggilan.
  • Tidak ada selai kacang yang terkait dengan pembungkus nilai pengembalian dan percabangan pemanggil.
  • Semua fungsi melempar dikenal dalam sistem tipe, memungkinkan gerakan kode yang lebih fleksibel.
  • Semua fungsi lemparan dikenal dalam sistem tipe, memberi kita pengoptimalan EH baru, seperti mencoba / akhirnya blok menjadi kode garis lurus ketika percobaan tidak bisa melempar.

Ada aspek pengecualian lain yang membantu kinerja . Saya sudah menyebutkan bahwa kami tidak merendahkan callstack mengumpulkan metadata seperti yang dilakukan kebanyakan sistem pengecualian. Kami menyerahkan diagnostik ke subsistem diagnostik kami. Namun, pola umum lainnya yang membantu adalah meng-cache pengecualian sebagai objek yang dibekukan, sehingga setiap lemparan tidak membutuhkan alokasi:

  const Exception retryLayout=new Exception (); ... lempar retryLayout;  

Untuk sistem dengan tingkat melempar dan menangkap yang tinggi – seperti pada parser kami, kerangka kerja FRP UI, dan area lainnya – ini penting untuk kinerja yang baik. Dan ini menunjukkan mengapa kita tidak bisa begitu saja menganggap “pengecualian lambat” sebagai diberikan.

Pola

Sejumlah pola berguna muncul yang kami hiasi dalam bahasa dan perpustakaan kami.

Konkurensi

Kembali pada tahun 2007, saya menulis catatan ini tentang konkurensi dan pengecualian

. Saya menulisnya terutama dari perspektif paralel, komputasi memori bersama, namun tantangan serupa ada di semua pola orkestrasi bersamaan. Masalah dasar adalah cara pengecualian diterapkan mengasumsikan tumpukan tunggal dan berurutan, dengan mode kegagalan tunggal. Di sebuah sistem bersamaan, Anda memiliki banyak tumpukan dan banyak mode kegagalan, di mana 0, 1, atau banyak mungkin terjadi “sekaligus”.

Peningkatan sederhana yang dibuat oleh Midori hanyalah memastikan semua Pengecualian – infrastruktur terkait menangani kasus dengan beberapa kesalahan dalam. Setidaknya seorang programmer tidak dipaksa untuk memutuskan untuk membuang 1/N dari kegagalan informasi, seperti yang didorong oleh kebanyakan sistem pengecualian saat ini. Lebih dari itu, bagaimanapun, penjadwalan dan perayapan tumpukan kami infrastruktur pada dasarnya tahu tentang tumpukan bergaya kaktus, berkat model asinkron kami, dan apa yang harus dilakukan dengannya.

Awalnya, kami tidak mendukung pengecualian lintas batas asinkron. Namun, akhirnya, kami memperluas kemampuan untuk menyatakan lemparan , bersama dengan klausa pengecualian yang diketik opsional, di seluruh proses asinkron batas. Ini membawa a model pemrograman yang kaya dan diketik hingga model pemrograman aktor asinkron dan terasa seperti ekstensi alami. Ini meminjam halaman dari penerus CLU,

Argus .

Infrastruktur diagnostik kami memperindah ini untuk memberi pengembang pengalaman debugging dengan lintas proses yang menyeluruh kausalitas dalam tampilan tumpukan mereka. Tidak hanya tumpukan kaktus dalam sistem yang sangat serentak, tetapi sering diolesi melintasi batas-batas proses pesan. Mampu men-debug sistem dengan cara ini adalah penghemat waktu yang besar.

Batal

Terkadang subsistem perlu “keluar dari Dodge”. Pengabaian adalah sebuah opsi, tetapi hanya untuk menanggapi bug. Dan tentu saja tidak ada dalam proses tersebut yang dapat menghentikannya. Bagaimana jika kita ingin membatalkan callstack ke beberapa titik, tahukah Anda bahwa tidak ada seorang pun di tumpukan yang akan menghentikan kami, tetapi kemudian memulihkan dan terus melanjutkan proses yang sama?

Pengecualian mendekati apa yang kami inginkan di sini. Namun sayangnya, kode di tumpukan dapat menangkap pengecualian dalam penerbangan, dengan demikian secara efektif menekan aborsi. Kami menginginkan sesuatu yang tidak dapat ditekan.

Masukkan dibatalkan. Kami menemukan abort terutama untuk mendukung framework UI kami yang menggunakan Pemrograman Reaktif Fungsional (FRP), meskipun polanya muncul di beberapa titik. Saat penghitungan ulang FRP terjadi, mungkin saja peristiwa itu terjadi akan memasuki sistem, atau penemuan baru dibuat, yang membatalkan penghitungan ulang saat ini. Jika itu terjadi – biasanya jauh di dalam beberapa perhitungan yang tumpukannya merupakan interleaving kode pengguna dan sistem – mesin FRP yang dibutuhkan untuk segera kembali ke atas tumpukannya agar dapat memulai penghitungan ulang dengan aman. Terima kasih untuk semua kode pengguna itu pada tumpukan yang berfungsi murni, membatalkannya di tengah aliran itu mudah. Tidak ada efek samping yang salah akan tertinggal. Dan semua kode mesin yang ditelusuri diaudit dan diperkuat secara menyeluruh, berkat pengecualian yang diketik, untuk memastikan invarian dipertahankan.

Desain abort meminjam halaman dari

pedoman kemampuan . Pertama, kami memperkenalkan tipe dasar yang disebut AbortException . Ini dapat digunakan secara langsung atau subclass. Salah satunya istimewa: tidak ada yang bisa menangkap-dan-mengabaikannya. Itu Exception dijalankan ulang secara otomatis di akhir blok catch apa pun yang mencoba menangkapnya. Kami berkata seperti itu pengecualian adalah tidak dapat disangkal .

Tapi seseorang harus melakukan aborsi. Ide keseluruhannya adalah untuk keluar dari konteks, bukan menghancurkan keseluruhan proses a la pengabaian. Dan di sinilah kemampuan memasuki gambaran. Inilah bentuk dasar AbortException :

  kelas publik yang tidak dapat diubah AbortException: Exception {     public AbortException (token objek tetap);     public void Reset (token objek tidak berubah);     // Anggota tidak menarik lainnya dihilangkan ... }  

Perhatikan bahwa, pada saat konstruksi, token yang tidak dapat diubah disediakan; untuk menekan lemparan, Reset adalah dipanggil, dan token yang cocok harus disediakan. Jika token tidak cocok, akan terjadi pengabaian. Idenya adalah itu pihak yang melempar dan menangkap yang dimaksudkan dari suatu aborsi biasanya sama, atau setidaknya bersekongkol satu sama lain, sehingga berbagi token dengan aman satu sama lain mudah dilakukan . Ini adalah contoh yang bagus dari objek sebagai kemampuan tak tergoyahkan dalam aksi.

Dan ya, potongan kode yang berubah-ubah pada tumpukan dapat memicu pengabaian, tetapi kode tersebut sudah dapat melakukannya dengan cukup dereferencing null . Teknik ini melarang eksekusi dalam konteks pembatalan yang mungkin sebenarnya tidak dilakukan siap untuk itu.

Framework lain memiliki pola serupa. .NET Framework memiliki ThreadAbortException yang juga tidak dapat disangkal kecuali Anda memanggil Thread.ResetAbort ; sayangnya, karena ini bukan berbasis kemampuan, kombinasi yang canggung dari anotasi keamanan dan hosting API diperlukan untuk menghentikan menelan abort yang tidak disengaja. Lebih sering, ini tidak dicentang.

Berkat pengecualian yang tidak dapat diubah, dan token di atas tidak dapat diubah, pola yang umum adalah men-cache orang-orang ini dalam variabel statis dan menggunakan lajang. Sebagai contoh:

  kelas MyComponent {     objek const abortToken=objek baru ();     const AbortException abortException=new AbortException (abortToken);      void Abort () melempar AbortException {         membuang abortException;     }      void TopOfTheStack () {         sementara (benar) {             // Lakukan sesuatu yang memanggil jauh ke dalam beberapa tumpukan panggilan;             // jauh di lubuk hati mungkin Batalkan, yang kita tangkap dan setel ulang:             biarkan hasil=coba ... lain tangkap ;             if (result.IsFailed) {                 result.Exception.Reset (abortToken);             }         }     } }  

Pola ini membuat pembatalan menjadi sangat efisien. Penghitungan ulang FRP rata-rata dibatalkan beberapa kali. Ingat, FRP adalah tulang punggung semua UI dalam sistem, jadi kelambatan yang sering dikaitkan dengan pengecualian jelas tidak dapat diterima. Bahkan mengalokasikan objek pengecualian akan sangat disayangkan, karena tekanan GC berikutnya.

Ikut serta dalam API “Coba”

Saya menyebutkan sejumlah operasi yang terbengkalai jika gagal. Itu termasuk mengalokasikan memori, melakukan aritmatika operasi yang meluap atau dibagi-nol, dll. Dalam beberapa contoh ini, sebagian kecil dari penggunaan yang sesuai untuk penyebaran dan pemulihan kesalahan dinamis, bukan pengabaian. Bahkan jika pengabaian lebih baik dalam kasus umum.

Ini ternyata sebuah pola. Tidak terlalu umum, tapi muncul. Hasilnya, kami memiliki seluruh rangkaian aritmatika API yang menggunakan propagasi gaya aliran data harus meluap, NaN, atau banyak hal terjadi.

Saya juga sudah menyebutkan contoh konkret dari ini sebelumnya, yaitu kemampuan untuk mencoba yang baru alokasi, saat OOM menghasilkan kesalahan yang dapat dipulihkan alih-alih ditinggalkan. Ini sangat tidak umum, tetapi dapat muncul jika Anda ingin, katakanlah, mengalokasikan buffer yang besar untuk beberapa operasi multimedia.

Penjaga

Pola terakhir yang akan saya bahas disebut pola penjaga .

Dalam banyak hal, cara penanganan pengecualian yang dapat dipulihkan adalah “luar dalam”. Sekelompok kode disebut, lewat argumen di bawah callstack, sampai akhirnya beberapa kode tercapai yang menganggap status itu tidak dapat diterima. Dalam pengecualian model, aliran kontrol kemudian disebarkan kembali ke callstack, melepaskannya, sampai beberapa kode ditemukan yang menangani kesalahan. Pada titik itu jika operasi akan dicoba lagi, urutan panggilan harus diterbitkan ulang, dll.

Pola alternatif adalah dengan menggunakan penjaga. Penjaga adalah objek yang memahami bagaimana memulihkan dari kesalahan “di situ, ”sehingga callstack tidak perlu dibatalkan. Sebaliknya, kode yang seharusnya memunculkan pengecualian berkonsultasi dengan penjaga, yang menginstruksikan kode bagaimana melanjutkan. Aspek yang bagus dari penjaga adalah sering, ketika dilakukan sebagai a kemampuan terkonfigurasi, kode di sekitar bahkan tidak perlu mengetahuinya – tidak seperti pengecualian yang, di sistem kami, harus dideklarasikan sebagai bagian dari sistem tipe. Aspek lain dari penjaga adalah bahwa mereka sederhana dan murah.

Keeper di Midori dapat digunakan untuk operasi yang cepat, tetapi lebih sering menjangkau batas asinkron.

Contoh kanonik penjaga adalah salah satu operasi sistem file yang menjaga. Mengakses file dan direktori pada sebuah file sistem biasanya memiliki mode kegagalan seperti:

  • Spesifikasi jalur tidak valid.
  • Berkas tidak ditemukan.
  • Direktori tidak ditemukan.
  • File sedang digunakan.
  • Hak istimewa tidak cukup.
  • Media penuh.
  • Media yang dilindungi dari penulisan.

Salah satu opsinya adalah memberi anotasi pada setiap API sistem file dengan file melempar klausa untuk masing-masing. Atau, seperti Java, untuk membuat file IOException hierarki dengan masing-masing sebagai subclass. Alternatifnya adalah dengan menggunakan penjaga. Ini memastikan keseluruhan aplikasi tidak perlu mengetahui atau peduli tentang kesalahan IO, sehingga logika pemulihan dapat dipusatkan. Penjaga seperti itu antarmuka mungkin terlihat seperti ini:

  antarmuka asinkron IFileSystemKeeper {     Async string InvalidPathSpecification (jalur string) melempar;     Async string FileNotFound (jalur string) melempar;     async string DirectoryNotFound (jalur string) melempar;     Async string FileInUse (jalur string) melempar;     async Credentials InsufficientPrivileges (Credentials creds, string path) melempar;     string asinkron MediaFull (jalur string) melempar;     string asinkron MediaWriteProtected (jalur string) melempar; }  

Idenya adalah, dalam setiap kasus, masukan yang relevan diberikan kepada penjaga ketika terjadi kegagalan. Penjaga itu kemudian diizinkan untuk melakukan operasi, mungkin asinkron, untuk memulihkan. Dalam banyak kasus, penjaga dapat kembali secara opsional argumen yang diperbarui untuk operasi tersebut. Misalnya, InsufficientPrivileges dapat mengembalikan alternatif Kredensial kepada menggunakan. (Mungkin program meminta pengguna dan dia beralih ke akun dengan akses tulis.) Dalam setiap kasus yang ditampilkan, file keeper dapat membuat pengecualian jika tidak ingin menangani error tersebut, meskipun bagian dari pola ini opsional.

Akhirnya, saya harus mencatat bahwa sistem Penanganan Pengecualian Terstruktur (SEH) Windows mendukung pengecualian “berkelanjutan” yang secara konseptual berusaha mencapai hal yang sama ini. Mereka membiarkan beberapa kode memutuskan bagaimana memulai kembali kesalahan tersebut komputasi. Sayangnya, mereka selesai menggunakan penangan ambien di callstack, bukan objek kelas satu di bahasa, dan jauh lebih tidak elegan – dan secara signifikan lebih rentan kesalahan – daripada pola penjaga.

Arah Masa Depan: Pengetikan Efek

Kebanyakan orang bertanya kepada kami tentang apakah memiliki asinkron dan melempar sebagai atribut sistem tipe bercabang seluruh alam semesta perpustakaan. Jawabannya adalah “Tidak, tidak juga.” Tapi itu pasti menyakitkan dalam kode perpustakaan yang sangat polimorfik.

Contoh yang paling mengejutkan adalah kombinator seperti peta, filter, sortir, dll. Dalam kasus tersebut, Anda sering memiliki sembarangan berfungsi dan menginginkan asinkron dan melempar atribut fungsi tersebut untuk “mengalir” secara transparan.

Desain yang harus kami selesaikan ini adalah membiarkan Anda membuat parameter di atas efek. Misalnya, inilah pemetaan universal fungsi, Peta , yang menyebarkan asinkron atau melempar efek dari fungsi -nya parameter:

Perhatikan di sini bahwa kami memiliki tipe generik biasa, E , kecuali deklarasinya diawali dengan kata kunci efek. Kami kemudian menggunakan E secara simbolis sebagai pengganti daftar efek Tanda tangan peta , selain menggunakannya di posisi “menyebarkan” melalui efek (E) saat menjalankan func . Ini latihan substitusi yang cukup sepele, mengganti E dengan melempar dan efek (E. ) dengan coba , untuk melihat transformasi logis.

Doa hukum mungkin:

  int [] xs=...; string [] ys=coba Peta  (xs, x=> ...);  

Perhatikan di sini bahwa melempar mengalir, sehingga kita dapat meneruskan panggilan balik yang memberikan pengecualian.

Secara keseluruhan, kami membahas langkah ini lebih jauh, dan mengizinkan pemrogram untuk mendeklarasikan efek arbitrer. Saya sudah
berhipotesis tentang sistem tipe seperti itu sebelumnya

. Namun, kami khawatir, bahwa program tingkat tinggi semacam ini mungkin sangat pintar dan sulit dipahami, tidak peduli seberapa kuatnya. Model sederhana di atas mungkin akan menjadi sweet spot dan saya pikir kami akan melakukannya setelah beberapa bulan lagi.

Kami telah mencapai akhir dari perjalanan khusus ini. Seperti yang saya katakan di awal, hasil yang relatif dapat diprediksi dan jinak. Tapi saya harap semua latar belakang itu membantu membawa Anda melalui evolusi saat kami memilah-milah lanskap kesalahan.

Singkatnya, model akhir menampilkan:

  • Arsitektur yang mengasumsikan isolasi halus dan pemulihan dari kegagalan.
  • Membedakan antara bug dan kesalahan yang dapat dipulihkan.
  • Menggunakan kontrak, pernyataan, dan, secara umum, pengabaian untuk semua bug.
  • Menggunakan model pengecualian yang dicentang dan diperkecil untuk kesalahan yang dapat dipulihkan, dengan sistem tipe kaya dan sintaks bahasa.
  • Mengadopsi beberapa aspek terbatas dari kode pengembalian – seperti pengecekan lokal – yang meningkatkan keandalan.

Dan, meskipun ini adalah perjalanan beberapa tahun , ada area peningkatan yang terus kami kerjakan hingga saat ini proyek kami berakhir sebelum waktunya. Saya mengklasifikasikannya secara berbeda karena kami tidak memiliki cukup pengalaman untuk menggunakannya mengklaim sukses. Saya berharap kami akan membereskan sebagian besar dari mereka dan mengirimkannya jika kami sampai sejauh itu. Di Khususnya, saya ingin memasukkan yang ini ke dalam kategori model terakhir:

  • Memanfaatkan tipe non-null secara default untuk menghilangkan kelas besar dari anotasi nullability.

Pengabaian, dan sejauh mana kami menggunakannya , menurut pendapat saya, adalah taruhan terbesar dan tersukses kami dengan Error Model. Kami menemukan bug lebih awal dan sering, yang paling mudah didiagnosis dan diperbaiki. Kesalahan berbasis pengabaian kalah jumlah kesalahan yang dapat dipulihkan dengan rasio mendekati 10: 1, membuat pengecualian yang diperiksa menjadi langka dan dapat ditoleransi oleh pengembang.

Meskipun kami tidak pernah memiliki kesempatan untuk mengirimkan ini, kami telah membawa beberapa pelajaran ini ke pengaturan lain.

Selama penulisan ulang browser Microsoft Edge dari Internet Explorer, misalnya, kami mengadopsi pengabaian di beberapa area. Kunci yang diterapkan oleh seorang insinyur Midori, adalah OOM. Kode lama akan mencoba untuk berjalan seperti yang saya jelaskan sebelumnya dan hampir selalu melakukan hal yang salah. Pemahaman saya adalah bahwa pengabaian telah menemukan banyak bug yang mengintai, seperti yang kami lakukan mengalami secara teratur di Midori saat mem-porting basis kode yang ada. Hal yang hebat juga adalah bahwa pengabaian lebih merupakan suatu disiplin arsitektur yang dapat diadopsi dalam basis kode yang ada mulai dari bahasa pemrograman.

Fondasi arsitektural dari isolasi halus sangat penting, namun banyak sistem memiliki gagasan informal tentang hal ini. Arsitektur. Alasan mengapa pengabaian OOM berfungsi dengan baik di browser adalah karena sebagian besar browser menyediakan proses terpisah untuk tab individu sudah. Peramban meniru sistem operasi dalam banyak hal dan di sini juga kami melihat hal ini berjalan.

Baru-baru ini, kami telah mengeksplorasi proposal untuk membawa beberapa disiplin ilmu ini – termasuk kontrak – ke C ++. Ada juga proposal konkret untuk membawa beberapa fitur ini ke C # juga. Kami secara aktif mengulang proposal yang akan membawa beberapa pemeriksaan non-null ke C # . saya harus akui, saya berharap semua proposal itu yang terbaik, namun tidak ada yang akan menjadi antipeluru seperti seluruh tumpukan yang ditulis di disiplin kesalahan yang sama. Dan ingat, seluruh model isolasi dan konkurensi sangat penting untuk pengabaian dalam skala besar.

Saya berharap bahwa berbagi pengetahuan yang berkelanjutan akan mengarah pada adopsi yang lebih luas dari beberapa ide ini.

Dan, tentu saja, saya telah menyebutkan bahwa Go, Rust, dan Swift telah memberi dunia beberapa kesalahan yang sangat bagus yang sesuai sistem model sementara itu. Saya mungkin memiliki beberapa telur kutu kecil di sana-sini, tetapi kenyataannya adalah bahwa mereka ada di luar dunia apa yang kami miliki di industri pada saat kami memulai perjalanan Midori. Saat yang tepat untuk menjadi pemrogram sistem!

Lain kali saya akan berbicara lebih banyak tentang bahasanya. Secara khusus, kita akan melihat bagaimana Midori mampu menjinakkan pemulung menggunakan ramuan ajaib arsitektur, dukungan bahasa, dan perpustakaan. Saya berharap untuk segera bertemu lagi!

Read More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments