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)?

Jon Schneider
sumber

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.

John Christensen
sumber
55

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

Burkhard
sumber
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.

Ferruccio
sumber
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 ...

John Rudy
sumber
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?

ravenspoint
sumber
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);
    }
}
Daren Thomas
sumber
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.

dgvid
sumber
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 . "

hutan kecil
sumber
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.

Timothy Carter
sumber
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

ykaganovich
sumber
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
EndangeredMassa
sumber
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.

jW.
sumber
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
}
Francesca
sumber
2

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


sumber
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);
     }
}
Guvante
sumber
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.

Steve Jessop
sumber
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.

zxcv
sumber
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.

Baji
sumber
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.

Jesper Blad Jensen
sumber
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.

Charles Graham
sumber
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
}
Ravi
sumber
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)

Tamberg
sumber
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.

jpbarto.dll
sumber
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.

kemiller2002
sumber
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.

Robert Paulson
sumber
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; }

Bermanfaat
sumber
-1

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

Pengembang SaaS
sumber
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;
})();
inti
sumber
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