Mengapa variabel tidak dideklarasikan dalam "coba" dalam lingkup "menangkap" atau "akhirnya"?


143

Di C # dan di Java (dan mungkin juga bahasa lain), variabel yang dideklarasikan dalam blok "coba" tidak berada dalam cakupan di blok "catch" atau "last" yang sesuai. Misalnya, kode berikut tidak dapat dikompilasi:

try {
  String s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

Dalam kode ini, kesalahan waktu kompilasi terjadi pada referensi ke s di blok catch, karena s hanya dalam ruang lingkup di blok percobaan. (Di Java, kesalahan kompilasi adalah "s tidak dapat diselesaikan"; di C #, "Nama 'tidak ada dalam konteks saat ini".)

Solusi umum untuk masalah ini tampaknya malah mendeklarasikan variabel tepat sebelum blok percobaan, bukan di dalam blok percobaan:

String s;
try {
  s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

Namun, setidaknya bagi saya, (1) ini terasa seperti solusi yang kikuk, dan (2) itu menghasilkan variabel yang memiliki ruang lingkup lebih besar daripada yang dimaksudkan pemrogram (seluruh sisa metode, bukan hanya dalam konteks coba-tangkap-akhirnya).

Pertanyaan saya adalah, apa alasan di balik keputusan desain bahasa ini (di Java, di C #, dan / atau dalam bahasa lain yang berlaku)?

Jawaban:


176

Dua hal:

  1. Secara umum, Java hanya memiliki 2 tingkat cakupan: global dan fungsi. Tapi, coba / tangkap adalah pengecualian (tidak ada permainan kata-kata). Ketika pengecualian dilempar dan objek pengecualian mendapatkan variabel yang ditugaskan padanya, variabel objek itu hanya tersedia dalam bagian "catch" dan dimusnahkan segera setelah tangkapan selesai.

  2. (dan yang lebih penting). Anda tidak dapat mengetahui di mana dalam blok percobaan pengecualian itu dilemparkan. Itu mungkin terjadi sebelum variabel Anda dideklarasikan. Oleh karena itu, tidak mungkin untuk mengatakan variabel apa yang akan tersedia untuk klausa catch / last. Pertimbangkan kasus berikut, di mana pelingkupan seperti yang Anda sarankan:

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }
    

Ini jelas merupakan masalah - ketika Anda mencapai penangan pengecualian, s tidak akan dideklarasikan. Mengingat bahwa tangkapan dimaksudkan untuk menangani keadaan luar biasa dan akhirnya harus dijalankan, aman dan menyatakan masalah ini pada waktu kompilasi jauh lebih baik daripada saat waktu proses.


55

Bagaimana Anda bisa yakin, bahwa Anda mencapai bagian deklarasi di blok tangkapan Anda? Bagaimana jika instantiation memunculkan pengecualian?


6
Hah? Deklarasi variabel tidak memunculkan pengecualian.
Joshua

7
Setuju, itu adalah instansiasi yang mungkin memunculkan pengecualian.
Burkhard

19

Biasanya, dalam bahasa C-style, apa yang terjadi di dalam kurung kurawal tetap berada di dalam kurung kurawal. Saya pikir memiliki rentang variabel seumur hidup di seluruh cakupan seperti itu tidak akan intuitif bagi sebagian besar programmer. Anda dapat mencapai apa yang Anda inginkan dengan menyertakan blok coba / tangkap / akhirnya di dalam level penjepit lain. misalnya

... code ...
{
    string s = "test";
    try
    {
        // more code
    }
    catch(...)
    {
        Console.Out.WriteLine(s);
    }
}

EDIT: Saya kira setiap aturan memang memiliki pengecualian. Berikut ini adalah C ++ yang valid:

int f() { return 0; }

void main() 
{
    int y = 0;

    if (int x = f())
    {
        cout << x;
    }
    else
    {
        cout << x;
    }
}

Cakupan x adalah kondisional, klausa then, dan klausa else.


10

Semua orang telah mengemukakan dasar-dasarnya - apa yang terjadi dalam satu blok tetap dalam satu blok. Tetapi dalam kasus .NET, mungkin berguna untuk memeriksa apa yang menurut compiler sedang terjadi. Ambil, misalnya, kode coba / tangkap berikut (perhatikan bahwa StreamReader dideklarasikan, dengan benar, di luar blok):

static void TryCatchFinally()
{
    StreamReader sr = null;
    try
    {
        sr = new StreamReader(path);
        Console.WriteLine(sr.ReadToEnd());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        if (sr != null)
        {
            sr.Close();
        }
    }
}

Ini akan mengkompilasi menjadi sesuatu yang mirip dengan berikut ini di MSIL:

.method private hidebysig static void  TryCatchFinallyDispose() cil managed
{
  // Code size       53 (0x35)    
  .maxstack  2    
  .locals init ([0] class [mscorlib]System.IO.StreamReader sr,    
           [1] class [mscorlib]System.Exception ex)    
  IL_0000:  ldnull    
  IL_0001:  stloc.0    
  .try    
  {    
    .try    
    {    
      IL_0002:  ldsfld     string UsingTest.Class1::path    
      IL_0007:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)    
      IL_000c:  stloc.0    
      IL_000d:  ldloc.0    
      IL_000e:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadToEnd()
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0018:  leave.s    IL_0028
    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_001a:  stloc.1
      IL_001b:  ldloc.1    
      IL_001c:  callvirt   instance string [mscorlib]System.Exception::ToString()    
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0026:  leave.s    IL_0028    
    }  // end handler    
    IL_0028:  leave.s    IL_0034    
  }  // end .try    
  finally    
  {    
    IL_002a:  ldloc.0    
    IL_002b:  brfalse.s  IL_0033    
    IL_002d:  ldloc.0    
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()    
    IL_0033:  endfinally    
  }  // end handler    
  IL_0034:  ret    
} // end of method Class1::TryCatchFinallyDispose

Apa yang kita lihat? MSIL menghormati blok - blok tersebut secara intrinsik merupakan bagian dari kode dasar yang dibuat saat Anda mengkompilasi C #. Cakupannya tidak hanya diatur dalam spesifikasi C #, tetapi juga dalam spesifikasi CLR dan CLS.

Ruang lingkup melindungi Anda, tetapi Anda terkadang harus mengatasinya. Seiring waktu, Anda akan terbiasa, dan itu mulai terasa alami. Seperti yang dikatakan orang lain, apa yang terjadi di blok tetap di blok itu. Anda ingin berbagi sesuatu? Anda harus keluar dari blok ...


8

Dalam C ++ bagaimanapun juga, cakupan variabel otomatis dibatasi oleh tanda kurung kurawal yang mengelilinginya. Mengapa ada orang yang mengharapkan ini berbeda dengan memasukkan kata kunci percobaan di luar tanda kurung kurawal?


1
Sepakat; "}" berarti end-of-scope. Namun, coba-tangkap-akhirnya tidak biasa karena setelah mencoba memblokir, Anda harus memiliki tangkapan dan / atau akhirnya blok; dengan demikian, pengecualian untuk aturan normal di mana ruang lingkup blok percobaan dibawa ke tangkapan terkait / akhirnya mungkin tampak dapat diterima?
Jon Schneider

7

Seperti yang ditunjukkan oleh ravenspoint, setiap orang mengharapkan variabel menjadi lokal ke blok tempat mereka didefinisikan. tryMemperkenalkan blok dan begitu juga catch.

Jika Anda ingin variabel lokal untuk keduanya trydan catch, coba masukkan keduanya dalam satu blok:

// here is some code
{
    string s;
    try
    {

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}

5

Jawaban sederhananya adalah C dan sebagian besar bahasa yang mewarisi sintaksnya memiliki cakupan blok. Itu berarti bahwa jika variabel didefinisikan dalam satu blok, yaitu di dalam {}, itu adalah cakupannya.

Pengecualiannya adalah JavaScript, yang memiliki sintaks serupa, tetapi memiliki cakupan fungsi. Dalam JavaScript, variabel yang dideklarasikan dalam blok percobaan berada dalam cakupan di blok catch, dan di mana pun dalam fungsinya yang memuatnya.


5

Menurut bagian berjudul "Cara Melempar dan Menangkap Pengecualian" di Pelajaran 2 Kit Pelatihan Swa-Pacu MCTS (Ujian 70-536): Microsoft® .NET Framework 2.0 — Yayasan Pengembangan Aplikasi , alasannya adalah bahwa pengecualian mungkin terjadi sebelum deklarasi variabel di blok try (seperti yang telah dicatat orang lain).

Kutipan dari halaman 25:

"Perhatikan bahwa deklarasi StreamReader dipindahkan ke luar blok Try pada contoh sebelumnya. Ini diperlukan karena blok Akhirnya tidak dapat mengakses variabel yang dideklarasikan dalam blok Try. Ini masuk akal karena bergantung pada tempat pengecualian terjadi, deklarasi variabel di dalam Coba blokir mungkin belum dijalankan . "


4

@burkhard memiliki pertanyaan mengapa menjawab dengan benar, tetapi sebagai catatan saya ingin menambahkan, sementara contoh solusi yang Anda rekomendasikan adalah 99,9999 +% waktu yang baik, ini bukan praktik yang baik, jauh lebih aman untuk memeriksa nol sebelum menggunakan sesuatu yang dipakai dalam blok percobaan, atau menginisialisasi variabel ke sesuatu alih-alih hanya mendeklarasikannya sebelum blok percobaan. Sebagai contoh:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

Atau:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

Ini harus memberikan skalabilitas dalam penyelesaiannya, sehingga bahkan ketika apa yang Anda lakukan di blok percobaan lebih kompleks daripada menetapkan string, Anda harus dapat mengakses data dengan aman dari blok tangkapan Anda.


4

Jawabannya, seperti yang ditunjukkan semua orang, adalah "begitulah cara blok didefinisikan".

Ada beberapa usulan untuk membuat kode lebih cantik. Lihat ARM

 try (FileReader in = makeReader(), FileWriter out = makeWriter()) {
       // code using in and out
 } catch(IOException e) {
       // ...
 }

Penutupan seharusnya mengatasi hal ini juga.

with(FileReader in : makeReader()) with(FileWriter out : makeWriter()) {
    // code using in and out
}

UPDATE: ARM diimplementasikan di Java 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html


2

Solusi Anda tepat seperti yang harus Anda lakukan. Anda tidak dapat memastikan bahwa deklarasi Anda bahkan tercapai di blok percobaan, yang akan menghasilkan pengecualian lain di blok catch.

Ini hanya harus berfungsi sebagai cakupan terpisah.

try
    dim i as integer = 10 / 0 ''// Throw an exception
    dim s as string = "hi"
catch (e)
    console.writeln(s) ''// Would throw another exception, if this was allowed to compile
end try

2

Variabelnya adalah tingkat blok dan terbatas pada blok Coba atau Tangkap itu. Mirip dengan mendefinisikan variabel dalam pernyataan if. Pikirkan situasi ini.

try {    
    fileOpen("no real file Name");    
    String s = "GO TROJANS"; 
} catch (Exception) {   
    print(s); 
}

String tidak akan pernah dideklarasikan, jadi tidak bisa diandalkan.


2

Karena blok try dan blok tangkap adalah 2 blok yang berbeda.

Dalam kode berikut, apakah Anda mengharapkan s yang ditentukan di blok A terlihat di blok B?

{ // block A
  string s = "dude";
}

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}

2

Dengan Python, mereka terlihat di blok catch / last jika garis yang menyatakan mereka tidak melempar.


2

Meskipun dalam contoh Anda aneh karena tidak berhasil, ambillah yang serupa ini:

    try
    {
         //Code 1
         String s = "1|2";
         //Code 2
    }
    catch
    {
         Console.WriteLine(s.Split('|')[1]);
    }

Ini akan menyebabkan tangkapan menampilkan pengecualian referensi nol jika Kode 1 rusak. Sekarang sementara semantik coba / tangkap cukup dipahami dengan baik, ini akan menjadi kasus sudut yang menjengkelkan, karena s didefinisikan dengan nilai awal, jadi secara teori seharusnya tidak pernah nol, tetapi di bawah semantik bersama, itu akan menjadi.

Sekali lagi, ini secara teori dapat diperbaiki dengan hanya mengizinkan definisi terpisah ( String s; s = "1|2";), atau beberapa kumpulan kondisi lainnya, tetapi umumnya lebih mudah untuk mengatakan tidak.

Selain itu, memungkinkan semantik ruang lingkup untuk didefinisikan secara global tanpa kecuali, khususnya, penduduk setempat bertahan selama {} mereka didefinisikan, dalam semua kasus. Poin kecil, tapi satu poin.

Terakhir, untuk melakukan apa yang Anda inginkan, Anda dapat menambahkan satu set tanda kurung di sekitar tombol try. Memberi Anda ruang lingkup yang Anda inginkan, meskipun biayanya sedikit mudah dibaca, tetapi tidak terlalu banyak.

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}

1

Dalam contoh spesifik yang Anda berikan, menginisialisasi s tidak dapat memunculkan pengecualian. Jadi Anda akan berpikir bahwa mungkin cakupannya dapat diperluas.

Namun secara umum, ekspresi penginisialisasi dapat memunculkan pengecualian. Ini tidak masuk akal untuk variabel yang penginisialisasinya melontarkan pengecualian (atau yang dideklarasikan setelah variabel lain di mana itu terjadi) berada dalam ruang lingkup untuk menangkap / akhirnya.

Selain itu, keterbacaan kode akan terganggu. Aturan dalam C (dan bahasa yang mengikutinya, termasuk C ++, Java dan C #) sederhana: cakupan variabel mengikuti blok.

Jika Anda ingin variabel berada dalam ruang lingkup untuk coba / tangkap / akhirnya tetapi tidak di tempat lain, maka bungkus semuanya dalam set kurung kurawal (blok kosong) dan deklarasikan variabel sebelum percobaan.


1

Sebagian alasan mereka tidak berada dalam cakupan yang sama adalah karena pada titik mana pun dari blok percobaan, Anda dapat melontarkan pengecualian. Jika mereka berada dalam ruang lingkup yang sama, itu adalah bencana menunggu, karena tergantung di mana pengecualian itu dilemparkan, itu bisa menjadi lebih ambigu.

Setidaknya ketika dideklarasikan di luar blok percobaan, Anda tahu pasti apa variabel minimalnya ketika pengecualian dilemparkan; Nilai variabel sebelum blok percobaan.


1

Ketika Anda mendeklarasikan variabel lokal, ia ditempatkan di tumpukan (untuk beberapa jenis, seluruh nilai objek akan berada di tumpukan, untuk jenis lain hanya referensi yang akan ada di tumpukan). Ketika ada pengecualian di dalam blok percobaan, variabel lokal di dalam blok dibebaskan, yang berarti tumpukan "dibatalkan" kembali ke keadaan di awal blok percobaan. Ini memang disengaja. Begitulah cara coba / tangkap dapat mundur dari semua panggilan fungsi di dalam blok dan mengembalikan sistem Anda ke keadaan fungsional. Tanpa mekanisme ini, Anda tidak akan pernah bisa memastikan keadaan apa pun saat pengecualian terjadi.

Memiliki kode penanganan kesalahan Anda bergantung pada variabel yang dideklarasikan secara eksternal yang nilainya berubah di dalam blok percobaan tampaknya seperti desain yang buruk bagi saya. Apa yang Anda lakukan pada dasarnya adalah membocorkan sumber daya dengan sengaja untuk mendapatkan informasi (dalam kasus khusus ini tidak terlalu buruk karena Anda hanya membocorkan informasi, tetapi bayangkan jika itu adalah sumber lain? Anda hanya membuat hidup Anda lebih sulit di masa depan). Saya akan menyarankan untuk memecah blok percobaan Anda menjadi potongan-potongan yang lebih kecil jika Anda memerlukan lebih banyak perincian dalam penanganan kesalahan.


1

Ketika Anda mencoba menangkap, Anda harus mengetahui sebagian besar kesalahan yang mungkin terjadi. Kelas Exception ini biasanya memberi tahu semua yang Anda butuhkan tentang pengecualian. Jika tidak, Anda harus membuat kelas pengecualian Anda sendiri dan meneruskan informasi itu. Dengan cara itu, Anda tidak perlu mendapatkan variabel dari dalam blok percobaan, karena Exception bersifat menjelaskan sendiri. Jadi jika Anda perlu melakukan banyak hal ini, pikirkan tentang desain Anda, dan coba pikirkan jika ada cara lain, bahwa Anda dapat memprediksi pengecualian yang akan datang, atau menggunakan informasi yang berasal dari pengecualian, dan kemudian mungkin mengembangkan kembali milik Anda sendiri. pengecualian dengan informasi lebih lanjut.


1

Seperti yang telah ditunjukkan oleh pengguna lain, tanda kurung kurawal menentukan cakupan di hampir semua bahasa gaya C yang saya ketahui.

Jika itu adalah variabel sederhana, lalu mengapa Anda peduli berapa lama akan berada dalam ruang lingkup? Ini bukan masalah besar.

di C #, jika itu adalah variabel kompleks, Anda akan ingin menerapkan IDisposable. Anda kemudian dapat menggunakan try / catch / last dan memanggil obj.Dispose () di blok last. Atau Anda dapat menggunakan kata kunci using, yang secara otomatis akan memanggil Buang di akhir bagian kode.


1

Bagaimana jika pengecualian dilempar ke beberapa kode yang berada di atas deklarasi variabel. Artinya, deklarasi itu sendiri tidak terjadi dalam kasus ini.

try {

       //doSomeWork // Exception is thrown in this line. 
       String s;
       //doRestOfTheWork

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}

1

The C # Spec (15,2) menyatakan "Ruang lingkup variabel lokal atau konstan dinyatakan dalam blok ist blok."

(dalam contoh pertama Anda, blok percobaan adalah blok tempat "s" dideklarasikan)


0

Saya pikir karena sesuatu di blok percobaan memicu pengecualian, isi namespace tidak dapat dipercaya - yaitu mereferensikan String 's' di blok tangkap dapat menyebabkan lemparan pengecualian lain.


0

Jika itu tidak memunculkan kesalahan kompilasi, dan Anda dapat mendeklarasikannya untuk sisa metode, maka tidak akan ada cara untuk hanya mendeklarasikannya hanya dalam lingkup percobaan. Ini memaksa Anda untuk secara eksplisit tentang di mana variabel seharusnya ada dan tidak membuat asumsi.


0

Jika kita mengabaikan masalah blok pelingkupan sejenak, pembuat komplemen harus bekerja lebih keras dalam situasi yang tidak didefinisikan dengan baik. Meskipun ini bukan tidak mungkin, kesalahan pelingkupan juga memaksa Anda, pembuat kode, untuk menyadari implikasi dari kode yang Anda tulis (bahwa string mungkin nol di blok catch). Jika kode Anda legal, dalam kasus pengecualian OutOfMemory, s bahkan tidak dijamin akan dialokasikan slot memori:

// won't compile!
try
{
    VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
    string s = "Help";
}
catch
{
    Console.WriteLine(s); // whoops!
}

CLR (dan karenanya compiler) juga memaksa Anda untuk menginisialisasi variabel sebelum digunakan. Dalam blok tangkap yang disajikan tidak dapat menjamin ini.

Jadi kami berakhir dengan kompiler harus melakukan banyak pekerjaan, yang dalam praktiknya tidak memberikan banyak manfaat dan mungkin akan membingungkan orang dan mengarahkan mereka untuk bertanya mengapa try / catch bekerja secara berbeda.

Selain konsistensi, dengan tidak mengizinkan sesuatu yang mewah dan mengikuti semantik pelingkupan yang sudah ada yang digunakan di seluruh bahasa, compiler dan CLR dapat memberikan jaminan yang lebih besar tentang status variabel di dalam blok catch. Bahwa itu ada dan telah diinisialisasi.

Perhatikan bahwa perancang bahasa telah melakukan pekerjaan yang baik dengan konstruksi lain seperti menggunakan dan mengunci di mana masalah dan ruang lingkup didefinisikan dengan baik, yang memungkinkan Anda untuk menulis kode yang lebih jelas.

misalnya kata kunci menggunakan dengan objek IDisposable di:

using(Writer writer = new Writer())
{
    writer.Write("Hello");
}

setara dengan:

Writer writer = new Writer();
try
{        
    writer.Write("Hello");
}
finally
{
    if( writer != null)
    {
        ((IDisposable)writer).Dispose();
    }
}

Jika percobaan / tangkap / akhirnya sulit dipahami, coba refactoring atau perkenalkan lapisan tipuan lain dengan kelas menengah yang merangkum semantik dari apa yang Anda coba capai. Tanpa melihat kode sebenarnya, sulit untuk lebih spesifik.


0

Alih-alih variabel lokal, properti publik dapat dideklarasikan; ini juga harus menghindari potensi kesalahan lain dari variabel yang tidak ditetapkan. string publik S {get; set; }


-1

Jika operasi penugasan gagal, pernyataan catch Anda akan memiliki referensi null kembali ke variabel yang tidak ditetapkan.


2
Ini belum ditetapkan. Ini bahkan tidak nol (tidak seperti variabel instan dan variabel statis).
Tom Hawtin - tackline

-1

C # 3.0:

string html = new Func<string>(() =>
{
    string webpage;

    try
    {
        using(WebClient downloader = new WebClient())
        {
            webpage = downloader.DownloadString(url);
        }
    }
    catch(WebException)
    {
        Console.WriteLine("Download failed.");  
    }

    return webpage;
})();

WTF? Mengapa tidak memilih? Enkapsulasi merupakan bagian integral dari OOP. Terlihat cantik juga.
inti

2
Saya bukan downvote, tapi yang salah adalah mengembalikan string yang tidak diinisialisasi.
Ben Voigt
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.