Dlaczego zmienne nie są zadeklarowane jako „try” w zakresie w „catch” lub „final”?

139

W C # i Javie (i być może także w innych językach) zmienne zadeklarowane w bloku „try” nie znajdują się w zakresie w odpowiednich blokach „catch” lub „final”. Na przykład poniższy kod nie jest kompilowany:

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

W tym kodzie błąd kompilacji występuje w odwołaniu do s w bloku catch, ponieważ s znajduje się tylko w zakresie w bloku try. (W Javie błąd kompilacji to „s nie można rozwiązać”; w C # to „nazwa„ s ”nie istnieje w bieżącym kontekście”).

Wydaje się, że ogólnym rozwiązaniem tego problemu jest deklarowanie zmiennych tuż przed blokiem try, a nie w bloku try:

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

Jednak przynajmniej dla mnie (1) wydaje się to niezgrabnym rozwiązaniem i (2) skutkuje tym, że zmienne mają większy zakres niż zamierzał programista (cała pozostała część metody, a nie tylko w kontekście spróbuj złapać wreszcie).

Moje pytanie brzmi: jakie były / są uzasadnienie (y) tej decyzji dotyczącej projektu języka (w Javie, w C # i / lub w innych odpowiednich językach)?

Jon Schneider
źródło

Odpowiedzi:

171

Dwie rzeczy:

  1. Ogólnie rzecz biorąc, Java ma tylko 2 poziomy zakresu: globalny i funkcjonalny. Ale try / catch jest wyjątkiem (gra słów nie jest przeznaczona). Kiedy wyjątek jest zgłaszany i obiekt wyjątku otrzymuje przypisaną do niego zmienną, ta zmienna obiektu jest dostępna tylko w sekcji "catch" i jest niszczona, gdy tylko przechwytywanie się zakończy.

  2. (i co ważniejsze). Nie możesz wiedzieć, gdzie w bloku try wyjątek został zgłoszony. Mogło to być przed zadeklarowaniem zmiennej. Dlatego nie można powiedzieć, jakie zmienne będą dostępne dla klauzuli catch / final. Rozważ następujący przypadek, w którym zakres jest taki, jak sugerowałeś:

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

Jest to oczywiście problem - kiedy dotrzesz do procedury obsługi wyjątków, s nie zostaną zadeklarowane. Biorąc pod uwagę, że połowy mają radzić sobie w wyjątkowych okolicznościach i ostatecznie muszą zostać wykonane, bycie bezpiecznym i deklarowanie tego problemu w czasie kompilacji jest znacznie lepsze niż w czasie wykonywania.

John Christensen
źródło
55

Skąd możesz być pewien, że dotarłeś do części deklaracji w swoim bloku catch? A co, jeśli instancja zgłosi wyjątek?

Burkhard
źródło
6
Co? Deklaracje zmiennych nie zgłaszają wyjątków.
Joshua,
6
Zgoda, to instancja może zgłosić wyjątek.
Burkhard,
19

Tradycyjnie w językach w stylu C to, co dzieje się w nawiasach klamrowych, pozostaje wewnątrz nawiasów klamrowych. Myślę, że posiadanie czasu życia zmiennej rozciągającej się w takich zakresach byłoby nieintuicyjne dla większości programistów. Możesz osiągnąć to, co chcesz, umieszczając bloki try / catch / final wewnątrz innego poziomu nawiasów. na przykład

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

EDIT: Myślę, że każda reguła ma mieć wyjątek. Oto poprawne C ++:

int f() { return 0; }

void main() 
{
    int y = 0;

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

Zakres x jest warunkowy, klauzula then i klauzula else.

Ferruccio
źródło
10

Wszyscy inni poruszyli podstawy - to, co dzieje się w bloku, pozostaje w bloku. Ale w przypadku .NET pomocne może być zbadanie, co według kompilatora się dzieje. Weźmy na przykład następujący kod try / catch (zwróć uwagę, że StreamReader jest poprawnie zadeklarowany poza blokami):

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();
        }
    }
}

Spowoduje to kompilację do czegoś podobnego do następującego w 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

Co widzimy MSIL szanuje bloki - są one nieodłączną częścią kodu źródłowego generowanego podczas kompilowania C #. Zakres jest nie tylko sztywny w specyfikacji C #, ale także w specyfikacji CLR i CLS.

Luneta Cię chroni, ale czasami trzeba ją obejść. Z biegiem czasu przyzwyczajasz się do tego i zaczyna być to naturalne. Jak wszyscy mówili, to, co dzieje się w bloku, pozostaje w tym bloku. Chcesz się czymś podzielić? Musisz wyjść poza bloki ...

John Rudy
źródło
8

