Situasi luar biasa: bagian 1 dari 4


Pendahuluan


Sudah waktunya untuk berbicara tentang pengecualian atau, lebih tepatnya, situasi luar biasa. Sebelum kita mulai, mari kita lihat definisi. Apa situasi yang luar biasa?


Ini adalah situasi yang membuat eksekusi kode saat ini atau selanjutnya salah. Maksud saya berbeda dari bagaimana itu dirancang atau dimaksudkan. Situasi seperti itu membahayakan integritas aplikasi atau bagiannya, misalnya suatu objek. Ini membawa aplikasi ke keadaan luar biasa atau luar biasa.


Tetapi mengapa kita perlu mendefinisikan terminologi ini? Karena itu akan menjaga kita dalam beberapa batasan. Jika kita tidak mengikuti terminologi, kita bisa terlalu jauh dari konsep yang dirancang yang dapat mengakibatkan banyak situasi ambigu. Mari kita lihat beberapa contoh praktis:


struct Number { public static Number Parse(string source) { // ... if(!parsed) { throw new ParsingException(); } // ... } public static bool TryParse(string source, out Number result) { // .. return parsed; } } 

Contoh ini agak aneh, dan itu karena suatu alasan. Saya membuat kode ini sedikit buatan untuk menunjukkan pentingnya masalah yang muncul di dalamnya. Pertama, mari kita lihat metode Parse . Mengapa harus membuang pengecualian?


  • Karena parameter yang diterimanya adalah string, tetapi outputnya adalah angka, yang merupakan tipe nilai. Angka ini tidak dapat menunjukkan validitas perhitungan: hanya ada. Dengan kata lain, metode ini tidak memiliki antarmuka untuk mengomunikasikan masalah potensial.
  • Di sisi lain, metode ini mengharapkan string yang benar yang berisi beberapa angka dan tidak ada karakter yang berlebihan. Jika tidak berisi, ada masalah dalam prasyarat untuk metode ini: kode yang memanggil metode kami telah melewati data yang salah.

Dengan demikian, situasi ketika metode ini mendapatkan string dengan data yang salah luar biasa karena metode ini tidak dapat mengembalikan nilai yang benar atau apa pun. Dengan demikian, satu-satunya cara adalah dengan melemparkan pengecualian.


Varian kedua dari metode ini dapat menandakan beberapa masalah dengan data input: nilai kembali di sini adalah boolean yang menunjukkan keberhasilan pelaksanaan metode. Metode ini tidak perlu menggunakan pengecualian untuk menandakan masalah: mereka semua tercakup oleh nilai balik false .


Ikhtisar


Penanganan pengecualian mungkin terlihat semudah ABC: kita hanya perlu menempatkan blok try-catch dan menunggu acara yang sesuai. Namun, kesederhanaan ini menjadi mungkin karena pekerjaan yang luar biasa dari tim CLR dan CoreCLR yang menyatukan semua kesalahan yang datang dari semua arah dan sumber ke dalam CLR. Untuk memahami apa yang akan kita bicarakan selanjutnya, mari kita lihat diagram:



Kita dapat melihat bahwa di dalam .NET Framework besar ada dua dunia: semua yang menjadi milik CLR dan semua yang tidak, termasuk semua kemungkinan kesalahan yang muncul di Windows dan bagian lain dari dunia yang tidak aman.


