Pourquoi les variables ne sont-elles pas déclarées dans «try» dans la portée de «catch» ou «finally»?


139

En C # et en Java (et peut-être aussi dans d'autres langages), 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 dans la portée du bloc try. (En Java, l'erreur de compilation est "s ne peut pas être résolu"; en C #, c'est "Le nom 's' n'existe pas dans le contexte actuel".)

La solution générale à ce problème semble être à la place de déclarer 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 large que celle prévue par le programmeur (tout le reste de la méthode, au lieu de seulement dans le contexte de la essayez-attrapez-enfin).

Ma question est la suivante: quelle était / sont la ou les raisons derrière cette décision de conception de langage (en Java, en C # et / ou dans tout autre langage applicable)?

Réponses:


171

Deux choses:

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

  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ù la portée est celle que vous avez suggérée:

    
    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, s n'aura pas été déclaré. Étant donné que les captures sont destinées à gérer des circonstances exceptionnelles et que les finally doivent 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.


55

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


6
Hein? Les déclarations de variables ne lèvent pas d'exceptions.
Joshua

6
D'accord, c'est l'instanciation qui pourrait lever l'exception.
Burkhard

19

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 variable s'étendant 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 toutes les règles n'ont une exception. Ce qui suit est un 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.


10

Tout le monde a évoqué 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 pense le compilateur. Prenez, par exemple, le code try / catch suivant (notez que 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 en dur dans la spécification C #, elle est également dans la spécification CLR et CLS.

La lunette 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. Vous souhaitez partager quelque chose? Il faut sortir des blocs ...


8

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


1
D'accord; "}" signifie fin de portée. Cependant, try-catch-finally est inhabituel dans la mesure où après un bloc try, vous devez avoir un bloc catch et / ou finally; ainsi, une exception à la règle normale où la portée d'un bloc try reporté dans la capture / finalement associée peut sembler acceptable?
Jon Schneider

7

Comme l'a souligné Ravenspoint, tout le monde s'attend à ce que les variables soient locales au bloc dans lequel elles sont définies. tryIntroduit un bloc et le fait catch.

Si vous voulez des variables locales à la fois tryet catch, essayez de les inclure dans un bloc:

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

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

5

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 qui a une portée fonction. En JavaScript, une variable déclarée dans un bloc try est dans la portée du bloc catch, et partout ailleurs dans sa fonction conteneur.


4

@burkhard a la question de savoir pourquoi a répondu correctement, mais en guise de note que je voulais ajouter, bien que votre exemple de solution recommandée soit bon 99,9999 +% du temps, ce n'est pas une bonne pratique, il est beaucoup plus sûr de vérifier la valeur nulle 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

Selon la section intitulée «Comment lancer et intercepter des exceptions» de la leçon 2 du kit de formation auto-rythmé des SCTM (examen 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation , la raison en 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 qui sont déclarées dans le bloc Try. Cela a du sens car selon l'endroit où une exception s'est produite, les déclarations de variables dans le Le bloc Try n'a peut-être pas encore été exécuté . "


4

La réponse, comme tout le monde l'a souligné, est quasiment «c'est ainsi que les blocs sont définis».

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

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

Les fermetures sont censées régler ce problème également.

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

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


2

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 portées 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

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 s'y fier.


2

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

Dans le code suivant, vous attendez-vous à ce que les s 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

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

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

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

Encore une fois, cela pourrait en théorie être résolu 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 dire simplement non.

De plus, il permet à la sémantique de la portée d'être définie globalement sans exception, en particulier, les locaux durent aussi longtemps que le {} qu'ils sont définis dans, dans tous les cas. Point mineur, mais un point.

Enfin, pour faire ce que vous voulez, vous pouvez ajouter un ensemble de crochets autour du crochet d'essai. Vous donne la portée que vous souhaitez, même si cela se fait au prix d'un peu de lisibilité, mais pas trop.

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

1

Dans l'exemple spécifique que vous avez donné, l'initialisation de s ne peut pas lever d'exception. On pourrait donc penser que sa portée pourrait peut-être être étendue.

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 où cela s'est produit) pour être dans la portée de catch / finally.

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 de variables suivent les blocs.

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


1

Une partie de la raison pour laquelle ils ne sont pas dans la même portée est que, à 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é lancée, cela pourrait être encore plus ambigu.

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


1

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" à l'état dans lequel elle se trouvait au début du bloc try. C'est par conception. C'est ainsi que try / catch est capable de se retirer de tous les appels de fonction dans le bloc et de remettre 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.

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


1

Lorsque vous avez un essai de capture, vous devez tout au plus savoir quelles erreurs il peut générer. Ces classes d'exception disent normalement tout ce dont vous avez besoin sur l'exception. Sinon, vous devez créer vos propres classes d'exceptions 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 auto-explicative. Donc, si vous avez besoin de faire beaucoup de choses, pensez à votre conception et essayez de penser s'il existe un autre moyen, que vous pouvez soit prédire les exceptions à venir, soit utiliser les informations provenant des exceptions, puis peut-être renvoyer les vôtres exception avec plus d'informations.


1

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 pendant laquelle elle restera dans la portée? Ce n'est pas si grave.

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


1

En Python, ils sont visibles dans les blocs catch / finally si la ligne les déclarant ne les a pas lancés.


1

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

La spécification C # (15.2) stipule que "La portée d'une variable locale ou d'une constante 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

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


0

Eh bien, s'il ne lève pas d'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 pas faire d'hypothèses.


0

Si nous ignorons le problème du cadrage pendant un moment, le complicateur devra travailler beaucoup plus dur dans une situation qui n'est pas bien définie. Bien que ce 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 que s soit alloué 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 qu'elles ne soient utilisées. Dans le bloc catch présenté, il ne peut pas le garantir.

Nous nous retrouvons donc avec le compilateur à devoir faire beaucoup de travail, ce qui en pratique n'apporte 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 d'extraordinaire 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'il existe et a été initialisé.

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

par exemple le mot-clé using avec des objets IDisposables 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 / finally 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 le vrai code, il est difficile d'être plus précis.


0

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


-1

Si l'opération d'affectation échoue, votre instruction catch aura une référence nulle à la variable non affectée.


2
Il n'est pas attribué. Ce n'est même pas nul (contrairement aux variables d'instance et statiques).
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? Pourquoi le vote négatif? L'encapsulation fait partie intégrante de la POO. Ça a l'air joli aussi.
core

2
Je n'étais pas le contre, mais ce qui ne va pas, c'est de renvoyer une chaîne non initialisée.
Ben Voigt
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.