W każdym razie w C ++ zakres zmiennej automatycznej jest ograniczony przez otaczające ją nawiasy klamrowe. Dlaczego ktoś miałby oczekiwać, że będzie inaczej, umieszczając słowo kluczowe try poza nawiasami klamrowymi?

ravenspoint
źródło
1
Zgoda; „}” oznacza koniec zakresu. Jednak try-catch-final jest niezwykłe, ponieważ po bloku try, musisz mieć catch i / lub final block; w ten sposób wyjątek od normalnej reguły, gdzie zakres bloku try przenoszony do powiązanego catch / w końcu może wydawać się akceptowalny?
Jon Schneider,
7

Jak zauważył ravenspoint, każdy oczekuje, że zmienne będą lokalne dla bloku, w którym są zdefiniowane. tryWprowadza blok i tak samo catch.

Jeśli chcesz, aby zmienne były lokalne dla obu tryi catch, spróbuj umieścić oba w bloku:

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

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}
Daren Thomas
źródło
5

Prosta odpowiedź jest taka, że ​​język C i większość języków, które odziedziczyły jego składnię, ma zasięg blokowy. Oznacza to, że jeśli zmienna jest zdefiniowana w jednym bloku, tj. Wewnątrz {}, to jest to jej zasięg.

Nawiasem mówiąc, wyjątkiem jest JavaScript, który ma podobną składnię, ale ma zakres funkcji. W JavaScript zmienna zadeklarowana w bloku try znajduje się w zakresie w bloku catch, a wszędzie indziej w funkcji zawierającej.

dgvid
źródło
4

@burkhard ma pytanie, dlaczego odpowiedział poprawnie, ale jako uwaga, którą chciałem dodać, chociaż zalecany przykład rozwiązania jest dobry 99,9999 +% czasu, nie jest to dobra praktyka, znacznie bezpieczniej jest sprawdzić wartość null przed użyciem coś tworzy instancję w bloku try lub inicjalizuje zmienną do czegoś, zamiast deklarować ją przed blokiem try. Na przykład:

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

Lub:

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

Powinno to zapewnić skalowalność obejścia, tak że nawet jeśli to, co robisz w bloku try jest bardziej złożone niż przypisywanie ciągu, powinieneś być w stanie bezpiecznie uzyskać dostęp do danych z bloku catch.

Timothy Carter
źródło
4

Zgodnie z sekcją zatytułowaną „Jak rzucać i łapać wyjątki” w lekcji 2 zestawu MCTS Self-Paced Training Kit (egzamin 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation , przyczyną może być wyjątek przed deklaracjami zmiennych w bloku try (jak już zauważyli inni).

Cytat ze strony 25:

„Zauważ, że deklaracja StreamReader została przeniesiona poza blok Try w poprzednim przykładzie. Jest to konieczne, ponieważ blok Finalnie nie może uzyskać dostępu do zmiennych zadeklarowanych w bloku Try. Ma to sens, ponieważ w zależności od miejsca wystąpienia wyjątku deklaracje zmiennych w Blok próbny mógł jeszcze nie zostać wykonany . "

gaj
źródło
4

Odpowiedź, jak wszyscy zauważyli, brzmi właściwie „tak definiuje się bloki”.

Istnieje kilka propozycji, aby uczynić kod ładniejszym. Zobacz ARM

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

Zamknięcia mają również rozwiązać ten problem.

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

AKTUALIZACJA: ARM jest zaimplementowany w Javie 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html

ykaganovich
źródło
2

Twoje rozwiązanie jest dokładnie tym, co powinieneś zrobić. Nie możesz być pewien, że Twoja deklaracja została osiągnięta w bloku try, co spowodowałoby kolejny wyjątek w bloku catch.

Po prostu musi działać jako oddzielne zakresy.

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
źródło
2

Zmienne są na poziomie bloku i ograniczone do tego bloku Try lub Catch. Podobny do definiowania zmiennej w instrukcji if. Pomyśl o tej sytuacji.

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

Ciąg nigdy nie zostałby zadeklarowany, więc nie można na nim polegać.

jW.
źródło
2

Ponieważ blok try i blok catch to 2 różne bloki.

Czy w poniższym kodzie spodziewasz się, że s zdefiniowane w bloku A będą widoczne w bloku B?

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

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}
Francesca
źródło
2

Podczas gdy w twoim przykładzie dziwne jest, że nie działa, weź ten podobny:

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

Spowodowałoby to, że catch zgłosiłby wyjątek odwołania zerowego, gdyby kod 1 zepsuł. Chociaż semantyka try / catch jest całkiem dobrze zrozumiana, byłby to irytujący przypadek narożny, ponieważ s jest zdefiniowane z wartością początkową, więc teoretycznie nigdy nie powinno być zerowe, ale w ramach semantyki współdzielonej tak byłoby.