  • Structured Exception Handling (SEH) adalah cara standar Windows menangani pengecualian. Ketika metode unsafe dipanggil dan pengecualian dilemparkan, ada konversi pengecualian CLR yang tidak aman di kedua arah: dari tidak aman ke CLR dan mundur. Ini karena CLR dapat memanggil metode yang tidak aman yang pada gilirannya dapat memanggil metode CLR.
  • Vectored Exception Handling (VEH) adalah akar dari SEH dan memungkinkan Anda untuk meletakkan penangan Anda di tempat-tempat di mana pengecualian mungkin dilemparkan. Secara khusus, ini digunakan untuk menempatkan FirstChanceException .
  • Pengecualian COM + muncul ketika sumber masalah adalah komponen COM. Dalam kasus ini, lapisan antara COM dan metode .NET harus mengubah kesalahan COM menjadi pengecualian .NET.
  • Dan, tentu saja, pembungkus untuk HRESULT. Mereka diperkenalkan untuk mengkonversi model WinAPI (kode kesalahan terkandung dalam nilai kembali, sementara nilai kembali diperoleh dengan menggunakan parameter metode) menjadi model pengecualian karena ini merupakan pengecualian yang merupakan standar untuk .NET.

Di sisi lain, ada bahasa di atas CLI yang masing-masing kurang lebih memiliki fungsi untuk menangani pengecualian. Misalnya, baru-baru ini VB.NET atau F # memiliki fungsi penanganan pengecualian yang lebih kaya yang dinyatakan dalam sejumlah filter yang tidak ada di C #.


Kembali kode vs pengecualian


Secara terpisah, saya harus menyebutkan model penanganan kesalahan aplikasi menggunakan kode kembali. Gagasan mengembalikan kesalahan itu jelas dan jelas. Selain itu, jika kami memperlakukan pengecualian sebagai operator goto , penggunaan kode pengembalian menjadi lebih masuk akal: dalam hal ini, pengguna metode melihat kemungkinan kesalahan dan dapat memahami kesalahan yang mungkin terjadi. Namun, jangan menebak apa yang lebih baik dan untuk apa, tetapi diskusikan masalah pilihan dengan menggunakan teori yang beralasan.


Misalkan semua metode memiliki antarmuka untuk mengatasi kesalahan. Maka semua metode akan terlihat seperti:


 public bool TryParseInteger(string source, out int result); public DialogBoxResult OpenDialogBox(...); public WebServiceResult IWebService.GetClientsList(...); public class DialogBoxResult : ResultBase { ... } public class WebServiceResult : ResultBase { ... } 

Dan penggunaannya akan terlihat seperti:


 public ShowClientsResult ShowClients(string group) { if(!TryParseInteger(group, out var clientsGroupId)) return new ShowClientsResult { Reason = ShowClientsResult.Reason.ParsingFailed }; var webResult = _service.GetClientsList(clientsGroupId); if(!webResult.Successful) { return new ShowClientsResult { Reason = ShowClientsResult.Reason.ServiceFailed, WebServiceResult = webResult }; } var dialogResult = _dialogsService.OpenDialogBox(webResult.Result); if(!dialogResult.Successful) { return new ShowClientsResult { Reason = ShowClientsResult.Reason.DialogOpeningFailed, DialogServiceResult = dialogResult }; } return ShowClientsResult.Success(); } 

Anda mungkin berpikir kode ini kelebihan beban dengan penanganan kesalahan. Namun, saya ingin Anda mempertimbangkan kembali posisi Anda: semuanya di sini adalah persaingan dari mekanisme yang melempar dan menangani pengecualian.


Bagaimana metode melaporkan masalah? Itu bisa melakukannya dengan menggunakan antarmuka untuk melaporkan kesalahan. Misalnya, dalam metode TryParseInteger antarmuka tersebut diwakili oleh nilai kembali: jika semuanya OK, metode akan mengembalikan true . Jika tidak OK, itu akan kembali false . Namun, ada kerugian di sini: nilai riil dikembalikan melalui parameter out int result . Kerugiannya adalah bahwa di satu sisi nilai kembali secara logis dan oleh persepsi memiliki esensi "nilai kembali" lebih dari yang out dari parameter. Di sisi lain, kami tidak selalu peduli tentang kesalahan. Memang, jika string dimaksudkan untuk parsing berasal dari layanan yang menghasilkan string ini, kami tidak perlu memeriksanya untuk kesalahan: string akan selalu benar dan baik untuk parsing. Namun, misalkan kita mengambil implementasi lain dari metode ini:


