Di C #, mengapa variabel yang dideklarasikan di dalam blok percobaan terbatas cakupannya?

23

Saya ingin menambahkan penanganan kesalahan ke:

var firstVariable = 1;
var secondVariable = firstVariable;

Di bawah ini tidak dapat dikompilasi:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Mengapa diperlukan blok uji coba untuk memengaruhi ruang lingkup variabel seperti yang dilakukan oleh blok kode lainnya? Selain dari konsistensi, bukankah masuk akal bagi kita untuk dapat membungkus kode kita dengan penanganan kesalahan tanpa perlu refactor?

JᴀʏMᴇᴇ
sumber
14
A try.. catchadalah 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.
Neil
"adalah tipe spesifik dari blok kode". Spesifik dengan cara apa? Terima kasih
JᴀʏMᴇᴇ
7
Maksud saya apa pun di antara kurung keriting adalah blok kode. Anda melihatnya mengikuti pernyataan if dan mengikuti pernyataan for, meskipun konsepnya sama. Konten berada dalam ruang lingkup yang ditinggikan sehubungan dengan ruang lingkup induknya. Saya cukup yakin ini akan bermasalah jika Anda hanya menggunakan kurung keriting {}tanpa mencobanya.
Neil
Kiat: Perhatikan bahwa menggunakan (IDisposable) {} dan hanya {} berlaku juga. Ketika Anda menggunakan penggunaan dengan IDisposable, itu akan secara otomatis membersihkan sumber daya terlepas dari keberhasilan atau kegagalan. Ada beberapa pengecualian untuk ini, seperti tidak semua kelas yang Anda harapkan menerapkan IDisposable ...
Julia McGuigan
1
Banyak diskusi tentang pertanyaan yang sama tentang StackOverflow ini, di sini: stackoverflow.com/questions/94977/…
Jon Schneider

Jawaban:

90

Bagaimana jika kode Anda adalah:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Sekarang Anda akan mencoba menggunakan variabel yang tidak dideklarasikan ( firstVariable) jika pemanggilan metode Anda melempar.

Catatan : Contoh di atas secara khusus menjawab pertanyaan asli, yang menyatakan "samping konsistensi-demi". Ini menunjukkan bahwa ada alasan selain konsistensi. Tetapi seperti yang ditunjukkan oleh jawaban Peter, ada juga argumen kuat dari konsistensi, yang tentunya akan menjadi faktor yang sangat penting dalam keputusan.

Ben Aaronson
sumber
Ahh, inilah tepatnya yang saya cari. Saya tahu ada beberapa fitur bahasa yang membuat apa yang saya sarankan tidak mungkin, tetapi saya tidak bisa membuat skenario. Terima kasih banyak.
JᴀʏMᴇᴇ
1
"Sekarang Anda akan mencoba menggunakan variabel yang tidak dideklarasikan jika panggilan metode Anda melempar." Lebih jauh, anggap ini dihindari dengan memperlakukan variabel seolah-olah telah dideklarasikan, tetapi tidak diinisialisasi, sebelum kode yang mungkin dibuang. Maka itu tidak akan dideklarasikan, tetapi masih berpotensi dibatalkan, dan analisis penugasan yang pasti akan melarang membaca nilainya (tanpa penugasan yang mengintervensi hal itu dapat terbukti terjadi).
Eliah Kagan
3
Untuk bahasa statis seperti C #, deklarasi hanya relevan pada waktu kompilasi. Kompiler dapat dengan mudah memindahkan deklarasi sebelumnya dalam cakupan. Fakta yang lebih penting saat runtime adalah bahwa variabel mungkin tidak diinisialisasi .
jpmc26
3
Saya tidak setuju dengan jawaban ini. C # sudah memiliki aturan bahwa variabel tidak diinisialisasi tidak dapat dibaca, dengan beberapa kesadaran aliran data. (Coba mendeklarasikan variabel dalam kasus a switchdan mengaksesnya di orang lain.) Aturan ini dapat dengan mudah diterapkan di sini dan mencegah kode ini untuk dikompilasi. Saya pikir jawaban Peter di bawah ini lebih masuk akal.
Sebastian Redl
2
Ada perbedaan antara yang tidak diinisialisasi dan C # yang melacaknya secara terpisah. Jika Anda diizinkan untuk menggunakan variabel di luar blok tempat ia dideklarasikan, itu berarti Anda akan dapat menugaskannya di catchblok pertama dan kemudian itu pasti akan ditetapkan di tryblok kedua .
svick
64