Teoretycznie można to naprawić, zezwalając tylko na oddzielne definicje ( String s; s = "1|2";) lub inny zestaw warunków, ale generalnie łatwiej jest po prostu powiedzieć nie.

Ponadto umożliwia globalne definiowanie semantyki zakresu bez wyjątku, a konkretnie {}, we wszystkich przypadkach , wartości lokalne trwają tak długo, jak długo są zdefiniowane w programie. Drobny punkt, ale punkt.

Wreszcie, aby zrobić to, co chcesz, możesz dodać zestaw nawiasów wokół try catch. Daje żądany zakres, chociaż kosztuje trochę czytelności, ale nie za dużo.

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}
Guvante
źródło
1

W konkretnym przykładzie, który podałeś, inicjalizacja s nie może zgłosić wyjątku. Można by więc pomyśleć, że może jego zakres mógłby zostać rozszerzony.

Ale ogólnie wyrażenia inicjatora mogą generować wyjątki. Nie miałoby sensu, gdyby zmienna, której inicjator zgłosił wyjątek (lub która została zadeklarowana po innej zmiennej, w której to się zdarzyło) znajdowała się w zasięgu catch / final.

Ucierpiałaby również czytelność kodu. Reguła w C (i językach, które za nią podążają, w tym C ++, Java i C #) jest prosta: zakresy zmiennych podążają za blokami.

Jeśli chcesz, aby zmienna znajdowała się w zakresie dla try / catch / w końcu, ale nigdzie indziej, zawiń całość w inny zestaw nawiasów klamrowych (czysty blok) i zadeklaruj zmienną przed try.

Steve Jessop
źródło
1

Jednym z powodów, dla których nie znajdują się w tym samym zakresie, jest to, że w dowolnym momencie bloku try możesz zgłosić wyjątek. Gdyby znajdowały się w tym samym zakresie, czekałaby katastrofa, ponieważ w zależności od tego, gdzie został zgłoszony wyjątek, może być jeszcze bardziej niejednoznaczny.

Przynajmniej wtedy, gdy jest zadeklarowana poza blokiem try, wiesz na pewno, jaka zmienna może być przynajmniej w przypadku zgłoszenia wyjątku; Wartość zmiennej przed blokiem try.

zxcv
źródło
1

Kiedy deklarujesz zmienną lokalną, jest ona umieszczana na stosie (dla niektórych typów cała wartość obiektu będzie na stosie, dla innych tylko odniesienie będzie na stosie). Gdy w bloku try występuje wyjątek, zmienne lokalne w bloku są zwalniane, co oznacza, że ​​stos jest „rozwijany” z powrotem do stanu, w którym znajdował się na początku bloku try. Jest to zgodne z projektem. W ten sposób try / catch jest w stanie wycofać się ze wszystkich wywołań funkcji w bloku i przywrócić system do stanu funkcjonalnego. Bez tego mechanizmu nigdy nie można było być pewnym stanu czegokolwiek w przypadku wystąpienia wyjątku.

Posiadanie kodu obsługi błędów polegającego na zewnętrznie zadeklarowanych zmiennych, których wartości zostały zmienione w bloku try, wydaje mi się złym projektem. To, co robisz, to celowe wyciekanie zasobów w celu uzyskania informacji (w tym konkretnym przypadku nie jest tak źle, ponieważ tylko wyciekasz informacje, ale wyobraź sobie, że to był jakiś inny zasób? Po prostu utrudniasz sobie życie w przyszłość). Sugerowałbym podzielenie bloków try na mniejsze fragmenty, jeśli potrzebujesz większej szczegółowości w obsłudze błędów.

Klin
źródło
1

Kiedy masz próbę złapania, powinieneś w większości wiedzieć, że może to spowodować błędy. Te klasy wyjątków normalnie informują wszystko, czego potrzebujesz o wyjątku. Jeśli nie, powinieneś stworzyć własne klasy wyjątków i przekazać te informacje. W ten sposób nigdy nie będziesz musiał pobierać zmiennych z wnętrza bloku try, ponieważ wyjątek nie wymaga wyjaśnień. Więc jeśli musisz to dużo zrobić, pomyśl o swoim projekcie i spróbuj pomyśleć, czy jest jakiś inny sposób, że możesz przewidzieć nadchodzące wyjątki lub użyć informacji pochodzących z wyjątków, a następnie może ponownie wrzucić własne wyjątek z dodatkowymi informacjami.

Jesper Blad Jensen
źródło
1

Jak zauważyli inni użytkownicy, nawiasy klamrowe definiują zakres w prawie każdym języku C, jaki znam.

Jeśli jest to prosta zmienna, to dlaczego obchodzi Cię, jak długo będzie ona objęta zakresem? To nie jest taka wielka sprawa.

w C #, jeśli jest to zmienna złożona, będziesz chciał zaimplementować IDisposable. Następnie możesz użyć try / catch / final i wywołać obj.Dispose () w bloku last. Możesz też użyć słowa kluczowego using, które automatycznie wywoła metodę Dispose na końcu sekcji kodu.

Charles Graham
źródło
1

W Pythonie są one widoczne w blokach catch / final jeśli linia deklarująca, że ​​nie została wyrzucona.


źródło
1

Co się stanie, jeśli wyjątek zostanie zgłoszony w jakimś kodzie, który znajduje się nad deklaracją zmiennej. Co oznacza, że ​​sama deklaracja nie miała miejsca w tym przypadku.

try {

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

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}
Ravi
źródło
1

C # Spec (15.2) stwierdza: „Zakres zmiennej lokalnej lub stałe zadeklarowane w bloku ist bloku.”

(w pierwszym przykładzie blok try to blok, w którym zadeklarowano „s”)

tamberg
źródło
0

Pomyślałem, że ponieważ coś w bloku try wyzwoliło wyjątek, nie można ufać zawartości przestrzeni nazw - tj. Odwoływanie się do ciągu znaków w bloku catch mogłoby spowodować wyrzucenie kolejnego wyjątku.

jpbarto
źródło
0

Cóż, jeśli nie zgłosi błędu kompilacji i możesz go zadeklarować do końca metody, nie byłoby sposobu, aby zadeklarować go tylko w zakresie try. Zmusza cię do wyraźnego określenia, gdzie zmienna ma istnieć i nie przyjmuje założeń.

kemiller2002
źródło
0

Jeśli przez chwilę zignorujemy kwestię blokowania zakresu, osoba udzielająca pomocy będzie musiała pracować znacznie ciężej w sytuacji, która nie jest dobrze zdefiniowana. Chociaż nie jest to niemożliwe, błąd określania zakresu zmusza również Ciebie, autora kodu, do uświadomienia sobie implikacji kodu, który piszesz (że ciąg znaków s może być pusty w bloku catch). Jeśli twój kod był legalny, w przypadku wyjątku OutOfMemory, s nie ma nawet gwarancji, że zostanie przydzielone miejsce w pamięci:

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

CLR (a tym samym kompilator) również wymusza inicjalizację zmiennych przed ich użyciem. W przedstawionym bloku catch nie może tego zagwarantować.

W rezultacie kompilator musi wykonać dużo pracy, co w praktyce nie zapewnia wielu korzyści i prawdopodobnie wprowadzałoby ludzi w błąd i prowadziłoby ich do pytania, dlaczego try / catch działa inaczej.

Oprócz spójności, nie dopuszczając niczego wymyślnego i zgodnie z już ustaloną semantyką określania zakresu używaną w całym języku, kompilator i środowisko CLR są w stanie zapewnić większą gwarancję stanu zmiennej wewnątrz bloku catch. Że istnieje i został zainicjowany.

Zwróć uwagę, że projektanci języka wykonali dobrą robotę z innymi konstrukcjami, takimi jak używanie i blokowanie, w których problem i zakres są dobrze zdefiniowane, co pozwala napisać jaśniejszy kod.

np. słowo kluczowe using z obiektami IDisposable w:

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

jest równa:

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

Jeśli twoja próba / złapanie / w końcu jest trudna do zrozumienia, spróbuj refaktoryzacji lub wprowadzenia innej warstwy pośredniej z klasą pośrednią, która zawiera semantykę tego, co próbujesz osiągnąć. Nie widząc prawdziwego kodu, trudno jest być bardziej szczegółowym.

Robert Paulson
źródło
0

Zamiast zmiennej lokalnej można by zadeklarować właściwość publiczną; Powinno to również uniknąć kolejnego potencjalnego błędu nieprzypisanej zmiennej. public string S {get; zestaw; }

przydatne
źródło
-1

Jeśli operacja przypisania nie powiedzie się, instrukcja catch będzie miała odwołanie o wartości null z powrotem do nieprzypisanej zmiennej.

Programista SaaS
źródło
2
To jest nieprzypisane. Nie ma nawet wartości null (w przeciwieństwie do instancji i zmiennych statycznych).
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;
})();
rdzeń
źródło
WTF? Dlaczego głosowanie przeciw? Hermetyzacja jest integralną częścią OOP. Wygląda też ładnie.
rdzeń
2
Nie byłem przeciw, ale nie tak jest zwracanie niezainicjowanego ciągu.
Ben Voigt