web-dev-qa-db-fra.com

Pourquoi les variables ne sont-elles pas déclarées dans "try" dans la portée dans "catch" ou "finalement"?

En C # et en Java (et éventuellement d'autres langages également), les variables déclarées dans un bloc "try" ne sont pas dans la portée des blocs "catch" ou "finally" correspondants. Par exemple, le code suivant ne compile pas:

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

Dans ce code, une erreur de compilation se produit sur la référence à s dans le bloc catch, car s est uniquement à portée dans le bloc try. (En Java, l'erreur de compilation est "ne peut pas être résolu"; en C #, c'est "le nom" n'existe pas dans le contexte actuel ".)

La solution générale à ce problème semble être de déclarer à la place des variables juste avant le bloc try, plutôt que dans le bloc try:

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

Cependant, au moins pour moi, (1) cela ressemble à une solution maladroite, et (2) il en résulte que les variables ont une portée plus grande que celle prévue par le programmeur (tout le reste de la méthode, au lieu de seulement dans le contexte de la essayer-attraper-enfin).

Ma question est, quelles étaient/sont la/les justification (s) derrière cette décision de conception de langage (en Java, en C # et/ou dans tout autre langage applicable)?

128
Jon Schneider

Deux choses:

  1. Généralement, Java a seulement 2 niveaux de portée: global et function. Mais, try/catch est une exception (sans jeu de mots). Lorsqu'une exception est levée et que l'objet exception reçoit une variable assignée pour elle, cette variable objet n'est disponible que dans la section "catch" et est détruite dès que le catch est terminé.

  2. (et plus important). Vous ne pouvez pas savoir où dans le bloc try l'exception a été levée. C'était peut-être avant que votre variable ne soit déclarée. Par conséquent, il est impossible de dire quelles variables seront disponibles pour la clause catch/finally. Considérez le cas suivant, où l'étendue est comme vous l'avez suggéré:

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

C'est clairement un problème - lorsque vous atteignez le gestionnaire d'exceptions, les s n'auront pas été déclarés. Étant donné que les captures sont destinées à gérer des circonstances exceptionnelles et que finalement doit s'exécuter, être sûr et déclarer cela comme un problème au moment de la compilation est bien meilleur qu'au moment de l'exécution.

161
John Christensen

Comment pouvez-vous être sûr que vous avez atteint la partie déclaration dans votre bloc catch? Et si l'instanciation lève l'exception?

55
Burkhard

Traditionnellement, dans les langages de style C, ce qui se passe à l'intérieur des accolades reste à l'intérieur des accolades. Je pense qu'avoir la durée de vie d'une étendue variable sur des étendues comme celle-ci ne serait pas intuitif pour la plupart des programmeurs. Vous pouvez réaliser ce que vous voulez en enfermant les blocs try/catch/finally dans un autre niveau d'accolades. par exemple.

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

EDIT: Je suppose que chaque règle fait a une exception. Ce qui suit est C++ valide:

int f() { return 0; }

void main() 
{
    int y = 0;

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

La portée de x est le conditionnel, la clause then et la clause else.

17
Ferruccio

Tout le monde a apporté les bases - ce qui se passe dans un bloc reste dans un bloc. Mais dans le cas de .NET, il peut être utile d'examiner ce que le compilateur pense qu'il se passe. Prenez, par exemple, le code try/catch suivant (notez que le StreamReader est déclaré, correctement, en dehors des blocs):

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

Cela compilera quelque chose de similaire à ce qui suit dans 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

Que voyons-nous? MSIL respecte les blocs - ils font intrinsèquement partie du code sous-jacent généré lorsque vous compilez votre C #. La portée n'est pas seulement définie dans la spécification C #, elle l'est également dans la spécification CLR et CLS.

La portée vous protège, mais vous devez parfois la contourner. Avec le temps, on s'y habitue et ça commence à se sentir naturel. Comme tout le monde l'a dit, ce qui se passe dans un bloc reste dans ce bloc. Tu veux partager quelque chose? Vous devez sortir des blocs ...

9
John Rudy

En C++ en tout cas, la portée d'une variable automatique est limitée par les accolades qui l'entourent. Pourquoi est-ce que quelqu'un s'attendrait à ce que ce soit différent en plongeant un mot clé try en dehors des accolades?

8
ravenspoint

Comme l'a souligné ravenspoint, tout le monde s'attend à ce que les variables soient locales au bloc dans lequel elles sont définies. try introduit un bloc, tout comme catch.

Si vous voulez des variables locales à la fois try et catch, essayez de les enfermer dans un bloc:

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

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

La réponse simple est que C et la plupart des langages qui ont hérité de sa syntaxe ont une portée de bloc. Cela signifie que si une variable est définie dans un bloc, c'est-à-dire à l'intérieur de {}, c'est sa portée.

L'exception, en passant, est JavaScript, qui a une syntaxe similaire, mais dont la portée est fonction. En JavaScript, une variable déclarée dans un bloc try a une portée dans le bloc catch, et partout ailleurs dans sa fonction conteneur.

5
dgvid

@burkhard a la question de savoir pourquoi a répondu correctement, mais comme une note que je voulais ajouter, bien que votre exemple de solution recommandé soit bon 99,9999 +% du temps, ce n'est pas une bonne pratique, il est beaucoup plus sûr de vérifier la nullité avant d'utiliser quelque chose instancie dans le bloc try, ou initialise la variable à quelque chose au lieu de simplement la déclarer avant le bloc try. Par exemple:

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

Ou:

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

Cela devrait fournir une évolutivité dans la solution de contournement, de sorte que même lorsque ce que vous faites dans le bloc try est plus complexe que l'attribution d'une chaîne, vous devriez pouvoir accéder en toute sécurité aux données de votre bloc catch.

4
Timothy Carter

Selon la section intitulée "Comment lever et intercepter des exceptions" dans la leçon 2 de Kit de formation auto-rythmé SCTM (examen 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation, la raison est que l'exception peut s'être produite avant les déclarations de variables dans le bloc try (comme d'autres l'ont déjà noté).

Citation de la page 25:

"Notez que la déclaration StreamReader a été déplacée en dehors du bloc Try dans l'exemple précédent. Cela est nécessaire car le bloc Final ne peut pas accéder aux variables déclarées dans le bloc Try. Cela rend sens car selon l'endroit où une exception s'est produite, les déclarations de variables dans le bloc Try peuvent ne pas avoir encore été exécutées . "

4
hurst

La réponse, comme tout le monde l'a souligné, est à peu près "c'est ainsi que les blocs sont définis".

Il y a quelques propositions pour rendre le code plus joli. Voir BRAS

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

fermetures sont censés résoudre ce problème également.

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

MISE À JOUR: ARM est implémenté dans Java 7. http : //download.Java.net/jdk7/docs/technotes/guides/language/try-with-resources.html

4
ykaganovich

Votre solution est exactement ce que vous devez faire. Vous ne pouvez pas être sûr que votre déclaration a même été atteinte dans le bloc try, ce qui entraînerait une autre exception dans le bloc catch.

Il doit simplement fonctionner comme des étendues distinctes.

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
EndangeredMassa

Les variables sont au niveau du bloc et limitées à ce bloc Try ou Catch. Similaire à la définition d'une variable dans une instruction if. Pensez à cette situation.

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

La chaîne ne serait jamais déclarée, donc on ne peut pas en dépendre.

2
jW.

Parce que le bloc try et le bloc catch sont 2 blocs différents.

Dans le code suivant, vous attendriez-vous à ce que les éléments définis dans le bloc A soient visibles dans le bloc B?

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

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

Comme cela a été souligné par d'autres utilisateurs, les accolades définissent la portée dans à peu près tous les langages de style C que je connais.

S'il s'agit d'une simple variable, pourquoi vous souciez-vous de la durée de sa portée? Ce n'est pas si grave.

en C #, s'il s'agit d'une variable complexe, vous voudrez implémenter IDisposable. Vous pouvez ensuite utiliser try/catch/finally et appeler obj.Dispose () dans le bloc finally. Ou vous pouvez utiliser le mot-clé using, qui appellera automatiquement Dispose à la fin de la section code.

1
Charles Graham

Que faire si l'exception est levée dans un code qui est au-dessus de la déclaration de la variable. Ce qui signifie que la déclaration elle-même n'a pas eu lieu dans ce cas.

try {

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

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

Dans l'exemple spécifique que vous avez donné, l'initialisation de s ne peut pas lever d'exception. On pourrait donc penser que son champ d'application pourrait être étendu.

Mais en général, les expressions d'initialisation peuvent lever des exceptions. Cela n'aurait aucun sens pour une variable dont l'initialiseur a levé une exception (ou qui a été déclarée après une autre variable là où cela s'est produit) d'être à portée de capture/enfin.

De plus, la lisibilité du code en souffrirait. La règle en C (et les langages qui la suivent, y compris C++, Java et C #) est simple: les portées variables suivent les blocs.

Si vous voulez qu'une variable soit à la portée de try/catch/enfin mais nulle part ailleurs, alors enveloppez le tout dans un autre ensemble d'accolades (un bloc nu) et déclarez la variable avant l'essai.

1
Steve Jessop

Une partie de la raison pour laquelle ils ne sont pas dans la même portée est parce qu'à tout moment du bloc try, vous pouvez avoir levé l'exception. S'ils étaient dans la même portée, c'est un désastre en attente, car selon l'endroit où l'exception a été levée, cela pourrait être encore plus ambigu.

Au moins lorsqu'elle est déclarée en dehors du bloc try, vous savez avec certitude ce que la variable au minimum pourrait être lorsqu'une exception est levée; La valeur de la variable avant le bloc try.

1
zxcv

En Python ils sont visibles dans les blocs catch/finally si la ligne les déclarant n'a pas été lancée.

1
adal

Bien que dans votre exemple, il est étrange que cela ne fonctionne pas, prenez celui-ci:

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

Cela entraînerait la capture de lever une exception de référence nulle si le code 1 se cassait. Maintenant, alors que la sémantique de try/catch est assez bien comprise, ce serait un cas de coin ennuyeux, puisque s est défini avec une valeur initiale, donc il ne devrait en théorie jamais être nul, mais sous une sémantique partagée, ce serait le cas.

Là encore, cela pourrait en théorie être corrigé en n'autorisant que des définitions séparées (String s; s = "1|2";), ou un autre ensemble de conditions, mais il est généralement plus facile de simplement dire non.

De plus, il permet de définir la sémantique de portée globalement sans exception, en particulier, les sections locales durent aussi longtemps que {} ils sont définis dans tous les cas. Petit point, mais un point.

Enfin, afin de faire ce que vous voulez, vous pouvez ajouter un ensemble de crochets autour de la prise d'essai. Vous donne la portée que vous voulez, bien qu'elle se fasse au détriment d'un peu de lisibilité, mais pas trop.

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

Lorsque vous déclarez une variable locale, elle est placée sur la pile (pour certains types, la valeur entière de l'objet sera sur la pile, pour d'autres types, seule une référence sera sur la pile). Lorsqu'il y a une exception à l'intérieur d'un bloc try, les variables locales dans le bloc sont libérées, ce qui signifie que la pile est "déroulée" dans l'état où elle était au début du bloc try. C'est par conception. C'est ainsi que le try/catch est capable de se retirer de tous les appels de fonction dans le bloc et remet votre système dans un état fonctionnel. Sans ce mécanisme, vous ne pourriez jamais être sûr de l'état de quoi que ce soit lorsqu'une exception se produit.

Le fait que votre code de gestion des erreurs repose sur des variables déclarées en externe dont les valeurs ont été modifiées à l'intérieur du bloc try me semble une mauvaise conception. Ce que vous faites, c'est essentiellement une fuite intentionnelle de ressources afin d'obtenir des informations (dans ce cas particulier, ce n'est pas si mal parce que vous ne faites que divulguer des informations, mais imaginez s'il s'agissait d'une autre ressource? Vous vous rendez la vie plus difficile dans le futur). Je suggérerais de diviser vos blocs d'essai en morceaux plus petits si vous avez besoin de plus de granularité dans la gestion des erreurs.

1
Wedge

Lorsque vous avez un catch catch, vous devez savoir dans la plupart des cas qu'il peut générer des erreurs. Ces classes d'exception indiquent normalement tout ce dont vous avez besoin concernant l'exception. Sinon, vous devez créer vos propres classes d'exception et transmettre ces informations. De cette façon, vous n'aurez jamais besoin d'obtenir les variables de l'intérieur du bloc try, car l'exception est explicite. Donc, si vous devez faire cela beaucoup, pensez à votre conception et essayez de penser s'il y a une autre manière, que vous pouvez prédire la venue d'exceptions, ou utiliser les informations provenant des exceptions, puis peut-être renvoyer la vôtre exception avec plus d'informations.

1
Jesper Blad Jensen

Ma pensée serait que parce que quelque chose dans le bloc try a déclenché l'exception, son contenu d'espace de noms ne peut pas être approuvé - c'est-à-dire que référencer les chaînes dans le bloc catch pourrait provoquer le lancement d'une autre exception.

0
jpbarto

Eh bien, s'il ne renvoie pas une erreur de compilation et que vous pouvez le déclarer pour le reste de la méthode, il n'y aurait aucun moyen de le déclarer uniquement dans la portée try. Cela vous oblige à être explicite quant à l'endroit où la variable est censée exister et ne fait pas d'hypothèses.

0
kemiller2002

Si nous ignorons le problème du bloc de portée pendant un moment, le complice devra travailler beaucoup plus dur dans une situation qui n'est pas bien définie. Bien que cela ne soit pas impossible, l'erreur de portée vous oblige également, l'auteur du code, à réaliser l'implication du code que vous écrivez (que la chaîne s peut être nulle dans le bloc catch). Si votre code était légal, dans le cas d'une exception OutOfMemory, il n'est même pas garanti de se voir attribuer un emplacement mémoire:

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

Le CLR (et donc le compilateur) vous oblige également à initialiser les variables avant de les utiliser. Dans le bloc catch présenté, il ne peut pas garantir cela.

Nous nous retrouvons donc avec le compilateur devant faire beaucoup de travail, ce qui en pratique ne fournit pas beaucoup d'avantages et pourrait probablement dérouter les gens et les amener à se demander pourquoi try/catch fonctionne différemment.

En plus de la cohérence, en ne permettant rien de fantaisie et en adhérant à la sémantique de portée déjà établie utilisée dans tout le langage, le compilateur et CLR sont en mesure de fournir une plus grande garantie de l'état d'une variable à l'intérieur d'un bloc catch. Qu'elle existe et qu'elle a été initialisée.

Notez que les concepteurs de langage ont fait du bon travail avec d'autres constructions comme en utilisant et lock où le problème et la portée sont bien définis, ce qui vous permet d'écrire du code plus clair.

par exemple. le en utilisant mot-clé avec IDisposable objets dans:

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

est équivalent à:

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

Si votre try/catch/enfin est difficile à comprendre, essayez de refactoriser ou d'introduire une autre couche d'indirection avec une classe intermédiaire qui encapsule la sémantique de ce que vous essayez d'accomplir. Sans voir du vrai code, il est difficile d'être plus précis.

0
Robert Paulson

Au lieu d'une variable locale, une propriété publique pourrait être déclarée; cela devrait également éviter une autre erreur potentielle d'une variable non affectée. chaîne publique S {get; ensemble; }

0
usefulBee

La spécification C # (15.2) indique "La portée d'une variable ou constante locale déclarée dans un bloc est le bloc."

(dans votre premier exemple, le bloc try est le bloc où "s" est déclaré)

0
tamberg