Selain dari konsistensi, bukankah masuk akal bagi kita untuk dapat membungkus kode kita dengan penanganan kesalahan tanpa perlu refactor?
Untuk menjawab ini, penting untuk melihat lebih dari sekadar lingkup variabel .
Bahkan jika variabel tetap dalam ruang lingkup, itu tidak akan ditetapkan secara pasti .
Mendeklarasikan variabel di blok coba menyatakan - ke kompiler, dan pembaca manusia - bahwa itu hanya bermakna di dalam blok itu. Berguna bagi kompiler untuk menerapkannya.
Jika Anda ingin variabel berada dalam lingkup setelah blok coba, Anda bisa mendeklarasikannya di luar blok:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
Itu menyatakan bahwa variabel mungkin bermakna di luar blok try. Kompiler akan mengizinkan ini.
Tetapi ini juga menunjukkan alasan lain mengapa biasanya tidak berguna untuk menyimpan variabel dalam ruang lingkup setelah memasukkannya dalam blok percobaan. Compiler C # melakukan analisis penugasan yang pasti dan melarang membaca nilai variabel yang belum terbukti telah diberi nilai. Jadi Anda masih belum bisa membaca dari variabel.
Misalkan saya mencoba membaca dari variabel setelah blok coba:
Console.WriteLine(firstVariable);
Itu akan memberikan kesalahan waktu kompilasi :
CS0165 Penggunaan variabel lokal yang tidak ditetapkan 'firstVariable'
Saya menelepon Lingkungan . Keluar di blok tangkap, jadi saya tahu variabel telah ditetapkan sebelum panggilan ke Console.WriteLine. Tetapi kompiler tidak menyimpulkan ini.
Mengapa kompilator begitu ketat?
Saya bahkan tidak bisa melakukan ini:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Salah satu cara untuk melihat batasan ini adalah dengan mengatakan analisis penugasan yang pasti dalam C # tidak terlalu canggih. Tetapi cara lain untuk melihatnya adalah bahwa, ketika Anda menulis kode dalam blok percobaan dengan klausa catch, Anda memberi tahu kompiler dan pembaca manusia bahwa itu harus diperlakukan seperti itu mungkin tidak semua dapat berjalan.
Untuk mengilustrasikan apa yang saya maksud, bayangkan jika kompiler mengizinkan kode di atas, tetapi kemudian Anda menambahkan panggilan di blok coba ke fungsi yang secara pribadi Anda tahu tidak akan memberikan pengecualian . Tidak dapat menjamin bahwa fungsi yang dipanggil tidak melempar IOException
, kompiler tidak dapat mengetahui apa yang n
ditugaskan, dan kemudian Anda harus refactor.
Ini untuk mengatakan bahwa, dengan menguraikan analisis yang sangat canggih dalam menentukan apakah suatu variabel yang ditetapkan dalam blok percobaan dengan klausa catch telah ditentukan secara pasti setelah itu, kompiler membantu Anda menghindari penulisan kode yang kemungkinan akan rusak kemudian. (Lagi pula, menangkap pengecualian biasanya berarti Anda berpikir seseorang akan dilempar.)
Anda dapat memastikan variabel ditugaskan melalui semua jalur kode.
Anda bisa membuat kompilasi kode dengan memberi nilai pada variabel sebelum blok coba, atau di blok tangkap. Dengan begitu, itu masih akan diinisialisasi atau ditugaskan, bahkan jika tugas di blok percobaan tidak terjadi. Sebagai contoh:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Atau:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Kompilasi itu. Tetapi yang terbaik adalah hanya melakukan sesuatu seperti itu jika nilai default yang Anda berikan masuk akal * dan menghasilkan perilaku yang benar.
Perhatikan bahwa, dalam kasus kedua ini di mana Anda menetapkan variabel di blok uji coba dan di semua blok tangkap, meskipun Anda dapat membaca variabel setelah uji coba, Anda masih tidak dapat membaca variabel di dalam finally
blok yang terpasang , karena eksekusi dapat meninggalkan blok percobaan dalam lebih banyak situasi daripada yang sering kita pikirkan .
* Omong-omong, beberapa bahasa, seperti C dan C ++, keduanya memungkinkan variabel yang tidak diinisialisasi dan tidak memiliki analisis tugas yang pasti untuk mencegah pembacaan dari mereka. Karena membaca memori yang tidak diinisialisasi menyebabkan program berperilaku dengan cara nondeterministik dan tidak menentu , umumnya disarankan untuk menghindari memasukkan variabel dalam bahasa-bahasa tersebut tanpa menyediakan inisialisasi. Dalam bahasa dengan analisis penugasan yang pasti seperti C # dan Java, kompiler menyelamatkan Anda dari membaca variabel yang tidak diinisialisasi dan juga dari kejahatan yang lebih kecil menginisialisasi mereka dengan nilai-nilai tidak berarti yang kemudian dapat disalahartikan sebagai bermakna.
Anda bisa membuatnya jadi jalur kode di mana variabel tidak ditugaskan melemparkan pengecualian (atau kembali).
Jika Anda berencana untuk melakukan beberapa tindakan (seperti penebangan) dan rethrow pengecualian atau melemparkan pengecualian lain, dan ini terjadi di setiap klausa menangkap di mana variabel tidak ditugaskan, maka kompiler akan tahu variabel telah ditugaskan:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Itu mengkompilasi, dan mungkin merupakan pilihan yang masuk akal. Namun, dalam aplikasi yang sebenarnya, kecuali pengecualian hanya dilemparkan dalam situasi di mana tidak masuk akal untuk mencoba memulihkan * , Anda harus memastikan bahwa Anda masih menangkap dan menangani dengan benar di suatu tempat .
(Anda tidak dapat membaca variabel di blok akhirnya dalam situasi ini juga, tapi rasanya Anda tidak bisa - setelah semua, akhirnya blok pada dasarnya selalu berjalan, dan dalam hal ini variabel tidak selalu ditugaskan .)
* Sebagai contoh, banyak aplikasi tidak memiliki klausa catch yang menangani OutOfMemoryException karena apa pun yang mereka dapat lakukan tentang hal itu mungkin paling tidak sama buruknya dengan crash .
Mungkin Anda benar - benar ingin memperbaiki kode.
Dalam contoh Anda, Anda memperkenalkan firstVariable
dan secondVariable
mencoba blok. Seperti yang telah saya katakan, Anda dapat mendefinisikan mereka sebelum blok percobaan di mana mereka ditugaskan sehingga mereka akan tetap berada dalam ruang lingkup sesudahnya, dan Anda dapat memuaskan / mengelabui kompiler agar Anda dapat membaca dari mereka dengan memastikan mereka selalu ditugaskan.
Tetapi kode yang muncul setelah blok itu mungkin tergantung pada mereka yang ditugaskan dengan benar. Jika demikian, maka kode Anda harus mencerminkan dan memastikan hal itu.
Pertama, bisakah (dan seharusnya) Anda benar-benar menangani kesalahan di sana? Salah satu alasan penanganan pengecualian ada adalah untuk membuatnya lebih mudah untuk menangani kesalahan di mana mereka dapat ditangani secara efektif , bahkan jika itu tidak dekat di mana mereka terjadi.
Jika Anda tidak dapat benar-benar menangani kesalahan dalam fungsi yang diinisialisasi dan menggunakan variabel-variabel itu, maka mungkin blok coba tidak boleh sama sekali dalam fungsi itu, tetapi sebaliknya di tempat yang lebih tinggi (yaitu, dalam kode yang memanggil fungsi itu, atau kode bahwa panggilan itu code). Pastikan Anda tidak secara tidak sengaja menangkap pengecualian yang dilemparkan ke tempat lain dan salah mengasumsikan bahwa itu dilemparkan saat inisialisasi firstVariable
dan secondVariable
.
Pendekatan lain adalah dengan meletakkan kode yang menggunakan variabel di blok try. Ini sering masuk akal. Sekali lagi, jika pengecualian yang sama yang Anda tangkap dari inisialisasi mereka juga bisa dilempar dari kode di sekitarnya, Anda harus memastikan bahwa Anda tidak mengabaikan kemungkinan itu saat menangani mereka.
(Saya berasumsi Anda menginisialisasi variabel dengan ekspresi lebih rumit daripada yang ditunjukkan dalam contoh Anda, sehingga mereka benar-benar bisa melempar pengecualian, dan juga bahwa Anda tidak benar-benar berencana untuk menangkap semua pengecualian yang mungkin , tetapi hanya untuk menangkap pengecualian spesifik apa pun Anda dapat mengantisipasi dan menangani secara berarti . Memang benar bahwa dunia nyata tidak selalu begitu baik dan kode produksi terkadang melakukan hal ini , tetapi karena tujuan Anda di sini adalah untuk menangani kesalahan yang terjadi saat menginisialisasi dua variabel tertentu, setiap klausa tangkapan yang Anda tulis untuk spesifik itu tujuan harus spesifik untuk kesalahan apa pun itu.)
Cara ketiga adalah mengekstraksi kode yang bisa gagal, dan try-catch yang menanganinya, ke dalam metodenya sendiri. Ini berguna jika Anda mungkin ingin menangani kesalahan sepenuhnya terlebih dahulu, dan kemudian tidak khawatir tentang menangkap secara tidak sengaja pengecualian yang seharusnya ditangani di tempat lain.
Misalkan, misalnya, Anda ingin segera keluar dari aplikasi setelah gagal menetapkan salah satu variabel. (Jelas tidak semua penanganan pengecualian adalah untuk kesalahan fatal; ini hanya sebuah contoh, dan mungkin atau mungkin bukan bagaimana Anda ingin aplikasi Anda bereaksi terhadap masalah tersebut.) Anda dapat melakukannya seperti ini:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
Kode itu mengembalikan dan mendekonstruksi ValueTuple dengan sintaks C # 7.0 untuk mengembalikan beberapa nilai, tetapi jika Anda masih menggunakan versi C # sebelumnya, Anda masih dapat menggunakan teknik ini; misalnya, Anda dapat menggunakan parameter, atau mengembalikan objek kustom yang menyediakan kedua nilai . Selain itu, jika kedua variabel tersebut tidak benar-benar terkait erat, mungkin akan lebih baik untuk memiliki dua metode terpisah.
Terutama jika Anda memiliki beberapa metode seperti itu, Anda harus mempertimbangkan memusatkan kode Anda untuk memberi tahu pengguna tentang kesalahan fatal dan berhenti. (Misalnya, Anda bisa menulis Die
metode dengan message
parameter.) The throw new InvalidOperationException();
line pernah benar-benar dijalankan sehingga Anda tidak perlu (dan tidak harus) menulis klausa catch untuk itu.
Selain berhenti ketika kesalahan tertentu terjadi, Anda mungkin kadang-kadang menulis kode yang terlihat seperti ini jika Anda membuang pengecualian dari tipe lain yang membungkus pengecualian asli . (Dalam situasi itu, Anda tidak perlu ekspresi lemparan kedua yang tidak dapat dijangkau.)
Kesimpulan: Ruang lingkup hanya bagian dari gambar.
Anda dapat mencapai efek membungkus kode Anda dengan penanganan kesalahan tanpa refactoring (atau, jika Anda suka, dengan hampir tidak ada refactoring), hanya dengan memisahkan deklarasi variabel dari tugas mereka. Kompilator memungkinkan ini jika Anda memenuhi aturan penugasan C # yang pasti, dan mendeklarasikan variabel di depan blok try menjadikan cakupannya lebih besar. Tetapi refactoring lebih lanjut mungkin masih menjadi pilihan terbaik Anda.
try.. catch
adalah tipe spesifik dari blok kode, dan sejauh semua blok kode pergi, Anda tidak dapat mendeklarasikan variabel dalam satu dan menggunakan variabel yang sama di yang lain sebagai masalah cakupan.