Saya tahu bahwa ini telah dijawab dengan baik oleh Ben, tetapi saya ingin membahas konsistensi POV yang mudah disingkirkan. Dengan anggapan bahwa try/catchblok tidak memengaruhi cakupan, maka Anda akan berakhir dengan:

{
    // new scope here
}

try
{
   // Not new scope
}

Dan bagi saya ini crash langsung ke Prinsip paling tidak tercengang (POLA) karena Anda sekarang memiliki {dan }melakukan tugas ganda tergantung pada konteks apa yang mendahuluinya.

Satu-satunya jalan keluar dari kekacauan ini adalah dengan menunjuk beberapa penanda lain untuk menggambarkan try/catchblok. Yang mulai menambahkan bau kode. Jadi pada saat Anda tidak memiliki try/catchbahasa dalam bahasa itu akan menjadi berantakan sehingga Anda akan lebih baik dengan versi cakupan.

Peter M
sumber
Jawaban bagus lainnya. Dan saya belum pernah mendengar tentang POLA, bacaan yang sangat bagus. Terima kasih banyak kawan.
JᴀʏMᴇᴇ
"Satu-satunya jalan keluar dari kekacauan ini adalah menunjuk beberapa penanda lain untuk menggambarkan try/ catchmemblokir." - maksud Anda: try { { // scope } }? :)
CompuChip
@CompuChip yang akan {}menarik dua tugas sebagai ruang lingkup dan bukan menciptakan ruang lingkup tergantung pada konteks masih. try^ //no-scope ^akan menjadi contoh penanda yang berbeda.
Leliel
1
Menurut pendapat saya, ini adalah alasan yang jauh lebih mendasar, dan lebih dekat dengan jawaban "nyata".
Jack Aidley
@JackAidley terutama karena Anda sudah bisa menulis kode di mana Anda menggunakan variabel yang mungkin tidak ditugaskan. Jadi, sementara jawaban Ben memiliki poin tentang bagaimana ini perilaku yang berguna, saya tidak melihatnya sebagai mengapa perilaku itu ada. Jawaban Ben memang mencatat OP mengatakan "demi konsistensi", tetapi konsistensi adalah alasan yang sangat baik! Ruang lingkup yang sempit memiliki segala macam manfaat lainnya.
Kat
21

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 nditugaskan, 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 finallyblok 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 firstVariabledan secondVariablemencoba 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 firstVariabledan 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 Diemetode dengan messageparameter.) 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.

Eliah Kagan
sumber
"Ketika Anda menulis kode dalam blok percobaan dengan klausa catch, Anda memberi tahu kompiler dan setiap pembaca manusia bahwa kode itu harus diperlakukan seperti itu mungkin tidak semua dapat dijalankan." Apa yang dikompilasi oleh kompiler adalah bahwa kontrol dapat mencapai pernyataan selanjutnya bahkan jika pernyataan sebelumnya melemparkan pengecualian. Kompilator biasanya mengasumsikan bahwa jika satu pernyataan melempar pengecualian, pernyataan berikutnya tidak akan dieksekusi, sehingga variabel yang tidak ditugaskan tidak akan dibaca. Menambahkan 'tangkapan' akan memungkinkan kontrol untuk mencapai pernyataan di kemudian hari - tangkapan itulah yang penting, bukan apakah kode di blok coba dilemparkan.
Pete Kirkham