 public int ParseInt(string source); 

Lalu, ada pertanyaan: jika string memang memiliki kesalahan, apa yang harus dilakukan metode ini? Haruskah mengembalikan nol? Ini tidak benar: tidak ada nol dalam string. Dalam kasus ini, kami memiliki konflik kepentingan: varian pertama memiliki terlalu banyak kode, sedangkan varian kedua tidak memiliki sarana untuk melaporkan kesalahan. Namun, sebenarnya mudah untuk memutuskan kapan harus menggunakan kode pengembalian dan kapan harus menggunakan pengecualian.


Jika mendapatkan kesalahan adalah norma, pilih kode kembali. Sebagai contoh, adalah normal ketika algoritma penguraian teks menemukan kesalahan dalam teks, tetapi jika algoritma lain yang bekerja dengan string yang diuraikan mendapat kesalahan dari pengurai, itu bisa kritis atau, dengan kata lain, luar biasa.

Coba-tangkap-akhirnya secara singkat


Blok try mencakup bagian di mana programmer berharap mendapatkan situasi kritis yang diperlakukan sebagai norma oleh kode eksternal. Dengan kata lain, jika beberapa kode menganggap keadaan internal tidak konsisten berdasarkan pada beberapa aturan dan melempar pengecualian, sistem eksternal, yang memiliki pandangan yang lebih luas dari situasi yang sama, dapat menangkap pengecualian ini menggunakan blok catch dan menormalkan pelaksanaan kode aplikasi . Dengan demikian, Anda melegalkan pengecualian di bagian kode ini dengan menangkapnya . Saya pikir ini adalah ide penting yang membenarkan larangan menangkap semua try-catch(Exception ex){ ...} berjaga-jaga .


Itu tidak berarti bahwa menangkap pengecualian bertentangan dengan beberapa ideologi. Saya mengatakan bahwa Anda harus menangkap hanya kesalahan yang Anda harapkan dari bagian kode tertentu. Misalnya, Anda tidak bisa mengharapkan semua jenis pengecualian yang diwarisi dari ArgumentException atau Anda tidak bisa mendapatkan NullReferenceException , karena seringkali itu berarti bahwa masalah lebih pada kode Anda daripada dalam yang disebut. Tetapi patut untuk berharap bahwa Anda tidak akan dapat membuka file yang dimaksud. Bahkan jika Anda 200% yakin Anda akan bisa, jangan lupa untuk memeriksanya.


Blok finally juga terkenal. Sangat cocok untuk semua kasus yang dicakup oleh blok try-catch . Kecuali untuk beberapa situasi khusus yang jarang, blok ini akan selalu berfungsi. Mengapa jaminan kinerja seperti itu diperkenalkan? Untuk membersihkan sumber daya dan kelompok objek yang dialokasikan atau ditangkap di blok try dan yang menjadi tanggung jawab blok ini.


Blok ini sering digunakan tanpa blok catch ketika kita tidak peduli kesalahan mana yang memecahkan suatu algoritma, tetapi kita perlu membersihkan semua sumber daya yang dialokasikan untuk algoritma ini. Mari kita lihat contoh sederhana: algoritma penyalinan file membutuhkan dua file terbuka dan rentang memori untuk buffer uang tunai. Bayangkan kita mengalokasikan memori dan membuka satu file, tetapi tidak bisa membuka yang lain. Untuk membungkus semuanya dalam satu "transaksi" secara atomis, kami menempatkan ketiga operasi dalam satu blok try (sebagai varian implementasi) dengan sumber daya yang dibersihkan pada finally . Ini mungkin tampak seperti contoh yang disederhanakan tetapi yang paling penting adalah menunjukkan esensinya.


Apa yang sebenarnya tidak dimiliki C # adalah blok fault yang diaktifkan setiap kali terjadi kesalahan. Ini seperti finally menggunakan steroid. Jika kita punya ini, kita bisa, misalnya, membuat titik masuk tunggal untuk mencatat situasi luar biasa:


 try { //... } fault exception { _logger.Warn(exception); } 

Hal lain yang harus saya sentuh dalam pengantar ini adalah filter pengecualian. Ini bukan fitur baru pada platform .NET tetapi pengembang C # mungkin baru di dalamnya: penyaringan pengecualian hanya muncul di v. 6.0 Filter harus menormalkan situasi ketika ada satu jenis pengecualian yang menggabungkan beberapa jenis kesalahan. Itu akan membantu kita ketika kita ingin berurusan dengan skenario tertentu tetapi harus menangkap seluruh kelompok kesalahan terlebih dahulu dan memfilternya nanti. Tentu saja, maksud saya kode dari jenis berikut:


 try { //... } catch (ParserException exception) { switch(exception.ErrorCode) { case ErrorCode.MissingModifier: // ... break; case ErrorCode.MissingBracket: // ... break; default: throw; } } 

Nah, sekarang kita dapat menulis ulang kode ini dengan benar:


 try { //... } catch (ParserException exception) when (exception.ErrorCode == ErrorCode.MissingModifier) { // ... } catch (ParserException exception) when (exception.ErrorCode == ErrorCode.MissingBracket) { // ... } 

Perbaikan di sini bukan karena kurangnya switch . Saya percaya konstruksi baru ini lebih baik dalam beberapa hal:


  • menggunakan when untuk menyaring kita menangkap apa yang kita inginkan dan itu benar dalam hal ideologi;
  • kode menjadi lebih mudah dibaca dalam bentuk baru ini. Melihat melalui kode, otak kita dapat mengidentifikasi blok untuk menangani kesalahan dengan lebih mudah karena pada awalnya mencari catch dan bukan switch-case ;
  • yang terakhir tetapi tidak kalah pentingnya: perbandingan pendahuluan adalah SEBELUM memasuki blok tangkapan. Ini berarti bahwa jika kita membuat perkiraan yang salah tentang situasi potensial, konstruk ini akan bekerja lebih cepat daripada switch jika melemparkan pengecualian lagi.

Banyak sumber mengatakan bahwa fitur khusus dari kode ini adalah pemfilteran terjadi sebelum tumpukan dibuka. Anda dapat melihat ini dalam situasi ketika tidak ada panggilan lain kecuali biasanya antara tempat di mana pengecualian dilemparkan dan tempat di mana pemeriksaan pemfilteran terjadi.


 static void Main() { try { Foo(); } catch (Exception ex) when (Check(ex)) { ; } } static void Foo() { Boo(); } static void Boo() { throw new Exception("1"); } static bool Check(Exception ex) { return ex.Message == "1"; } 

Tumpukan tanpa membuka gulungan


Anda dapat melihat dari gambar bahwa jejak stack tidak hanya berisi panggilan pertama Main sebagai titik untuk menangkap pengecualian, tetapi seluruh tumpukan sebelum titik melempar pengecualian ditambah yang kedua masuk ke Main melalui kode yang tidak dikelola. Kita dapat menganggap bahwa kode ini adalah kode yang tepat untuk melempar pengecualian yang ada dalam tahap penyaringan dan memilih penangan akhir. Namun, tidak semua panggilan dapat ditangani tanpa tumpukan membuka gulungan . Saya percaya bahwa keseragaman yang berlebihan dari platform menghasilkan terlalu banyak kepercayaan di dalamnya. Misalnya, ketika satu domain memanggil metode dari domain lain itu benar-benar transparan dalam hal kode. Namun, cara metode panggilan bekerja adalah cerita yang sangat berbeda. Kita akan membicarakannya di bagian selanjutnya.


Serialisasi


Mari kita mulai dengan melihat hasil menjalankan kode berikut (saya menambahkan transfer panggilan melintasi batas antara dua domain aplikasi).


  class Program { static void Main() { try { ProxyRunner.Go(); } catch (Exception ex) when (Check(ex)) { ; } } static bool Check(Exception ex) { var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe return ex.Message == "1"; } public class ProxyRunner : MarshalByRefObject { private void MethodInsideAppDomain() { throw new Exception("1"); } public static void Go() { var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory }); var proxy = (ProxyRunner) dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName); proxy.MethodInsideAppDomain(); } } } 

Kita dapat melihat bahwa tumpukan membuka gulungan terjadi sebelum kita mulai memfilter. Mari kita lihat screenshot. Yang pertama diambil sebelum generasi pengecualian:


Stackunnroll


Yang kedua adalah setelahnya:


Stackacknroll2


Mari pelajari penelusuran panggilan sebelum dan sesudah pengecualian difilter. Apa yang terjadi di sini Kita dapat melihat bahwa pengembang platform membuat sesuatu yang sekilas tampak seperti perlindungan subdomain. Pelacakan dipotong setelah metode terakhir dalam rantai panggilan dan kemudian ada transfer ke domain lain. Tapi saya pikir ini terlihat aneh. Untuk memahami mengapa hal ini terjadi, mari kita ingat aturan utama untuk tipe yang mengatur interaksi antar domain. Jenis-jenis ini harus mewarisi MarshalByRefObject dan dapat serial. Namun, meskipun ketatnya tipe pengecualian C # dapat bersifat apa saja. Apa artinya Ini berarti bahwa situasi dapat terjadi ketika pengecualian di dalam subdomain dapat ditangkap dalam domain induk. Juga, jika objek data yang bisa masuk ke situasi luar biasa memiliki beberapa metode yang berbahaya dalam hal keamanan mereka dapat dipanggil dalam domain induk. Untuk menghindari ini, pengecualian pertama adalah serial dan kemudian melintasi batas antara domain aplikasi dan muncul lagi dengan tumpukan baru. Mari kita periksa teori ini:


 [StructLayout(LayoutKind.Explicit)] class Cast { [FieldOffset(0)] public Exception Exception; [FieldOffset(0)] public object obj; } static void Main() { try { ProxyRunner.Go(); Console.ReadKey(); } catch (RuntimeWrappedException ex) when (ex.WrappedException is Program) { ; } } static bool Check(Exception ex) { var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe return ex.Message == "1"; } public class ProxyRunner : MarshalByRefObject { private void MethodInsideAppDomain() { var x = new Cast {obj = new Program()}; throw x.Exception; } public static void Go() { var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory }); var proxy = (ProxyRunner)dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName); proxy.MethodInsideAppDomain(); } } 

Untuk kode C # bisa melempar pengecualian dari jenis apa pun (saya tidak ingin menyiksa Anda dengan MSIL) Saya melakukan trik dalam contoh ini melemparkan jenis ke jenis yang tidak dapat dibandingkan, sehingga kami dapat melempar pengecualian untuk jenis apa pun, tetapi penerjemah akan berpikir bahwa kami menggunakan tipe Exception . Kami membuat turunan dari jenis Program , yang tidak dapat dipastikan serial, dan melemparkan pengecualian menggunakan jenis ini sebagai beban kerja. Berita baiknya adalah Anda mendapatkan pembungkus untuk pengecualian non-pengecualian dari RuntimeWrappedException yang akan menyimpan instance objek tipe Program kami di dalam dan kami akan dapat menangkap pengecualian ini. Namun, ada berita buruk yang mendukung ide kami: memanggil proxy.MethodInsideAppDomain(); akan menghasilkan SerializationException :



Dengan demikian, Anda tidak dapat mentransfer pengecualian antara domain karena tidak mungkin untuk membuat serial. Ini, pada gilirannya, berarti bahwa menggunakan filter pengecualian untuk metode pembungkus panggilan di domain lain akan tetap menyebabkan tumpukan membuka gulungan meskipun serialisasi tampaknya tidak perlu dengan pengaturan FullTrust dari subdomain.


Kita harus memberi perhatian tambahan pada alasan mengapa serialisasi antar domain sangat diperlukan. Dalam contoh buatan kami, kami membuat subdomain yang tidak memiliki pengaturan apa pun. Itu berarti ia bekerja dengan cara FullTrust. CLR sepenuhnya mempercayai kontennya dan tidak menjalankan pemeriksaan tambahan apa pun. Namun, ketika Anda memasukkan setidaknya satu pengaturan keamanan, kepercayaan penuh akan hilang dan CLR akan mulai mengendalikan semua yang terjadi di dalam subdomain. Jadi, ketika Anda memiliki domain yang sepenuhnya tepercaya Anda tidak perlu serialisasi. Akui, kita tidak perlu melindungi diri kita sendiri. Tetapi serialisasi ada tidak hanya untuk perlindungan. Setiap domain memuat semua rakitan yang diperlukan untuk kedua kalinya dan membuat salinannya. Dengan demikian, itu membuat salinan dari semua jenis dan semua VMT. Tentu saja, ketika meneruskan objek dari domain ke domain Anda akan mendapatkan objek yang sama. Tetapi VMT-nya tidak akan menjadi miliknya dan objek ini tidak dapat dilemparkan ke tipe lain. Dengan kata lain, jika kita membuat instance dari tipe Boo dan mendapatkannya di domain lain, casting (Boo)boo tidak akan berfungsi. Dalam hal ini, serialisasi dan deserialisasi akan menyelesaikan masalah karena objek akan ada di dua domain secara bersamaan. Itu akan ada dengan semua data di mana ia dibuat dan itu akan ada di domain penggunaan sebagai objek proxy, memastikan bahwa metode objek asli disebut.

Dengan mentransfer objek serial antara domain Anda mendapatkan salinan penuh objek dari satu domain di domain lain sambil menyimpan beberapa batasan dalam memori. Namun, batasan ini adalah fiksi. Ini digunakan hanya untuk tipe-tipe yang tidak ada dalam Shared AppDomain . Dengan demikian, jika Anda melempar sesuatu yang non-serializable sebagai pengecualian, tetapi dari Shared AppDomain , Anda tidak akan mendapatkan kesalahan serialisasi (kami dapat mencoba melempar Action alih-alih Program ). Namun, tumpukan membuka gulungan akan tetap terjadi dalam kasus ini: karena kedua varian harus bekerja dengan cara standar. Sehingga tidak ada yang akan bingung.


Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.

Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu github / sidristij / dotnetbook .

Source: https://habr.com/ru/post/id454630/


All Articles